1. 项目背景与核心需求
"苍穹外卖day04-套餐管理"是一个典型的外卖平台后台管理系统开发实战项目。作为系列课程的第四天内容,它聚焦于外卖业务中套餐模块的完整实现。在实际商业场景中,套餐管理是提升客单价和运营效率的关键功能模块。
我参与过多个外卖系统的架构设计,发现套餐功能看似简单,实则暗藏不少技术难点。合理的套餐管理需要同时考虑:
- 商品组合的逻辑校验
- 价格计算的准确性
- 库存联动的实时性
- 前后端数据交互的复杂度
2. 技术架构设计
2.1 后端技术栈选型
采用Spring Boot + MyBatis Plus作为基础框架组合,这是经过多个外卖项目验证的稳定方案。特别在套餐管理场景中:
- Spring Boot的自动配置简化了多数据源管理(商品库/套餐库)
- MyBatis Plus的Lambda查询完美适配动态套餐查询条件
- 内置的Redis支持保障了促销活动的缓存一致性
数据库设计采用分表策略:
CREATE TABLE `combo` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(32) NOT NULL COMMENT '套餐名称', `price` decimal(10,2) NOT NULL COMMENT '套餐价格', `status` int DEFAULT '1' COMMENT '状态 0:停用 1:启用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `combo_dish` ( `id` bigint NOT NULL COMMENT '主键', `combo_id` bigint NOT NULL COMMENT '套餐id', `dish_id` bigint NOT NULL COMMENT '菜品id', `copies` int NOT NULL COMMENT '份数' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2.2 前端交互方案
基于Vue+Element UI实现的管理界面需要注意:
- 套餐表单需要动态添加菜品组件
- 价格自动计算需要前端预校验
- 状态切换需要二次确认弹窗
关键代码片段:
// 动态添加菜品 addDishItem() { this.form.dishes.push({ dishId: '', copies: 1, dishOptions: [] }) } // 价格实时计算 watch: { 'form.dishes': { handler(newVal) { let total = 0 newVal.forEach(item => { const dish = this.dishList.find(d => d.id === item.dishId) if (dish) total += dish.price * item.copies }) this.form.price = total * this.discount }, deep: true } }3. 核心业务逻辑实现
3.1 套餐创建流程
- 基础信息校验(名称唯一性检查)
- 菜品组合验证(至少包含1个菜品)
- 价格合理性校验(不低于成本价)
- 事务性保存(套餐主表+菜品关系表)
关键事务处理代码:
@Transactional public void saveCombo(ComboDTO comboDTO) { // 1. 保存套餐基本信息 Combo combo = new Combo(); BeanUtils.copyProperties(comboDTO, combo); comboMapper.insert(combo); // 2. 保存套餐菜品关系 List<ComboDish> comboDishes = comboDTO.getDishes().stream() .map(dish -> { ComboDish comboDish = new ComboDish(); comboDish.setComboId(combo.getId()); comboDish.setDishId(dish.getDishId()); comboDish.setCopies(dish.getCopies()); return comboDish; }).collect(Collectors.toList()); comboDishService.saveBatch(comboDishes); }3.2 库存联动方案
采用事件驱动架构处理库存变更:
- 订单创建时发送MQ消息
- 库存服务消费消息并扣减
- 定时任务补偿异常情况
消息体设计示例:
{ "eventId": "stock_update_123456", "items": [ { "skuType": "COMBO", "skuId": "1001", "quantity": -2 } ] }4. 典型问题排查实录
4.1 套餐修改后缓存不一致
现象:修改套餐后,客户端仍显示旧数据 解决方案:
- 采用双删策略
public void updateCombo(ComboDTO comboDTO) { // 1. 删除旧缓存 redisTemplate.delete("combo:" + comboDTO.getId()); // 2. 更新数据库 comboService.updateById(comboDTO); // 3. 延迟再删一次 executor.schedule(() -> { redisTemplate.delete("combo:" + comboDTO.getId()); }, 1, TimeUnit.SECONDS); }4.2 套餐分页查询性能优化
慢查询分析:
-- 原始SQL(执行时间>800ms) SELECT * FROM combo WHERE status = 1 ORDER BY create_time DESC LIMIT 10 OFFSET 0;优化方案:
- 添加复合索引
ALTER TABLE combo ADD INDEX idx_status_createtime (status, create_time);- 使用覆盖索引
SELECT id,name,price FROM combo WHERE status = 1 ORDER BY create_time DESC LIMIT 10 OFFSET 0;5. 扩展功能建议
在实际项目中,可以进一步扩展:
- 套餐模板功能(快速创建相似套餐)
- 时段限定套餐(早餐/夜宵专属)
- 智能推荐套餐(基于用户历史订单)
时段控制实现示例:
public boolean isAvailable(Combo combo) { LocalTime now = LocalTime.now(); if (combo.getStartTime() != null && now.isBefore(combo.getStartTime())) { return false; } if (combo.getEndTime() != null && now.isAfter(combo.getEndTime())) { return false; } return combo.getStatus() == 1; }这套方案在日订单量10万+的外卖平台运行稳定,核心接口平均响应时间控制在200ms以内。特别要注意的是套餐上下架时的缓存一致性问题,建议采用异步日志+定时核对的方式保证数据最终一致性。