Skip to content

feat(production,dcprint,inventory): 工单工序接真实数据 + 追溯主材修复 + QA BUG-001/BUG-002 修复 - #22

Open
snqig wants to merge 6 commits into
feat/seed-correlated-allfrom
feat/workorder-process-trace-labels
Open

snqig wants to merge 6 commits into
feat/seed-correlated-allfrom
feat/workorder-process-trace-labels

Conversation

@snqig

@snqig snqig commented Sep 14, 2026

Copy link
Copy Markdown
Owner

概述

本轮共 3 条主线:① /production/orders 工序进度接入真实工单工序数据;② /dcprint/trace 追溯列表为空修复 + QA BUG-002;③ QA BUG-001 采购入库并发死锁。

⚠️ 本 PR 堆叠在 #21feat/seed-correlated-all)之上:本 PR 对 scripts/seed-correlated-all.cjs 的改动依赖 #21 引入的该文件,故 base 指向它,diff 才干净。#21 合并后 GitHub 会自动把 base 改回 main


1. production/orders 工序进度接真实数据(5b277854

  • 新增 GET/PUT /api/production/work-orders/[workOrderNo]/process-step:GET 按需从默认工艺路线 RT-001 播种并返回工序;PUT 经 ProcessStepStateMachine.canTransition 校验后推进状态并写 start_time/end_time
  • 页面移除硬编码 mock 工单与工序,改拉真实工单列表 + 每单工序;工序进度列可点击推进 pending→in_progress→completed 并持久化
  • 二维码/查看详情/编辑/开始生产/暂停 五个按钮补齐事件与弹窗(暂停不在工单状态机内,明确提示)
  • 4 语言补齐 18 个 Production 文案
  • 实测GET 200 / 21 步(首步 印前准备 pending)PUT 200 / 印前准备→in_progress,回读确认已落库

2. dcprint/trace 追溯修复(9bdad5d5

空列表真因inv_trace_record 0 行(从未成功追溯)+ POST 直接 500 —— 明细 INSERT 依赖 (SELECT id FROM inv_material_label WHERE label_no=?),而该表为空返回 NULL,违反 inv_trace_detail.label_id NOT NULL;且 POST 非原子,主记录已插导致留下孤儿脏数据。

修复

  1. 主记录 + 明细纳入同一 transaction(),失败整体回滚
  2. 明细 label_id 改用物料自身 label_id,不再依赖子查询
  3. material_type(tinyint 1=主材/2=辅材)统一映射为 main/auxiliary,修复主材/辅材统计恒为 0
  4. generateTraceNo 随机位 4→6 位,降低批量追溯 uk_trace_no 碰撞

数据补齐inv_material_label 0 → 24 行(material_code 全部取自 inv_material 真实主数据);prd_process_card.main_label_id 回填 20 行;inv_trace_record 20 行 / inv_trace_detail 13 行。GET total=20主材字段 20/20 有真实值

3. QA BUG-002(c925d7f9

  • proxy.tsisPublicApicsrf.tsCSRF_EXEMPT_PATHS/api/auth/reset-lock,与 api-permissions.PUBLIC_ROUTES 对齐
  • 验证:匿名 POST /api/auth/reset-lock401 Authentication required200 {"success":true}

4. QA BUG-001 采购入库并发死锁(f26a20df + f29ec496

复现基线tests/concurrency/purchase-inbound.test.ts → 6 并发审核,1 成功 / 5 失败,成功率 16.67%,失败原因 100% 为 Deadlock found when trying to get lock

根因(InnoDB 间隙锁 → 插入意向锁 环等待):批次写入原为「SELECT ... FROM inv_inventory_batch WHERE ... FOR UPDATE 判存在 → UPDATE/INSERT」。在 REPEATABLE READ 下:

  • 批次行尚不存在时,FOR UPDATE 会在唯一索引 uk_warehouse_material_batch(warehouse_id, material_id, batch_no, deleted) 上加间隙锁;间隙锁之间互相兼容,N 个事务可同时持有同一间隙的间隙锁;
  • 随后的 INSERT 需要在同一间隙上加插入意向锁,而插入意向锁与其它事务已持有的间隙锁互斥
  • N 个事务各持间隙锁、又都在等对方 → 环等待 → InnoDB 死锁检测回滚 N-1 个。

(6 个批次号虽各不相同,但都落在同一索引间隙上,因此必然互撞。)

修复(三层)

  1. 新增共享写入原语 src/lib/inventory-write.tsupsertInventorySummary / upsertInventoryBatch,一律 INSERT ... ON DUPLICATE KEY UPDATE,复用 uk_material_warehouseuk_warehouse_material_batch。UPSERT 不持间隙锁,插入意向锁之间互相兼容 → 从物理层面消除该类死锁;并发写同一唯一键时由 InnoDB 自然串行化。
  2. 统一锁序InventorySyncHandler 固定为 inv_inventory(汇总行)→ inv_inventory_batch(批次行),与 InventoryRollbackHandler / DeliveryShippedHandler / SalesShippedHandler / WorkOrderMaterialIssuedHandler 等所有库存写入路径一致,杜绝跨链路环等待。
  3. 应用层死锁重试src/lib/db/index.ts 新增 isRetryableTransactionError() 并扩展 transactionWithRetry() —— 原实现只重试乐观锁冲突(错误文案含 version/affectedRows),现同时识别 InnoDB 死锁/锁等待超时(errno 1213/1205SQLSTATE 40001Deadlock found)。InnoDB 死锁会整体回滚事务,因此重放安全。

验证

修复前 修复后
采购入库 6 并发成功率 1/6 = 16.67%(5×Deadlock) 6/6 = 100%
库存一致性 100(1×100) 600 = 6×100
生产链路(PUT /api/warehouse/inbound → outbox → OutboxPoller → InventorySyncHandler 库存 250→300、新批次 50、事件 processedretry=0

回归tests/concurrency 全量 4 文件 / 8 用例通过 —— 出库 10/10 + 3/2、领料 8/8 + 3/2、盘点 5/5 + 0/3,成功率与拒绝原因均符合预期,无回退。

顺带修复 2 处测试资产缺陷(假绿灯,同属并发套件)

  • purchase-inbound.test.ts「测试超收校验并发」误用不存在的表名 po_purchase_order / po_purchase_order_item(正确为 pur_purchase_order / pur_purchase_order_line),4/4 因 Table doesn't exist 失败却因断言过弱被判通过;已修表名并断言 4/4 必须以「超收校验失败」业务规则被拒。
  • inventory-outbound.test.ts「测试库存不足时的并发处理」插入列名笔误 qty(正确 quantity),5/5 因 Unknown column 'qty' 失败同样被判通过;已修并断言恰好 3 成功 / 2 因「库存不足」被拒。
  • 两处均已补强断言,避免同类假绿灯再次逃过门禁。

5. 顺带修复(bede6fe3

/dashboard/warehouse 图表改用本地 Recharts;/purchase/request 类型列 i18n;5 个页面/组件选择器默认参数 ts() 的 TDZ 崩溃(ts is not defined)


门禁

  • tsc1201 ≤ 基线 1204(类型债未回涨,实际减少 3)
  • 并发一致性:4 场景全通过,采购入库由 16.67% 恢复至 100%
  • 页面编译/dcprint/trace/production/orders/dashboard 均无 500

QA 准出条件对照

准出条件 状态
① 修复 BUG-001 并复跑并发测试至采购入库成功率达标 ✅ 6/6 = 100%
② 修复 BUG-002 白名单并验证 ✅ 401 → 200
③ 补齐 4 个重点业务 spec 的 CSRF 适配 ⬜ 未做(属 E2E 测试资产,BUG-004)
④ 对齐 E2E 登录凭据与跳转断言 ⬜ 未做(BUG-003)

本 PR 覆盖 P1 两项(BUG-001 / BUG-002)。BUG-003、BUG-004 属 E2E 测试资产范畴,未在本轮范围。

- /dashboard/warehouse:物料分类分布与出入库趋势改用本地 Recharts,移除外部 AI 图片接口依赖
- /purchase/request:类型列由存储码映射为 i18n 文案,不再显示英文 material
- /finance/cost、/hr/training、/hr/salary/piece-work:列表页字段与国际化修正
- warehouse-select / user-select / search-input / QRCodeScanner:修复默认参数中直接调用 ts() 导致的 TDZ 崩溃(ts is not defined)
- 新增 GET/PUT /api/production/work-orders/[workOrderNo]/process-step:GET 按需从默认工艺路线 RT-001 播种并返回工序,PUT 经 ProcessStepStateMachine.canTransition 校验后推进状态并写 start_time/end_time
- production/orders 移除硬编码 mock 工单与工序,改拉真实工单列表及每单工序;工序进度列可点击推进(pending→in_progress→completed)并持久化
- 二维码/查看详情/编辑/开始生产/暂停 五个按钮补齐事件与弹窗(暂停不在工单状态机内,明确给出提示)
- 4 种语言补齐 18 个 Production 命名空间文案
- proxy.ts isPublicApi 增加 /api/auth/reset-lock,与 api-permissions.PUBLIC_ROUTES 对齐
- csrf.ts CSRF_EXEMPT_PATHS 同步豁免:账号锁定恢复必须在登录前匿名调用,且该路由在生产环境直接返回 403
- 验证:匿名 POST /api/auth/reset-lock 由 401 Authentication required 变为 200 {"success":true}
- POST 主记录与明细纳入同一事务:原实现明细失败会留下孤儿主记录(实测 500 仍写入 1 条脏数据)
- 明细 label_id 改用物料自身 label_id,不再依赖 inv_material_label 子查询(空表返回 NULL 触发 label_id NOT NULL 报错)
- material_type(tinyint 1=主材/2=辅材)统一映射为 main/auxiliary,修复主材/辅材统计恒为 0
- generateTraceNo 随机位 4→6 位,降低批量追溯时 uk_trace_no 碰撞
- seed-correlated-all 新增第 14 节:流程卡主材标签补种 + prd_process_card.main_label_id 回填
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
vnerp Error Error Sep 14, 2026 7:11am UTC

根因:批次写入采用「SELECT ... FOR UPDATE 判存在 → INSERT/UPDATE」反模式。
在 REPEATABLE READ 下,对**不存在的行**做 FOR UPDATE 会在唯一索引上加间隙锁
(间隙锁之间互相兼容,N 个事务可同时持有),随后 INSERT 需要插入意向锁
(与其它事务的间隙锁互斥)→ 环形等待 → InnoDB 死锁。
实测指纹:6 并发审核同一「物料+仓库」的 6 张入库单 → 1 成功 / 5×
"Deadlock found when trying to get lock"(成功率 16.67%,两轮复现)。

修复:
- 新增 src/lib/inventory-write.ts:upsertInventorySummary / upsertInventoryBatch,
  一律 UPSERT(复用 uk_material_warehouse 与 uk_warehouse_material_batch),
  不持间隙锁,从物理层面消除该类死锁。
- InventorySyncHandler 统一锁序为 inv_inventory(汇总行)→ inv_inventory_batch
  (批次行),与 Rollback/Delivery/Sales 等其它库存写入路径一致;批次写入改用
  upsertInventoryBatch,替换间隙锁反模式。
- db/index.ts 新增 isRetryableTransactionError 并扩展 transactionWithRetry:
  除乐观锁冲突外,同时识别 InnoDB 死锁/锁等待超时(errno 1213/1205、
  SQLSTATE 40001),把 InnoDB 死锁检测的「重试」语义落到应用层。

验证:并发测试 6/6 = 100%(原 16.67%),库存 600=6×100 一致;生产链路
PUT /api/warehouse/inbound → outbox → OutboxPoller → InventorySyncHandler
实测库存 250→300、新批次 50、事件 processed 且 retry=0。
purchase-inbound:
- approveInbound 改为统一锁序(inv_inventory 汇总行 → inv_inventory_batch 批次行)
  并复用 upsertInventorySummary/upsertInventoryBatch 共享原语,
  使其真正覆盖生产所用的写入模式,而非此前的间隙锁反模式。
- 外层改 transactionWithRetry,兜底重试 InnoDB 死锁。
- 补断言:6 并发必须全部成功(锁死回归门槛,修复前为 1/6)。
- 修复「测试超收校验并发」误用不存在的 po_purchase_order / po_purchase_order_item
  表名(正确为 pur_purchase_order / pur_purchase_order_line),
  此前 4/4 因 "Table doesn't exist" 失败却因断言过弱被判通过(假绿灯);
  现断言 4/4 必须以「超收校验失败」业务规则被拒。

inventory-outbound:
- 修复「测试库存不足时的并发处理」插入列名笔误(qty → quantity),
  此前 5/5 因 "Unknown column 'qty'" 失败却因断言过弱被判通过(假绿灯);
  现断言恰好 3 成功 / 2 因「库存不足」被拒。

验证:tests/concurrency 全量 4 文件 / 8 用例通过;
采购入库 6/6=100%、出库 10/10=100% 与 3+2、领料 8/8=100%、盘点 5/5=100% 均无回退。
@snqig snqig changed the title feat(production,dcprint): 工单工序进度接真实数据 + 追溯主材修复 + reset-lock 白名单(BUG-002) feat(production,dcprint,inventory): 工单工序接真实数据 + 追溯主材修复 + QA BUG-001/BUG-002 修复 Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant