--- description: 库存模型、库存流水与并发安全规则(任何涉及库存数量变化的代码) globs: backend/app/**/Inventory/**,backend/app/**/Inbound/**,backend/app/**/Outbound/**,backend/app/**/Picking/**,backend/app/**/Return/** alwaysApply: false --- # 库存规则 详见 `docs/05-inventory.md`。库存是整个 WMS 的核心。 ## 模型 - 维度:仓库 → 库区 → 库位 → SKU → 批次 → 库存状态,不要简化为 `sku + warehouse + quantity`。 - 数量:`available_qty`、`reserved_qty`、`picked_qty`、`hold_qty`、`damaged_qty`、`in_transit_qty`。 ## 流水 - 任何库存数量变化,必须在**同一事务**内写入 `wms_stock_log`(变化量、变化前后、业务单据类型与单号、操作人)。 - 库存变更只能通过库存领域服务 / Action 完成,业务模块不得直接 update 库存表。 - 流水只追加,不修改、不删除。 ## 并发 禁止 "先 SELECT → PHP 计算 → UPDATE"。使用事务 + 原子条件更新 / 行锁 / 乐观锁,必要时 Redis 锁。 ```php // ❌ 并发下会超卖 $inv = Inventory::find($id); if ($inv->available_qty >= $qty) { $inv->available_qty -= $qty; $inv->save(); } // ✅ 原子条件更新,受影响行数为 0 即库存不足 $affected = Inventory::whereKey($id) ->where('available_qty', '>=', $qty) ->update([ 'available_qty' => DB::raw('available_qty - '.(int) $qty), 'reserved_qty' => DB::raw('reserved_qty + '.(int) $qty), ]); if ($affected === 0) { throw new InsufficientStockException(); } ``` - 多行扣减按固定顺序(如库存主键升序)加锁,避免死锁。 - 必须有并发测试:库存 10,两个请求同时扣 8,只能成功一个,且不会出现负库存。