第三天项目

苍穹外卖项目 - 第3天冲刺日志

日期:2025-11-28
冲刺周期:第3天/共7天
参与人员:李靖华 温尚熙 谢斯越 郑哲磊


一、站立会议照片

440be925a0c6a3541faec4ff732b18f5


二、会议内容记录

郑哲磊(后端负责人)

昨天已完成的工作

  • ✅ [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并发下运行稳定。
posted @ 2025-12-03 19:52  清月明风  阅读(5)  评论(0)    收藏  举报