第三天项目
苍穹外卖项目 - 第3天冲刺日志
日期:2025-11-28
冲刺周期:第3天/共7天
参与人员:李靖华 温尚熙 谢斯越 郑哲磊
一、站立会议照片

二、会议内容记录
郑哲磊(后端负责人)
昨天已完成的工作:
- ✅ [WI-017] 完成员工管理CRUD接口
- ✅ [WI-018] 实现JWT token认证机制
- ✅ [WI-019] 完成菜品分类管理接口
- ✅ [WI-020] 编写接口文档(Swagger)
今天计划完成的工作:
- [WI-033] 完成菜品管理CRUD接口
- [WI-034] 实现图片上传功能(阿里云OSS)
- [WI-035] 完成套餐管理接口
- [WI-036] 实现微信用户登录接口
工作中遇到的困难:
- 阿里云OSS配置较复杂,需要申请AccessKey
- 菜品口味数据结构设计需要优化
谢斯越(前端负责人)
昨天已完成的工作:
- ✅ [WI-021] 完成主页布局和导航菜单
- ✅ [WI-022] 实现员工管理页面
- ✅ [WI-023] 完成分类管理页面
- ✅ [WI-024] 实现token自动刷新机制
今天计划完成的工作:
- [WI-037] 完成菜品管理页面
- [WI-038] 实现图片上传组件
- [WI-039] 完成套餐管理页面
- [WI-040] 优化页面加载性能
工作中遇到的困难:
- 图片上传组件需要支持裁剪和压缩功能
- 菜品口味选择器的交互设计需要优化
温尚熙(小程序开发)
昨天已完成的工作:
- ✅ [WI-025] 实现菜品列表页面
- ✅ [WI-026] 完成菜品详情页面
- ✅ [WI-027] 实现购物车功能
- ✅ [WI-028] 完成用户授权登录
今天计划完成的工作:
- [WI-041] 完成地址管理功能
- [WI-042] 实现订单确认页面
- [WI-043] 完成订单提交功能
- [WI-044] 实现订单列表页面
工作中遇到的困难:
- 地址选择器需要集成腾讯地图API
- 订单金额计算逻辑较复杂,需要仔细测试
李靖华(测试与文档)
昨天已完成的工作:
- ✅ [WI-029] 执行登录接口测试
- ✅ [WI-030] 编写员工管理测试用例
- ✅ [WI-031] 更新API文档
- ✅ [WI-032] 进行代码质量检查
今天计划完成的工作:
- [WI-045] 执行菜品管理接口测试
- [WI-046] 编写集成测试用例
- [WI-047] 进行性能测试
- [WI-048] 更新用户手册
工作中遇到的困难:
- 性能测试工具JMeter配置需要学习
- 部分接口响应时间超过预期,需要优化
三、燃尽图
剩余工作量(小时)
120 |●
| \
100 | ●
| \\
80 | \
| ●
60 | \\
| \
40 | \
| \
20 | \
| ●
0 |____________________________
1 2 3 4 5 6 7 (天数)
图例:
● —— 实际进度(实线)
- - - 理想进度(虚线)
燃尽图说明:
- 当前剩余工作量:68小时(完成52小时)
- 理想剩余工作量:77小时
- 进度状态:✅ 快于预期进度
- 燃尽速度:28小时/天
项目收敛分析:
- 第3天进入快速开发阶段,团队协作效率提升
- 实际进度持续快于理想进度,项目风险降低
- 核心功能模块开发进展顺利,预计可提前完成基础功能
四、代码/文档签入记录
郑哲磊 - 菜品管理与图片上传模块
- 模块名称:sky-server 菜品管理模块
- 提交内容:
- 完成菜品增删改查接口
- 实现阿里云OSS图片上传
- 添加菜品口味关联表设计
代码示例:
// DishController.java - 菜品管理控制器
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
@DeleteMapping
@ApiOperation("批量删除菜品")
public Result delete(@RequestParam List<Long> ids) {
log.info("批量删除菜品:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}
}
// AliOssUtil.java - 阿里云OSS工具类
@Component
@Slf4j
public class AliOssUtil {
@Autowired
private AliOssProperties aliOssProperties;
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(
aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret()
);
try {
// 上传文件
ossClient.putObject(
aliOssProperties.getBucketName(),
objectName,
new ByteArrayInputStream(bytes)
);
// 返回文件访问路径
String url = "https://" + aliOssProperties.getBucketName() + "."
+ aliOssProperties.getEndpoint() + "/" + objectName;
return url;
} finally {
ossClient.shutdown();
}
}
}
// DishService.java - 菜品业务逻辑
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
// 向菜品表插入1条数据
dishMapper.insert(dish);
// 获取insert语句生成的主键值
Long dishId = dish.getId();
// 向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(flavor -> {
flavor.setDishId(dishId);
});
dishFlavorMapper.insertBatch(flavors);
}
}
}
谢斯越- 管理端菜品管理页面模块
- 模块名称:sky-admin 菜品管理页面
- 提交内容:
- 完成菜品管理页面CRUD功能
- 实现图片上传和预览组件
- 添加菜品口味动态表单
代码示例:
<!-- DishEdit.vue - 菜品编辑页面 -->
<template>
<el-form :model="dishForm" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="菜品名称" prop="name">
<el-input v-model="dishForm.name" placeholder="请输入菜品名称" />
</el-form-item>
<el-form-item label="菜品分类" prop="categoryId">
<el-select v-model="dishForm.categoryId" placeholder="请选择分类">
<el-option v-for="item in categoryList" :key="item.id"
:label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="菜品价格" prop="price">
<el-input-number v-model="dishForm.price" :precision="2" :min="0" />
</el-form-item>
<el-form-item label="菜品图片" prop="image">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
:show-file-list="false"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload">
<img v-if="dishForm.image" :src="dishForm.image" class="avatar">
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="菜品口味">
<div v-for="(flavor, index) in dishForm.flavors" :key="index" class="flavor-item">
<el-input v-model="flavor.name" placeholder="口味名称" style="width: 150px" />
<el-input v-model="flavor.value" placeholder="口味选项(逗号分隔)" style="width: 300px" />
<el-button type="danger" @click="removeFlavor(index)">删除</el-button>
</div>
<el-button type="primary" @click="addFlavor">添加口味</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="handleCancel">取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { saveDishApi, updateDishApi } from '@/api/dish'
import { ElMessage } from 'element-plus'
const dishForm = reactive({
name: '',
categoryId: null,
price: 0,
image: '',
description: '',
status: 1,
flavors: []
})
const uploadUrl = import.meta.env.VITE_API_URL + '/admin/common/upload'
const handleUploadSuccess = (response) => {
dishForm.image = response.data
ElMessage.success('图片上传成功')
}
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
const addFlavor = () => {
dishForm.flavors.push({ name: '', value: '' })
}
const removeFlavor = (index) => {
dishForm.flavors.splice(index, 1)
}
const handleSubmit = async () => {
if (dishForm.id) {
await updateDishApi(dishForm)
ElMessage.success('修改成功')
} else {
await saveDishApi(dishForm)
ElMessage.success('新增成功')
}
}
</script>
温尚熙 - 小程序地址管理模块
- 模块名称:sky-user 地址管理模块
- 提交内容:
- 完成地址管理页面
- 实现地址选择器
- 添加默认地址设置功能
代码示例:
// pages/address/address.js - 地址管理页面
Page({
data: {
addressList: []
},
onLoad() {
this.getAddressList()
},
// 获取地址列表
getAddressList() {
wx.request({
url: getApp().globalData.baseUrl + '/user/addressBook/list',
method: 'GET',
header: {
'token': wx.getStorageSync('token')
},
success: (res) => {
this.setData({
addressList: res.data.data
})
}
})
},
// 新增地址
onAddAddress() {
wx.navigateTo({
url: '/pages/address/edit/edit'
})
},
// 编辑地址
onEditAddress(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/address/edit/edit?id=${id}`
})
},
// 删除地址
onDeleteAddress(e) {
const id = e.currentTarget.dataset.id
wx.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
wx.request({
url: getApp().globalData.baseUrl + '/user/addressBook',
method: 'DELETE',
data: { id },
header: {
'token': wx.getStorageSync('token')
},
success: () => {
wx.showToast({ title: '删除成功', icon: 'success' })
this.getAddressList()
}
})
}
}
})
},
// 设置默认地址
onSetDefault(e) {
const id = e.currentTarget.dataset.id
wx.request({
url: getApp().globalData.baseUrl + '/user/addressBook/default',
method: 'PUT',
data: { id },
header: {
'token': wx.getStorageSync('token')
},
success: () => {
wx.showToast({ title: '设置成功', icon: 'success' })
this.getAddressList()
}
})
}
})
<!-- pages/address/address.wxml - 地址列表页面 -->
<view class="container">
<view class="address-list">
<view wx:for="{{addressList}}" wx:key="id" class="address-item">
<view class="address-info">
<view class="address-header">
<text class="consignee">{{item.consignee}}</text>
<text class="phone">{{item.phone}}</text>
<view wx:if="{{item.isDefault === 1}}" class="default-tag">默认</view>
</view>
<view class="address-detail">
{{item.provinceName}}{{item.cityName}}{{item.districtName}}{{item.detail}}
</view>
</view>
<view class="address-actions">
<button class="btn-edit" data-id="{{item.id}}" bindtap="onEditAddress">编辑</button>
<button class="btn-delete" data-id="{{item.id}}" bindtap="onDeleteAddress">删除</button>
<button wx:if="{{item.isDefault !== 1}}" class="btn-default"
data-id="{{item.id}}" bindtap="onSetDefault">设为默认</button>
</view>
</view>
</view>
<view class="add-btn" bindtap="onAddAddress">
<text>+ 新增地址</text>
</view>
</view>
// pages/address/edit/edit.js - 地址编辑页面
Page({
data: {
addressForm: {
consignee: '',
phone: '',
provinceName: '',
cityName: '',
districtName: '',
detail: '',
sex: '1',
isDefault: 0
}
},
// 选择地址
onChooseLocation() {
wx.chooseLocation({
success: (res) => {
this.setData({
'addressForm.detail': res.address + res.name
})
}
})
},
// 保存地址
onSave() {
const { addressForm } = this.data
const url = addressForm.id
? '/user/addressBook'
: '/user/addressBook'
const method = addressForm.id ? 'PUT' : 'POST'
wx.request({
url: getApp().globalData.baseUrl + url,
method: method,
data: addressForm,
header: {
'token': wx.getStorageSync('token')
},
success: () => {
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
})
}
})
李靖华 - 菜品接口测试与性能测试模块
- 模块名称:菜品接口测试与性能测试
- 提交内容:
- 完成菜品管理接口测试
- 编写性能测试脚本
- 生成测试报告v3.0
测试代码示例:
// dish.test.js - 菜品管理接口测试
const request = require('supertest');
const app = require('../app');
describe('菜品管理接口测试', () => {
let token = '';
let dishId = '';
beforeAll(async () => {
// 登录获取token
const response = await request(app)
.post('/admin/employee/login')
.send({ username: 'admin', password: '123456' });
token = response.body.data.token;
});
// 测试新增菜品
test('POST /admin/dish - 新增菜品', async () => {
const response = await request(app)
.post('/admin/dish')
.set('token', token)
.send({
name: '测试菜品',
categoryId: 1,
price: 38.00,
image: 'http://example.com/dish.jpg',
description: '这是一道测试菜品',
status: 1,
flavors: [
{ name: '辣度', value: '不辣,微辣,中辣,特辣' },
{ name: '温度', value: '热,冷' }
]
});
expect(response.status).toBe(200);
expect(response.body.code).toBe(1);
dishId = response.body.data;
});
// 测试菜品分页查询
test('GET /admin/dish/page - 菜品分页查询', async () => {
const response = await request(app)
.get('/admin/dish/page')
.set('token', token)
.query({ page: 1, pageSize: 10, name: '测试' });
expect(response.status).toBe(200);
expect(response.body.code).toBe(1);
expect(response.body.data).toHaveProperty('records');
expect(response.body.data.records.length).toBeGreaterThan(0);
});
// 测试修改菜品
test('PUT /admin/dish - 修改菜品', async () => {
const response = await request(app)
.put('/admin/dish')
.set('token', token)
.send({
id: dishId,
name: '测试菜品(已修改)',
price: 48.00
});
expect(response.status).toBe(200);
expect(response.body.code).toBe(1);
});
// 测试删除菜品
test('DELETE /admin/dish - 删除菜品', async () => {
const response = await request(app)
.delete('/admin/dish')
.set('token', token)
.query({ ids: dishId });
expect(response.status).toBe(200);
expect(response.body.code).toBe(1);
});
});
JMeter性能测试脚本:
<!-- dish-performance-test.jmx - JMeter测试计划 -->
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="菜品接口性能测试">
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.name">BASE_URL</stringProp>
<stringProp name="Argument.value">http://localhost:8080</stringProp>
</elementProp>
<elementProp name="TOKEN" elementType="Argument">
<stringProp name="Argument.name">TOKEN</stringProp>
<stringProp name="Argument.value">${__P(token)}</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="菜品查询并发测试">
<intProp name="ThreadGroup.num_threads">100</intProp>
<intProp name="ThreadGroup.ramp_time">10</intProp>
<longProp name="ThreadGroup.duration">60</longProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="菜品分页查询">
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
<stringProp name="HTTPSampler.path">/admin/dish/page</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="page" elementType="HTTPArgument">
<stringProp name="Argument.value">1</stringProp>
</elementProp>
<elementProp name="pageSize" elementType="HTTPArgument">
<stringProp name="Argument.value">10</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<elementProp name="HTTPSampler.header_manager" elementType="HeaderManager">
<collectionProp name="HeaderManager.headers">
<elementProp name="token" elementType="Header">
<stringProp name="Header.value">${TOKEN}</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
测试报告示例:
## 菜品管理接口性能测试报告
### 测试环境
- 服务器:本地开发环境
- 数据库:MySQL 8.0
- 并发用户数:100
- 测试时长:60秒
### 测试结果
| 接口 | 平均响应时间 | 最大响应时间 | TPS | 错误率 |
|------|------------|------------|-----|--------|
| 菜品分页查询 | 120ms | 350ms | 450 | 0% |
| 新增菜品 | 180ms | 420ms | 280 | 0% |
| 修改菜品 | 150ms | 380ms | 320 | 0% |
| 删除菜品 | 100ms | 280ms | 380 | 0% |
### 结论
所有接口性能指标均符合要求,系统在100并发下运行稳定。
浙公网安备 33010602011771号