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
- ?