M6物资目录
M7组合申领包
M8申领车
M 6
链路 1:员工端物资列表
GET /user/goods/list?categoryId=23
↓ JwtTokenUserInterceptor(认证)
↓ user/GoodsController.list → Redis GET goods:list:23
│ 命中 → 直接返回(0 次 SQL)
│ 未命中 ↓
↓ GoodsServiceImpl.listWithSpecs(goods) ← 第 212-229 行
│
├─【1】List<Goods> goodsList = goodsMapper.list(goods) ← ★ 第 1 次 SQL
│ SELECT * FROM dish
│ <where> name LIKE ? / category_id = ? / status = ? </where>
│ ORDER BY create_time DESC
│
└─【2】for (Goods d : goodsList) { ← ★ 循环 N 次
List<GoodsSpec> flavors = goodsFlavorMapper.getByGoodsId(d.getId()); ← 第 N 次 SQL
GoodsVO goodsVO = new GoodsVO();
BeanUtils.copyProperties(d, goodsVO);
goodsVO.setFlavors(flavors);
goodsVOList.add(goodsVO);
}
↓ Redis SET goods:list:23(TTL 1800 + random(0,300)s)

链路 2:管理员新增/修改物资
修改物资

物资规格(GoodsSpec)是一对多关系:一个物资(Goods)对应多条规格记录。
如果直接做更新,很难判断:哪些规格要改、哪些要删、哪些新增。
最简单稳妥的方案:先把这个物资旧的全部规格删掉,再一次性插入前端传过来的全新规格列表。
业内俗称:先删后插,全量覆盖。
插入

链路 3:物资停售时的级联
POST /admin/goods/status/{status} @RequireRole(ADMIN)
↓ admin/GoodsController.startOrStop(status, id)
↓ GoodsServiceImpl.startOrStop(status, id) @Transactional
│ ├─ goodsMapper.update(Goods{id, status}) ← 改物资状态
│ └─ ★ if (status == StatusConstant.DISABLE) { ← 只有停用时联动
│ List<Long> comboIds = comboDishMapper.getComboIdsByGoodsIds([id]);
│ SQL: SELECT setmeal_id FROM setmeal_dish WHERE dish_id IN (?)
│ for (Long comboId : comboIds) {
│ comboMapper.update(Combo{id: comboId, status: DISABLE}); ← 级联停用组合包
│ }
│ }
↓ ★ cleanCache(RedisKeyConstant.GOODS_LIST_PREFIX + "*") ← 【只清物资缓存!】
↓
⚠️⚠️⚠️ 上一步【修改了 setmeal.status】,但这里【没有清 combo:list:*】
→ 员工端组合包缓存里继续显示已停用的组合包,最长 35 分钟(TTL)
→ 这正是 M5-1 记录的缓存一致性 bug
M 7
链路 1:新增/修改组合包
【新增】POST /admin/combo @RequireRole(ADMIN)
↓ ComboServiceImpl.saveWithItems(dto) @Transactional
│ ├─ BeanUtils.copyProperties(dto, combo) ← combo.name / combo.price(包价)
│ ├─ comboMapper.insert(combo) ← @AutoFill(INSERT),回填 id
│ ├─ Long comboId = combo.getId()
│ ├─ comboItems.forEach(cd -> cd.setComboId(comboId)) ← 只补 comboId
│ └─ comboDishMapper.insertBatch(comboItems)
│ INSERT INTO setmeal_dish (setmeal_id, dish_id, name, price, copies)
│ VALUES (?, ?, #{sd.name}, #{sd.price}, #{sd.copies}) ← ★★★ 直接写前端传的值
↓ admin/ComboController.cleanComboCache()
【修改】PUT /admin/combo @RequireRole(ADMIN)
↓ ComboServiceImpl.update(dto) @Transactional 第 123-147 行
│ ├─ comboMapper.update(combo) ← 改包的基本信息
│ ├─ comboDishMapper.deleteByComboId(comboId) ← 【先全删关联】
│ └─ comboDishMapper.insertBatch(comboItems) ← 【再全插关联】(同样是快照值)
↓ cleanComboCache()

将组合包有关的物品关联的 id 都是组合包的 id

链路 2:启用包前校验
POST /admin/combo/status/{status} @RequireRole(ADMIN)
↓ ComboServiceImpl.startOrStop(status, id) 第 150-173 行
│ ├─ ★ if (status == StatusConstant.ENABLE) { ← 只在启用时校验
│ │ List<Goods> goodsList = goodsMapper.getByComboId(id);
│ │ SQL: SELECT a.* FROM dish a
│ │ LEFT JOIN setmeal_dish b ON a.id = b.dish_id
│ │ WHERE b.setmeal_id = ?
│ │ if (goodsList != null && goodsList.size() > 0) {
│ │ goodsList.forEach(goods -> {
│ │ if (StatusConstant.DISABLE == goods.getStatus())
│ │ throw new ComboEnableFailedException(
│ │ "组合申领包内包含已停用物资,无法启用");
│ │ });
│ │ }
│ │ }
│ └─ comboMapper.update(Combo{id, status})
↓ cleanComboCache()