fix: MKH-BOM 配件库存增加箱表兜底,修正「配件有货却判配件不足」 #55

Merged
yanhui merged 1 commits from codex/mkh-component-stock-pack-fallback-0930 into main 2026-09-30 19:32:35 +08:00
Collaborator

目的与范围

解决的问题:配件监控页显示某配件有库存(判「正常」),MKH-BOM 却把引用它的成品判为「配件不足」、可组装数 0。

  • 真实案例:MKHDNZ0409260600849,瓶颈配件唯一 = DNZ040947N1440V01
    • 模块化配件库存监控页:200(货位 ZT-30,判「正常」)
    • MKH-BOM:0 → 判「配件不足」、可组装数 0
  • 根因:两个页面取「配件实时库存」的来源层数不同
    • 配件监控页(modularStockMonitorService.buildRealtimeStockSql):inventoryInfo → 同仓箱表兜底 → 监控快照
    • MKH-BOM(mkhBomService.loadComponentStock):只查 rpt.jst_inventoryInfo,一级到底
    • 该表上游滞后时(本案该行停在 09-28 14:51 值 0),同仓箱表 rpt.jst_packInventoryInfo(小时级,当日 10:30 已记 bin=ZT-30 qty=200)已有货 → 两页结论相反
  • 同文件里成品库存早就有箱表兜底(⑦-d),唯独配件判定漏了 —— 是遗漏,不是设计意图

修改的模块/文件

  • backend/src/services/mkhBomService.js
    • loadComponentStock():改为一条语句同时取两张表(UNION ALL + src 标记)。库存表有正库存以它为准;为 0 / 缺行时用同仓 jst_packInventoryInfo 的 bin 非空行合计兜底。只补 0、不覆盖正库存,与配件监控页同口径。
    • 文件头 ⑪:修正「不能用 jst_packInventoryInfo 当配件库存 —— 与 MKH 配件零交集」这条结论。它是 09-17 的表状态(当时该表 85,152 行、仅 3 个箱、bin 全表为空),09-18 上游整表重建后已失效(09-19 目标仓 794 个 SKU 命中 727 个;09-30 配件仓 743 个 SKU 有箱记录)。
    • 文件头 ⑤:同理修正该表的关联列与数据现状结论。PR #22 的提交信息声称已更新 ⑤,但实际 diff 未改,此处补齐。
  • backend/test/mkhBomService.test.js:新增 3 个用例。

是否使用 AI 及负责开发者:AI 辅助定位与实现,开发者 yanhui 负责。

同步与兼容性

  • 起始基线 SHA:b0b618e
  • 最近同步的 main SHA:b0b618e(建 PR 时本地领先 1 / 落后 0)
  • 待审核任务分支 SHA:a93691f
  • 并行 PR、共享接口、迁移/配置影响:无并行 PR;无接口签名变化、无 DB 迁移、无配置项变化。
  • 冲突及处理理由:无冲突。git merge-tree --write-tree origin/main <branch> 预演 → 冲突文件 0 个(tree 958409a6);且合并产物与我提交的 blob 完全一致(mkhBomService.js = 6fe8844,测试 = ce4b006),即「本分支所测即合并后所得」。
  • 如何确认保留了其他开发者的已有行为:
    • 只新增一条取数来源,不改原有优先级顺序(库存表正数仍优先),不改预级/排序/聚合口径。
    • 同数据快照下的新旧口径对照:0 处其他字段变化、0 处预警级别变化(见下)。
    • 未触碰 AGENTS.md / 仓库提交与合并规范.md / 服务器部署规范.md / README.md(git diff --stat 为空)。

验证

1. 单元测试(含对照组实验)

  • 新增 3 个用例:①箱表兜底生效且不覆盖正库存 ②SQL 形状防腐化(必须查两张表 + bin 非空 + 限定仓)③查询失败仍降级不做判定。
  • 对照组:把源码 stash 回修复前再跑 → 用例 ① 失败(expected: 200 / actual: 0,正是业务现象)、用例 ② 失败,用例 ③ 通过;恢复后 54/54 全通过。证明测试确实在验这条修复,而非空过。
  • 命令:cd backend && node --test test/mkhBomService.test.js → # tests 54 / # pass 54 / # fail 0

2. 后端全量

  • cd backend && npm run check(= check-syntax + node --test test/*.test.js)
  • 结果:Syntax check passed for 179 JavaScript files;# tests 393 / # pass 392 / # fail 1
  • 唯一失败 = resilientSecretStore.test.js 的 secret backup survives outside the repository with owner-only permissions(Windows 权限断言 0o666 vs 0o600)。既有环境差异,与本改动无关。

3. 真库验证(本地直连业务库)

  • loadComponentStock(['DNZ040947N1440V01','DNZ040959N1440V01','DNZ040946N1440V01','DNZ039802N1410V01'])
  • 结果:200 / 200 / 60 / 87,degraded=false,耗时 1.9s
    • 前两个 = 本修复目标(修复前为 0)
    • 后两个 = 库存表本就有正数的对照配件,未被箱表覆盖 ✅

4. 同一数据快照下的新旧口径对照(最有说服力的验证)

  • 方法:同一监控日、同一份库数据,分别用「修复前源码」与「修复后源码」各跑一次 getMkhBomList(scope='warning'),逐行 diff。
  • 结果(监控日 2026-09-29):
指标 修复前 修复后
预警行数 29 29
缺货 23 23
配件不足 26 24
冷查耗时 62.8s 61.2s
  • 逐行差异:配件不足 true→false 2 行(MKHDNZ0409260601376、MKHDNZ0409260601298,瓶颈均为 DNZ040947N1440V01:0/0 → 消失,可组装数 0 → 30);反向变化 0 行;其他字段变化 0 处;预警级别变化 0 处。
    → 改动只影响「配件不足」标记,不动预警级别、排序与总数。

5. 端到端(本地接口)

  • POST /api/mkh-bom/sync 清进程内缓存(49.0s,证明真重查库)后取全部预警行:不再有任何行的瓶颈是那两个兜底配件。

6. 负载与数据量边界(规范第 6 节要求)

  • 数据量:配件仓 jst_inventoryInfo 837 行;jst_packInventoryInfo 1163 行(bin 非空 1024 行)。
  • SET STATISTICS IO(300 编码 = 一个分片):jst_inventoryInfo scan 9 / 逻辑读 4543 页;jst_packInventoryInfo scan 9 / 逻辑读 947 页;物理读 0、Worktable 0(无落盘)。
  • 实测耗时(300 编码,3 次中位):旧 SQL 215ms → 新 SQL 391ms(+176ms/分片)。配件全集 784 个约 3 个分片,增量占冷查 61s 的 1% 以内;且结果按编码缓存 10 分钟,热路径零查询。
  • 分片参数沿用 COMPONENT_STOCK_CHUNK_SIZE = 300(远低于 MSSQL 单语句 2100 参数上限)。

未运行或失败的检查及原因

  • 执行计划 XML 取不到:SET SHOWPLAN_XML 返回 SHOWPLAN permission denied in database 'JSReportReplica'(该库账号无 SHOWPLAN 权限)。改用 STATISTICS IO + 实测耗时 + 数据量边界 作为执行计划等价证据(上条)。rpt 下两个对象均为 VIEW(sys.objects.type = 'V'),索引在前端基表上,本视图无索引可查。
  • 前端未改,未跑前端检查。

风险与回退

已知风险

  • 箱表只记「已装箱」的部分量,若直接覆盖会低估库存 → 用「只补 0、不覆盖正库存」规避:库存表有正数一律以它为准。
  • 箱表存在 bin 为空的装箱记录 → SQL 已强制过滤 NULLIF(LTRIM(RTRIM(p.bin)),N'') IS NOT NULL。本案若不过滤会读到 400(200 装箱记录 + 200 货位)而非正确的 200。
  • rpt 对象是视图、无独立索引,上游整表重建可能变慢 → 已实测数据量(837 + 1163 行)、逻辑读(4543 + 947 页)、耗时增量(+176ms/分片);分片 + 10 分钟缓存控制负载。
  • 兜底后「配件不足」标记会减少 —— ���好标记不等于该成品不缺货:这批成品本身仍可能因成品库存为 0 而判「缺货」,预警总数不变(已实测 29/23 两边一致)。
  • 查询失败时仍整体 degraded=true → 调用方放弃配件判定(宁可少标不误标),语义未变。

回退方式

  • 单提交 revert:git revert a93691f(无其他提交依赖)。回退后行为立即恢复为「只查库存表」,无需数据修复。
  • 数据兼容性:本改动只读,不写任何表,无迁移、无状态残留。缓存为进程内 10 分钟 TTL,回退后最多 10 分钟自然失效(或调 /api/mkh-bom/sync 立即清)。
## 目的与范围 **解决的问题**:配件监控页显示某配件有库存(判「正常」),MKH-BOM 却把引用它的成品判为「配件不足」、可组装数 0。 - 真实案例:`MKHDNZ0409260600849`,瓶颈配件唯一 = `DNZ040947N1440V01` - 模块化配件库存监控页:**200**(货位 `ZT-30`,判「正常」) - MKH-BOM:**0** → 判「配件不足」、可组装数 0 - 根因:两个页面取「配件实时库存」的**来源层数不同** - 配件监控页(`modularStockMonitorService.buildRealtimeStockSql`):`inventoryInfo` → **同仓箱表兜底** → 监控快照 - MKH-BOM(`mkhBomService.loadComponentStock`):**只查 `rpt.jst_inventoryInfo`**,一级到底 - 该表上游滞后时(本案该行停在 **09-28 14:51** 值 0),同仓箱表 `rpt.jst_packInventoryInfo`(小时级,当日 **10:30** 已记 `bin=ZT-30 qty=200`)已有货 → 两页结论相反 - 同文件里**成品**库存早就有箱表兜底(⑦-d),**唯独配件判定漏了** —— 是遗漏,不是设计意图 **修改的模块/文件** - `backend/src/services/mkhBomService.js` - `loadComponentStock()`:改为一条语句同时取两张表(`UNION ALL` + `src` 标记)。**库存表有正库存以它为准;为 0 / 缺行时**用同仓 `jst_packInventoryInfo` 的 **`bin` 非空行**合计兜底。只补 0、不覆盖正库存,与配件监控页同口径。 - 文件头 ⑪:修正「**不能用 `jst_packInventoryInfo` 当配件库存 —— 与 MKH 配件零交集**」这条结论。它是 **09-17 的表状态**(当时该表 85,152 行、仅 3 个箱、`bin` 全表为空),**09-18 上游整表重建后已失效**(09-19 目标仓 794 个 SKU 命中 727 个;09-30 配件仓 743 个 SKU 有箱记录)。 - 文件头 ⑤:同理修正该表的关联列与数据现状结论。**PR #22 的提交信息声称已更新 ⑤,但实际 diff 未改**,此处补齐。 - `backend/test/mkhBomService.test.js`:新增 3 个用例。 **是否使用 AI 及负责开发者**:AI 辅助定位与实现,开发者 yanhui 负责。 ## 同步与兼容性 - 起始基线 SHA:`b0b618e` - 最近同步的 main SHA:`b0b618e`(建 PR 时本地领先 1 / 落后 0) - 待审核任务分支 SHA:`a93691f` - 并行 PR、共享接口、迁移/配置影响:无并行 PR;**无接口签名变化、无 DB 迁移、无配置项变化**。 - 冲突及处理理由:**无冲突**。`git merge-tree --write-tree origin/main <branch>` 预演 → 冲突文件 0 个(tree `958409a6`);且合并产物与我提交的 blob **完全一致**(`mkhBomService.js` = `6fe8844`,测试 = `ce4b006`),即「本分支所测即合并后所得」。 - 如何确认保留了其他开发者的已有行为: - 只**新增**一条取数来源,不改原有优先级顺序(库存表正数仍优先),不改预级/排序/聚合口径。 - 同数据快照下的新旧口径对照:**0 处其他字段变化、0 处预警级别变化**(见下)。 - 未触碰 `AGENTS.md` / `仓库提交与合并规范.md` / `服务器部署规范.md` / `README.md`(`git diff --stat` 为空)。 ## 验证 **1. 单元测试(含对照组实验)** - 新增 3 个用例:①箱表兜底生效且不覆盖正库存 ②SQL 形状防腐化(必须查两张表 + `bin` 非空 + 限定仓)③查询失败仍降级不做判定。 - **对照组**:把源码 stash 回修复前再跑 → **用例 ① 失败(`expected: 200` / `actual: 0`,正是业务现象)、用例 ② 失败**,用例 ③ 通过;恢复后 54/54 全通过。证明测试确实在验这条修复,而非空过。 - 命令:`cd backend && node --test test/mkhBomService.test.js` → `# tests 54 / # pass 54 / # fail 0` **2. 后端全量** - `cd backend && npm run check`(= `check-syntax` + `node --test test/*.test.js`) - 结果:`Syntax check passed for 179 JavaScript files`;`# tests 393 / # pass 392 / # fail 1` - **唯一失败** = `resilientSecretStore.test.js` 的 `secret backup survives outside the repository with owner-only permissions`(Windows 权限断言 `0o666` vs `0o600`)。**既有环境差异,与本改动无关。** **3. 真库验证(本地直连业务库)** - `loadComponentStock(['DNZ040947N1440V01','DNZ040959N1440V01','DNZ040946N1440V01','DNZ039802N1410V01'])` - 结果:`200 / 200 / 60 / 87`,`degraded=false`,耗时 1.9s - 前两个 = 本修复目标(修复前为 `0`) - 后两个 = **库存表本就有正数的对照配件,未被箱表覆盖** ✅ **4. 同一数据快照下的新旧口径对照(最有说服力的验证)** - 方法:同一监控日、同一份库数据,分别用「修复前源码」与「修复后源码」各跑一次 `getMkhBomList(scope='warning')`,逐行 diff。 - 结果(监控日 `2026-09-29`): | 指标 | 修复前 | 修复后 | |---|---|---| | 预警行数 | 29 | 29 | | 缺货 | 23 | 23 | | **配件不足** | **26** | **24** | | 冷查耗时 | 62.8s | 61.2s | - 逐行差异:**配件不足 `true`→`false` 2 行**(`MKHDNZ0409260601376`、`MKHDNZ0409260601298`,瓶颈均为 `DNZ040947N1440V01:0/0` → 消失,可组装数 `0 → 30`);**反向变化 0 行**;**其他字段变化 0 处**;**预警级别变化 0 处**。 → 改动只影响「配件不足」标记,**不动预警级别、排序与总数**。 **5. 端到端(本地接口)** - `POST /api/mkh-bom/sync` 清进程内缓存(49.0s,证明真重查库)后取全部预警行:**不再有任何行的瓶颈是那两个兜底配件**。 **6. 负载与数据量边界(规范第 6 节要求)** - 数据量:配件仓 `jst_inventoryInfo` **837 行**;`jst_packInventoryInfo` **1163 行**(`bin` 非空 **1024 行**)。 - `SET STATISTICS IO`(300 编码 = 一个分片):`jst_inventoryInfo` scan 9 / 逻辑读 **4543 页**;`jst_packInventoryInfo` scan 9 / 逻辑读 **947 页**;**物理读 0、Worktable 0**(无落盘)。 - 实测耗时(300 编码,3 次中位):旧 SQL **215ms** → 新 SQL **391ms**(**+176ms/分片**)。配件全集 784 个约 3 个分片,增量占冷查 61s 的 1% 以内;且结果按编码缓存 10 分钟,热路径零查询。 - 分片参数沿用 `COMPONENT_STOCK_CHUNK_SIZE = 300`(远低于 MSSQL 单语句 2100 参数上限)。 **未运行或失败的检查及原因** - **执行计划 XML 取不到**:`SET SHOWPLAN_XML` 返回 `SHOWPLAN permission denied in database 'JSReportReplica'`(该库账号无 SHOWPLAN 权限)。改用 **`STATISTICS IO` + 实测耗时 + 数据量边界** 作为执行计划等价证据(上条)。`rpt` 下两个对象均为 **VIEW**(`sys.objects.type = 'V'`),索引在前端基表上,本视图无索引可查。 - 前端未改,未跑前端检查。 ## 风险与回退 **已知风险** - **箱表只记「已装箱」的部分量**,若直接覆盖会低估库存 → 用「只补 0、不覆盖正库存」规避:库存表有正数一律以它为准。 - **箱表存在 `bin` 为空的装箱记录** → SQL 已强制过滤 `NULLIF(LTRIM(RTRIM(p.bin)),N'') IS NOT NULL`。本案若不过滤会读到 **400**(200 装箱记录 + 200 货位)而非正确的 200。 - **`rpt` 对象是视图、无独立索引**,上游整表重建可能变慢 → 已实测数据量(837 + 1163 行)、逻辑读(4543 + 947 页)、耗时增量(+176ms/分片);分片 + 10 分钟缓存控制负载。 - 兜底后「配件不足」标记会**减少** —— ���好标记不等于该成品不缺货:这批成品本身仍可能因成品库存为 0 而判「缺货」,**预警总数不变**(已实测 29/23 两边一致)。 - 查询失败时仍整体 `degraded=true` → 调用方**放弃**配件判定(宁可少标不误标),语义未变。 **回退方式** - 单提交 revert:`git revert a93691f`(无其他提交依赖)。回退后行为立即恢复为「只查库存表」,**无需数据修复**。 - **数据兼容性**:本改动**只读**,不写任何表,无迁移、无状态残留。缓存为进程内 10 分钟 TTL,回退后最多 10 分钟自然失效(或调 `/api/mkh-bom/sync` 立即清)。
yanhui added 1 commit 2026-09-30 17:44:39 +08:00
根因
- 配件监控页取实时库存是三级兜底(inventoryInfo → 同仓箱表 packInventoryInfo
  → 监控快照 jsStock),而 MKH-BOM 的 loadComponentStock **只查 jst_inventoryInfo**,
  一级来源到底就结束。
- 上游 jst_inventoryInfo 存在滞后:2026-09-30 该行停在 09-28 14:51 的值 0,
  而 jst_packInventoryInfo 当日 10:30 已记 bin=ZT-30 qty=200。
- 结果:同一个仓(15718196)、同一个配件,配件监控页显示 200(判「正常」),
  MKH-BOM 判「配件不足」、可组装数 0。
  案例 MKHDNZ0409260600849 的配件 DNZ040947N1440V01(瓶颈唯一)。

改动
- backend/src/services/mkhBomService.js
  · loadComponentStock():一条语句同时取两张表(UNION ALL + src 标记),
    库存表有正库存以它为准;为 0 / 缺行时用同仓 jst_packInventoryInfo 的
    bin 非空行合计兜底。**只补 0、不覆盖正库存**,与配件监控页
    buildRealtimeStockSql 同口径;查询失败仍整体 degraded(不做判定)。
  · 文件头 ⑪:修正「不能用 jst_packInventoryInfo 当配件库存 —— 与 MKH 配件
    零交集」这条结论 —— 它是 09-17 的表状态(当时该表 bin 全空),
    09-18 上游整表重建后已失效。
  · 文件头 ⑤:同理修正该表的关联列与数据现状结论。PR #22 的提交信息声称
    已更新 ⑤,但实际 diff 未改,此处补齐(该表 wms_co_id 关联、目标仓
    727/794 命中、配件仓 743 个 SKU 有箱记录)。
- backend/test/mkhBomService.test.js
  · 新增 3 个用例:箱表兜底生效且不覆盖正库存 / SQL 形状防腐化(两张表 +
    bin 非空 + 限定仓)/ 查询失败仍降级不做判定。
    对照组已验证:在修复前的实现上前两个用例失败(expected 200、actual 0)。

影响与验证
- 仅影响 MKH-BOM 的「配件不足」标记与可组装数;不改预警级别、排序与预警总数。
- 同一数据快照下的新旧口径对照(监控日 2026-09-29):
  配件不足 26 → 24(2 行翻盘)、预警总数 29、缺货 23 两边一致,
  0 行反向变化、0 处其他字段变化、0 处级别变化。
- 真库实测:DNZ040947N1440V01 0 → 200、DNZ040959N1440V01 0 → 200;
  库存表本就有正数的对照配件(60 / 87)未被箱表覆盖。
- 负载(SET STATISTICS IO,300 编码一个分片):jst_inventoryInfo 4543 逻辑读 +
  jst_packInventoryInfo 947 逻辑读,物理读 0、Worktable 0;实测中位耗时
  215ms → 391ms(+176ms/分片,配件全集 784 个约 3 个分片,占冷查 61s 的 1% 以内,
  且结果按编码缓存 10 分钟)。
  说明:SHOWPLAN 权限被拒(账号无该权限),改用 STATISTICS IO + 实测耗时 + 数据量
  作为执行计划等价证据。
- 数据量边界:配件仓 jst_inventoryInfo 837 行、jst_packInventoryInfo 1163 行
  (bin 非空 1024 行)。
- 后端:check-syntax 179 文件通过;node --test 393 项 392 通过
  (唯一失败为既有环境问题 resilientSecretStore 权限断言,与本改动无关)。
yanhui merged commit ada8d09c27 into main 2026-09-30 19:32:35 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lunanting/cangku#55
No description provided.