苍穹外卖——项目实战:套餐管理模块的CRUD全流程解析
1. 套餐管理模块的业务规则解析
开发外卖系统的套餐管理模块时,首先要吃透业务规则。这就像做菜前要先了解食材特性一样重要。我遇到过不少开发者直接上手写代码,结果发现业务逻辑不匹配又要返工的情况。下面这些关键业务规则,建议你在开发前用马克笔写在白板上:
唯一性校验是基础中的基础。套餐名称必须全局唯一,这就像餐厅里不能有两道同名菜品。实际开发中我推荐在数据库层和代码层做双重校验,避免并发问题。具体实现时可以在setmeal表的name字段加唯一索引,同时在Service层先做查询校验。
必填项控制包括套餐名称、分类、价格、图片四个核心字段。前端可以做基础校验,但后端必须进行二次验证。这里有个容易踩的坑:价格字段要用BigDecimal类型,用double会出现精度问题。我曾在项目中发现0.1+0.2=0.30000000000000004这种经典问题。
菜品关联规则要求每个套餐至少包含一个菜品。这个校验要放在事务的最后一步,确保套餐基础信息入库成功后再校验菜品列表。曾经有同事把校验放在最前面,结果遇到套餐信息保存失败时,用户却收到了"菜品不能为空"的误导提示。
状态机设计方面,新增套餐默认处于停售状态,需要手动启售。这符合餐饮行业"先准备后上架"的流程。启售时要特别注意连锁反应:如果套餐内包含停售的菜品,整个套餐也不能启售。这个逻辑要放在Service层实现,建议用Stream API过滤菜品状态:
boolean hasDisabledDish = setmealDishes.stream() .anyMatch(dish -> dish.getStatus() == StatusConstant.DISABLE);2. 数据库设计与优化技巧
数据库设计就像盖房子的地基,我见过太多项目后期因为早期设计缺陷而推倒重来。套餐管理涉及setmeal和setmeal_dish两张核心表,有些设计细节值得展开说说:
主键策略推荐使用数据库自增ID,相比UUID等方案更符合餐饮业务特点。注意MyBatis要配置@Options(useGeneratedKeys = true)实现主键回填,否则新增套餐后拿不到ID,导致关联菜品失败。这个坑我踩过,调试了两小时才发现问题。
冗余字段在setmeal_dish表中存在菜品名称和单价。这属于典型的空间换时间策略,避免每次查询都要联表查菜品表。但要注意数据一致性,当菜品信息变更时,记得用触发器或代码同步更新关联套餐。我有次忘了处理,导致用户看到的套餐价格与实际不符。
索引优化方面,这几个字段必须建索引:
setmeal.name:唯一索引setmeal.category_id:普通索引setmeal_dish.setmeal_id:外键索引setmeal_dish.dish_id:外键索引
联表查询时,建议使用LEFT JOIN代替子查询。例如分页查询时要关联分类表获取分类名称:
SELECT s.*, c.name AS category_name FROM setmeal s LEFT JOIN category c ON s.category_id = c.id WHERE s.status = 1事务管理是另一个重点。新增套餐时要同时操作两张表,必须用@Transactional保证原子性。建议将事务隔离级别设为REPEATABLE_READ,防止脏读。曾经有个线上bug就是因为事务配置不当,导致套餐显示不全。
3. 核心接口实现详解
接口实现是业务逻辑的落脚点,这里我结合自己趟过的坑,详细解析几个关键接口。
3.1 新增套餐接口
这个接口要处理文件上传、数据校验、主表从表操作等多个步骤。建议拆分为以下子流程:
- 参数校验:用Hibernate Validator做基础校验
@PostMapping public Result save(@Valid @RequestBody SetmealDTO setmealDTO) { // 业务校验 if (setmealDTO.getSetmealDishes() == null || setmealDTO.getSetmealDishes().isEmpty()) { throw new BusinessException("套餐必须包含菜品"); } }- 对象转换:使用BeanUtils要注意深浅拷贝问题。我更喜欢用MapStruct,性能更好:
@Mapper(componentModel = "spring") public interface SetmealMapper { Setmeal toEntity(SetmealDTO dto); }- 主表操作:特别注意主键回填配置
@Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into setmeal(...) values(...)") void insert(Setmeal setmeal);- 从表操作:批量插入要优化性能
<insert id="insertBatch"> INSERT INTO setmeal_dish VALUES <foreach collection="list" item="item" separator=","> (#{item.setmealId}, #{item.dishId}, ...) </foreach> </insert>3.2 分页查询接口
分页查询要注意性能优化。我推荐使用PageHelper配合自定义VO:
public PageResult pageQuery(SetmealPageQueryDTO dto) { PageHelper.startPage(dto.getPage(), dto.getPageSize()); Page<SetmealVO> page = setmealMapper.pageQuery(dto); return new PageResult(page.getTotal(), page.getResult()); }VO对象要包含关联表字段:
@Data public class SetmealVO { private Long id; private String name; private String categoryName; // 关联字段 private List<SetmealDish> dishes; }3.3 批量删除接口
删除操作要特别注意数据一致性:
- 检查套餐状态,启售中的不能删除
- 使用事务保证两张表同步删除
- 批量操作要优化SQL
@Transactional public void deleteBatch(List<Long> ids) { // 状态检查 if (setmealMapper.countEnabledByIds(ids) > 0) { throw new BusinessException("存在启售中的套餐"); } // 批量删除 setmealMapper.deleteBatch(ids); setmealDishMapper.deleteBySetmealIds(ids); }4. 前后端联调实战技巧
前后端联调阶段最容易出现"扯皮"情况,这里分享几个实用技巧:
Swagger配置要规范,推荐这样写:
@Operation(summary = "修改套餐") @Parameters({ @Parameter(name = "id", description = "套餐ID", required = true), @Parameter(name = "status", description = "状态 1启售 0停售") }) @PostMapping("/status/{status}") public Result updateStatus(@PathVariable Integer status, Long id) { //... }参数校验要前后端统一规则。比如价格校验:
@NotNull @DecimalMin(value = "0.01", message = "价格不能小于0.01") private BigDecimal price;异常处理建议全局统一格式:
@ExceptionHandler(BusinessException.class) public Result handleException(BusinessException ex) { log.error("业务异常", ex); return Result.error(ex.getMessage()); }联调技巧:
- 使用Postman先调试接口
- 开启MyBatis日志检查SQL
- 用Mock数据绕过复杂依赖
- 善用Chrome开发者工具检查请求
5. 常见问题排查指南
开发过程中难免会遇到各种问题,这里整理几个典型问题的解决方案:
问题一:主键未回填现象:套餐菜品关联表中的setmeal_id为null 解决方案:
- 检查Mapper是否配置
@Options(useGeneratedKeys = true) - 确认数据库表主键是自增的
- 检查MyBatis版本是否兼容
问题二:批量插入失败现象:报错SQL语法错误 解决方案:
- 检查MySQL连接参数要加
allowMultiQueries=true - 确认批量SQL语法正确
- 检查字段数量与值数量是否匹配
问题三:事务不生效现象:部分操作失败未回滚 解决方案:
- 确认方法为public且被外部调用
- 检查异常类型是否被捕获
- 确认数据库引擎支持事务(InnoDB)
问题四:分页查询慢现象:数据量大了查询变慢 解决方案:
- 确保分页字段有索引
- 使用延迟关联优化:
SELECT * FROM setmeal WHERE id IN ( SELECT id FROM setmeal WHERE ... LIMIT 10000, 10 )6. 性能优化建议
当套餐数据量上来后,这些优化手段能显著提升性能:
缓存策略:
- 使用Redis缓存热门套餐
- 采用多级缓存架构
- 注意缓存与数据库一致性
SQL优化:
- 避免SELECT *,只查必要字段
- 复杂查询走索引覆盖
- 大数据量分页用游标方式
异步处理:
- 菜品变更消息发MQ
- 异步更新套餐冗余字段
- 日志记录走异步线程池
代码优化:
- 使用批量操作代替循环
- 预编译SQL语句
- 对象复用减少GC压力
7. 安全防护措施
餐饮系统涉及交易,安全必须重视:
输入校验:
- 防XSS:对用户输入转义处理
- 防SQL注入:用预编译语句
- 文件上传:限制类型和大小
权限控制:
- 接口级权限校验
- 数据权限过滤
- 操作日志审计
敏感数据:
- 价格等字段加密传输
- 日志脱敏处理
- 防爬虫频率限制
8. 扩展功能思路
基础功能上线后,可以考虑这些扩展方向:
组合套餐:允许用户自定义搭配时段套餐:不同时段展示不同套餐智能推荐:根据用户历史推荐套餐套餐分析:销量统计和预测多规格支持:套餐内菜品可选规格
在实际项目中,我遇到过需要支持套餐预售的场景,这需要额外增加预售时间和库存字段。建议在设计初期就考虑这些可能的扩展点,预留好字段和接口。
