配件库存监控:未配置MKH 不进任何缺货/预警

业务确认(2026-09-17):未配置MKH 的配件不属于模块化体系,预警列一律显示「正常」,
既不报缺货(实时库存<=0)也不报预警(可用天数<=7)。

实现:
- 新增 buildWarningOrderSql() 统一表达式(0缺货/1预警/2正常),未配置MKH(cfg.configured=0) 直接判 2。
- 列表映射 mapRow 改用 SQL 算好的 warningOrder 推导 warningLevel(删除重复 normalizeWarningLevel,
  避免前后端各算一份导致排序与显示不一致)。
- 汇总计数 buildSummary.outOfStockCount / lowStockDaysCount 改用统一表达式,不再计入未配置MKH。
- 筛选 buildFilters:按预警等级筛选、onlyWarning=1(仅看缺货/预警)均排除未配置MKH。

实测(接口直跑,目标仓 794 SKU):
- 未配置MKH 95 个全部判「正常」(其中 55 个库存=0、58 个可用天数<=7,原会预警)。
- summary.outOfStockCount=13 / lowStockDaysCount=3,与已配置MKH 页面分布一致。
- onlyWarning=1 返回 16 条,0 条未配置MKH。
This commit is contained in:
yanhui 2026-09-17 16:41:25 +08:00
parent 0bb7c5905d
commit bc8c46d5be

View File

@ -41,7 +41,8 @@ const WAREHOUSE_WMS_CO_ID = 15718196;
const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 200;
const DEFAULT_SAFETY_DAYS = 15;
// 预警阈值:库存可用天数 <= 7 天即预警
// 预警阈值:库存可用天数 <= 7 天即预警(缺货 = 实时库存 <= 0)
// ⚠️ 2026-09-17 业务确认:未配置MKH 的配件不进任何预警(见 buildWarningOrderSql)
const LOW_STOCK_DAYS_THRESHOLD = 7;
const WARNING_LEVEL = Object.freeze({
@ -93,6 +94,22 @@ const buildConfiguredExistsSql = () => (
)`
);
/**
* 统一的「预警等级(数值)」表达式(2026-09-17 业务确认:未配置MKH 不进任何预警)。
* 0 = 缺货 / 1 = 预警 / 2 = 正常
* 未配置MKH(cfg.configured = 0)一律判为「正常」,不报缺货也不报预警。
* 注意 cfg / inv / m 三个别名都来自 buildBaseSql 的 OUTER APPLY,WHERE / SELECT 中均可引用。
* 列表映射、汇总计数、筛选、排序四处共用此表达式,避免口径不一致。
*/
const buildWarningOrderSql = () => (
`CASE
WHEN cfg.configured = 0 THEN 2
WHEN COALESCE(inv.qty, m.jsStock, 0) <= 0 THEN 0
WHEN m.jsUseDays IS NOT NULL AND m.jsUseDays <= ${LOW_STOCK_DAYS_THRESHOLD} THEN 1
ELSE 2
END`
);
const buildBaseSql = () => `
FROM dbo.jst_sku s
OUTER APPLY (
@ -139,16 +156,6 @@ const toText = (value) => {
return text === '' ? null : text;
};
const normalizeWarningLevel = (realtimeStock, stockDays) => {
const stock = Number(realtimeStock || 0);
if (stock <= 0) return WARNING_LEVEL.OUT_OF_STOCK;
const days = toNumber(stockDays);
if (days !== null && days <= LOW_STOCK_DAYS_THRESHOLD) return WARNING_LEVEL.WARNING;
return WARNING_LEVEL.NORMAL;
};
const WARNING_ORDER = {
[WARNING_LEVEL.OUT_OF_STOCK]: 0,
[WARNING_LEVEL.WARNING]: 1,
@ -209,20 +216,14 @@ const buildFilters = (query = {}, monitorDate = null) => {
const warningLevel = toText(query.warningLevel);
if (warningLevel && Object.prototype.hasOwnProperty.call(WARNING_ORDER, warningLevel)) {
params.warningOrder = WARNING_ORDER[warningLevel];
conditions.push(`
CASE
WHEN COALESCE(inv.qty, m.jsStock, 0) <= 0 THEN 0
WHEN m.jsUseDays IS NOT NULL AND m.jsUseDays <= ${LOW_STOCK_DAYS_THRESHOLD} THEN 1
ELSE 2
END = @warningOrder`);
// 用统一表达式,未配置MKH 不参与预警判定(筛「缺货 / 预警」时不会漏出未配置MKH)
conditions.push(`${buildWarningOrderSql()} = @warningOrder`);
}
const onlyWarning = String(query.onlyWarning || '').toLowerCase();
if (['1', 'true', 'yes'].includes(onlyWarning)) {
conditions.push(`(
COALESCE(inv.qty, m.jsStock, 0) <= 0
OR (m.jsUseDays IS NOT NULL AND m.jsUseDays <= ${LOW_STOCK_DAYS_THRESHOLD})
)`);
// 仅看缺货 / 预警:统一表达式 <= 1(未配置MKH 一律为 2,自动排除)
conditions.push(`${buildWarningOrderSql()} <= 1`);
}
return { conditions, params };
@ -283,7 +284,14 @@ const mapRow = (row = {}) => {
const realtimeStock = Number(row.realtimeStock || 0);
const stockDays = toNumber(row.stockDays);
const safetyDays = toNumber(row.safetyDays) ?? DEFAULT_SAFETY_DAYS;
const warningLevel = normalizeWarningLevel(realtimeStock, stockDays);
// 预警等级以 SQL 端 buildWarningOrderSql() 的判定为准(已含「未配置MKH 不进预警」规则),
// 前端显示与排序口径保持一致,避免前后端各算一份。
const warningOrder = Number(row.warningOrder ?? WARNING_ORDER[WARNING_LEVEL.NORMAL]);
const warningLevel = warningOrder === WARNING_ORDER[WARNING_LEVEL.OUT_OF_STOCK]
? WARNING_LEVEL.OUT_OF_STOCK
: warningOrder === WARNING_ORDER[WARNING_LEVEL.WARNING]
? WARNING_LEVEL.WARNING
: WARNING_LEVEL.NORMAL;
return {
skuId: toText(row.skuId),
@ -292,7 +300,7 @@ const mapRow = (row = {}) => {
// 图片优先取产品资料系统数据库(业务库 pic/pic_big 目标仓全为空)
imageUrl: toText(row.productImageUrl) || toText(row.imageUrl),
warningLevel,
warningOrder: WARNING_ORDER[warningLevel],
warningOrder,
mkhStatus: toText(row.mkhStatus) || MKH_STATUS.UNCONFIGURED,
// 业务库无出入库流水表:入库取采购到货(监控日),出库取 pm_stockMonitor 近一日发货量
inQtyToday: toNumber(row.inQtyToday) ?? 0,
@ -326,11 +334,7 @@ const buildProjection = () => `
cost.pictureSkucode
) AS imageUrl,
CASE WHEN cfg.configured = 1 THEN N'已配置MKH' ELSE N'未配置MKH' END AS mkhStatus,
CASE
WHEN COALESCE(inv.qty, m.jsStock, 0) <= 0 THEN 0
WHEN m.jsUseDays IS NOT NULL AND m.jsUseDays <= ${LOW_STOCK_DAYS_THRESHOLD} THEN 1
ELSE 2
END AS warningOrder,
${buildWarningOrderSql()} AS warningOrder,
m.date AS monitorDate`;
async function getModularStockList(query = {}) {
@ -395,9 +399,9 @@ async function buildSummary(whereSql, params) {
const rows = await cloudDatabase.query(
`SELECT
COUNT_BIG(*) AS skuCount,
SUM(CASE WHEN COALESCE(inv.qty, m.jsStock, 0) <= 0 THEN 1 ELSE 0 END) AS outOfStockCount,
SUM(CASE WHEN ${buildWarningOrderSql()} = 0 THEN 1 ELSE 0 END) AS outOfStockCount,
CAST(ISNULL(SUM(COALESCE(inv.qty, m.jsStock, 0)), 0) AS BIGINT) AS totalStock,
SUM(CASE WHEN m.jsUseDays IS NOT NULL AND m.jsUseDays <= ${LOW_STOCK_DAYS_THRESHOLD} THEN 1 ELSE 0 END) AS lowStockDaysCount,
SUM(CASE WHEN ${buildWarningOrderSql()} = 1 THEN 1 ELSE 0 END) AS lowStockDaysCount,
SUM(CASE WHEN cfg.configured = 1 THEN 1 ELSE 0 END) AS configuredCount,
MAX(m.date) AS monitorDate
${whereSql}`,