From 3e02c3e393017af0b9812afcbaf46114480bb285 Mon Sep 17 00:00:00 2001 From: yanhui <2353496031@qq.com> Date: Thu, 17 Sep 2026 14:06:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=97=A5=E9=94=80=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E7=94=B1=E8=BF=91=207=20=E5=A4=A9=E6=94=B9=E4=B8=BA=E8=BF=91?= =?UTF-8?q?=2015=20=E5=A4=A9=EF=BC=88MKH=20=E8=A1=A5=E8=B4=A7=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=20+=20=E6=A8=A1=E5=9D=97=E5=8C=96=E5=BA=93=E5=AD=98?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务要求「近7日日销」改「近30天日销」。实测业务库与产品资料库均无 30 天日销, 已实现的最长周期是 pm_stockMonitor.fifteenAvgSale(15 天),经与业务确认改用 15 天。 为什么没有 30 天(2026-09-17 实测,避免重复排查): - rpt.pm_stockMonitor 只有 threeAvgSale(3天)/sevenAvgSale(7天)/fifteenAvgSale(15天), 且整表只保留 1 天快照(2026-09-16),攒不出 30 天历史 - rpt.dws_sn_sales_full_info_di(stat_date+skuid+goods_count,新鲜到 09-15) 里 MKH 商品 0 个;目标仓 794 个 SKU 命中也是 0 - rpt.dws_order_sales_cost_detail_di 停在 2025-04(历史归档) - rpt.jst_salesOutboundInfo 只有出库单头,无 SKU 明细;业务库无出库明细表 - rpt.op_BarcodeSalePrice30dDaily 名字带 30d,但只有 salePrice 没有销量 改动: - backend/src/services/mkhBomService.js · SALE_WINDOW_DAYS 7 → 15,新增 DAILY_SALE_COLUMN='fifteenAvgSale', 监控快照 SQL 按该常量插值取数(以后改口径只改常量) · buildReplenishMetrics 入参 sevenAvgSale → avgSale(语义与周期解耦) · 文件头 ⑦ 补「为什么是 15 天」的完整排查结论;⑦ / ⑩ / ⑪ 实测数字按新口径更新 - backend/src/services/modularStockMonitorService.js · 取数列与 API 字段名 sevenAvgSale/avgShip7d → fifteenAvgSale/avgShip15d (字段名同步改,避免名不副实) - frontend/src/pages/MkhBomData/index.jsx · 列头与口径说明「近7日」→「近15天」;动态文案兜底 7 → 15 - frontend/src/pages/ModularStockMonitor/index.jsx · 列头「近7日日均发货量」→「近15天日均发货量」,dataIndex 同步改 · 口径说明补充:可用天数仍沿用 jsUseDays(源系统口径),与日销并非同一时间窗 影响(同一份 09-16 快照,7 天 vs 15 天): 有销商品 114 → 170 周期拉长,更多商品进得了「有销」集合 补货预警 25 → 19 反而变少:15 天日销普遍低于 7 天(近期销量走低), 可用天数 = 库存 ÷ 日销 随之变大,跌破 3 天的变少 缺货 9 → 10 已排产 11 → 13 订单驱动 14 → 8 更多商品自己有日销,不再需要靠订单兜底 配件不足 10 → 12 「预警变少」是口径变化的正常结果,不是 bug。 验证: - 后端语法检查 151 个文件通过 - mkhBomService + mkhReplenishAlertJob 单测 52/52 通过 - 后端全量 334 tests / 333 pass(唯一失败是 resilientSecretStore 的 Windows 文件权限断言,与本改动无关的环境固有问题) - 接口实测:total 4223 / warningCount 19 / outOfStockCount 10 / saleWindowDays 15 / componentShortageCount 12 --- backend/src/services/mkhBomService.js | 74 +++++++++++++------ .../services/modularStockMonitorService.js | 12 +-- backend/test/mkhBomService.test.js | 40 +++++----- backend/test/mkhReplenishAlertJob.test.js | 2 +- frontend/src/pages/MkhBomData/index.jsx | 28 +++---- .../src/pages/ModularStockMonitor/index.jsx | 10 ++- 6 files changed, 99 insertions(+), 67 deletions(-) diff --git a/backend/src/services/mkhBomService.js b/backend/src/services/mkhBomService.js index 14bd9a4..c6fb7b2 100644 --- a/backend/src/services/mkhBomService.js +++ b/backend/src/services/mkhBomService.js @@ -64,19 +64,40 @@ // ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总(**商品**库存,不是配件仓位)。 // // ── ⑦ 补货预警(2026-09-17 业务确认口径)────────────────────────── -// 规则:近 7 日日销 > 0 且 现有库存 < 3 天日销 → 预警补货 -// · 近7日日销 rpt.pm_stockMonitor.sevenAvgSale +// 规则:近 15 天日销 > 0 且 现有库存 < 3 天日销 → 预警补货 +// · 近15天日销 rpt.pm_stockMonitor.fifteenAvgSale // (warehouse = '简尚-崔桥工厂品牌仓',按 date 取最新一条快照) // · 现有库存 rpt.jst_inventoryInfo(wms_co_id = 15870495 品牌仓)实时库存; // 该仓对 MKH 成品只覆盖约 256 / 4408 条(2026-09-16 快照;成品总数口径见 ①), // 缺失时回退监控快照 jsStock -// · 可用天数 = 现有库存 / 近7日日销 -// · 建议补货量 = ceil(3 × 近7日日销 − 现有库存),不足 0 取 0 -// ⚠️ 刻意**不用**源系统的 jsUseDays 列:实测它与 jsStock / sevenAvgSale 不同源 +// · 可用天数 = 现有库存 / 近15天日销 +// · 建议补货量 = ceil(3 × 近15天日销 − 现有库存),不足 0 取 0 +// ⚠️ 刻意**不用**源系统的 jsUseDays 列:实测它与 jsStock / fifteenAvgSale 不同源 // (同一 SKU 可差 20 天以上,如 MKHDNZ0409260700488 库存 44 / 日销 0.67, -// 除法 65.7 天而 jsUseDays 为 44),本页口径以业务给定的「近7日日销」为准。 -// 实测(2026-09-16 快照,当时成品口径尚未收紧为「正式 BOM 父项」): -// 4408 个 MKH 成品中 114 个有日销、13 个可用天数 < 3 天。 +// 除法 65.7 天而 jsUseDays 为 44),本页口径以业务给定的「近15天日销」为准。 +// +// ── 为什么是 15 天而不是业务最初要求的 30 天 ── +// 2026-09-17 业务要求「近7日日销」改「近30天日销」,但实测全库**没有 30 天日销**: +// · pm_stockMonitor 只有 threeAvgSale(3天) / sevenAvgSale(7天) / fifteenAvgSale(15天), +// 且整表**只保留 1 天快照**(2026-09-16),攒不出 30 天历史 +// · rpt.dws_sn_sales_full_info_di(stat_date+skuid+goods_count,数据新鲜到 09-15) +// 里 MKH 商品 **0 个**;目标仓 794 个 SKU 命中也是 0 +// · rpt.dws_order_sales_cost_detail_di 停在 2025-04(历史归档) +// · rpt.jst_salesOutboundInfo 只有出库**单头**,无 SKU 明细;业务库无出库明细表 +// · rpt.op_BarcodeSalePrice30dDaily 名字带 30d,但只有 salePrice **没有销量** +// → 经与业务确认,改用十五天口径 fifteenAvgSale(已实现的最长周期)。 +// ⚠️ 若日后上游补了 30 天字段,只需改 loadReplenishIndex 里的列名 + DAILY_SALE_DAYS。 +// +// 实测(2026-09-16 品牌仓快照,当时成品口径尚未收紧为「正式 BOM 父项」): +// 4408 个 MKH 成品中,7 天有销 114 个、15 天有销 170 个 +// 实测(2026-09-17 切换当天,同一份 09-16 快照,7 天 vs 15 天): +// 有销商品 114 → 170 ← 周期拉长,更多商品进得了「有销」集合 +// 补货预警 25 → 19 ← 反而变少:15 天日销普遍低于 7 天(近期销量走低), +// 可用天数 = 库存 ÷ 日销 随之变大,跌破 3 天的变少 +// 缺货 9 → 10 +// 已排产 11 → 13 +// 订单驱动 14 → 8 ← 更多商品自己有日销了,不再需要靠订单兜底 +// ⚠️ 「预警变少」不是 bug,是口径变化的正常结果;若业务想要 30 天,需上游先补数。 // // ── ⑧ 在途加工单(2026-09-17 业务确认口径)──────────────────────── // 规则:**只要该 MKH 商品存在未完成的加工单,就不再列入补货预警** @@ -107,7 +128,7 @@ // 响应会膨胀到几十 MB,而商品清单本身用不到明细。 // // ── ⑩ 待发货订单(2026-09-17 业务确认口径)──────────────────────── -// 规则:**近 7 日日销 = 0、但当前存在未发货订单的 MKH 成品,同样列入补货预警**。 +// 规则:**近 15 天日销 = 0、但当前存在未发货订单的 MKH 成品,同样列入补货预警**。 // 背景:日销取自监控快照(日更)的滚动均值,新上的 MKH 成品「昨天没卖、今天来了订单」 // 时快照仍是 0,会被判成「无销量」而完全排除在预警之外,导致漏备货。 // · 数据源:业务库 rpt.jst_Orders(订单**主表**,实时更新,实测最新到今天) @@ -128,8 +149,10 @@ // 缺货时记 stockDays = 0(保证排在最前),预警(库存已够发)时记 null。 // ⚠️ 与「在途加工单」的优先级:**先判已排产**(见 ⑧)—— // 无日销但有订单、且工厂已开加工单的商品,仍标「已排产」不预警,避免重复提示。 -// 实测(2026-09-17):未发货且含 MKH 成品的订单 75 单 → 48 个成品编码 / 71 件, +// 实测(2026-09-17,7 天口径下):未发货且含 MKH 成品的订单 75 单 → 48 个成品编码 / 71 件, // 其中 30 个本来就有日销、**18 个是本次新增**(日销=0,靠订单才进预警)。 +// ⚠️ 改 15 天口径后实测:订单驱动从 14 个降到 **8 个** +// (日销基数 114 → 170,原本靠订单兜底的商品自己有日销了),与预期一致。 // // ── ⑪ 配件不足 / 可组装数(2026-09-17 业务确认口径)──────────────── // 规则:**缺货 / 预警的 MKH 成品,若其配件现有库存「可组装数 = 0」,则提示 @@ -152,8 +175,8 @@ // · 判定范围:**缺货 + 预警都判**(业务确认),不改变原有预警级别与排序, // 只额外打标记;「已排产」商品本来就不预警,不参与判定。 // · 无 BOM 配件明细的商品 → 可组装数未知(组件缺失 ≠ 配件不足),不做判定。 -// 实测(2026-09-17,并入最新 main 后复核):预警 25 个(缺货 9 + 预警 16)中 **10 个可组装数 = 0** -// (缺货 6 + 预警 4);扣加工单占用前后仅 1 个商品变化(185 → 180 台), +// 实测(2026-09-17,15 天口径):预警 19 个(缺货 10 + 预警 9)中 **12 个可组装数 = 0** +// (缺货 7 + 预警 5);扣加工单占用前后仅 1 个商品变化(185 → 180 台), // 说明占用规模小但口径上必须扣。 // ⚠️ 配件库存查询失败时**不做任何判定**(宁可少标也不误标), // 接口透出 componentStockDegraded=true。 @@ -192,7 +215,13 @@ const MKH_WHERE = `s.sku_id LIKE N'MKH%' // ── 补货预警参数(见文件头 ⑦)──────────────────────────────────── // 品牌仓 wms_co_id(rpt.jst_warehouseInfo 查得) const BRAND_WMS_CO_ID = 15870495; -const SALE_WINDOW_DAYS = 7; +// 日销统计窗口(天)。见文件头 ⑦:业务最初要 30 天,但全库无 30 天日销数据, +// 实测最长只有 fifteenAvgSale(15 天),经业务确认改用 15 天。 +// ⚠️ SALE_WINDOW_DAYS 决定页面文案与口径展示,DAILY_SALE_COLUMN 决定实际取数列, +// 两者必须成对修改;下方 SQL 已按 DAILY_SALE_COLUMN 插值,改常量即可生效。 +const SALE_WINDOW_DAYS = 15; +// 日销取数列(rpt.pm_stockMonitor)—— 必须与 SALE_WINDOW_DAYS 保持一致 +const DAILY_SALE_COLUMN = 'fifteenAvgSale'; // 预警阈值:现有库存可用天数低于 3 天即需补货 const REPLENISH_DAYS_THRESHOLD = 3; @@ -299,8 +328,8 @@ const SCOPES = Object.freeze({ WARNING: 'warning', SOLD: 'sold', ALL: 'all' }); /** * 解析展示范围: - * warning —— 仅需补货(近7日日销 > 0 且 可用天数 < 3 天) - * sold —— 有近7日日销的商品 + * warning —— 仅需补货(近15天日销 > 0 且 可用天数 < 3 天) + * sold —— 有近15天日销的商品 * all —— 全部 MKH 成品 * 兼容旧参数 warningOnly=1(等价 scope=warning)。 */ @@ -776,13 +805,13 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber( * 计算单个 MKH 成品的补货预警指标(纯函数,便于回归测试)。 * * 口径(见文件头 ⑦ + ⑩): - * ① 近7日日销 > 0 且 现有库存 < 3 天日销 → 预警 - * ② 近7日日销 = 0 但存在未发货订单 → 订单驱动预警(见文件头 ⑩) + * ① 近15天日销 > 0 且 现有库存 < 3 天日销 → 预警 + * ② 近15天日销 = 0 但存在未发货订单 → 订单驱动预警(见文件头 ⑩) * 两者都不满足 → 无销量,不参与预警。 * 已有未完成加工单的商品一律标「已排产」,两种口径都不预警。 * * @param {object} row - * row.sevenAvgSale 近7日日均发货量 + * row.avgSale 近15天日均发货量(取自 rpt.pm_stockMonitor.fifteenAvgSale) * row.realtimeStock 品牌仓实时库存(可为 null,表示该仓无记录) * row.monitorStock 监控快照库存(fallback) * row.safetyDays 源系统安全天数(仅展示) @@ -792,7 +821,7 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber( * row.pendingOrderQty 待发货订单量(未发货订单件数) */ function buildReplenishMetrics(row = {}) { - const dailySale = Math.max(0, toNumber(row.sevenAvgSale) ?? 0); + const dailySale = Math.max(0, toNumber(row.avgSale) ?? 0); const realtimeStock = toNumber(row.realtimeStock); const monitorStock = toNumber(row.monitorStock) ?? 0; const hasRealtime = realtimeStock !== null; @@ -828,7 +857,7 @@ function buildReplenishMetrics(row = {}) { monitorDate: toDateText(row.monitorDate), }; - // 既无近7日日销、也无未发货订单 → 不参与预警 + // 既无近15天日销、也无未发货订单 → 不参与预警 if (dailySale <= 0 && !orderDriven) { return { ...base, @@ -1179,7 +1208,7 @@ async function loadReplenishIndex() { const pending = Promise.all([ // 监控快照:同一天只有一行,按 date DESC 取首条即可 cloudDatabase.query( - `SELECT m.sku_id AS skuId, m.sevenAvgSale AS sevenAvgSale, m.jsStock AS monitorStock, + `SELECT m.sku_id AS skuId, m.${DAILY_SALE_COLUMN} AS fifteenAvgSale, m.jsStock AS monitorStock, m.safetyDays AS safetyDays, m.suggestStock AS suggestStock, m.date AS monitorDate FROM dbo.pm_stockMonitor m WHERE m.warehouse = @brandWarehouse AND m.sku_id LIKE N'MKH%' @@ -1218,7 +1247,8 @@ async function loadReplenishIndex() { seenMonitor.add(key); if (!monitorDate) monitorDate = toDateText(row.monitorDate); bySku.set(key, buildReplenishMetrics({ - sevenAvgSale: row.sevenAvgSale, + // 日销取数见文件头 ⑦:十五天口径(业务要 30 天但全库无此数据) + avgSale: row.fifteenAvgSale, // 实时库存缺失(而非为 0)时才回退监控快照 realtimeStock: latestStock.has(key) ? latestStock.get(key) : null, monitorStock: row.monitorStock, diff --git a/backend/src/services/modularStockMonitorService.js b/backend/src/services/modularStockMonitorService.js index 1d219cd..ea2dc2b 100644 --- a/backend/src/services/modularStockMonitorService.js +++ b/backend/src/services/modularStockMonitorService.js @@ -5,7 +5,7 @@ // 需求来源:《模块化库存监控.xlsx》 // 一级功能:模块化库存监控 二级功能:配件库存监控 // 表头:图片 | 预警 | SKU编码 | 名称 | 当日入库库存 | 当日出库库存 | 实时库存 | -// 仓位 | 近7日日均发货量 | 库存可用天数 | 所在仓库 +// 仓位 | 近15天日均发货量 | 库存可用天数 | 所在仓库 // 数据范围:仅取「简尚-崔桥工厂半成品包装项目仓」仓库下的 SKU 编码 // // 数据来源(业务库 JSReportReplica / schema=rpt,2026-09-12 实测确认): @@ -14,7 +14,7 @@ // 库存 rpt.jst_inventoryInfo —— 实时库存 qty;按 wms_co_id 区分仓库 // 目标仓库 wms_co_id = 15718196(rpt.jst_warehouseInfo 查得) // 监控 rpt.pm_stockMonitor —— 日更快照,目标仓每天 794 行,覆盖全部 SKU: -// sevenAvgSale 近7日日均发货量 +// fifteenAvgSale 近15天日均发货量(2026-09-17 由 7 天改 15 天,见下) // jsUseDays 库存可用天数 // safetyDays 安全天数(默认 15) // suggestStock 建议备货量 @@ -76,7 +76,7 @@ const SORTABLE_FIELDS = Object.freeze({ productName: 's.name', category: 's.category', realtimeStock: 'realtimeStock', - avgShip7d: 'm.sevenAvgSale', + avgShip15d: 'm.fifteenAvgSale', stockDays: 'm.jsUseDays', warningLevel: 'warningOrder', mkhStatus: 'mkhStatus', @@ -108,7 +108,7 @@ OUTER APPLY ( ORDER BY i.updateTime DESC ) inv OUTER APPLY ( - SELECT TOP 1 m2.sevenAvgSale, m2.jsUseDays, m2.safetyDays, m2.suggestStock, + SELECT TOP 1 m2.fifteenAvgSale, m2.jsUseDays, m2.safetyDays, m2.suggestStock, m2.date, m2.jsStock, m2.nearSecondSale FROM dbo.pm_stockMonitor m2 WHERE m2.sku_id = s.sku_id AND m2.warehouse = @warehouseName @@ -352,7 +352,7 @@ const mapRow = (row = {}) => { outQtyToday: toNumber(row.outQtyToday) ?? 0, realtimeStock, location: toText(row.location), - avgShip7d: toNumber(row.avgShip7d), + avgShip15d: toNumber(row.avgShip15d), stockDays, safetyDays, suggestStock: toNumber(row.suggestStock), @@ -368,7 +368,7 @@ const buildProjection = () => ` ISNULL(pin.inQty, 0) AS inQtyToday, ISNULL(m.nearSecondSale, 0) AS outQtyToday, COALESCE(NULLIF(LTRIM(RTRIM(s.bin)), N''), NULLIF(LTRIM(RTRIM(s.other_bin)), N'')) AS location, - m.sevenAvgSale AS avgShip7d, + m.fifteenAvgSale AS avgShip15d, m.jsUseDays AS stockDays, m.safetyDays AS safetyDays, m.suggestStock AS suggestStock, diff --git a/backend/test/mkhBomService.test.js b/backend/test/mkhBomService.test.js index 5cfcfb8..1dd6578 100644 --- a/backend/test/mkhBomService.test.js +++ b/backend/test/mkhBomService.test.js @@ -190,17 +190,17 @@ test('pack inventory without bin/bin_group falls back to the box id, never to a assert.equal(locations.has('PART-2'), false); }); -// ── 补货预警:近 7 日日销 > 0 且 现有库存 < 3 天日销 ──────────────── +// ── 补货预警:近 15 天日销 > 0 且 现有库存 < 3 天日销 ─────────────── const metrics = (row) => _private.buildReplenishMetrics(row); test('replenish: stock below 3 days of sales is flagged as stockout or warning', () => { - const stockout = metrics({ sevenAvgSale: 2.55, realtimeStock: 0, monitorStock: 0 }); + const stockout = metrics({ avgSale: 2.55, realtimeStock: 0, monitorStock: 0 }); assert.equal(stockout.stockDays, 0); assert.equal(stockout.warningLevel, '缺货'); assert.equal(stockout.hasWarning, true); assert.equal(stockout.replenishQty, 8); // ceil(3 * 2.55 - 0) = ceil(7.65) - const warning = metrics({ sevenAvgSale: 2, realtimeStock: 1, monitorStock: 1 }); + const warning = metrics({ avgSale: 2, realtimeStock: 1, monitorStock: 1 }); assert.equal(warning.stockDays, 0.5); assert.equal(warning.warningLevel, '预警'); assert.equal(warning.hasWarning, true); @@ -208,34 +208,34 @@ test('replenish: stock below 3 days of sales is flagged as stockout or warning', }); test('replenish: the threshold is exactly 3 days of sales', () => { - const atThreshold = metrics({ sevenAvgSale: 2, realtimeStock: 6, monitorStock: 6 }); + const atThreshold = metrics({ avgSale: 2, realtimeStock: 6, monitorStock: 6 }); assert.equal(atThreshold.stockDays, 3); assert.equal(atThreshold.warningLevel, '正常'); assert.equal(atThreshold.hasWarning, false); assert.equal(atThreshold.replenishQty, 0); - const below = metrics({ sevenAvgSale: 2, realtimeStock: 5.9, monitorStock: 5.9 }); + const below = metrics({ avgSale: 2, realtimeStock: 5.9, monitorStock: 5.9 }); assert.equal(below.hasWarning, true); }); test('replenish: stock prefers the brand-warehouse realtime value, snapshot only as fallback', () => { - const realtime = metrics({ sevenAvgSale: 1, realtimeStock: 4, monitorStock: 9 }); + const realtime = metrics({ avgSale: 1, realtimeStock: 4, monitorStock: 9 }); assert.equal(realtime.stock, 4); assert.equal(realtime.stockSource, 'realtime'); // 实时库存「无记录」(null)才回退监控快照 - const fallback = metrics({ sevenAvgSale: 1, realtimeStock: null, monitorStock: 9 }); + const fallback = metrics({ avgSale: 1, realtimeStock: null, monitorStock: 9 }); assert.equal(fallback.stock, 9); assert.equal(fallback.stockSource, 'monitor'); // 实时库存为 0 是有效值,不得被快照覆盖 - const zero = metrics({ sevenAvgSale: 1, realtimeStock: 0, monitorStock: 9 }); + const zero = metrics({ avgSale: 1, realtimeStock: 0, monitorStock: 9 }); assert.equal(zero.stock, 0); assert.equal(zero.warningLevel, '缺货'); }); test('replenish: products without 7-day sales never warn', () => { - const noSale = metrics({ sevenAvgSale: 0, realtimeStock: 0, monitorStock: 0 }); + const noSale = metrics({ avgSale: 0, realtimeStock: 0, monitorStock: 0 }); assert.equal(noSale.hasWarning, false); assert.equal(noSale.warningLevel, '无销量'); assert.equal(noSale.stockDays, null); @@ -286,7 +286,7 @@ test('replenish sort puts the most urgent first and no-sale products last', () = test('replenish: a pending work order suppresses the replenish warning', () => { // 库存为 0、本该判「缺货」,但半成品包装项目仓已有未完成加工单 const scheduled = metrics({ - sevenAvgSale: 2, + avgSale: 2, realtimeStock: 0, monitorStock: 0, pendingWorkOrderQty: 20, @@ -307,13 +307,13 @@ test('replenish: a pending work order suppresses the replenish warning', () => { assert.equal(scheduled.stockDays, 0); // 同样库存但无加工单时仍然预警 —— 确认抑制效果确实来自加工单 - const bare = metrics({ sevenAvgSale: 2, realtimeStock: 0, monitorStock: 0 }); + const bare = metrics({ avgSale: 2, realtimeStock: 0, monitorStock: 0 }); assert.equal(bare.hasWarning, true); assert.equal(bare.warningLevel, '缺货'); }); test('replenish: pending work order fields default to empty without any order', () => { - const none = metrics({ sevenAvgSale: 1, realtimeStock: 0 }); + const none = metrics({ avgSale: 1, realtimeStock: 0 }); assert.equal(none.hasPendingWorkOrder, false); assert.equal(none.pendingWorkOrderQty, 0); assert.equal(none.pendingWorkOrderCount, 0); @@ -322,7 +322,7 @@ test('replenish: pending work order fields default to empty without any order', // 数量为 0 但确实挂着加工单(未入库)时,同样按「已排产」处理 const zeroQty = metrics({ - sevenAvgSale: 1, + avgSale: 1, realtimeStock: 0, pendingWorkOrderQty: 0, pendingWorkOrderCount: 2, @@ -334,7 +334,7 @@ test('replenish: pending work order fields default to empty without any order', test('replenish: products without sales stay "no sale" even with a pending work order', () => { const noSale = metrics({ - sevenAvgSale: 0, + avgSale: 0, realtimeStock: 0, pendingWorkOrderQty: 5, pendingWorkOrderCount: 1, @@ -394,7 +394,7 @@ test('pending order: only MKH finished products count, combination codes are exc test('replenish: no sales but unshipped orders still trigger the warning', () => { // 库存不够发货 → 缺货,补货量 = 待发货件数 − 现有库存 const notEnough = metrics({ - sevenAvgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4, pendingOrderCount: 2, + avgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4, pendingOrderCount: 2, }); assert.equal(notEnough.hasWarning, true); assert.equal(notEnough.warningLevel, '缺货'); @@ -407,7 +407,7 @@ test('replenish: no sales but unshipped orders still trigger the warning', () => // 库存刚好够发 → 预警(不再缺货),补货量为 0 const exactlyEnough = metrics({ - sevenAvgSale: 0, realtimeStock: 4, monitorStock: 4, pendingOrderQty: 4, + avgSale: 0, realtimeStock: 4, monitorStock: 4, pendingOrderQty: 4, }); assert.equal(exactlyEnough.warningLevel, '预警'); assert.equal(exactlyEnough.hasWarning, true); @@ -418,7 +418,7 @@ test('replenish: no sales but unshipped orders still trigger the warning', () => test('replenish: order-driven warning does not override the pending work order exemption', () => { const scheduled = metrics({ - sevenAvgSale: 0, + avgSale: 0, realtimeStock: 0, pendingOrderQty: 5, pendingOrderCount: 1, @@ -434,19 +434,19 @@ test('replenish: order-driven warning does not override the pending work order e }); test('replenish: pending order fields default to empty without any unshipped order', () => { - const none = metrics({ sevenAvgSale: 1, realtimeStock: 0 }); + const none = metrics({ avgSale: 1, realtimeStock: 0 }); assert.equal(none.pendingOrderQty, 0); assert.equal(none.pendingOrderCount, 0); assert.equal(none.pendingOrderEarliest, null); assert.equal(none.orderDriven, false); // 无日销且无订单 → 仍是「无销量」,不参与预警 - const noSale = metrics({ sevenAvgSale: 0, realtimeStock: 0, pendingOrderQty: 0 }); + const noSale = metrics({ avgSale: 0, realtimeStock: 0, pendingOrderQty: 0 }); assert.equal(noSale.warningLevel, '无销量'); assert.equal(noSale.orderDriven, false); // 有日销的商品照旧走原口径,但待发货量也要带上(页面新增列) - const withSale = metrics({ sevenAvgSale: 2, realtimeStock: 0, pendingOrderQty: 3 }); + const withSale = metrics({ avgSale: 2, realtimeStock: 0, pendingOrderQty: 3 }); assert.equal(withSale.warningLevel, '缺货'); assert.equal(withSale.orderDriven, false); assert.equal(withSale.pendingOrderQty, 3); diff --git a/backend/test/mkhReplenishAlertJob.test.js b/backend/test/mkhReplenishAlertJob.test.js index 8e4bf57..7e90c92 100644 --- a/backend/test/mkhReplenishAlertJob.test.js +++ b/backend/test/mkhReplenishAlertJob.test.js @@ -83,7 +83,7 @@ test('notification carries recipient, idempotency key and shortfall details', () const all = [...fresh, alert('MKHDNZ0409260700486', '预警', { dailySale: 2, stock: 1, replenishQty: 5 })]; const snapshot = { monitorDate: '2026-09-16', - saleWindowDays: 7, + saleWindowDays: 15, replenishDaysThreshold: 3, outOfStockCount: 1, warningOnlyCount: 1, diff --git a/frontend/src/pages/MkhBomData/index.jsx b/frontend/src/pages/MkhBomData/index.jsx index 47aa9c4..1c6b546 100644 --- a/frontend/src/pages/MkhBomData/index.jsx +++ b/frontend/src/pages/MkhBomData/index.jsx @@ -71,7 +71,7 @@ const PRODUCT_EXPORT_COLUMNS = [ { title: 'MKH商品编码', dataIndex: 'skuId' }, { title: '商品名称', dataIndex: 'productName' }, { title: '预警', dataIndex: 'warningLevel' }, - { title: '近7日日销', dataIndex: 'dailySale' }, + { title: '近15天日销', dataIndex: 'dailySale' }, { title: '现有库存', dataIndex: 'stock' }, { title: '在途加工量', dataIndex: 'pendingWorkOrderQty' }, { title: '待发货订单', dataIndex: 'pendingOrderQty' }, @@ -512,7 +512,7 @@ const MkhBomData = () => { ), }, { - title: '近7日日销', + title: '近15天日销', dataIndex: 'dailySale', width: 106, align: 'right', @@ -562,7 +562,7 @@ const MkhBomData = () => { `聚水潭未发货订单 ${row.pendingOrderCount || 0} 单,合计 ${number(value)} 件`, row.pendingOrderEarliest ? `最早一单下单于 ${row.pendingOrderEarliest}` : '', '已取消 / 已合并单据不核算', - row.orderDriven ? '该商品近 7 日暂无日销,按待发货订单量列入补货预警' : '', + row.orderDriven ? '该商品近 15 天暂无日销,按待发货订单量列入补货预警' : '', ].filter(Boolean).join(';')} > {number(value)} @@ -577,9 +577,9 @@ const MkhBomData = () => { sorter: true, render: (value, row) => { if (value === null || value === undefined) { - // 订单驱动(近7日无日销、按待发货订单预警)与「无销量」都算不出天数,但含义不同 + // 订单驱动(近15天无日销、按待发货订单预警)与「无销量」都算不出天数,但含义不同 return row.orderDriven - ? 按订单 + ? 按订单 : 无销量; } return {number(value, 2)}; @@ -717,7 +717,7 @@ const MkhBomData = () => {
MKH-BOM 资料 - 模块化(MKH 开头)成品的配件明细与补货预警:近 7 日日销核算,库存低于 3 天日销即预警 + 模块化(MKH 开头)成品的配件明细与补货预警:近 15 天日销核算,库存低于 3 天日销即预警 数据口径:MKH 商品编码取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”,并且必须是聚水潭正式 BOM 的父项; @@ -725,11 +725,11 @@ const MkhBomData = () => { 无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位只取 rpt.jst_packInventoryInfo, 优先显示具体仓位,没有具体仓位时显示箱号,表中没有有效记录时显示「未维护」。
- 补货预警:近7日日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、 - 该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近7日日销,低于 3 天即预警,缺货单独标识; - 建议补货量 = 3 × 近7日日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。 + 补货预警:近15天日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、 + 该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近15天日销,低于 3 天即预警,缺货单独标识; + 建议补货量 = 3 × 近15天日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。
- 待发货订单:近7日暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警 + 待发货订单:近15天暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警 (新品刚接到订单时监控快照的日销还是 0,避免漏备货)。未发货 = 订单无发货时间且状态非「取消 / 已合并」; 预警级别按「现有库存 < 待发货件数 → 缺货,否则预警」,建议补货量 = 待发货件数 − 现有库存, 可用天数因无日销基数而显示「按订单」。 @@ -793,7 +793,7 @@ const MkhBomData = () => { className="mkh-alert" type="warning" showIcon - message="待发货订单数据源暂时不可用,待发货量按 0 处理,可能遗漏「近7日无日销但有订单」的商品,请稍后点击「刷新数据」重试。" + message="待发货订单数据源暂时不可用,待发货量按 0 处理,可能遗漏「近15天无日销但有订单」的商品,请稍后点击「刷新数据」重试。" action={} /> ) : null} @@ -816,14 +816,14 @@ const MkhBomData = () => { message={`补货预警:${data.warningCount} 个 MKH 商品需要补货`} description={( - 按近 {data.saleWindowDays || 7} 日日销核算(监控日 {data.monitorDate || '—'}): + 按近 {data.saleWindowDays || 15} 天日销核算(监控日 {data.monitorDate || '—'}): 其中 {data.outOfStockCount || 0} 个已缺货, {Math.max(0, (data.warningCount || 0) - (data.outOfStockCount || 0))} 个可用天数不足 {data.replenishDaysThreshold || 3} 天。 - 建议补货量 = {data.replenishDaysThreshold || 3} × 近7日日销 − 现有库存。点「只看预警」即为补货清单。 + 建议补货量 = {data.replenishDaysThreshold || 3} × 近15天日销 − 现有库存。点「只看预警」即为补货清单。 {(data.orderDrivenCount || 0) > 0 ? ( <>
- 其中 {data.orderDrivenCount} 个近 7 日暂无日销,因聚水潭存在未发货订单 + 其中 {data.orderDrivenCount} 个近 15 天暂无日销,因聚水潭存在未发货订单 (待发货合计 {number(data.pendingOrderQtyTotal || 0)} 件)而列入预警,建议补货量按待发货件数核算。 ) : null} diff --git a/frontend/src/pages/ModularStockMonitor/index.jsx b/frontend/src/pages/ModularStockMonitor/index.jsx index f9d6cc6..d121390 100644 --- a/frontend/src/pages/ModularStockMonitor/index.jsx +++ b/frontend/src/pages/ModularStockMonitor/index.jsx @@ -233,8 +233,8 @@ const AccessoryStockPanel = () => { ), }, { - title: '近7日日均发货量', - dataIndex: 'avgShip7d', + title: '近15天日均发货量', + dataIndex: 'avgShip15d', width: 138, align: 'right', sorter: true, @@ -402,13 +402,15 @@ const ModularStockMonitor = () => (
模块化库存监控 - 配件库存监控:仅统计「简尚-崔桥工厂半成品包装项目仓」下的 SKU 实时库存、近 7 日日均发货量与可用天数 + 配件库存监控:仅统计「简尚-崔桥工厂半成品包装项目仓」下的 SKU 实时库存、近 15 天日均发货量与可用天数 - 数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 7 日日均发货量、 + 数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 15 天日均发货量、 库存可用天数、安全天数取 pm_stockMonitor 最新快照(按完整 SKU 编码关联,794 个 SKU 全覆盖); 当日入库取采购到货、当日出库取近一日发货量,均以监控日为基准;仓位上游未回传,暂显示「未维护」; 图片取自「产品资料系统数据库」商品档案(业务库图片字段均为空)。 + 注:日均发货量 2026-09-17 起由近 7 天改为近 15 天(业务原要求 30 天,但业务库无 30 天日销,最长只到 15 天); + 库存可用天数仍沿用监控表 jsUseDays(源系统口径),故两者并非同一时间窗。 状态列:基于「产品资料系统数据库」判断该 SKU 是否已建立商品档案——已配置MKH = 资料库中可查到该 SKU;未配置MKH = 查不到。