From e43f17fde4bedf3a28317c51a8a8fe7e81139ae5 Mon Sep 17 00:00:00 2001 From: lunanting Date: Fri, 23 Oct 2026 08:35:11 +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=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 = () => {
仓库 -