从产品原型到数据库:我是如何设计‘苍穹外卖’套餐管理模块的(含表结构设计思路)

发布时间:2026/6/23 18:37:13

从产品原型到数据库:我是如何设计‘苍穹外卖’套餐管理模块的(含表结构设计思路) 从产品原型到数据库我是如何设计‘苍穹外卖’套餐管理模块的含表结构设计思路在餐饮SaaS系统的开发中套餐管理模块往往是业务复杂度最高的部分之一。它不仅需要处理套餐本身的CRUD操作还要维护套餐与菜品之间复杂的多对多关系。本文将分享我在苍穹外卖项目中设计套餐管理模块的完整思考过程包括如何从产品原型反推数据库结构以及在实际开发中遇到的典型问题与解决方案。1. 需求分析与领域建模当产品经理交付新增套餐原型图时我们需要从界面元素中提取出核心业务规则。以苍穹外卖为例原型中包含了以下关键约束唯一性约束套餐名称需全局唯一完整性约束必须关联到某个分类且至少包含一个菜品状态机设计新增套餐默认停售需手动起售级联限制起售套餐时需检查包含菜品是否全部可用这些业务规则直接影响我们的数据库设计。通过领域驱动设计(DDD)的方法我们识别出两个核心实体classDiagram class Setmeal { Long id String name Long categoryId BigDecimal price String image Integer status } class SetmealDish { Long setmealId Long dishId String dishName BigDecimal dishPrice Integer copies } Setmeal 1 -- n SetmealDish注意实际开发中我们采用逻辑外键而非物理外键以保持灵活性并避免级联操作带来的性能问题2. 数据库表结构设计基于上述分析我们设计了以下表结构2.1 套餐主表(setmeal)字段名类型说明设计考量idbigint主键自增主键namevarchar(32)套餐名称添加唯一索引category_idbigint分类ID逻辑外键pricedecimal(10,2)套餐价格避免浮点精度问题statusint售卖状态使用常量类管理(1起售/0停售)descriptionvarchar(255)套餐描述可为空imagevarchar(255)图片路径存储相对路径CREATE TABLE setmeal ( id bigint NOT NULL AUTO_INCREMENT, name varchar(32) COLLATE utf8mb4_bin NOT NULL, category_id bigint NOT NULL, price decimal(10,2) NOT NULL, image varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, description varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, status int DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, create_user bigint DEFAULT NULL, update_user bigint DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin;2.2 套餐菜品关联表(setmeal_dish)字段名类型说明设计考量setmeal_idbigint套餐ID联合主键dish_idbigint菜品ID联合主键namevarchar(32)菜品名称冗余字段pricedecimal(10,2)菜品单价历史价格快照copiesint菜品份数默认值1CREATE TABLE setmeal_dish ( id bigint NOT NULL AUTO_INCREMENT, setmeal_id bigint NOT NULL, dish_id bigint NOT NULL, name varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, price decimal(10,2) DEFAULT NULL, copies int NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY idx_setmeal_id (setmeal_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin;关键设计决策冗余字段在关联表中存储菜品名称和价格避免频繁联表查询逻辑删除采用status状态字段而非物理删除价格快照存储下单时的菜品价格不受后续调价影响3. 核心业务逻辑实现3.1 新增套餐的原子性操作新增套餐需要同时操作setmeal和setmeal_dish两张表必须保证事务原子性Transactional public void saveWithDish(SetmealDTO setmealDTO) { // 1. 保存套餐基本信息 Setmeal setmeal new Setmeal(); BeanUtils.copyProperties(setmealDTO, setmeal); setmealMapper.insert(setmeal); // 主键回填 // 2. 保存套餐菜品关系 ListSetmealDish setmealDishes setmealDTO.getSetmealDishes(); setmealDishes.forEach(dish - dish.setSetmealId(setmeal.getId())); // 3. 批量插入 setmealDishMapper.insertBatch(setmealDishes); }踩坑提醒首次实现时忘记设置status默认值导致新增套餐直接起售违反业务规则3.2 分页查询的联表优化套餐列表需要显示分类名称我们采用左连接避免N1查询问题select idpageQuery resultTypecom.sky.vo.SetmealVO SELECT s.*, c.name AS categoryName FROM setmeal s LEFT JOIN category c ON s.category_id c.id where if testname ! null s.name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND s.category_id #{categoryId} /if if teststatus ! null AND s.status #{status} /if /where ORDER BY s.create_time DESC /select3.3 状态变更的校验逻辑起售套餐时需要检查关联菜品状态public void startOrStop(Integer status, Long id) { if (status StatusConstant.ENABLE) { ListDish dishes dishMapper.getBySetmealId(id); dishes.forEach(dish - { if (dish.getStatus() StatusConstant.DISABLE) { throw new SetmealEnableFailedException(套餐包含停售菜品); } }); } setmealMapper.updateStatus(id, status); }4. 性能优化实践4.1 批量操作优化套餐菜品关系采用批量插入提升性能insert idinsertBatch INSERT INTO setmeal_dish (setmeal_id, dish_id, name, price, copies) VALUES foreach collectionlist itemitem separator, (#{item.setmealId}, #{item.dishId}, #{item.name}, #{item.price}, #{item.copies}) /foreach /insert4.2 缓存策略设计针对高频访问的套餐数据采用二级缓存方案本地缓存使用Caffeine缓存热门套餐基本信息分布式缓存Redis缓存完整套餐数据缓存更新通过CacheEvict注解保证一致性Cacheable(value setmeal, key #id) public SetmealVO getByIdWithCache(Long id) { return getByIdWithDishes(id); } CacheEvict(value setmeal, key #setmealDTO.id) public void updateWithDish(SetmealDTO setmealDTO) { // 更新逻辑 }5. 异常处理与事务管理针对套餐管理的特殊场景我们设计了专属异常体系// 删除起售套餐异常 public class DeletionNotAllowedException extends RuntimeException { public DeletionNotAllowedException(String message) { super(message); } } // 起售包含停售菜品的套餐异常 public class SetmealEnableFailedException extends RuntimeException { public SetmealEnableFailedException(String message) { super(message); } }事务管理采用Spring声明式事务特别注意以下场景跨表操作必须添加Transactional只读查询添加Transactional(readOnly true)设置合适的事务隔离级别和传播行为Transactional(rollbackFor Exception.class) public void deleteWithDish(ListLong ids) { // 检查状态 ids.forEach(id - { Setmeal setmeal setmealMapper.getById(id); if (setmeal.getStatus() StatusConstant.ENABLE) { throw new DeletionNotAllowedException(套餐正在售卖中); } }); // 删除套餐 setmealMapper.deleteByIds(ids); // 删除关联 setmealDishMapper.deleteBySetmealIds(ids); }在苍穹外卖项目中这套设计方案经受住了日均10万订单的考验。特别在促销期间套餐模块的QPS达到500时仍保持稳定响应。后续我们通过分库分表进一步提升了系统容量但核心表结构设计始终未变验证了初期设计的合理性。

相关新闻