配件库存监控:已配置MKH改为"被MKH模块化成品引用"判定

- 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)。
This commit is contained in:
yanhui 2026-09-17 16:01:20 +08:00
parent 75a8795723
commit e6ea1246f9
2 changed files with 30 additions and 97 deletions

View File

@ -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 }

View File

@ -157,7 +157,7 @@ const AccessoryStockPanel = () => {
{
title: <TipTitle
title="状态"
tip="该商品是否已在「产品资料系统数据库」中建立商品档案。已配置MKH = 产品资料库中能查到该商品编码的商品资料;未配置MKH = 产品资料库中查不到该商品档案"
tip="该商品是否被 MKH 模块化成品作为配件引用。已配置MKH = 业务库 jst_comboProductItem 中 src_sku_id 命中该商品编码且 combine_sku_id 以 MKH 开头(被某 MKH 组合装/成品引用为配件);未配置MKH = 未被任何 MKH 模块化成品引用"
/>,
dataIndex: 'mkhStatus',
width: 110,