
1. 套餐管理模块架构设计在苍穹外卖系统中套餐管理模块采用经典的三层架构设计这种分层结构我在多个电商项目中验证过其稳定性。先来看整体架构Controller层接收前端请求处理参数校验调用Service层方法Service层实现核心业务逻辑处理事务控制Mapper层负责数据库操作通过MyBatis实现实际开发中我习惯先设计领域模型套餐管理涉及两个核心实体// 套餐主表实体 public class Setmeal { private Long id; private String name; private Long categoryId; // 分类ID private BigDecimal price; private Integer status; // 状态 1起售 0停售 // 其他字段... } // 套餐菜品关系实体 public class SetmealDish { private Long id; private Long setmealId; // 套餐ID private Long dishId; // 菜品ID private Integer copies; // 份数 // 其他字段... }数据库表设计遵循几个原则套餐表(setmeal)与菜品表(dish)通过中间表(setmeal_dish)建立多对多关系套餐名称添加唯一索引确保唯一性外键关联分类表(category)保证数据完整性2. 新增套餐实现细节新增套餐是典型的分布式事务场景需要同时操作套餐主表和套餐菜品关系表。我在实际项目中遇到过因事务未处理导致的脏数据问题这里分享解决方案Transactional public void saveWithDish(SetmealDTO setmealDTO) { // 1. 保存套餐基本信息 Setmeal setmeal new Setmeal(); BeanUtils.copyProperties(setmealDTO, setmeal); setmealMapper.insert(setmeal); // 使用Options获取自增ID // 2. 保存套餐菜品关系 ListSetmealDish dishes setmealDTO.getSetmealDishes(); dishes.forEach(dish - dish.setSetmealId(setmeal.getId())); setmealDishMapper.insertBatch(dishes); }几个关键点需要注意事务控制使用Transactional注解保证原子性性能优化批量插入代替循环单条插入业务校验// 校验套餐名称唯一性 LambdaQueryWrapperSetmeal queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(Setmeal::getName, setmealDTO.getName()); if(setmealMapper.selectCount(queryWrapper) 0){ throw new BusinessException(套餐名称已存在); } // 校验关联菜品状态 setmealDTO.getSetmealDishes().forEach(dish - { Dish dbDish dishMapper.selectById(dish.getDishId()); if(dbDish null || dbDish.getStatus() 0){ throw new BusinessException(包含无效或停售菜品); } });3. 套餐分页查询优化分页查询是高频操作经过多次性能测试后我总结出以下优化方案public PageResult pageQuery(SetmealPageQueryDTO dto) { // 使用PageHelper实现物理分页 PageHelper.startPage(dto.getPage(), dto.getPageSize()); // 动态SQL查询 PageSetmealVO page setmealMapper.pageQuery(dto); return new PageResult(page.getTotal(), page.getResult()); }对应的XML映射文件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 and name ! and 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 /select实际开发中容易踩的坑N1查询问题避免在循环中查询关联数据分页插件冲突MyBatis-Plus与PageHelper不要混用模糊查询优化大数据量时考虑使用全文索引4. 套餐状态流转设计套餐的起售/停售是典型的状态模式应用场景我通过状态机实现状态流转控制public void startOrStop(Integer status, Long id) { // 状态转换校验 Setmeal setmeal setmealMapper.selectById(id); if(setmeal null){ throw new BusinessException(套餐不存在); } // 启售时校验菜品状态 if(status StatusConstant.ENABLE){ ListDish dishes dishMapper.getBySetmealId(id); if(dishes.stream().anyMatch(d - d.getStatus() StatusConstant.DISABLE)){ throw new BusinessException(套餐内包含停售菜品); } } // 更新状态 Setmeal update new Setmeal(); update.setId(id); update.setStatus(status); setmealMapper.update(update); // 清理缓存如有 cleanCache(setmeal.getCategoryId()); }状态流转的注意事项并发控制使用乐观锁防止状态覆盖缓存一致性状态变更后及时更新缓存关联影响套餐状态变更可能影响订单业务5. 事务与异常处理实践在套餐删除操作中我采用声明式事务保证数据一致性Transactional public void deleteBatch(ListLong ids) { // 校验套餐状态 ids.forEach(id - { Setmeal setmeal setmealMapper.selectById(id); if(setmeal.getStatus() StatusConstant.ENABLE){ throw new BusinessException(起售套餐不可删除); } }); // 删除套餐记录 setmealMapper.deleteByIds(ids); // 删除关联菜品关系 setmealDishMapper.deleteBySetmealIds(ids); }异常处理的最佳实践自定义异常体系public class BusinessException extends RuntimeException { private Integer code; public BusinessException(String message) { super(message); this.code 500; } }全局异常处理器RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public Result handleBusinessException(BusinessException ex){ return Result.error(ex.getMessage()); } }事务回滚策略默认对RuntimeException回滚6. 工程化实践建议经过多个项目迭代我总结出以下工程化实践接口文档管理使用Swagger生成实时API文档配合YAPI进行接口Mock代码规范Controller方法不超过50行Service方法保持单一职责复杂SQL写在XML中性能监控// 使用AOP监控方法耗时 Around(execution(* com.sky.service.impl.*.*(..))) public Object monitorTime(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); log.info(方法执行耗时: {}ms, System.currentTimeMillis()-start); return result; }缓存策略套餐列表使用Redis缓存采用Cache-Aside模式设置合理的过期时间在实际开发中我发现套餐管理模块最常出现的问题是事务边界划分不清。建议按照以下原则划分事务跨表操作需要事务查询方法不加事务批量操作使用独立事务最后分享一个调试技巧在开发套餐状态流转时可以使用Spring的PostConstruct模拟状态变化Component public class SetmealStatusDebug { Autowired private SetmealService setmealService; PostConstruct public void init(){ // 模拟启售操作 setmealService.startOrStop(1, 1L); } }