From d74b4b649a84807348cd70c09c2b66a8bf13cbf2 Mon Sep 17 00:00:00 2001 From: lunanting Date: Thu, 22 Oct 2026 15:07:29 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E7=BC=BA=E8=B4=A7=E8=A1=A5=E8=B4=A7?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E6=94=B9=E4=B8=BA=E5=8F=AA=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=BE=85=E5=8F=91=E8=AE=A2=E5=8D=95=EF=BC=88=E6=8E=92=E9=99=A4?= =?UTF-8?q?=E5=B7=B2=E5=8F=91/=E5=8F=96=E6=B6=88/=E6=8B=86=E5=8D=95/?= =?UTF-8?q?=E5=90=88=E5=8D=95=EF=BC=89=EF=BC=8C=E4=B8=8D=E9=99=90=E6=97=A5?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/binReplenish.js | 4 +- backend/src/services/binReplenishService.js | 100 ++++++++---------- frontend/src/api/binReplenish.js | 2 +- .../src/pages/BinReplenishMonitor/index.jsx | 50 ++++----- 4 files changed, 71 insertions(+), 85 deletions(-) diff --git a/backend/src/api/binReplenish.js b/backend/src/api/binReplenish.js index a9afc4b..98af8b5 100644 --- a/backend/src/api/binReplenish.js +++ b/backend/src/api/binReplenish.js @@ -17,7 +17,7 @@ const EXPORT_COLUMNS = [ { key: 'pickQty', title: '拣货位库存' }, { key: 'storageQty', title: '存储位库存' }, { key: 'totalQty', title: '现有库存合计' }, - { key: 'orderQty', title: '当日订单' }, + { key: 'orderQty', title: '待发订单' }, { key: 'excess', title: '超出订单数' }, { key: 'suggestQty', title: '建议补货量' }, { key: 'shortfall', title: '存储缺口' }, @@ -49,7 +49,7 @@ router.get('/export', async (req, res, next) => { keyword: req.query.keyword, status: req.query.status, }); - const fileName = `${payload.warehouse.shortName}缺货补货监控_${payload.orderDate || ''}`; + const fileName = `${payload.warehouse.shortName}缺货补货监控_待发订单`; sendWorkbookDownload(res, { fileName, diff --git a/backend/src/services/binReplenishService.js b/backend/src/services/binReplenishService.js index 870701e..018dc4e 100644 --- a/backend/src/services/binReplenishService.js +++ b/backend/src/services/binReplenishService.js @@ -4,7 +4,7 @@ * 缺货补货监控(订单驱动版,2026-10-22 重写) * ========================================================================== * 业务需求(用户确认): - * 监控崔桥工厂仓(主仓)现有库存 —— 看哪些「拣货库位」的货不够今天的订单拣, + * 监控崔桥工厂仓(主仓)现有库存 —— 看哪些「拣货库位」的货不够待发订单拣, * 需要从「存储库位」把货补过去。 * * ── 货位口径(前缀规则,用户指定)────────────────────────────────────── @@ -19,16 +19,22 @@ * + 组合装(sku_type='combine')→ jst_comboProductItem 展开为子件 * ⚠️ 仓库维度必须用 **wms_co_id**:wh_id 是货主(简尚=1), * 按 wh_id 会把多仓混成一个桶。 - * 订单日期:取该仓订单表 send_date 的最大日期(即「最近一个有订单的营业日」, - * 副本 ETL 有延迟时不会为空,页面会明确标注统计的是哪一天)。 + * + * ── 待发口径(用户 2026-10-22 二次明确)────────────────────────────── + * 补货需求只来自「还没发的订单(待发)」,**已发货(Sent)的订单不参与计算** —— + * 已发订单的货已经拣走发出,不再消耗拣货位库存,不该驱动补货。 + * 判定方式:排除掉「已发 / 取消 / 删除 / 拆单 / 合单」这几类非待发状态, + * 其余(WaitConfirm 待确认 / Question 异常挂起 / Delivering 发货中 / 任何新增待发状态) + * 一律按「待发」计入。如此新增的待发状态也会自动落入统计,无需改代码。 + * 待发订单不限日期:含历史未发积压,最贴近「需要补货去拣的货」。 * * ── 判断逻辑(核心)────────────────────────────────────────────────── * 对每一个 SKU(在拣货/存储位上有库存): * 拣货位库存 pickQty = 该 SKU 在 XA/XD/XE 货位上的 qty 合计 * 存储位库存 storageQty = 该 SKU 在 XX/X9/X5/X4 货位上的 qty 合计 - * 当日订单 orderQty = 该 SKU 在订单日期当天的订单量(含组合装展开) + * 待发订单 orderQty = 该 SKU 的「待发」订单量(含组合装展开,不限日期) * 超出订单数 excess = max(0, orderQty − pickQty) - * —— 拣货位不够今天的订单拣,超出的这部分要从存储位补到拣货位 + * —— 拣货位不够待发订单拣,超出的这部分要从存储位补到拣货位 * 建议补货量 suggestQty = min(excess, storageQty) (不能超过存储位现有量) * 存储缺口 shortfall = max(0, excess − storageQty)(存储位也不够,需采购/急调) * @@ -36,10 +42,10 @@ * short 存储不足(orderQty > pickQty 且 storageQty < excess)—— 最紧急 * need 需补货 (orderQty > pickQty 且 storageQty ≥ excess) * enough 拣货位充足(0 < orderQty ≤ pickQty) - * no_order 今日无单(orderQty = 0) + * no_order 无待发单(orderQty = 0) * (pickQty = 0 且 orderQty > 0 归入 need/short,并标记 pickEmpty 强调) * - * ⚠️ 性能:库存视图聚合 + 当日订单 4 分支 union 各自都要 30s~1min, + * ⚠️ 性能:库存视图聚合 + 待发订单 4 分支 union 各自都要 30s~1min, * 必须缓存(默认 10 分钟)+ 启动预热,不要每次请求都回源。 */ @@ -75,6 +81,14 @@ const STORE_PREFIXES = Object.freeze(['XX', 'X9', 'X5', 'X4']); const PICK_PREFIX_IN = PICK_PREFIXES.map((p) => `'${p}'`).join(','); const STORE_PREFIX_IN = STORE_PREFIXES.map((p) => `'${p}'`).join(','); +// ── 待发口径:排除掉「非待发」状态,其余一律算待发 ────────────────────── +// Sent=已发(唯一 100% 带 send_date 的状态)、Cancelled=已取消、Delete=删除、 +// Split=拆单父单(子单另计)、Merged=合单(合后单另计)。 +// 白名单式排除比「只认某几个待发状态」更稳:新增的待发状态会自动落入统计。 +const PENDING_EXCLUDE_STATUS = Object.freeze(['Sent', 'Cancelled', 'Delete', 'Split', 'Merged']); +const PENDING_EXCLUDE_IN = PENDING_EXCLUDE_STATUS.map((s) => `'${s}'`).join(','); +const PENDING_ORDER_SCOPE = '待发订单(不含已发/取消/拆单/合单),不限日期'; + const STATUS = Object.freeze({ SHORT: 'short', NEED: 'need', @@ -86,7 +100,7 @@ const STATUS_LABELS = Object.freeze({ short: '存储不足', need: '需补货', enough: '拣货位充足', - no_order: '今日无单', + no_order: '无待发单', }); // 排序权重:最紧急的排最前 @@ -125,14 +139,6 @@ function resolveCacheTtlMs() { return num; } -/** 订单日期的窗口边界(仅用日期字符串,避免时区问题;副本 send_date 按中国营业日存储) */ -function orderWindow(orderDate) { - const d = new Date(`${orderDate}T00:00:00Z`); - d.setUTCDate(d.getUTCDate() + 1); - const next = d.toISOString().slice(0, 10); - return { shipFrom: `${orderDate} 00:00:00`, shipTo: `${next} 00:00:00` }; -} - // ── 副本取数 ──────────────────────────────────────────────────────── /** * 按 SKU 聚合库存,并按货位前缀切出拣货位 / 存储位库存。 @@ -156,36 +162,20 @@ async function loadReplicaInventory(warehouse) { return cloudDb.query(sql, { wmsCoId: warehouse.wmsCoId }, { requestTimeout: 600000 }); } -/** 该仓最近的订单日期(线上 + 全民客户 取最大 send_date 日期) */ -async function loadOrderDate(warehouse) { - const rows = await cloudDb.query( - `SELECT MAX(d) AS d FROM ( - SELECT MAX(CONVERT(VARCHAR(10), send_date, 23)) AS d - FROM rpt.jst_Orders WHERE wms_co_id = @wmsCoId - UNION ALL - SELECT MAX(CONVERT(VARCHAR(10), send_date, 23)) AS d - FROM rpt.jst_qmcust_orderlist WHERE wms_co_id = @wmsCoId - ) t`, - { wmsCoId: warehouse.wmsCoId }, - { requestTimeout: 120000 } - ); - return (rows && rows[0] && rows[0].d) || null; -} - -/** 当日(订单日期)各 SKU 的订单量:4 个来源分支,组合装展开为子件。 */ -async function loadTodayOrderMap(warehouse, orderDate) { - if (!orderDate) return new Map(); - const { shipFrom, shipTo } = orderWindow(orderDate); +/** + * 各 SKU 的「待发」订单量:4 个来源分支,组合装展开为子件。 + * 不再按 send_date 窗口(那是「已发」),改为排除非待发状态、不限日期。 + */ +async function loadPendingOrderMap(warehouse) { const sql = ` WITH ship AS ( - -- 1) 线上店铺订单:普通商品行 + -- 1) 线上店铺订单:普通商品行(仅统计待发:排除 已发/取消/删除/拆单/合单) SELECT oi.sku_id AS skuId, SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18,2))) AS orderQty FROM rpt.jst_OrderItem oi JOIN rpt.jst_Orders o ON o.o_id = oi.o_id WHERE o.wms_co_id = @wmsCoId - AND o.send_date >= @shipFrom AND o.send_date < @shipTo - AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled') + AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) AND ISNULL(oi.sku_type, 'normal') <> 'combine' GROUP BY oi.sku_id UNION ALL @@ -196,19 +186,17 @@ async function loadTodayOrderMap(warehouse, orderDate) { JOIN rpt.jst_Orders o ON o.o_id = oi.o_id JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id WHERE o.wms_co_id = @wmsCoId - AND o.send_date >= @shipFrom AND o.send_date < @shipTo - AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled') + AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) AND oi.sku_type = 'combine' GROUP BY ci.src_sku_id UNION ALL - -- 3) 全民客户(B2B 分销)订单:普通商品行(拆单父单 Split 与子单重复,需排除) + -- 3) 全民客户(B2B 分销)订单:普通商品行(排除 已发/取消/删除/拆单/合单) SELECT oi.sku_id AS skuId, SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18,2))) AS orderQty FROM rpt.jst_qmcust_orderlist_item oi JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id WHERE o.wms_co_id = @wmsCoId - AND o.send_date >= @shipFrom AND o.send_date < @shipTo - AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled', 'Split', 'Question') + AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) AND ISNULL(oi.sku_type, 'normal') <> 'combine' GROUP BY oi.sku_id UNION ALL @@ -219,8 +207,7 @@ async function loadTodayOrderMap(warehouse, orderDate) { JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id WHERE o.wms_co_id = @wmsCoId - AND o.send_date >= @shipFrom AND o.send_date < @shipTo - AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled', 'Split', 'Question') + AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) AND oi.sku_type = 'combine' GROUP BY ci.src_sku_id ) @@ -231,7 +218,7 @@ async function loadTodayOrderMap(warehouse, orderDate) { const runQuery = typeof cloudDb.querySlow === 'function' ? cloudDb.querySlow : cloudDb.query; const rows = await runQuery( sql, - { wmsCoId: warehouse.wmsCoId, shipFrom, shipTo }, + { wmsCoId: warehouse.wmsCoId }, { requestTimeout: 300000 } ); const map = new Map(); @@ -243,7 +230,7 @@ async function loadTodayOrderMap(warehouse, orderDate) { } // ── 组装 ──────────────────────────────────────────────────────────── -function buildSkuRows({ invRows, orderMap, orderDate }) { +function buildSkuRows({ invRows, orderMap }) { const rows = []; (invRows || []).forEach((row) => { const skuId = toText(row.skuId, 120); @@ -331,9 +318,8 @@ function buildSummary(rows, orderSkuCount) { } async function loadDataset(warehouse, { refresh = false } = {}) { - const orderDate = await loadOrderDate(warehouse); - // 把订单日期纳入缓存键:跨天后自动失效,无需等 TTL - const cacheKey = `${warehouse.key}::${orderDate || 'none'}`; + // 待发口径不再依赖日期,缓存键只用仓库;跨天靠 TTL 失效即可。 + const cacheKey = warehouse.key; const cached = cacheStore.get(cacheKey); const ttl = resolveCacheTtlMs(); const now = Date.now(); @@ -344,16 +330,16 @@ async function loadDataset(warehouse, { refresh = false } = {}) { const epoch = cacheEpoch; const [invRows, orderMap] = await Promise.all([ loadReplicaInventory(warehouse), - loadTodayOrderMap(warehouse, orderDate).catch(() => new Map()), + loadPendingOrderMap(warehouse).catch(() => new Map()), ]); - const rows = buildSkuRows({ invRows, orderMap, orderDate }); + const rows = buildSkuRows({ invRows, orderMap }); const dataset = { at: now, rows, warehouse, - orderDate, + orderScope: PENDING_ORDER_SCOPE, meta: { rowCount: rows.length, orderSkuCount: orderMap.size, @@ -416,7 +402,7 @@ async function getReplenishList(options = {}) { summary, warehouse, warehouses: Object.values(WAREHOUSES), - orderDate: dataset.orderDate, + orderScope: PENDING_ORDER_SCOPE, cacheHit: dataset.cacheHit, updatedAt: new Date(dataset.at).toISOString(), cacheTtlMs: resolveCacheTtlMs(), @@ -428,13 +414,13 @@ async function getReplenishExportRows(options = {}) { const dataset = await loadDataset(warehouse, { refresh: String(options.refresh) === 'true' }); return { warehouse, - orderDate: dataset.orderDate, + orderScope: PENDING_ORDER_SCOPE, rows: sortRows(filterRows(dataset.rows, options)), summary: buildSummary(dataset.rows, dataset.meta.orderSkuCount), }; } -/** 启动预热:库存视图 + 当日订单 union 各自 30s~1min,不能让首个用户请求等 */ +/** 启动预热:库存视图 + 待发订单 union 各自 30s~1min,不能让首个用户请求等 */ async function warmBinReplenishCache() { try { const warehouse = WAREHOUSES[DEFAULT_WAREHOUSE_KEY]; diff --git a/frontend/src/api/binReplenish.js b/frontend/src/api/binReplenish.js index dd33343..50cd831 100644 --- a/frontend/src/api/binReplenish.js +++ b/frontend/src/api/binReplenish.js @@ -3,7 +3,7 @@ import request from './request'; const LONG_TIMEOUT_MS = 10 * 60 * 1000; // 缺货补货监控(订单驱动版) -// 判断口径:当日订单量 > 拣货位库存 的超出部分 = 需从存储位补到拣货位的量 +// 判断口径:待发订单量 > 拣货位库存 的超出部分 = 需从存储位补到拣货位的量 export const getBinReplenishList = (params = {}) => request.get( '/api/bin-replenish/list', { params, timeout: LONG_TIMEOUT_MS } diff --git a/frontend/src/pages/BinReplenishMonitor/index.jsx b/frontend/src/pages/BinReplenishMonitor/index.jsx index aadd4a8..4188995 100644 --- a/frontend/src/pages/BinReplenishMonitor/index.jsx +++ b/frontend/src/pages/BinReplenishMonitor/index.jsx @@ -19,14 +19,14 @@ const STATUS_TAGS = { short: { color: 'red', label: '存储不足' }, need: { color: 'gold', label: '需补货' }, enough: { color: 'green', label: '拣货位充足' }, - no_order: { color: 'default', label: '今日无单' }, + no_order: { color: 'default', label: '无待发单' }, }; const STATUS_OPTIONS = [ { label: '待补货', value: 'replenish' }, { label: '存储不足', value: 'short' }, { label: '拣货位充足', value: 'enough' }, - { label: '今日无单', value: 'no_order' }, + { label: '无待发单', value: 'no_order' }, { label: '全部', value: 'all' }, ]; @@ -41,7 +41,7 @@ const BinReplenishMonitor = () => { const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(''); const [data, setData] = useState({ - rows: [], total: 0, summary: {}, warehouse: null, warehouses: [], orderDate: '', updatedAt: '', cacheHit: false, + rows: [], total: 0, summary: {}, warehouse: null, warehouses: [], orderScope: '', updatedAt: '', cacheHit: false, }); const keywordTimer = useRef(null); @@ -66,7 +66,7 @@ const BinReplenishMonitor = () => { summary: payload.summary || {}, warehouse: payload.warehouse || null, warehouses: payload.warehouses || [], - orderDate: payload.orderDate || '', + orderScope: payload.orderScope || '', updatedAt: payload.updatedAt || '', cacheHit: Boolean(payload.cacheHit), }); @@ -105,7 +105,7 @@ const BinReplenishMonitor = () => { value: needCount, unit: '个', foot: `建议补货合计 ${num(summary.suggestQtyTotal)} 件`, - tip: '当日订单量 > 拣货位库存:拣货位不够今天的订单拣,需要从存储位把货补过来', + tip: '待发订单量 > 拣货位库存:拣货位不够待发订单拣,需要从存储位把货补过来', }, { key: 'short', @@ -122,24 +122,24 @@ const BinReplenishMonitor = () => { label: '超出订单数合计', value: summary.exceedTotal || 0, unit: '件', - foot: '今日拣货位缺口订单量', - tip: '所有待补货 SKU 的「订单量 − 拣货位库存」之和:这是今天需要从存储位搬到拣货位的总量', + foot: '待发拣货位缺口订单量', + tip: '所有待补货 SKU 的「待发订单量 − 拣货位库存」之和:这是需要从存储位搬到拣货位的总量', }, { key: 'order', tone: 'blue', - label: '当日有单 SKU', + label: '有待发单 SKU', value: summary.orderSkuCount || 0, unit: '个', - foot: `当日订单量 ${num(summary.orderQtyTotal)} 件`, - tip: '统计日当天下过单的商品种类数(含线上店铺 + 全民客户,组合装已展开为子件)', + foot: `待发订单量 ${num(summary.orderQtyTotal)} 件`, + tip: '有待发订单的商品种类数(含线上店铺 + 全民客户,组合装已展开为子件;不含已发/取消/拆单/合单)', }, ]), [summary, needCount]); const handleExport = async () => { try { const response = await exportBinReplenish({ warehouse, status, keyword }); - downloadBlobAsFile(response.data, `${(data.warehouse || {}).shortName || '仓库'}缺货补货监控_${data.orderDate || ''}.xlsx`); + downloadBlobAsFile(response.data, `${(data.warehouse || {}).shortName || '仓库'}缺货补货监控_待发订单.xlsx`); } catch (error_) { const text = await readBlobErrorMessage(error_); message.error(text || '导出失败'); @@ -188,7 +188,7 @@ const BinReplenishMonitor = () => { render: (value) => {num(value)}, }, { - title: '当日订单', + title: '待发订单', dataIndex: 'orderQty', width: 100, align: 'right', @@ -238,8 +238,8 @@ const BinReplenishMonitor = () => {

缺货补货监控

- 盯的是拣货位:今天下的订单,如果拣货位库存不够拣,超出的那部分就要从存储位补到拣货位。 - 库存取自聚水潭「箱及仓位库存」,订单取自崔桥工厂仓当日订单。 + 盯的是拣货位:待发订单如果需要拣的货超过拣货位库存,超出的那部分就要从存储位补到拣货位。 + 库存取自聚水潭「箱及仓位库存」,订单只统计待发(已发货的不再计入,因为货已经发出、不再消耗拣货位)。

@@ -247,15 +247,14 @@ const BinReplenishMonitor = () => { 仓库:{(data.warehouse || {}).name || '简尚-崔桥工厂仓'}
- 订单日期:{data.orderDate || '--'} - (取该仓最近一个有订单的营业日) + 统计口径:{data.orderScope || '待发订单(不含已发/取消/拆单/合单),不限日期'}
取数时间:{data.updatedAt ? new Date(data.updatedAt).toLocaleString('zh-CN') : '--'} {data.cacheHit ? (缓存) : null}
- 共 {num(summary.skuTotal)} 个 SKU,其中今日无单 {num(summary.noOrder)} 个 + 共 {num(summary.skuTotal)} 个 SKU,其中无待发单 {num(summary.noOrder)} 个
@@ -336,7 +335,7 @@ const BinReplenishMonitor = () => { description={( 拣货库位以 XA / XD / XE 开头,存储库位以 XX / X9 / X5 / X4 开头;其余货位(DB / Y* / Z* 等,占比极小)不计入。 - 判断规则:当日订单量 > 拣货位库存 时,超出的订单数 = 需要从存储位补到拣货位的量(上限为存储位现有库存)。 + 判断规则:待发订单量 > 拣货位库存 时,超出的订单数 = 需要从存储位补到拣货位的量(上限为存储位现有库存)。 )} /> @@ -353,7 +352,7 @@ const BinReplenishMonitor = () => { { 「拣货位库存」= 货位号以 XA/XD/XE 开头的库存合计;「存储位库存」= 货位号以 XX/X9/X5/X4 开头的库存合计。
  • - 订单:崔桥工厂仓当日订单 = rpt.jst_Orders(线上店铺)+ rpt.jst_qmcust_orderlist(全民客户 B2B), - 均按 send_date 落在「订单日期」当天;组合装(sku_type=combine)已按 jst_comboProductItem 展开为子件。 + 订单:只统计待发订单 = rpt.jst_Orders(线上店铺)+ rpt.jst_qmcust_orderlist(全民客户 B2B), + 排除 已发(Sent) / 取消(Cancelled) / 删除(Delete) / 拆单(Split) / 合单(Merged);组合装(sku_type=combine)已按 jst_comboProductItem 展开为子件。 + 已发货的订单不计入——货已发出、不再消耗拣货位,不该驱动补货。
  • - 判断规则:对每个 SKU,若 当日订单量 > 拣货位库存,超出的订单数即「需要从存储位补到拣货位」的量; + 判断规则:对每个 SKU,若 待发订单量 > 拣货位库存,超出的订单数即「需要从存储位补到拣货位」的量; 「建议补货量」= min(超出订单数, 存储位库存)。存储位也不够时标记「存储不足」,缺口需采购 / 紧急调拨。
  • - 订单日期:取该仓订单表 send_date 的最大日期(即最近一个有订单的营业日),副本 ETL 有延迟时不会为空; - 页面顶部明确标注统计的是哪一天。 + 统计范围:待发订单不限日期,含历史未发积压。这样最贴近「需要补货去拣的货」; + 已发/取消/拆单/合单一律排除,任何新增的待发状态也会自动落入统计。
  • - 时效:仓位库存视图聚合 + 当日订单 4 分支 union 各需 30s~1min,服务端缓存 10 分钟(启动时已预热); + 时效:仓位库存视图聚合 + 待发订单 4 分支 union 各需 30s~1min,服务端缓存 10 分钟(启动时已预热); 点「刷新数据」会强制回源,需要等 1 分钟左右。
  • From 5b0c44c9e2c8c7b26952d14590c7d9add59e853a Mon Sep 17 00:00:00 2001 From: lunanting Date: Thu, 22 Oct 2026 16:22:02 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E7=BC=BA=E8=B4=A7=E8=A1=A5=E8=B4=A7?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E3=80=8C=E5=BE=85=E5=8F=91=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E3=80=8D=E6=94=B9=E4=B8=BA=E8=81=9A=E6=B0=B4=E6=BD=AD=E6=89=93?= =?UTF-8?q?=E5=8D=95=E6=8B=A3=E8=B4=A7/=E5=8F=91=E8=B4=A7=E4=B8=AD?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loadPendingOrderMap 改用两步 querySlow(先取该仓最新快照日,再按日汇总 status='发货中'), 替换会 60s 超时的 CTE+CROSS JOIN 写法(副本库实测 ~12s 返回 697 SKU/1598 件) - 待发口径对齐用户在聚水潭「打单拣货 → 简尚崔桥工厂仓」看到的发货中单据(每日 07:00 快照) - 仅聚水潭线上店铺渠道,不含全民(qmcust) B2B 客户单(属预期收窄,前端已注明) - 前端口径说明补充渠道范围与「不含全民」提示 --- backend/src/services/binReplenishService.js | 124 +++++++----------- .../src/pages/BinReplenishMonitor/index.jsx | 19 +-- 2 files changed, 61 insertions(+), 82 deletions(-) diff --git a/backend/src/services/binReplenishService.js b/backend/src/services/binReplenishService.js index 018dc4e..4ae0a94 100644 --- a/backend/src/services/binReplenishService.js +++ b/backend/src/services/binReplenishService.js @@ -14,25 +14,24 @@ * * ── 数据源 ─────────────────────────────────────────────────────────── * 库存:rpt.jst_packInventoryInfo(按 wms_co_id + sku_id 聚合,按前缀切拣货/存储) - * 订单:rpt.jst_Orders × jst_OrderItem(线上店铺) - * + rpt.jst_qmcust_orderlist × jst_qmcust_orderlist_item(全民客户 B2B) - * + 组合装(sku_type='combine')→ jst_comboProductItem 展开为子件 + * 订单:rpt.op_OrderNotDispatched(聚水潭「打单拣货/未发货」每日快照,SKU 级、组合装已展开) + * —— 直接对齐用户在聚水潭「打单拣货 → 简尚崔桥工厂仓」看到的单据。 * ⚠️ 仓库维度必须用 **wms_co_id**:wh_id 是货主(简尚=1), * 按 wh_id 会把多仓混成一个桶。 * - * ── 待发口径(用户 2026-10-22 二次明确)────────────────────────────── - * 补货需求只来自「还没发的订单(待发)」,**已发货(Sent)的订单不参与计算** —— - * 已发订单的货已经拣走发出,不再消耗拣货位库存,不该驱动补货。 - * 判定方式:排除掉「已发 / 取消 / 删除 / 拆单 / 合单」这几类非待发状态, - * 其余(WaitConfirm 待确认 / Question 异常挂起 / Delivering 发货中 / 任何新增待发状态) - * 一律按「待发」计入。如此新增的待发状态也会自动落入统计,无需改代码。 - * 待发订单不限日期:含历史未发积压,最贴近「需要补货去拣的货」。 + * ── 待发口径(用户 2026-10-22 三次明确:「取打单拣货里的数据」)────────── + * 补货需求只来自「聚水潭打单拣货 / 发货中」的单据,**已发(Sent)不参与**。 + * 数据源 rpt.op_OrderNotDispatched 是**每日快照**(一天一批,local_updated≈07:00): + * 取该仓 **最新快照日** 且 status='发货中'(历史标签曾为「发货中(打单拣货)」)的行, + * 按 sku_id 汇总 qty —— 即当前待拣/待发的订单量(组合装已展开为子件,带 src_combine_sku_id)。 + * **不含**:已发(Sent)/取消/删除/拆单/合单、异常(Question/异常)、已付款待审核、待付款。 + * (异常挂起单不保证近期发货,不计入补货需求;如需要可再把「异常」并入。) * * ── 判断逻辑(核心)────────────────────────────────────────────────── * 对每一个 SKU(在拣货/存储位上有库存): * 拣货位库存 pickQty = 该 SKU 在 XA/XD/XE 货位上的 qty 合计 * 存储位库存 storageQty = 该 SKU 在 XX/X9/X5/X4 货位上的 qty 合计 - * 待发订单 orderQty = 该 SKU 的「待发」订单量(含组合装展开,不限日期) + * 待发订单 orderQty = 该 SKU 的「打单拣货/发货中」快照待发量(组合装已展开) * 超出订单数 excess = max(0, orderQty − pickQty) * —— 拣货位不够待发订单拣,超出的这部分要从存储位补到拣货位 * 建议补货量 suggestQty = min(excess, storageQty) (不能超过存储位现有量) @@ -45,7 +44,7 @@ * no_order 无待发单(orderQty = 0) * (pickQty = 0 且 orderQty > 0 归入 need/short,并标记 pickEmpty 强调) * - * ⚠️ 性能:库存视图聚合 + 待发订单 4 分支 union 各自都要 30s~1min, + * ⚠️ 性能:库存视图聚合 30s~1min;待发快照表(op_OrderNotDispatched)查询较快; * 必须缓存(默认 10 分钟)+ 启动预热,不要每次请求都回源。 */ @@ -81,13 +80,13 @@ const STORE_PREFIXES = Object.freeze(['XX', 'X9', 'X5', 'X4']); const PICK_PREFIX_IN = PICK_PREFIXES.map((p) => `'${p}'`).join(','); const STORE_PREFIX_IN = STORE_PREFIXES.map((p) => `'${p}'`).join(','); -// ── 待发口径:排除掉「非待发」状态,其余一律算待发 ────────────────────── -// Sent=已发(唯一 100% 带 send_date 的状态)、Cancelled=已取消、Delete=删除、 -// Split=拆单父单(子单另计)、Merged=合单(合后单另计)。 -// 白名单式排除比「只认某几个待发状态」更稳:新增的待发状态会自动落入统计。 -const PENDING_EXCLUDE_STATUS = Object.freeze(['Sent', 'Cancelled', 'Delete', 'Split', 'Merged']); -const PENDING_EXCLUDE_IN = PENDING_EXCLUDE_STATUS.map((s) => `'${s}'`).join(','); -const PENDING_ORDER_SCOPE = '待发订单(不含已发/取消/拆单/合单),不限日期'; +// ── 待发口径:聚水潭「打单拣货/发货中」快照(rpt.op_OrderNotDispatched)──────── +// 该表是每日快照;取该仓「最新快照日」且 status='发货中' 的行。 +// 历史标签在 2026-03 之前叫「发货中(打单拣货)」,两个都认,避免换名漏数。 +// 不含:已发/取消/删除/拆单/合单、异常、已付款待审核、待付款。 +const PENDING_STATUS_LIST = Object.freeze(['发货中', '发货中(打单拣货)']); +const PENDING_STATUS_IN = PENDING_STATUS_LIST.map((s) => `'${s}'`).join(','); +const PENDING_ORDER_SCOPE = '待发订单 = 聚水潭「打单拣货」发货中快照(不含已发/异常/待审核)'; const STATUS = Object.freeze({ SHORT: 'short', @@ -163,63 +162,42 @@ async function loadReplicaInventory(warehouse) { } /** - * 各 SKU 的「待发」订单量:4 个来源分支,组合装展开为子件。 - * 不再按 send_date 窗口(那是「已发」),改为排除非待发状态、不限日期。 + * 各 SKU 的「待发」订单量:直接取聚水潭打单拣货快照表 rpt.op_OrderNotDispatched。 + * 该表 SKU 级、组合装已展开为子件(src_combine_sku_id),qty 即该子件待发件数。 + * 取「该仓最新快照日」+ status IN ('发货中','发货中(打单拣货)'),按 sku_id 汇总。 + * + * ⚠️ 性能红线:副本库 mssql 池默认 60s requestTimeout,CTE+CROSS JOIN 写法实测 60s + * 超时失败。改成**两步 querySlow**(先取最新快照日,再按日范围汇总),实测 ~12s, + * 走独立连接 + 池级 requestTimeout(5 分钟),必须用它。 */ async function loadPendingOrderMap(warehouse) { - const sql = ` - WITH ship AS ( - -- 1) 线上店铺订单:普通商品行(仅统计待发:排除 已发/取消/删除/拆单/合单) - SELECT oi.sku_id AS skuId, - SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18,2))) AS orderQty - FROM rpt.jst_OrderItem oi - JOIN rpt.jst_Orders o ON o.o_id = oi.o_id - WHERE o.wms_co_id = @wmsCoId - AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) - AND ISNULL(oi.sku_type, 'normal') <> 'combine' - GROUP BY oi.sku_id - UNION ALL - -- 2) 线上店铺订单:组合装行 → 展开为子件 - SELECT ci.src_sku_id AS skuId, - SUM(CAST(ISNULL(oi.qty, 0) * ISNULL(ci.qty, 0) AS DECIMAL(18,2))) AS orderQty - FROM rpt.jst_OrderItem oi - JOIN rpt.jst_Orders o ON o.o_id = oi.o_id - JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id - WHERE o.wms_co_id = @wmsCoId - AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) - AND oi.sku_type = 'combine' - GROUP BY ci.src_sku_id - UNION ALL - -- 3) 全民客户(B2B 分销)订单:普通商品行(排除 已发/取消/删除/拆单/合单) - SELECT oi.sku_id AS skuId, - SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18,2))) AS orderQty - FROM rpt.jst_qmcust_orderlist_item oi - JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id - WHERE o.wms_co_id = @wmsCoId - AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) - AND ISNULL(oi.sku_type, 'normal') <> 'combine' - GROUP BY oi.sku_id - UNION ALL - -- 4) 全民客户订单:组合装行 → 展开为子件 - SELECT ci.src_sku_id AS skuId, - SUM(CAST(ISNULL(oi.qty, 0) * ISNULL(ci.qty, 0) AS DECIMAL(18,2))) AS orderQty - FROM rpt.jst_qmcust_orderlist_item oi - JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id - JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id - WHERE o.wms_co_id = @wmsCoId - AND ISNULL(o.status, '') NOT IN (${PENDING_EXCLUDE_IN}) - AND oi.sku_type = 'combine' - GROUP BY ci.src_sku_id - ) - SELECT skuId, SUM(orderQty) AS orderQty - FROM ship - GROUP BY skuId`; - const runQuery = typeof cloudDb.querySlow === 'function' ? cloudDb.querySlow : cloudDb.query; - const rows = await runQuery( - sql, + const slowOpts = { requestTimeout: 300000, connectRequestTimeoutMs: 300000 }; + + // step1:该仓最新快照日(local_updated≈每日 07:00 一批,MAX 落在最新那批) + const snapRows = await runQuery( + `SELECT CAST(MAX(local_updated) AS date) AS snapDate + FROM rpt.op_OrderNotDispatched + WHERE wms_co_id = @wmsCoId`, { wmsCoId: warehouse.wmsCoId }, - { requestTimeout: 300000 } + slowOpts + ); + const snapDate = snapRows && snapRows[0] && snapRows[0].snapDate; + if (!snapDate) return new Map(); + + // step2:最新快照日当天的「发货中」待发量,按 sku_id 汇总(组合装已展开) + const rows = await runQuery( + `SELECT o.sku_id AS skuId, + SUM(CAST(ISNULL(o.qty, 0) AS DECIMAL(18,2))) AS orderQty + FROM rpt.op_OrderNotDispatched o + WHERE o.wms_co_id = @wmsCoId + AND o.status IN (${PENDING_STATUS_IN}) + AND o.local_updated >= @snapDate + AND o.local_updated < DATEADD(day, 1, @snapDate) + AND ISNULL(o.sku_id, '') <> '' + GROUP BY o.sku_id`, + { wmsCoId: warehouse.wmsCoId, snapDate }, + slowOpts ); const map = new Map(); (rows || []).forEach((row) => { @@ -318,7 +296,7 @@ function buildSummary(rows, orderSkuCount) { } async function loadDataset(warehouse, { refresh = false } = {}) { - // 待发口径不再依赖日期,缓存键只用仓库;跨天靠 TTL 失效即可。 + // 缓存键只用仓库(内部仍取「最新快照日」,不固定某天);跨天靠 TTL 失效即可。 const cacheKey = warehouse.key; const cached = cacheStore.get(cacheKey); const ttl = resolveCacheTtlMs(); diff --git a/frontend/src/pages/BinReplenishMonitor/index.jsx b/frontend/src/pages/BinReplenishMonitor/index.jsx index 4188995..a367567 100644 --- a/frontend/src/pages/BinReplenishMonitor/index.jsx +++ b/frontend/src/pages/BinReplenishMonitor/index.jsx @@ -132,7 +132,7 @@ const BinReplenishMonitor = () => { value: summary.orderSkuCount || 0, unit: '个', foot: `待发订单量 ${num(summary.orderQtyTotal)} 件`, - tip: '有待发订单的商品种类数(含线上店铺 + 全民客户,组合装已展开为子件;不含已发/取消/拆单/合单)', + tip: '有待发订单的商品种类数(取聚水潭「打单拣货/发货中」快照,组合装已展开为子件;不含已发/异常/待审核)', }, ]), [summary, needCount]); @@ -247,7 +247,7 @@ const BinReplenishMonitor = () => { 仓库:{(data.warehouse || {}).name || '简尚-崔桥工厂仓'}
    - 统计口径:{data.orderScope || '待发订单(不含已发/取消/拆单/合单),不限日期'} + 统计口径:{data.orderScope || '待发订单 = 聚水潭「打单拣货」发货中快照(不含已发/异常/待审核)'}
    取数时间:{data.updatedAt ? new Date(data.updatedAt).toLocaleString('zh-CN') : '--'} @@ -394,21 +394,22 @@ const BinReplenishMonitor = () => { 「拣货位库存」= 货位号以 XA/XD/XE 开头的库存合计;「存储位库存」= 货位号以 XX/X9/X5/X4 开头的库存合计。
  • - 订单:只统计待发订单 = rpt.jst_Orders(线上店铺)+ rpt.jst_qmcust_orderlist(全民客户 B2B), - 排除 已发(Sent) / 取消(Cancelled) / 删除(Delete) / 拆单(Split) / 合单(Merged);组合装(sku_type=combine)已按 jst_comboProductItem 展开为子件。 - 已发货的订单不计入——货已发出、不再消耗拣货位,不该驱动补货。 + 订单:直接取聚水潭「打单拣货 → 发货中」快照 = rpt.op_OrderNotDispatched(该仓最新快照日、status=发货中), + SKU 级、组合装已展开为子件。这就是你在聚水潭「打单拣货 · 简尚崔桥工厂仓」看到的待发单据。 + 已发货(Sent)、异常挂起、已付款待审核、待付款均不计入——货已发出或短期不会拣,不该驱动补货。 + 口径仅覆盖聚水潭线上店铺渠道(运营中心 / 分销等),不含全民(qmcust) B2B 客户单——与旧「按 send_date 窗口统计当日订单」口径不同,属预期收窄。
  • 判断规则:对每个 SKU,若 待发订单量 > 拣货位库存,超出的订单数即「需要从存储位补到拣货位」的量; 「建议补货量」= min(超出订单数, 存储位库存)。存储位也不够时标记「存储不足」,缺口需采购 / 紧急调拨。
  • - 统计范围:待发订单不限日期,含历史未发积压。这样最贴近「需要补货去拣的货」; - 已发/取消/拆单/合单一律排除,任何新增的待发状态也会自动落入统计。 + 统计范围:取该仓最新一天的「打单拣货」快照(ETL 约每日 07:00 刷新),即当前待拣/待发订单; + 已发/异常/待审核一律排除。
  • - 时效:仓位库存视图聚合 + 待发订单 4 分支 union 各需 30s~1min,服务端缓存 10 分钟(启动时已预热); - 点「刷新数据」会强制回源,需要等 1 分钟左右。 + 时效:仓位库存视图聚合需 30s~1min,待发快照表较快;服务端缓存 10 分钟(启动时已预热); + 点「刷新数据」会强制回源,需要等约 1 分钟。
  • From e43f17fde4bedf3a28317c51a8a8fe7e81139ae5 Mon Sep 17 00:00:00 2001 From: lunanting Date: Fri, 23 Oct 2026 08:35:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E7=BC=BA=E8=B4=A7=E8=A1=A5=E8=B4=A7?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E6=94=B9=E4=B8=BA=E3=80=8C=E5=B4=94=E6=A1=A5?= =?UTF-8?q?=E4=B8=BB=E4=BB=93=C2=B7=E4=BB=85=E5=BD=93=E6=97=A5=E5=BE=85?= =?UTF-8?q?=E8=A1=A5=E3=80=8D=E5=B9=B6=E4=BF=AE=E6=AD=A3=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 看板只展示当日待补 SKU(待发订单>拣货位库存,需存储→拣货补货),去除充足/无待发干扰行 - 库存收紧到崔桥工厂仓主仓(jst_packInventoryInfo 加 wh_id=1),数据源对齐「箱及仓位库存·主仓」 - 补齐「有打单拣货待发单但货位库存视图无记录」的 SKU(拣货位为0也应进待补明细) - 指标卡改为四项:实时代发订单数 / 拣货库存数 / 存储库存数 / 缺货数(缺货=待补SKU数,脚注含存储不足子项) - 各库底待补明细=need+short 行;范围筛「待补货/仅缺货」;移除多仓选择器,固定崔桥 - 待发订单仍取 op_OrderNotDispatched 打单拣货/发货中 每日快照(两步 querySlow) - 副本实测:当日快照待补 45 SKU(need42/short3),待发132件/拣货40件/存储2381件/缺口5件 --- backend/src/api/binReplenish.js | 2 +- backend/src/services/binReplenishService.js | 126 ++++++++++++------ .../src/pages/BinReplenishMonitor/index.jsx | 123 ++++++++--------- 3 files changed, 146 insertions(+), 105 deletions(-) diff --git a/backend/src/api/binReplenish.js b/backend/src/api/binReplenish.js index 98af8b5..beceb61 100644 --- a/backend/src/api/binReplenish.js +++ b/backend/src/api/binReplenish.js @@ -49,7 +49,7 @@ router.get('/export', async (req, res, next) => { keyword: req.query.keyword, status: req.query.status, }); - const fileName = `${payload.warehouse.shortName}缺货补货监控_待发订单`; + const fileName = `${payload.warehouse.shortName}待补明细`; sendWorkbookDownload(res, { fileName, diff --git a/backend/src/services/binReplenishService.js b/backend/src/services/binReplenishService.js index 4ae0a94..6a8ce5d 100644 --- a/backend/src/services/binReplenishService.js +++ b/backend/src/services/binReplenishService.js @@ -155,10 +155,40 @@ async function loadReplicaInventory(warehouse) { FROM rpt.jst_packInventoryInfo p LEFT JOIN rpt.jst_sku s ON s.sku_id = p.sku_id WHERE p.wms_co_id = @wmsCoId + AND p.wh_id = 1 AND ISNULL(p.bin, '') <> '' AND UPPER(LEFT(LTRIM(p.bin), 2)) IN (${PICK_PREFIX_IN}, ${STORE_PREFIX_IN}) GROUP BY p.sku_id`; - return cloudDb.query(sql, { wmsCoId: warehouse.wmsCoId }, { requestTimeout: 600000 }); + // 单仓按货位前缀聚合约 1.7s;走 querySlow(独立连接 + 池级超时)避免 60s 池超时误杀。 + return cloudDb.querySlow(sql, { wmsCoId: warehouse.wmsCoId }, { requestTimeout: 300000 }); +} + +/** + * 批量取 SKU 主数据(品名 / 供应商),用于补齐「有订单但无货位库存」的 SKU 名称。 + * 这批 SKU 不在库存视图里,但确实有打单拣货待发单,必须进待补明细。 + */ +async function loadSkuNames(skuIds) { + const uniq = [...new Set((skuIds || []).filter(Boolean).map((id) => toText(id, 120)))]; + if (!uniq.length) return new Map(); + const map = new Map(); + const CHUNK = 400; + for (let i = 0; i < uniq.length; i += CHUNK) { + const chunk = uniq.slice(i, i + CHUNK); + const params = {}; + const ph = chunk.map((_, idx) => `@s${idx}`).join(','); + chunk.forEach((id, idx) => { params[`s${idx}`] = id; }); + const rows = await cloudDb.querySlow( + `SELECT sku_id AS skuId, name AS productName, supplier_name AS supplier + FROM rpt.jst_sku WHERE sku_id IN (${ph})`, + params, + { requestTimeout: 120000 } + ); + (rows || []).forEach((r) => { + const id = toText(r.skuId, 120); + if (id) map.set(id, { productName: toText(r.productName, 300), supplier: toText(r.supplier, 120) }); + }); + } + return map; } /** @@ -258,40 +288,43 @@ function buildSkuRows({ invRows, orderMap }) { return rows; } -function buildSummary(rows, orderSkuCount) { +/** + * 汇总只统计「待补」SKU(need / short)—— + * 即需要从存储仓位补货到拣货仓位的部分。对应看板四张指标卡: + * 实时代发订单数 = 待补 SKU 待发件数合计 + * 拣货库存数 = 待补 SKU 拣货位库存合计 + * 存储库存数 = 待补 SKU 存储位库存合计 + * 缺货数 = 存储也补不齐的 SKU 数(status=short) + */ +function buildSummary(rows) { const summary = { - skuTotal: rows.length, + skuTotal: 0, need: 0, short: 0, - enough: 0, - noOrder: 0, - pickEmpty: 0, - suggestQtyTotal: 0, - orderSkuCount: orderSkuCount || 0, orderQtyTotal: 0, - exceedTotal: 0, - shortfallTotal: 0, pickTotal: 0, storageTotal: 0, + suggestQtyTotal: 0, + exceedTotal: 0, + shortfallTotal: 0, }; - const STATUS_SUM_KEY = { need: 'need', short: 'short', enough: 'enough', no_order: 'noOrder' }; rows.forEach((row) => { - const key = STATUS_SUM_KEY[row.status]; - if (key && summary[key] !== undefined) summary[key] += 1; - summary.suggestQtyTotal += row.suggestQty || 0; + summary.skuTotal += 1; + if (row.status === STATUS.NEED) summary.need += 1; + else if (row.status === STATUS.SHORT) summary.short += 1; summary.orderQtyTotal += row.orderQty || 0; - summary.exceedTotal += row.excess || 0; - summary.shortfallTotal += row.shortfall || 0; summary.pickTotal += row.pickQty || 0; summary.storageTotal += row.storageQty || 0; - if (row.pickEmpty) summary.pickEmpty += 1; + summary.suggestQtyTotal += row.suggestQty || 0; + summary.exceedTotal += row.excess || 0; + summary.shortfallTotal += row.shortfall || 0; }); - summary.suggestQtyTotal = round2(summary.suggestQtyTotal); summary.orderQtyTotal = round2(summary.orderQtyTotal); - summary.exceedTotal = round2(summary.exceedTotal); - summary.shortfallTotal = round2(summary.shortfallTotal); summary.pickTotal = round2(summary.pickTotal); summary.storageTotal = round2(summary.storageTotal); + summary.suggestQtyTotal = round2(summary.suggestQtyTotal); + summary.exceedTotal = round2(summary.exceedTotal); + summary.shortfallTotal = round2(summary.shortfallTotal); return summary; } @@ -311,7 +344,22 @@ async function loadDataset(warehouse, { refresh = false } = {}) { loadPendingOrderMap(warehouse).catch(() => new Map()), ]); - const rows = buildSkuRows({ invRows, orderMap }); + // 把「有打单拣货待发单、但货位库存视图里没有记录」的 SKU 也补进明细, + // 否则这批最该补货(拣货位为 0)的行会被漏掉。 + const invKeys = new Set((invRows || []).map((r) => toText(r.skuId, 120))); + const orderOnly = [...orderMap.keys()] + .filter((k) => !invKeys.has(k)) + .map((k) => ({ skuId: k, pickQty: 0, storageQty: 0, productName: null, supplier: null })); + const combined = (invRows || []).concat(orderOnly); + if (orderOnly.length) { + const nameMap = await loadSkuNames(orderOnly.map((r) => r.skuId)); + orderOnly.forEach((r) => { + const n = nameMap.get(r.skuId); + if (n) { r.productName = n.productName; r.supplier = n.supplier; } + }); + } + + const rows = buildSkuRows({ invRows: combined, orderMap }); const dataset = { at: now, @@ -334,15 +382,6 @@ function invalidateCache(warehouseKey) { else cacheStore.clear(); } -function matchesStatus(row, status) { - const key = toText(status, 20).toLowerCase(); - if (!key || key === 'all') return true; - if (key === 'action' || key === 'todo' || key === 'replenish') { - return row.status === STATUS.NEED || row.status === STATUS.SHORT; - } - return row.status === key; -} - function sortRows(rows) { return rows.slice().sort((a, b) => ( (STATUS_RANK[b.status] || 0) - (STATUS_RANK[a.status] || 0) @@ -352,10 +391,19 @@ function sortRows(rows) { )); } -function filterRows(rows, { keyword, status }) { +/** + * 只看「待补」数据:需要从存储仓位补货到拣货仓位的 SKU(need / short)。 + * status='short' 时仅保留存储不足(缺货)的 SKU。 + */ +function filterReplenish(rows, { keyword, status }) { const kw = toText(keyword, 120).toLowerCase(); + const onlyShort = toText(status, 12).toLowerCase() === 'short'; return rows.filter((row) => { - if (!matchesStatus(row, status)) return false; + if (onlyShort) { + if (row.status !== STATUS.SHORT) return false; + } else if (row.status !== STATUS.NEED && row.status !== STATUS.SHORT) { + return false; + } if (!kw) return true; return row.skuId.toLowerCase().includes(kw) || String(row.productName || '').toLowerCase().includes(kw) @@ -366,15 +414,16 @@ function filterRows(rows, { keyword, status }) { async function getReplenishList(options = {}) { const warehouse = resolveWarehouse(options.warehouse); const dataset = await loadDataset(warehouse, { refresh: String(options.refresh) === 'true' }); - const filtered = sortRows(filterRows(dataset.rows, options)); - const summary = buildSummary(dataset.rows, dataset.meta.orderSkuCount); + const replenished = filterReplenish(dataset.rows, options); + const sorted = sortRows(replenished); + const summary = buildSummary(replenished); const page = Math.max(1, Number.parseInt(options.page, 10) || 1); const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, Number.parseInt(options.pageSize, 10) || 50)); const start = (page - 1) * pageSize; return { - rows: filtered.slice(start, start + pageSize), - total: filtered.length, + rows: sorted.slice(start, start + pageSize), + total: sorted.length, page, pageSize, summary, @@ -390,11 +439,12 @@ async function getReplenishList(options = {}) { async function getReplenishExportRows(options = {}) { const warehouse = resolveWarehouse(options.warehouse); const dataset = await loadDataset(warehouse, { refresh: String(options.refresh) === 'true' }); + const replenished = filterReplenish(dataset.rows, options); return { warehouse, orderScope: PENDING_ORDER_SCOPE, - rows: sortRows(filterRows(dataset.rows, options)), - summary: buildSummary(dataset.rows, dataset.meta.orderSkuCount), + rows: sortRows(replenished), + summary: buildSummary(replenished), }; } diff --git a/frontend/src/pages/BinReplenishMonitor/index.jsx b/frontend/src/pages/BinReplenishMonitor/index.jsx index a367567..30f1310 100644 --- a/frontend/src/pages/BinReplenishMonitor/index.jsx +++ b/frontend/src/pages/BinReplenishMonitor/index.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Alert, Button, Empty, Input, Segmented, Select, Spin, Table, Tag, Tooltip, + Alert, Button, Empty, Input, Segmented, Spin, Table, Tag, Tooltip, message, } from 'antd'; import { @@ -24,10 +24,7 @@ const STATUS_TAGS = { const STATUS_OPTIONS = [ { label: '待补货', value: 'replenish' }, - { label: '存储不足', value: 'short' }, - { label: '拣货位充足', value: 'enough' }, - { label: '无待发单', value: 'no_order' }, - { label: '全部', value: 'all' }, + { label: '仅缺货', value: 'short' }, ]; const BinReplenishMonitor = () => { @@ -99,40 +96,40 @@ const BinReplenishMonitor = () => { const metrics = useMemo(() => ([ { - key: 'replenish', - tone: 'red', - label: '待补货 SKU', - value: needCount, - unit: '个', - foot: `建议补货合计 ${num(summary.suggestQtyTotal)} 件`, - tip: '待发订单量 > 拣货位库存:拣货位不够待发订单拣,需要从存储位把货补过来', + key: 'order', + tone: 'blue', + label: '实时代发订单数', + value: summary.orderQtyTotal || 0, + unit: '件', + foot: `待补 ${num(summary.skuTotal)} 个 SKU`, + tip: '待补 SKU 的待发订单件数合计(取聚水潭「打单拣货 → 发货中」每日快照,组合装已展开为子件;不含已发/异常/待审核)', + }, + { + key: 'pick', + tone: 'green', + label: '拣货库存数', + value: summary.pickTotal || 0, + unit: '件', + foot: '待补 SKU 拣货位库存', + tip: '待补 SKU 在 XA / XD / XE 拣货位上的库存合计(库存取自聚水潭「箱及仓位库存 · 简尚-崔桥工厂仓 主仓」)', + }, + { + key: 'store', + tone: 'orange', + label: '存储库存数', + value: summary.storageTotal || 0, + unit: '件', + foot: '待补 SKU 存储位库存', + tip: '待补 SKU 在 XX / X9 / X5 / X4 存储位上的库存合计(同上来源,主仓)', }, { key: 'short', - tone: 'grey', - label: '其中存储不足', - value: summary.short || 0, + tone: 'red', + label: '缺货数', + value: summary.skuTotal || 0, unit: '个', - foot: `存储缺口 ${num(summary.shortfallTotal)} 件`, - tip: '拣货位缺货,且存储位也不够补(存储缺口 > 0):光靠库内调拨补不齐,需要采购 / 紧急调拨', - }, - { - key: 'exceed', - tone: 'orange', - label: '超出订单数合计', - value: summary.exceedTotal || 0, - unit: '件', - foot: '待发拣货位缺口订单量', - tip: '所有待补货 SKU 的「待发订单量 − 拣货位库存」之和:这是需要从存储位搬到拣货位的总量', - }, - { - key: 'order', - tone: 'blue', - label: '有待发单 SKU', - value: summary.orderSkuCount || 0, - unit: '个', - foot: `待发订单量 ${num(summary.orderQtyTotal)} 件`, - tip: '有待发订单的商品种类数(取聚水潭「打单拣货/发货中」快照,组合装已展开为子件;不含已发/异常/待审核)', + foot: `其中存储不足 ${num(summary.short)} 个,缺口 ${num(summary.shortfallTotal)} 件`, + tip: '需要从存储位补到拣货位的 SKU 数(待发订单量 > 拣货位库存);其中存储位也补不齐的标为「缺货」,需采购 / 紧急调拨', }, ]), [summary, needCount]); @@ -238,8 +235,9 @@ const BinReplenishMonitor = () => {

    缺货补货监控

    - 盯的是拣货位:待发订单如果需要拣的货超过拣货位库存,超出的那部分就要从存储位补到拣货位。 - 库存取自聚水潭「箱及仓位库存」,订单只统计待发(已发货的不再计入,因为货已经发出、不再消耗拣货位)。 + 本看板只盯崔桥工厂仓(主仓)的当日待补数据:也就是那些待发订单超过拣货位库存、 + 需要把货从存储位补到拣货位的 SKU。订单取自聚水潭「打单拣货 → 简尚-崔桥工厂仓」的发货中单据, + 库存取自聚水潭「箱及仓位库存 → 简尚-崔桥工厂仓 主仓」。已发货的不再计入(货已发出、不再消耗拣货位)。

    @@ -254,7 +252,7 @@ const BinReplenishMonitor = () => { {data.cacheHit ? (缓存) : null}
    - 共 {num(summary.skuTotal)} 个 SKU,其中无待发单 {num(summary.noOrder)} 个 + 共 {num(summary.skuTotal)} 个待补 SKU,其中存储不足(缺货){num(summary.short)} 个
    @@ -280,15 +278,7 @@ const BinReplenishMonitor = () => {
    仓库 -