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()

链路 3:删除组合包

M 8

链路 1:加入申领车