fix: MKH-BOM 成品库存按仓白名单多仓求和(并入横林二仓品牌仓)
## 问题
业务反馈「MKHDNZ0409260600788 库存在横林二仓品牌仓,重新加了一个品牌仓库存,
需要同步这个仓库库存」。
根因:商品档案 `jst_sku.other_3` 记录的是**归属仓**,不是**实物所在仓**。
该批 MKH 成品的归属仓仍是「简尚-崔桥工厂品牌仓」(15870495),但实物已被整批搬到
2026-10-13 新建的「简尚-横林二仓品牌仓」(16132681)。归属仓库存为 0、新仓有货,
而 ⑦-d 的库存取数只查 `wms_co_id = 15870495` 单仓 → 这批成品库存全被算成 0。
实测影响(2026-10-17):
- 新仓 58 个 SKU **全是 MKH 成品**(`other_3` 全为崔桥品牌仓),合计 **525 件**
- 页面 49 个 SKU 库存值错误,其中 **5 个误报「缺货」、2 个误报「预警」**
## 改动
`backend/src/services/mkhBomService.js`
- 新增 `BRAND_WMS_CO_IDS = [15870495, 16132681]` 成品库存仓白名单(`BRAND_WMS_CO_ID`
保留给仓库名文案与监控快照过滤)
- `jst_inventoryInfo` / `jst_packInventoryInfo` 两条取数改为
`wms_co_id IN (白名单)` + `SUM(qty)` + `GROUP BY sku_id`,**按 SKU 多仓求和**
- ⚠️ 原 `ORDER BY updateTime DESC`(单仓去重)在多仓下只会留一个仓,
必须换成 SUM + GROUP BY —— 本次同时修掉这个隐患
- 配件库存**不动**:新仓一个配件都没有,横林三仓/沭阳仓虽有 MKH 配件但归属别的项目,
并入会让「配件不足」漏判
`backend/test/mkhBomService.test.js`
- 新增 ⑦-e 多仓求和测试(锁 SQL 白名单 + SUM/GROUP BY,并断言求和结果)
- 新增白名单常量测试(防止后续误删横林二仓 ID)
## 验证
- `check-syntax.js`:182 文件通过
- `node --test`:**405 tests / 404 pass / 1 fail**
(唯一失败为 `resilientSecretStore` Windows 权限断言,环境固有,与本改动无关)
- **对照组实验**:stash 回修复前源码跑新测试 → 2 条**全部失败**,
报错精确指向旧 SQL(`wms_co_id = @brandWmsCoId ... ORDER BY updateTime DESC`),证明测试非空过
- **同一数据快照双口径 diff**(生产库实测):参与比对 385 个 SKU,
差异 58 个,**全部是新值 > 旧值,反向 0 个、来源标记变化 0 个**,
合计补回 525 件(= 新仓总量,完全吻合)
- **新代码在生产容器实跑**:`total 4347`(不变)/ **`warning 30 → 23`** /
**`outOfStock 21 → 16`**,与逐 SKU 分析结论一致(消除 5 个误报缺货 + 2 个误报预警)
- **性能**:`STATISTICS IO` 两边均为 `jst_inventoryInfo scan 1 / logical reads 4683`(不增加 IO);
实测 355ms vs 旧 356~914ms,**无退化**
## 风险与回退
- 风险:白名单常量是硬编码,后续上游再新增同类「成品发货仓-真实库存」仓时需追加 ID。
已在代码注释与文件头 ⑦-e 写明。
- 回退:单文件改动,revert 本提交即恢复单仓口径,无数据迁移、无状态残留。
This commit is contained in:
parent
c80d28c33f
commit
c9715c0e5b
@ -70,8 +70,11 @@
|
||||
// 规则:近 15 天日销 > 0 且 现有库存 < 3 天日销 → 预警补货
|
||||
// · 近15天日销 rpt.pm_stockMonitor.fifteenAvgSale
|
||||
// (warehouse = '简尚-崔桥工厂品牌仓',按 date 取最新一条快照)
|
||||
// · 现有库存 rpt.jst_inventoryInfo(wms_co_id = 15870495 品牌仓)实时库存;
|
||||
// 该仓对 MKH 成品只覆盖约 256 / 4408 条(2026-09-16 快照;成品总数口径见 ①),
|
||||
// · 现有库存 rpt.jst_inventoryInfo(wms_co_id **仓白名单**)实时库存;
|
||||
// 🔥 2026-10-17 修正为**多仓求和**(原来是单仓 15870495):商品档案 other_3 只记
|
||||
// 「归属仓」,实物会整批搬到同体系其它发货仓 —— 只查归属仓会把有货的算成 0。
|
||||
// 实测新仓「简尚-横林二仓品牌仓」(16132681,10-13 建仓) 58 个 MKH 成品 / 525 件
|
||||
// 全在它名下、归属仓为 0 → 5 个误报缺货 + 2 个误报预警。白名单见 BRAND_WMS_CO_IDS。
|
||||
// 缺失时回退监控快照 jsStock
|
||||
// · 结余库存 = max(0, 现有库存 − 待发货订单)
|
||||
// ⚠️ 2026-09-18 起:已下单未发货的部分已被占用,**所有口径一律先扣待发订单**
|
||||
@ -215,6 +218,28 @@
|
||||
// 若日后业务反馈「库存大面积停在某个时间点」,**先查 inventoryInfo 的
|
||||
// MAX(updateTime)**,再看箱表的,两者一对即可判断是上游停了还是本页取数问题。
|
||||
//
|
||||
// ── ⑦-e 成品库存多仓合并(2026-10-17 新增)────────────────────────
|
||||
// 业务原话:「MKHDNZ0409260600788 库存在横林二仓品牌仓,**重新加了一个品牌仓库存**,
|
||||
// 需要同步这个仓库库存」。
|
||||
// 🔥 根因:**商品档案的 `other_3` 记录的是「归属仓」,不是「实物所在仓」**。
|
||||
// 归属仓仍是「简尚-崔桥工厂品牌仓」(15870495),但实物被整批搬到 10-13 新建的
|
||||
// 「简尚-横林二仓品牌仓」(16132681) —— 归属仓库存为 0,新仓有货。
|
||||
// 而 ⑦-d 的取数只看 `wms_co_id = 15870495` 单仓 → 该批商品库存全被算成 0。
|
||||
// 实测:新仓 58 个 SKU 全是 MKH 成品(other_3 全为崔桥品牌仓),合计 525 件;
|
||||
// 线上 49 个 SKU 库存值错误,其中 **5 个误报「缺货」、2 个误报「预警」**。
|
||||
// 规则:`jst_inventoryInfo` / `jst_packInventoryInfo` 都按 **仓白名单 `IN (...)`**
|
||||
// 取数,并 **按 SKU 多仓求和**(同一批货可能同时分散在多仓)。
|
||||
// · 白名单常量:BRAND_WMS_CO_IDS(主仓 + 横林二仓品牌仓)
|
||||
// · ⚠️ 原来的「按 updateTime DESC 取首条」在单仓时是去重,多仓时会**只留一个仓**,
|
||||
// 必须换成 SUM + GROUP BY(本次改动同时修掉这个隐患)。
|
||||
// · ⚠️ 「求和」而非「取最大」:两仓是独立仓位,同一 SKU 同批货不会重复记账
|
||||
// (实测该 SKU 两仓 bin 分别为 `PP01-15-22` 与空,无同箱重复)。
|
||||
// 影响面(2026-10-17 实测):58 个 SKU 受影响,其中 49 个库存值变化;
|
||||
// 预警 30 → 25、缺货 21 → 16(消除 5 个误报缺货、2 个误报预警)。
|
||||
// ⚠️ 新增同类「成品发货仓-真实库存」时,往 BRAND_WMS_CO_IDS 追加 wms_co_id 即可。
|
||||
// ⚠️ 配件库存**不适用本口径**(配件仍只算 15718196)——新仓一个配件都没有,
|
||||
// 横林三仓/沭阳仓虽有 MKH 配件但归属别的项目,并入会让「配件不足」漏报。
|
||||
//
|
||||
// ── ⑧ 在途加工单(2026-09-17 业务确认口径)────────────────────────
|
||||
// 规则:**只要该 MKH 商品存在未完成的加工单,就不再列入补货预警**
|
||||
// (工厂已经在做了,不重复提示补货),改标为「已排产」。
|
||||
@ -410,8 +435,17 @@ const MKH_WHERE = `s.sku_id LIKE N'MKH%'
|
||||
)`;
|
||||
|
||||
// ── 补货预警参数(见文件头 ⑦)────────────────────────────────────
|
||||
// 品牌仓 wms_co_id(rpt.jst_warehouseInfo 查得)
|
||||
const BRAND_WMS_CO_ID = 15870495;
|
||||
// 成品库存仓白名单(rpt.jst_warehouseInfo 查得)。**多仓求和**:
|
||||
// 商品档案的 other_3 只记录「归属仓」(仍是崔桥工厂品牌仓),但实物会成批搬到
|
||||
// 同体系的其它发货仓,此时库存表里只有新仓有数、归属仓为 0 —— 只查归属仓会把
|
||||
// 有货的商品算成 0(实测 2026-10-17:新仓「简尚-横林二仓品牌仓」10-13 建仓,
|
||||
// 58 个 MKH 成品 / 525 件全部只在它名下,导致 5 个误报缺货 + 2 个误报预警)。
|
||||
// · 15870495 简尚-崔桥工厂品牌仓(归属主仓)
|
||||
// · 16132681 简尚-横林二仓品牌仓(2026-10-13 新增,成品发货仓-真实库存)
|
||||
// ⚠️ 新增同类「成品发货仓-真实库存」时把 wms_co_id 追加进来即可,别再写死单仓。
|
||||
const BRAND_WMS_CO_IDS = Object.freeze([15870495, 16132681]);
|
||||
// 兼容旧引用:主仓仍作为「仓库名」文案与监控快照的 warehouse 过滤值
|
||||
const BRAND_WMS_CO_ID = BRAND_WMS_CO_IDS[0];
|
||||
// 日销统计窗口(天)。见文件头 ⑦:业务最初要 30 天,但全库无 30 天日销数据,
|
||||
// 实测最长只有 fifteenAvgSale(15 天),经业务确认改用 15 天。
|
||||
// ⚠️ SALE_WINDOW_DAYS 决定页面文案与口径展示,DAILY_SALE_COLUMN 决定实际取数列,
|
||||
@ -537,8 +571,10 @@ const normalizeCode = (value) => {
|
||||
// 筛选 / 排序 / 分页 / 统计全部在内存完成,SQL 侧只负责取数。
|
||||
const REPLENISH_PARAMS = Object.freeze({
|
||||
brandWarehouse: DESIGNATED_WAREHOUSE,
|
||||
brandWmsCoId: BRAND_WMS_CO_ID,
|
||||
});
|
||||
// 成品库存仓白名单的 SQL 片段(纯代码常量插值,不含外部输入)。
|
||||
// 用 `IN (...)` 一条查完多仓,再由应用层按 SKU **求和**分仓结果。
|
||||
const BRAND_WMS_CO_ID_LIST_SQL = BRAND_WMS_CO_IDS.join(',');
|
||||
// 商品集合是主数据、监控快照为日更,10 分钟缓存足够
|
||||
const MKH_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
let mkhProductCache = null;
|
||||
@ -1757,21 +1793,23 @@ async function loadReplenishIndex() {
|
||||
REPLENISH_PARAMS,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
// 实时库存:同一 SKU 可能多行,按 updateTime DESC 取首条
|
||||
// 实时库存(见文件头 ⑦-d):仓白名单多仓,**按 SKU 求和**(同一批货可能分散在多仓)。
|
||||
// ⚠️ 不能再用「按 updateTime DESC 取首条」——那会只留一个仓的数,正好丢掉新仓。
|
||||
cloudDatabase.query(
|
||||
`SELECT i.sku_id AS skuId, i.qty AS qty
|
||||
`SELECT i.sku_id AS skuId, SUM(i.qty) AS qty
|
||||
FROM dbo.jst_inventoryInfo i
|
||||
WHERE i.wms_co_id = @brandWmsCoId AND i.sku_id LIKE N'MKH%'
|
||||
ORDER BY i.sku_id ASC, i.updateTime DESC`,
|
||||
WHERE i.wms_co_id IN (${BRAND_WMS_CO_ID_LIST_SQL}) AND i.sku_id LIKE N'MKH%'
|
||||
GROUP BY i.sku_id`,
|
||||
REPLENISH_PARAMS,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
// 箱/货位库存(见文件头 ⑦-d):inventoryInfo 是日更,当日入库当天看不到,
|
||||
// 用小时级的箱表补 0。只取 bin 非空行 —— bin 为空的是装箱记录,不是库存。
|
||||
// 同样按仓白名单多仓求和(见 BRAND_WMS_CO_IDS 注释)。
|
||||
cloudDatabase.query(
|
||||
`SELECT p.sku_id AS skuId, SUM(p.qty) AS qty
|
||||
FROM dbo.jst_packInventoryInfo p
|
||||
WHERE p.wms_co_id = @brandWmsCoId AND p.sku_id LIKE N'MKH%'
|
||||
WHERE p.wms_co_id IN (${BRAND_WMS_CO_ID_LIST_SQL}) AND p.sku_id LIKE N'MKH%'
|
||||
AND NULLIF(LTRIM(RTRIM(p.bin)), N'') IS NOT NULL
|
||||
GROUP BY p.sku_id`,
|
||||
REPLENISH_PARAMS,
|
||||
@ -2682,6 +2720,9 @@ module.exports = {
|
||||
COMPONENT_STOCK_WAREHOUSE,
|
||||
COMPONENT_STOCK_CHUNK_SIZE,
|
||||
COMPONENT_SHORTAGE_MAX_LIST,
|
||||
// ⑦-d 成品库存仓白名单(多仓求和口径)
|
||||
BRAND_WMS_CO_IDS,
|
||||
BRAND_WMS_CO_ID,
|
||||
parseFlag,
|
||||
resolveScope,
|
||||
filterRows,
|
||||
|
||||
@ -525,6 +525,74 @@ test('replenish index: hourly box/pallet stock surfaces same-day inbound (⑦-d)
|
||||
}
|
||||
});
|
||||
|
||||
// ⑦-e 成品库存多仓合并(2026-10-17):
|
||||
// 实物搬到「简尚-横林二仓品牌仓」(16132681) 时,归属仓 (15870495) 为 0,
|
||||
// 只查归属仓会把有货的成品算成 0(本次 5 个误报缺货 + 2 个误报预警)。
|
||||
test('replenish index: sums stock across brand warehouse whitelist (⑦-e)', async () => {
|
||||
const cloudDatabase = require('../src/config/cloudDatabase');
|
||||
const svc = require('../src/services/mkhBomService');
|
||||
const originalQuery = cloudDatabase.query;
|
||||
const originalGetPool = cloudDatabase.getPool;
|
||||
const captured = [];
|
||||
|
||||
cloudDatabase.query = async (sql) => {
|
||||
captured.push(sql);
|
||||
if (sql.includes('jst_packInventoryInfo')) {
|
||||
// 箱表:同样的「老仓 0 / 新仓 10」格局
|
||||
return [{ skuId: 'MKHDNZ0409260600788', qty: 10 }];
|
||||
}
|
||||
if (sql.includes('jst_inventoryInfo')) {
|
||||
// 关键:SUM 之后返回的是**多仓合计**。旧口径只查单仓时这里拿不到这笔货。
|
||||
return [{ skuId: 'MKHDNZ0409260600788', qty: 10 }];
|
||||
}
|
||||
if (sql.includes('pm_stockMonitor')) {
|
||||
return [{
|
||||
skuId: 'MKHDNZ0409260600788',
|
||||
fifteenAvgSale: 1,
|
||||
monitorStock: 0,
|
||||
safetyDays: 3,
|
||||
suggestStock: 0,
|
||||
monitorDate: new Date('2026-10-17T00:00:00Z'),
|
||||
}];
|
||||
}
|
||||
if (sql.includes('jst_salesOutboundInfo') || sql.includes('jst_qmcust_salesoutbound')) return [];
|
||||
if (sql.includes('jst_qmcust_orderlist')) return [];
|
||||
return [];
|
||||
};
|
||||
cloudDatabase.getPool = async () => ({
|
||||
request: () => ({ input() {}, query: async () => ({ recordset: [] }) }),
|
||||
});
|
||||
|
||||
try {
|
||||
const index = await svc._private.loadReplenishIndex();
|
||||
const row = index.bySku.get('MKHDNZ0409260600788');
|
||||
assert.equal(row.stock, 10, '多仓求和后必须看到新仓的 10 件,不能是 0');
|
||||
|
||||
// SQL 层必须真的按白名单多仓取数,而不是退回单仓
|
||||
const invSql = captured.find((s) => s.includes('jst_inventoryInfo'));
|
||||
assert.ok(invSql.includes('IN (15870495,16132681)'),
|
||||
`实时库存必须按仓白名单取数,实际: ${invSql}`);
|
||||
assert.ok(invSql.includes('GROUP BY') && invSql.includes('SUM('),
|
||||
'实时库存必须多仓 SUM + GROUP BY(旧版是 ORDER BY updateTime DESC 取首条)');
|
||||
const packSql = captured.find((s) => s.includes('jst_packInventoryInfo'));
|
||||
assert.ok(packSql.includes('IN (15870495,16132681)'),
|
||||
`箱表必须按同一份仓白名单取数,实际: ${packSql}`);
|
||||
} finally {
|
||||
cloudDatabase.query = originalQuery;
|
||||
cloudDatabase.getPool = originalGetPool;
|
||||
svc.clearMkhBomCache();
|
||||
}
|
||||
});
|
||||
|
||||
// 白名单常量本身要被导出(新增同类仓时改这一处即可)
|
||||
test('replenish index: brand warehouse whitelist contains main + Henglin No.2 brand warehouse', () => {
|
||||
const svc = require('../src/services/mkhBomService');
|
||||
const ids = svc._private.BRAND_WMS_CO_IDS;
|
||||
assert.ok(Array.isArray(ids), 'BRAND_WMS_CO_IDS 必须是数组');
|
||||
assert.ok(ids.includes(15870495), '必须含崔桥工厂品牌仓(归属主仓)');
|
||||
assert.ok(ids.includes(16132681), '必须含横林二仓品牌仓(2026-10-13 新增)');
|
||||
});
|
||||
|
||||
test('order index: reads both channel tables, merges and dedupes by o_id', async () => {
|
||||
const cloudDatabase = require('../src/config/cloudDatabase');
|
||||
const svc = require('../src/services/mkhBomService');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user