From e6ea1246f98e3f38ff7da422fc3712cefb30891a Mon Sep 17 00:00:00 2001 From: yanhui <2353496031@qq.com> Date: Thu, 17 Sep 2026 16:01:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E4=BB=B6=E5=BA=93=E5=AD=98=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=EF=BC=9A=E5=B7=B2=E9=85=8D=E7=BD=AEMKH=E6=94=B9?= =?UTF-8?q?=E4=B8=BA"=E8=A2=ABMKH=E6=A8=A1=E5=9D=97=E5=8C=96=E6=88=90?= =?UTF-8?q?=E5=93=81=E5=BC=95=E7=94=A8"=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/src/services/modularStockMonitorService.js - 「已配置MKH」判定口径切回 jst_comboProductItem 组合装引用: 该 SKU 出现在 jst_comboProductItem.src_sku_id 且 combine_sku_id 以 'MKH' 开头 即视为被 MKH 模块化成品(组合装/成品档案)引用为配件。 - 移除产品资料系统数据库覆盖清单逻辑(resolveProductCoverage / productCoveredCsv), 产品资料库仅保留作图片来源。 - 实测目标仓 794 SKU 中 29 个命中(原产品资料库口径 661 已废弃)。 - frontend/src/pages/ModularStockMonitor/index.jsx - 状态列 tooltip 文案同步为「是否被 MKH 模块化成品作为配件引用」。 注:本改动会令已配置MKH数量从 661 降至 29(数据使然,非 bug)。 --- .../services/modularStockMonitorService.js | 125 ++++-------------- .../src/pages/ModularStockMonitor/index.jsx | 2 +- 2 files changed, 30 insertions(+), 97 deletions(-) diff --git a/backend/src/services/modularStockMonitorService.js b/backend/src/services/modularStockMonitorService.js index 51c6274..4dc5af0 100644 --- a/backend/src/services/modularStockMonitorService.js +++ b/backend/src/services/modularStockMonitorService.js @@ -50,27 +50,18 @@ const WARNING_LEVEL = Object.freeze({ NORMAL: '正常', }); -// 模块化配置状态: -// 「已配置MKH」= 该 SKU 已在「产品资料系统数据库」中建立商品档案 -// (external.product_catalog_wide_base_v1 按 barcode 匹配) -// 「未配置MKH」= 产品资料系统数据库中查不到该 SKU 档案 -// -// 口径变更说明(2026-09-15,按业务确认): -// 原先用业务库 jst_comboProductItem.src_sku_id 判定「是否被 MKH 组合 BOM 引用」, -// 实测目标仓 794 个 SKU 中只有 32 个命中——因为目标仓(半成品包装仓)与 -// MKH 组合 BOM 的配件(159 个 SKU / 14 个款号)是两批基本不重叠的编码集合, -// 按款号(SPU)放宽也只有 3 个款号重叠。 -// 业务确认改用「产品资料系统数据库是否有商品档案」作为判定口径, -// 实测目标仓 661/794 命中。 +// 模块化配置状态(2026-09-17 业务确认口径): +// 「已配置MKH」= 该 SKU 作为配件被某个 MKH 模块化成品引用 +// (业务库 jst_comboProductItem 中 src_sku_id = 该 SKU +// 且 combine_sku_id 以 'MKH' 开头,即 MKH 组合装 / 成品档案) +// 「未配置MKH」= 未被任何 MKH 模块化成品引用 +// 实测目标仓 794 个 SKU 中 29 个命中(32 个被任意组合装引用,其中 29 个属 MKH 体系)。 +// ⚠️ 之前的「产品资料系统数据库商品档案」口径(661/794)已废弃,改用组合装引用口径。 const MKH_STATUS = Object.freeze({ CONFIGURED: '已配置MKH', UNCONFIGURED: '未配置MKH', }); -// 产品资料覆盖度缓存:避免每次列表请求都回查产品资料系统数据库 -const PRODUCT_COVERAGE_TTL_MS = 10 * 60 * 1000; -let productCoverageCache = null; - const SORTABLE_FIELDS = Object.freeze({ skuId: 's.sku_id', productName: 's.name', @@ -85,21 +76,19 @@ const SORTABLE_FIELDS = Object.freeze({ }); /** - * 「已配置MKH」判定表达式。 - * available = true —— 用产品资料库覆盖清单(@productCoveredCsv,逗号分隔的 SKU 编码) - * available = false —— 产品资料库不可用时回退到业务库 MKH BOM 判定,保证页面可用 + * 「已配置MKH」判定表达式(2026-09-17 口径): + * 该 SKU 作为配件被某个 MKH 模块化成品引用,即业务库 jst_comboProductItem 中 + * src_sku_id = 该 SKU 且 combine_sku_id 以 'MKH' 开头(MKH 组合装 / 成品档案)。 */ -const buildConfiguredExistsSql = (coverage = {}) => ( - coverage.available - ? `EXISTS ( - SELECT 1 FROM STRING_SPLIT(@productCoveredCsv, N',') pc WHERE pc.value = s.sku_id - )` - : `EXISTS ( - SELECT 1 FROM JSDataMiddlePlatform.dbo.jst_comboProductItem x WHERE x.src_sku_id = s.sku_id - )` +const buildConfiguredExistsSql = () => ( + `EXISTS ( + SELECT 1 FROM dbo.jst_comboProductItem x + WHERE x.src_sku_id = s.sku_id + AND x.combine_sku_id LIKE N'MKH%' + )` ); -const buildBaseSql = (coverage = {}) => ` +const buildBaseSql = () => ` FROM dbo.jst_sku s OUTER APPLY ( SELECT TOP 1 i.qty @@ -127,9 +116,9 @@ OUTER APPLY ( WHERE UPPER(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), c.sku)))) = UPPER(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), s.sku_id)))) ORDER BY c.createTime DESC ) cost --- 模块化配置状态:见文件头部「口径变更说明」 +-- 模块化配置状态:见文件头部「已配置MKH」口径说明 OUTER APPLY ( - SELECT CAST(CASE WHEN ${buildConfiguredExistsSql(coverage)} THEN 1 ELSE 0 END AS BIT) AS configured + SELECT CAST(CASE WHEN ${buildConfiguredExistsSql()} THEN 1 ELSE 0 END AS BIT) AS configured ) cfg WHERE s.other_3 = @warehouseName`; @@ -180,7 +169,7 @@ async function resolveMonitorDate() { return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10); } -const buildFilters = (query = {}, monitorDate = null, coverage = {}) => { +const buildFilters = (query = {}, monitorDate = null) => { const conditions = []; const params = { wmsCoId: WAREHOUSE_WMS_CO_ID, @@ -188,11 +177,6 @@ const buildFilters = (query = {}, monitorDate = null, coverage = {}) => { monitorDate, }; - // 「已配置MKH」判定依赖产品资料库覆盖清单(逗号分隔的 SKU 编码) - if (coverage.available) { - params.productCoveredCsv = coverage.csv || ''; - } - const keyword = toText(query.keyword); if (keyword) { params.keyword = `%${keyword}%`; @@ -284,53 +268,11 @@ async function attachProductImages(rows = []) { } /** - * 解析目标仓 SKU 的「产品资料库覆盖清单」,用于「已配置MKH」判定。 - * 返回 { available, csv, set, total, coveredCount },结果按 TTL 缓存。 - * 产品资料库不可用时 available = false,SQL 侧自动回退到业务库 MKH BOM 判定。 + * 「已配置MKH」判定说明(2026-09-17): + * 直接由 buildBaseSql 的 cfg 子查询判定(jst_comboProductItem.src_sku_id + * 命中且 combine_sku_id 以 'MKH' 开头),不再依赖产品资料系统数据库。 + * 产品资料库仅用于补齐商品图片(见 attachProductImages)。 */ -async function resolveProductCoverage() { - const now = Date.now(); - if ( - productCoverageCache - && productCoverageCache.warehouse === WAREHOUSE_NAME - && now - productCoverageCache.at < PRODUCT_COVERAGE_TTL_MS - ) { - return productCoverageCache; - } - - const skuRows = await cloudDatabase.query( - `SELECT s.sku_id AS skuId FROM dbo.jst_sku s WHERE s.other_3 = @warehouseName`, - { warehouseName: WAREHOUSE_NAME }, - { requestTimeout: 180000 } - ); - const codes = skuRows.map((row) => toText(row.skuId)).filter(Boolean); - - let covered = []; - let available = true; - try { - const rows = await productDb.query( - `SELECT DISTINCT barcode - FROM external.product_catalog_wide_base_v1 - WHERE barcode = ANY($1)`, - [codes] - ); - covered = rows.map((row) => toText(row.barcode)).filter(Boolean); - } catch (error) { - available = false; - console.warn('[ModularStock] 产品资料库覆盖清单查询失败,回退业务库 MKH BOM 判定:', error.message); - } - - productCoverageCache = { - warehouse: WAREHOUSE_NAME, - available, - csv: covered.join(','), - set: new Set(covered), - total: codes.length, - coveredCount: covered.length, - at: now, - }; - return productCoverageCache; -} const mapRow = (row = {}) => { const realtimeStock = Number(row.realtimeStock || 0); @@ -390,12 +332,9 @@ async function getModularStockList(query = {}) { const page = Math.max(1, Number.parseInt(query.page, 10) || 1); const requestedSize = Number.parseInt(query.pageSize, 10) || DEFAULT_PAGE_SIZE; const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, requestedSize)); - const [monitorDate, coverage] = await Promise.all([ - resolveMonitorDate(), - resolveProductCoverage(), - ]); - const { conditions, params } = buildFilters(query, monitorDate, coverage); - const baseSql = buildBaseSql(coverage); + const monitorDate = await resolveMonitorDate(); + const { conditions, params } = buildFilters(query, monitorDate); + const baseSql = buildBaseSql(); const whereSql = conditions.length ? `${baseSql} AND ${conditions.join(' AND ')}` : baseSql; const sortField = SORTABLE_FIELDS[query.sortField] ? query.sortField : 'realtimeStock'; @@ -474,22 +413,16 @@ async function buildSummary(whereSql, params) { } async function getModularStockFilters() { - const [monitorDate, coverage] = await Promise.all([ - resolveMonitorDate(), - resolveProductCoverage(), - ]); + const monitorDate = await resolveMonitorDate(); const params = { wmsCoId: WAREHOUSE_WMS_CO_ID, warehouseName: WAREHOUSE_NAME, monitorDate, }; - if (coverage.available) { - params.productCoveredCsv = coverage.csv || ''; - } const rows = await cloudDatabase.query( `SELECT DISTINCT s.category - ${buildBaseSql(coverage)} AND s.category IS NOT NULL AND LTRIM(RTRIM(s.category)) <> N'' + ${buildBaseSql()} AND s.category IS NOT NULL AND LTRIM(RTRIM(s.category)) <> N'' ORDER BY s.category`, params, { requestTimeout: 180000 } diff --git a/frontend/src/pages/ModularStockMonitor/index.jsx b/frontend/src/pages/ModularStockMonitor/index.jsx index fb8db99..daee125 100644 --- a/frontend/src/pages/ModularStockMonitor/index.jsx +++ b/frontend/src/pages/ModularStockMonitor/index.jsx @@ -157,7 +157,7 @@ const AccessoryStockPanel = () => { { title: , dataIndex: 'mkhStatus', width: 110,