购物车功能
结合你之前展示的 save(增)、list(查)以及 updateNumber(改)的具体开发过程,我为你整理了一份针对你实际开发轨迹的深度复盘。这不仅是代码的总结,更是你从“写功能”到“做系统”的成长记录。
🛒 “苍穹外卖”购物车模块:实战开发全景复盘
1. 实际开发中遇到的核心问题与突破
在你的开发轨迹中,这三个问题的解决标志着代码质量的飞跃:
| 阶段 | 核心问题 (Pains) | 你的解决方案 (Actions) | 进步点 (Gains) |
|---|---|---|---|
| 初期:简单增删 | 数据覆盖:添加同名菜品时,旧数据被覆盖,无法累加数量。 | 先查后判:引入 list 方法查询是否存在,存在则 update,不存在则 save。 | 掌握了 Read-Check-Write 经典业务模型。 |
| 中期:业务补完 | 维度缺失:忽略了“口味”和“用户隔离”,导致 A 用户改了 B 用户的菜,或者辣味盖掉了原味。 | 多条件锁定:在 SQL 中强制加入 user_id 和 dish_flavor 作为查询与更新的共同主键。 | 深刻理解了数据库复合逻辑主键的重要性。 |
| 后期:代码重构 | 多参数绑定报错:Mapper 传入多个参数(DTO + Long)时 MyBatis 报错。 | 参数注解:引入 @Param 明确映射关系,并改用对象传参优化 XML。 | 熟练掌握了 MyBatis 参数绑定机制。 |
2. 你的设计思想演进 (Architecture Evolution)
从你的代码结构中,我看到了三种重要的编程思想落地:
A. 防御性编程 (Defensive Programming)
- 多重校验:你在
save接口开头就判掉“同时传菜品和套餐”的非法请求,并在 SQL 的where子句中硬编码user_id过滤。 - 事务保障:给
save方法加上@Transactional,确保了在“查询-判断-操作”这长达数秒(并发下)的过程中,数据的一致性不会被破坏。
B. 高效的“主键回溯”策略
- 重构亮点:你最精妙的一次改动是将
count改为list。 - 思想:
count只告诉你“有”,而list给了你“全”。拿到cart.getId()后执行精确的update ... where id = ?,这避免了复杂的where条件索引扫描,是极高频率操作(如购物车点击)下的性能最优解。
C. 优雅的对象构建
- Builder 模式:你灵活使用了
ShoppingCart.builder(),将LocalDateTime.now()和userId等公共字段提取,代码不仅整洁,更减少了赋值漏掉字段的低级错误。
3. 给你的“加分项”进阶建议
在完成购物车逻辑后,如果你想更进一步,可以从以下两个维度进行局部重构:
SQL 原子化更新 (Atomic Update):
目前你是
cart.getNumber() + 1然后传回数据库。在极高并发下,这依然有“覆盖更新”的风险。- 建议:将 SQL 改为
update shopping_cart set number = number + 1 where id = #{id}。这样加法运算由数据库行锁保护,绝对安全。
- 建议:将 SQL 改为
结果集唯一性保护:
由于
list返回的是List<ShoppingCart>,虽然业务上我们认为一个口味对应一行,但为了防止代码 Bug 导致查出多条,取值时可以用list.get(0)并在前置加上!list.isEmpty()判断。
4. 结语与下一步建议
你的购物车模块现在已经是一个**“生产就绪” (Production-Ready)** 的版本了。它既考虑了业务的复杂性(口味、套餐、菜品区分),又兼顾了代码的可维护性。
后期可以考虑将购物车板块移植到Redis。
5. 自我复盘
回想开发过程,购物车部分最难的就是增加购物车记录,需要明确业务场景以及需求。初次开发时,没有考虑到用户ID、口味数据等重要字段作为数据库结构的区分。
在插入购物车数据时,我初版使用当前购物车此商品的数量作为依据,来更新我的商品。再版则使用有无这条记录为依据,更新我的商品。以下为初版再版代码对比。
初版:
javaInteger oldNumber = shoppingCartMapper.countByDishORSetmealId(shoppingCartDTO, userId); Integer newNumber = oldNumber + 1; // 2. 当前种类菜品或套餐数量大于0,则只更新数量即可 if (oldNumber > 0) { shoppingCartMapper.updateNumberForGoods(newNumber, shoppingCartDTO, userId); return; }再版:
java// 1. 判断添加的菜品或套餐是否已经添加过 ShoppingCart shoppingCart1 = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart1); shoppingCart1.setUserId(userId); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart1); // 2. 当前种类菜品或套餐已经存在,则只更新数量即可 if (list != null && !list.isEmpty()) { ShoppingCart cart = list.get(0); // 若已存在套餐或菜品,则可按其id添加 shoppingCartMapper.updateNumberForGoods(cart.getNumber() + 1, cart.getId()); return; }初版更新表时,使用多种逻辑外键结合,来确定记录的唯一性。而再版则考虑到数据库中有记录时,完全可以使用主键作为记录的唯一性,并且以主键作为更改条件性能更高。
删除一条记录时,一开始忘记了要判断当前购物车此商品个数,导致了误删。