# 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 放宽更新条件后,以订单为准展示待支付内容。 --- **文档基于当前代码逻辑整理,若后续接口或锁定策略有改动,需同步更新本文档。**