# StoreOrderController 点餐与餐具费逻辑分析

基于 `StoreOrderController` 与 `CartServiceImpl`、`StoreOrderServiceImpl` 的代码梳理。

---

## 一、点餐逻辑（菜品）

### 1.1 入口与流程

| 接口 | 方法 | 说明 |
|------|------|------|
| `/cart/add` | `addCartItem` | 添加菜品到购物车 |
| `/cart/update` | `updateCartItem` | 修改购物车中某菜品数量 |
| `/cart/remove` | `removeCartItem` | 从购物车删除某菜品 |
| `/cart/clear` | `clearCart` | 清空购物车（保留已下单商品和餐具） |
| `/create` | `createOrder` | 从购物车生成订单（下单） |

- **点餐**：先加购物车（add/update），再**创建订单** `/create`，把当前购物车内容落单。
- **加餐**：与点餐同一套流程。用户**先增减购物车**（加菜或改数量），再点**下单**；若该桌已有待支付订单，`createOrder` 会**更新该订单**（用当前购物车全量覆盖订单明细），从而实现加餐。没有单独的「加餐接口」，全程用「购物车 + 下单/更新订单」完成。

### 1.2 购物车项的两个数量

- **quantity**：当前购物车中的数量（用户可改）。
- **lockedQuantity**：已下单数量（每次调用 `createOrder` 后由 `lockCartItems` 更新，只增不减，直到支付后清空购物车）。

---

## 二、餐具费逻辑

### 2.1 餐具在系统中的表示

- 餐具**不是**门店菜品表里的菜，而是购物车里的**特殊项**：
  - **cuisineId = -1**（常量 `TABLEWARE_CUISINE_ID`）
  - 名称固定为「餐具」，单价来自**门店配置** `store_info.tableware_fee`（单位：元/人）。

### 2.2 两个入口

| 接口 | 方法 | 说明 |
|------|------|------|
| `POST /cart/set-diner-count` | `setDinerCount(tableId, dinerCount)` | 设置用餐人数，**自动添加或更新餐具** |
| `PUT /cart/update-tableware` | `updateTablewareQuantity(tableId, quantity)` | 直接改餐具数量 |

**setDinerCount（设置用餐人数）**

- 入参：`tableId`、`dinerCount`（必须 > 0）。
- 逻辑：
  - 若购物车已有餐具项：把该餐具的 **数量 = dinerCount**，单价按当前门店 `tableware_fee` 重算，小计 = 单价 × dinerCount。
  - 若没有餐具项：新增一条 cuisineId=-1 的项，数量=dinerCount，单价来自门店，小计同上。
- 下单后（餐具 lockedQuantity > 0）：不允许把人数改得小于已下单数量，否则抛「餐具数量不能少于已下单数量（x）」。

**updateTablewareQuantity（更新餐具数量）**

- 入参：`tableId`、`quantity`（≥ 0）。
- 逻辑：
  - **quantity = 0**：调用 `removeItem` 删除餐具项；若餐具已有 **lockedQuantity > 0** 会抛「商品已下单，不允许删除」。
  - **quantity > 0**：若已有餐具项且 **lockedQuantity > 0**，则 quantity 不能小于 lockedQuantity，否则抛「餐具数量不能少于已下单数量（x）」；否则新建或更新餐具数量与单价。

### 2.3 餐具单价来源

- `CartServiceImpl.getTablewareUnitPrice(storeId)`：
  - 查 `store_info` 表，用 `store_info.tableware_fee`（Integer，单位一般为分或元，以当前代码使用方式看是按「元」参与计算）。
  - 门店不存在或未配置则按 0.00 元。

---

## 三、什么时候点餐（菜品）可以增减

- **可加**：
  - 未下单：直接 `addCartItem` 或 `updateCartItem` 增加数量。
  - 已下单：同一桌有待支付订单时，可再次 `addCartItem`（会在原数量上叠加，再次下单时 lockCartItems 会更新 lockedQuantity）。
- **可减**：
  - **仅当该菜品的「当前数量」大于「已下单数量」时**才能减：
    - `updateCartItem` 把数量改为比 `lockedQuantity` 小会报错：「商品数量不能少于已下单数量（x），该数量已下单」。
  - 即：已下单的那部分数量不能减，只能减「多出来的」部分。
- **可删**：
  - **仅当该菜品 `lockedQuantity == 0` 或 null** 时才能 `removeCartItem`。
  - 若有已下单数量，删除会报错：「商品已下单，已下单数量为 x，不允许删除」。

**清空购物车**（`clearCart`）：

- 未下单的菜品：删除。
- 已下单的菜品：**保留**，且把当前数量**恢复为已下单数量**（lockedQuantity），相当于只清掉「多选未下单」的部分。
- 餐具：**始终保留**，不删。

---

## 四、什么时候餐具可以增减（与菜品规则一致）

- **未下单时**：可随意增减。通过 `setDinerCount` 或 `updateTablewareQuantity` 修改人数/餐具数量；`updateTablewareQuantity(0)` 可删除餐具项。
- **下单后（餐具已有 lockedQuantity > 0）**：
  - **可加**：可随时通过 `setDinerCount` 或 `updateTablewareQuantity` 增加人数/餐具数量。
  - **不能减**：数量不能少于已下单数量。若 `setDinerCount(dinerCount)` 或 `updateTablewareQuantity(quantity)` 导致餐具数量小于 lockedQuantity，会报错「餐具数量不能少于已下单数量（x）」。
  - **不能删**：`updateTablewareQuantity(0)` 会调 `removeItem`，若餐具 lockedQuantity > 0 会报错「商品已下单，已下单数量为 x，不允许删除」。

---

## 五、流程小结（含下单、锁定）

1. 用户选桌、**设置用餐人数** → `setDinerCount` → 购物车出现或更新餐具项（数量 = 人数，单价 = 门店餐具费）。
2. 用户**加菜** → `addCartItem` / `updateCartItem` → 购物车有菜品与餐具。
3. 用户**下单** → `createOrder` → 订单写入订单表+订单明细（含菜品和餐具），然后 **lockCartItems**：所有购物车项（含餐具）的 `lockedQuantity` 设为当前数量。
4. 之后：
   - **菜品**：只能加不能减到低于 lockedQuantity，不能删除已下单部分。
   - **餐具**：与菜品一致，只能加不能减到低于 lockedQuantity，不能删除已下单的餐具。
5. 支付完成后：购物车被清空（或重置），lockedQuantity 不再存在，下一轮重新点餐、重新设人数和餐具。

---

## 六、说明与建议

1. **餐具与菜品规则一致**  
   `updateTablewareQuantity` 和 `setDinerCount` 已与菜品一致：下单后（lockedQuantity > 0）只能增不能减，数量不能小于已下单数量；`updateTablewareQuantity(0)` 删除餐具时，若已下单则走 removeItem 报错不允许删除。
2. **createOrder 时餐具费**  
   创建订单时，订单头的 `tablewareFee`、`totalAmount`、`payAmount` 等**完全采用前端传参**，后端不校验；订单明细中的餐具行仍来自购物车（单价、数量、小计）。若前端与购物车不一致，会出现订单头与明细不一致，需前端保证与购物车一致或后端在需要时做一致性校验。

---

## 七、潜在问题（已知）

1. **创建订单金额 vs 支付时重算**  
   - 创建订单时：`totalAmount`、`tablewareFee`、`discountAmount`、`payAmount` 完全采用前端传参，后端不校验。
   - 支付时：`payOrder` 会按订单的 `totalAmount`、`tablewareFee` 和优惠券**重新计算** `discountAmount`、`payAmount` 并写回订单，即支付时以后端计算为准。
   - 若前端在创建订单时传的优惠/实付与后端规则不一致，支付后订单上的优惠与实付会被覆盖。需与产品确认：是否接受「创建时仅展示用、支付以重算为准」的语义。

2. **更新订单优惠券时后端重算**  
   `updateOrderCoupon` 会根据订单的 `totalAmount`、`tablewareFee` 和新的优惠券重新计算 `discountAmount`、`payAmount` 并更新订单，与支付逻辑一致。若希望「改券不改金额、由前端传」需单独约定并改逻辑。

3. **门店餐具费单位**  
   `store_info.tableware_fee` 为 Integer，当前代码按「数值直接作为元」参与计算（如 1 表示 1 元）。若实际配置为「分」，需在 `getTablewareUnitPrice` 中做单位换算（如 ÷100），并与配置约定一致。

---

## 八、潜藏风险（排查结论）

1. **【高】用户将菜品/餐具数量减至「已下单数量」后，再次点「下单」会报错，订单未同步**（按产品策略不修复）  
   - **现象**：`createOrder` 要求「有新增商品或商品数量增加」才放行；若用户仅把某品数量减少后点下单，接口会报错，订单未同步。  
   - **产品策略**：**下单后只能增不能减**，已下单数量不允许通过再次下单减少，故不开放「减量更新订单」。  
   - **建议**：前端在用户减少数量后提示「已下单部分不可减少，如需改单请联系店员」或类似文案，避免用户误以为可减量后再次下单同步。

2. **【中】清空购物车时餐具数量未恢复为已下单数量**（已修复）  
   - **现象**：`clearCart` 对菜品会把「当前数量」恢复为 `lockedQuantity`，此前对餐具只做「保留」、不恢复数量。  
   - **修复**：清空购物车时，若餐具存在且 `lockedQuantity > 0`，将其数量与小计恢复为 `lockedQuantity`，与菜品逻辑一致。

3. **【低】Redis 过期后从 DB 加载购物车的时序**  
   - **现象**：购物车先写 Redis 再异步写 DB；若 Redis 过期（如 24h）且异步写未完成或失败，`loadCartFromDatabase` 可能读到旧数据。  
   - **后果**：极端情况下购物车内容或 `lockedQuantity` 与当前订单不一致。  
   - **建议**：保证 `lockCartItems` 后同步写 DB 一次（或关键操作后同步落库），或缩短 Redis TTL + 监控异步写失败。

4. **【低】订单与购物车展示不一致的体验**  
   - 因问题 1，当用户「只减不加」时无法通过再次下单同步，订单与购物车可能不一致，前端若以购物车为主展示会误导用户，支付金额以订单为准。  
   - **建议**：前端在「下单」失败时提示「当前无新增或加量，无法更新订单；若已减少数量，请先加一件商品再下单，或联系店员」；或后端按问题 1 放宽更新条件后，以订单为准展示待支付内容。

---

**文档基于当前代码逻辑整理，若后续接口或锁定策略有改动，需同步更新本文档。**
