From 5b0c44c9e2c8c7b26952d14590c7d9add59e853a Mon Sep 17 00:00:00 2001 From: lunanting Date: Thu, 22 Oct 2026 16:22:02 +0800 Subject: [PATCH] =?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 分钟。