外卖套餐管理系统开发实战:架构设计与核心实现
1. 项目背景与核心需求最近在开发一个外卖套餐管理系统时遇到了一个有趣的技术挑战。这个系统需要处理大量实时订单数据同时还要保证套餐组合的灵活性和库存管理的准确性。作为一个从零开始的项目我们需要自己实现所有核心功能模块。这个外卖套餐项目的核心需求可以归纳为以下几点支持动态配置套餐组合主餐小食饮料的任意搭配实时库存管理防止超卖订单处理流水线从下单到配送的全流程跟踪商家后台管理系统菜品管理、套餐配置、数据统计2. 技术架构设计2.1 整体架构方案经过多次技术选型讨论我们最终确定了以下技术栈前端Vue3 Element Plus管理后台 Uni-app小程序端后端Spring Boot 2.7 MyBatis Plus数据库MySQL 8.0主业务库 Redis缓存和计数器消息队列RabbitMQ订单异步处理部署Docker Jenkins CI/CD选择这套技术栈主要基于以下考虑团队成员对这些技术都比较熟悉开发效率有保障成熟的生态圈和社区支持良好的性能表现和扩展性2.2 数据库设计要点套餐系统的数据库设计有几个关键点需要特别注意-- 套餐基础表 CREATE TABLE combo ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 套餐名称, description text COMMENT 套餐描述, price decimal(10,2) NOT NULL COMMENT 套餐价格, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-上架 0-下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 套餐项关联表 CREATE TABLE combo_item_relation ( id bigint NOT NULL AUTO_INCREMENT, combo_id bigint NOT NULL COMMENT 套餐ID, item_id bigint NOT NULL COMMENT 商品ID, item_type tinyint NOT NULL COMMENT 商品类型1-主餐 2-小食 3-饮料, is_required tinyint NOT NULL DEFAULT 1 COMMENT 是否必选1-必选 0-可选, PRIMARY KEY (id), KEY idx_combo_id (combo_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意套餐与商品的关联关系设计是系统的核心我们采用了灵活的关联表结构支持不同类型的商品组合并且可以标记是否为必选项。3. 核心功能实现3.1 动态套餐配置套餐配置的后台管理界面需要支持灵活的拖拽式组合。我们基于Vue的draggable组件实现了这个功能// 套餐配置组件核心逻辑 export default { data() { return { comboItems: { mains: [], // 主餐列表 sides: [], // 小食列表 drinks: [] // 饮料列表 }, selectedItems: [] // 已选中的商品 } }, methods: { handleDragEnd(evt) { // 处理拖拽结束后的逻辑 const item evt.item.__vue__.$attrs[data-item] if (this.validateSelection(item)) { this.selectedItems.push(item) } else { this.$message.error(已达到该类型商品的最大选择数量) } }, validateSelection(item) { // 验证选择是否合法 const typeCount this.selectedItems.filter( i i.type item.type ).length return typeCount this.getMaxSelection(item.type) } } }3.2 库存扣减方案库存管理是外卖系统的关键难点我们实现了多层次的库存控制前端限流在加入购物车时就进行库存预检查缓存计数使用Redis原子操作保证库存扣减的原子性数据库最终一致性通过定时任务补偿Redis和MySQL的数据差异Redis库存扣减的核心代码public boolean reduceStock(Long itemId, int quantity) { String key stock: itemId; long value redisTemplate.opsForValue().increment(key, -quantity); if (value 0) { // 扣减成功发送MQ消息异步更新数据库 mqTemplate.convertAndSend(stock.update, new StockUpdateMessage(itemId, quantity)); return true; } else { // 库存不足回滚 redisTemplate.opsForValue().increment(key, quantity); return false; } }4. 订单处理流水线4.1 订单状态机设计我们采用状态机模式管理订单生命周期public enum OrderStatus { CREATED, // 已创建 PAID, // 已支付 PREPARING, // 备餐中 DELIVERING, // 配送中 COMPLETED, // 已完成 CANCELLED // 已取消 } // 状态转换规则 public class OrderStateMachine extends StateMachineOrderStatus, OrderEvent { public OrderStateMachine() { super(OrderStatus.CREATED); addTransition(OrderStatus.CREATED, OrderEvent.PAY, OrderStatus.PAID); addTransition(OrderStatus.PAID, OrderEvent.PREPARE, OrderStatus.PREPARING); addTransition(OrderStatus.PREPARING, OrderEvent.DELIVER, OrderStatus.DELIVERING); addTransition(OrderStatus.DELIVERING, OrderEvent.COMPLETE, OrderStatus.COMPLETED); // 取消相关的转换规则 addTransition(OrderStatus.CREATED, OrderEvent.CANCEL, OrderStatus.CANCELLED); addTransition(OrderStatus.PAID, OrderEvent.CANCEL, OrderStatus.CANCELLED); } }4.2 分布式事务处理订单创建涉及多个服务调用我们采用Saga模式保证最终一致性订单服务创建订单记录库存服务扣减库存支付服务处理支付通知服务发送订单确认通知每个步骤都有对应的补偿操作任一步骤失败都会触发前面步骤的回滚。5. 性能优化实践5.1 缓存策略我们实现了多级缓存来提升系统响应速度本地缓存使用Caffeine缓存热点数据如套餐基本信息分布式缓存Redis缓存库存数据、用户常用地址等数据库缓存MySQL查询缓存优化缓存更新策略采用先更新数据库再删除缓存的方式避免缓存一致性问题。5.2 SQL优化案例在订单查询接口中我们发现了一个性能瓶颈-- 优化前的慢查询 SELECT * FROM orders WHERE user_id 123 ORDER BY create_time DESC LIMIT 10; -- 优化后的查询 SELECT * FROM orders WHERE user_id 123 AND create_time 2023-01-01 -- 添加时间范围条件 ORDER BY create_time DESC LIMIT 10;通过添加合理的时间范围条件这个查询的性能提升了5倍以上。6. 踩坑与经验总结6.1 库存超卖问题初期我们遇到了严重的库存超卖问题主要原因是前端只做了一次库存检查高并发下多个请求同时通过库存检查数据库更新存在延迟解决方案引入Redis原子操作保证扣减的原子性实现二次校验机制添加库存预警阈值6.2 套餐配置的灵活性最初的设计中套餐配置过于死板无法满足商家的多样化需求。我们通过以下改进解决了这个问题将固定套餐改为可配置模板支持设置必选和可选项目允许设置不同类型商品的最大选择数量7. 扩展与未来优化虽然当前版本已经实现了核心功能但还有几个值得优化的方向智能推荐基于用户历史订单推荐套餐组合动态定价根据时段、库存情况自动调整价格备餐预测利用历史数据预测备餐量这个外卖套餐项目从零开始开发前后历时32天期间遇到了各种技术挑战。最大的收获是深刻理解了分布式系统中的数据一致性问题以及如何在保证系统性能的同时提供灵活的业务功能。