## Context 生产部门当前仅有耗材目录表(`biz_consumable`)和试剂目录表(`biz_reagent`),管理编码、名称、规格、标准用量等基础信息,无库存管理。市场部门和质检部门已有完整的库存管理体系,采用「主表含库存字段 + 变动记录表」的双表模式。 现有耗材/试剂被 10 张下游关联表(耗材包、工艺、工艺路线、生产任务步骤)引用,但全部使用快照复制(denormalized code/name),无 FK 依赖,对主表加字段零影响。 ### 现有参照实现 - **市场物料**:`biz_material`(含 stock_qty)+ `biz_material_stock_log`(变动记录,rk/ck,正数) - **质检物料**:`biz_qc_material`(含 stock_qty)+ `biz_qc_material_usage`(变动记录,in/use,正负数,含 task_complete 自动扣减 + revert 回退) ### 步骤保存挂钩点 生产任务步骤的耗材/试剂保存入口: - `PUT /production-tasks/{task_id}/steps/{step_id}/reagents` → `replace_step_reagents()` - `PUT /production-tasks/{task_id}/steps/{step_id}/consumables` → `replace_step_consumables()` 这两个函数使用"软删旧 + 插入新"模式,需在此处加入 diff 结算逻辑。 ## Goals / Non-Goals **Goals:** - 为耗材和试剂增加库存管理能力(库存量、预警量、出入库操作) - 支持手动单个/批量入库出库,参照质检物料模式 - 支持生产任务步骤保存时按 actual_usage 自动 diff 结算库存 - 变动记录在耗材/试剂页面内以子弹窗和 Tab 形式展示 **Non-Goals:** - 不新建独立菜单页面(变动记录嵌入现有页面) - 不修改现有耗材/试剂目录字段(只加库存字段) - 不修改下游 10 张关联表 - 不实现仓库/库位管理(单库存维度) - 不实现库存预警通知(仅记录 warning_qty 字段) - 不统一耗材/试剂为统一物料表(保持两个独立目录) ## Decisions ### Decision 1: 在现有目录表上加库存字段,不新建库存主表 **选择**:ALTER TABLE `biz_consumable` / `biz_reagent` 新增 `stock_qty`、`warning_qty`、`status` 字段。 **理由**: - 下游 10 张关联表全是快照复制,加字段零影响 - 与市场/质检物料的「主表含库存字段」模式一致 - 避免引入目录表→库存表的间接层和数据同步问题 - 改动最小:复用现有 CRUD/API/前端,只需扩展 **替代方案**:新建 `biz_consumable_stock` 表,通过 consumable_id 关联。放弃原因:多一层间接、需维护目录-库存同步、与已有模式不一致。 ### Decision 2: 新建独立的变动记录表 **选择**:新建 `biz_consumable_stock_log` 和 `biz_reagent_stock_log`,参照 `biz_material_stock_log` 结构。 **理由**: - 变动记录是独立的关注点,不应混在目录表中 - 需要快照字段(code/name)和 before/after 库存 - 需要溯源字段(source_type、step_id、task_id、task_no) - 参照 `biz_qc_material_usage` 的 source_type 模式:manual_in / manual_out / step_save **关键设计**:参照质检物料采用正负数表示方向(入库正、出库负),同时保留 `change_type` 字段(in/use)。 ### Decision 3: 步骤保存 diff 结算算法 **选择**:全量回退 + 重算模式(参照质检物料的 revert + deduct 模式)。 **算法**: 1. 查出该步骤当前所有 source_type=step_save 的变动记录 2. 按变动记录逐条回退库存(加回扣减量),软删变动记录 3. 遍历新的步骤耗材/试剂列表,按 actual_usage 扣减库存,生成新变动记录 4. 整体在同一事务中 **理由**: - 步骤保存频率不高,全量回退+重算简单可靠 - 避免复杂的逐项 diff 计算(需处理编码变更等边界情况) - 与质检物料 deduct_for_task() 模式一致,代码可复用 **替代方案**:逐项 diff(旧用量 - 新用量 = 差额)。放弃原因:需处理编码变更(物料A改成物料B)、处理删除、新增等复杂场景,容易出 bug。全量回退更简单且已验证。 ### Decision 4: 变动记录子弹窗 + Tab 双入口 **选择**: - 每行操作列「变动记录」按钮 → Dialog 弹窗,按 material_id 筛选 - 页面顶部 Tab「全部变动记录」→ Tab 切换视图,展示所有物料的汇总记录 **理由**: - 不新增菜单位,保持生产管理菜单简洁 - 行按钮满足"看某个物料历史"的快速需求 - Tab 视图满足"看所有变动流水"的审计需求 ### Decision 5: 挂钩点在 replace_step_reagents/consumables **选择**:在 `replace_step_reagents()` 和 `replace_step_consumables()` 函数中,在 soft delete 旧记录之后、insert 新记录之前,执行库存 diff 结算。 **调用链**: ``` API: PUT /production-tasks/{task_id}/steps/{step_id}/consumables → replace_step_consumables(db, step_id, tenant_no, user_id, items) → 1. 查出步骤信息(step_id → task_id, task_no) → 2. 调用 diff_settle_step_consumables(db, step_id, task_id, task_no, tenant_no, user_id) → revert step 的所有 step_save 变动记录(回退库存) → 3. soft delete 旧步骤耗材子记录 → 4. insert 新步骤耗材子记录 → 5. 调用 deduct_step_consumables(db, step_id, task_id, task_no, tenant_no, user_id, new_items) → 按 actual_usage 扣减库存,生成 step_save 变动记录 → 6. commit ``` ## Risks / Trade-offs ### Risk: 步骤保存时库存不足 **影响**:任务执行保存步骤时,若耗材/试剂库存不足,保存会失败。 **缓解**:提供清晰的错误提示("物料「XX」库存不足,当前库存 N,需使用 M"),让用户先去入库。同时前端可在步骤编辑时显示当前库存量供参考。 ### Risk: actual_usage 为空或非数字 **影响**:步骤保存时 actual_usage 字段可能为空字符串或非数字。 **缓解**:diff 结算时跳过 actual_usage 为空或无法转为正数的条目,仅处理有效数据。 ### Risk: 并发保存同一物料库存 **影响**:多个步骤同时保存涉及同一耗材/试剂。 **缓解**:使用 `with_for_update()` 行锁,与质检物料一致。 ### Trade-off: 全量回退 vs 逐项 diff **选择**全量回退,牺牲少量性能换取代码简洁性和正确性保证。步骤保存频率低,性能影响可忽略。 ## Migration Plan 1. **数据库迁移**:ALTER TABLE 加字段(有默认值) + CREATE TABLE 建变动记录表 2. **后端部署**:模型 → CRUD → API 顺序部署 3. **前端部署**:扩展现有页面 4. **无需数据迁移**:现有耗材/试剂的 stock_qty 默认为 0,warning_qty 默认为 0,status 默认为 '1' 5. **回滚方案**:ALTER TABLE 删除新增字段,DROP TABLE 变动记录表,回滚代码