diff --git a/backend/src/services/returnStockWarehouseRegistry.js b/backend/src/services/returnStockWarehouseRegistry.js index ab3a110..38c2f45 100644 --- a/backend/src/services/returnStockWarehouseRegistry.js +++ b/backend/src/services/returnStockWarehouseRegistry.js @@ -14,13 +14,20 @@ * · returnBinName = 用户口径的聚水潭仓位名,写进页面/导出的「数据来源」,便于核对; * · returnSlot = 副本里真正取数的那一列,必须与上面的仓位名一一对应。 * - * 仓位名 → 库存位 对照(2026-09-25 复测,条件 ISNULL(col,0) > 0): - * 箱及仓位库存 · 简尚-崔桥工厂仓 · 崔桥工厂客退临时上架仓 customize_qty_1 2 编码 / 2 件 ⚠ - * 箱及仓位库存 · 简尚-新苏北仓 · 新苏北客退上架仓 customize_qty_1 75 编码 / 362 件 - * 箱及仓位库存 · 简尚-睢宁仓 · 睢宁客退上架仓 customize_qty_1 98 编码 / 243 件 - * 箱及仓位库存 · 简尚-山东青岗集仓 · 青岗集客退上架仓 customize_qty_2 403 编码 / 912 件 + * 仓位名 → 库存位 对照(2026-09-27 复测,条件 ISNULL(col,0) > 0): + * 箱及仓位库存 · 简尚-崔桥工厂仓 · 崔桥工厂客退临时上架仓 customize_qty_1 349 编码 / 507 件 + * 箱及仓位库存 · 简尚-新苏北仓 · 新苏北客退上架仓 customize_qty_1 75 编码 / 362 件 + * 箱及仓位库存 · 简尚-睢宁仓 · 睢宁客退上架仓 customize_qty_1 98 编码 / 243 件 + * 箱及仓位库存 · 简尚-山东青岗集仓 · 青岗集客退上架仓 customize_qty_2 403 编码 / 912 件 * 箱及仓位库存 · 简尚-河南洛宁仓 · 河南洛宁客退上架仓 三位全为 0(无数据) * + * ── 2026-09-27 重要更新:崔桥工厂仓的客退库存已经同步进副本了 ── + * 09-25 首次切换时该仓客退位只有 2 编码 / 2 件(当时判定「未同步进副本」), + * 09-26 起 IT 侧补上了同步:09-26 当天写入 317 编码 / 460 件,09-27 又补 31 编码 / 46 件, + * 09-27 10:11 实测 349 编码 / 507 件(= 副本 c1 合计,页面与副本完全对得上)。 + * ⚠ 所以下面「崔桥必然近乎空白 / 只能靠导入」的旧结论已经作废,不要再拿它当依据。 + * 现在这张看板能正常出数,**不需要**用导入功能兜底。 + * * ⚠ 崔桥工厂仓(15038426)是 2026-09-25 用户指定替换横林二仓的。复测结论: * 副本里该仓客退位只有 2 编码 / 2 件,但退货单量很大(ERP 售后单 3,115 条 + * 奇门退款单 1,812 条,warehouse='简尚-崔桥工厂仓销退仓')——即「退货有,客退上架库存 @@ -58,10 +65,8 @@ const WAREHOUSE_LIST = [ returnBinName: '崔桥工厂客退临时上架仓', // 客退临时上架仓占用的自定义库存位 returnSlot: 'customize_qty_1', - // ⚠ 2026-09-25 实测:该仓在副本 jst_inventoryInfo 里客退位只有 2 个编码 / 2 件 - //(qty 总库存 92,818 件;return_qty / c2 / c3 / sale_refund_qty 全为 0)。 - // 退货单是有的(ERP 售后单 3,115 条 + 奇门退款单 1,812 条), - // 说明是「客退上架仓」这一仓位没同步进副本,不是业务上没有退货。 + // 2026-09-27 复测:客退位已同步进副本 —— 349 编码 / 507 件(c1 合计), + // 退货单口径:ERP 售后单 3,115 条 + 奇门退款单 1,812 条(warehouse='简尚-崔桥工厂仓销退仓')。 hasReturnStock: true, }, { diff --git a/backend/src/services/returnStockWarningService.js b/backend/src/services/returnStockWarningService.js index d13a146..1ef2960 100644 --- a/backend/src/services/returnStockWarningService.js +++ b/backend/src/services/returnStockWarningService.js @@ -367,10 +367,16 @@ const buildFullSql = (createdAtBasis = DEFAULT_CREATED_AT_BASIS, warehouse = DEF ]; if (useFirstInbound) selectColumns.push('f.first_inbound_time AS [firstInboundTime]'); selectColumns.push("ISNULL(sk.supplier_name, N'') AS [supplier]"); + // 供应商是否合作:取自简尚供应商主数据 js_supplier.coopersgn(1=合作 / 2=不合作) + selectColumns.push('sp.coopersgn AS [supplierCoopSign]'); // 单价取自商品资料 jst_sku:成本价优先,缺失/为 0 时回退采购价(见 pickUnitPrice) selectColumns.push('sk.cost_price AS [costPrice]'); selectColumns.push('sk.purchase_price AS [purchasePrice]'); const joins = ['LEFT JOIN dbo.jst_sku sk ON sk.sku_id = s.product_code']; + // ⚠ 关联键必须是 scmname(供应商简称),与 jst_sku.supplier_name 是同一套命名(2026-09-28 实测 + // 匹配 100/109);tradebody(工商全称)只能匹配 1 条,别用。scmname 在 js_supplier 里唯一 + // (1,497 行 / 1,497 个不同值),所以 LEFT JOIN 不会放大行数。 + joins.push('LEFT JOIN dbo.js_supplier sp ON sp.scmname = sk.supplier_name'); if (useFirstInbound) joins.push('LEFT JOIN first_in f ON f.product_code = s.product_code'); return ` @@ -392,7 +398,13 @@ const buildStockSyncedSql = (warehouse = DEFAULT_WAREHOUSE) => ` FROM dbo.jst_inventoryInfo WHERE wms_co_id = ${warehouse.wmsCoId}`; -const CACHE_TTL_MS = 10 * 60 * 1000; +// 默认 5 分钟:副本侧 ETL 约 10 分钟一批增量(2026-09-27 实测), +// 这里再压一层缓存只是防止多人反复翻页把副本打爆;想更快用 env 调,或前端点「刷新数据」强制回源。 +const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; +const resolveCacheTtlMs = () => { + const raw = Number.parseInt(process.env.RETURN_STOCK_WARNING_CACHE_TTL_MS, 10); + return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_CACHE_TTL_MS; +}; // 缓存键 = 仓库 + 创建时间口径(两者都会改变 SQL / 取到的行) const buildCacheKey = (warehouse, basis) => `${warehouse.key}|${basis}`; const rowCache = new Map(); @@ -437,7 +449,7 @@ async function loadReplicaRows({ /** 副本库存表最近一次同步时间(YYYY-MM-DD HH:mm:ss),页面「库存更新时间」用 */ async function loadStockSyncedAt({ refresh = false, warehouse = DEFAULT_WAREHOUSE } = {}) { const state = stockSyncedAtState.get(warehouse.key); - if (!refresh && state?.value && Date.now() - state.at < CACHE_TTL_MS) { + if (!refresh && state?.value && Date.now() - state.at < resolveCacheTtlMs()) { return state.value; } try { @@ -567,6 +579,24 @@ const resolveReturnLimitDays = (value) => { * 其余 → pending,returnRemainDays = 剩余天数(时限 − 在库天数) * 拿不到创建时间(算不出在库天数)时按 pending 处理,剩余天数留空,页面提示「待补创建时间」。 */ +// --------------------------------------------------------------------------- +// 供应商是否合作(2026-09-28 新增) +// 依据:简尚供应商主数据 dbo.js_supplier.coopersgn —— 1 = 合作中,2 = 不合作。 +// 语义已交叉验证:与 dbo.fys_supplier.IsCoop('合作'/'不合作')比对, +// coopersgn=2 ↔ 不合作 823 条一致、coopersgn=1 ↔ 合作 270 条一致。 +// 两边冲突的 262 条以 js_supplier 为准 —— fys_supplier 是旧快照(DownloadTime 停在 +// 2025-11,样本多为 2023/2024),而 js_supplier 的 local_updated 已到 2026-09。 +// 匹配不上(供应商没进主数据、或该编码没有供应商)一律按「未知」展示,不要当「不合作」。 +// --------------------------------------------------------------------------- +const SUPPLIER_COOP_LABELS = { cooperative: '合作', ended: '不合作', unknown: '未知' }; +const resolveSupplierCoop = (sign) => { + if (sign === null || sign === undefined || sign === '') return 'unknown'; + const n = Number(sign); + if (n === 1) return 'cooperative'; + if (n === 2) return 'ended'; + return 'unknown'; +}; + const resolveReturnDeadline = ({ unitPriceLevel, stockDays, isOutbound, returnLimitDays }) => { if (unitPriceLevel !== 'high') return { status: 'none', remainDays: null }; if (isOutbound) return { status: 'done', remainDays: null }; @@ -821,6 +851,8 @@ function composeRows({ const deadline = resolveReturnDeadline({ unitPriceLevel, stockDays, isOutbound, returnLimitDays, }); + // 供应商是否合作(js_supplier.coopersgn) + const supplierCoop = resolveSupplierCoop(row.supplierCoopSign); if (scope === 'imported' && !override) return; if (qty <= 0) return; @@ -851,6 +883,8 @@ function composeRows({ stockDays, warningLevel: resolveWarningLevel(stockDays, focusDays, alertDays), isOutbound, + supplierCoop, + supplierCoopLabel: SUPPLIER_COOP_LABELS[supplierCoop] || '', source: override ? 'jst' : 'replica', noInboundRecord: !createdAtRaw, }); @@ -870,6 +904,8 @@ function composeRows({ const deadline = resolveReturnDeadline({ unitPriceLevel, stockDays, isOutbound, returnLimitDays, }); + // 这条是「导入里有、副本里没有」补进来的编码,没走副本 join,拿不到 coopersgn → 标「未知」 + const supplierCoop = 'unknown'; rows.push({ productCode: code, createdAt: formatDateTime(override.createdAt), @@ -895,6 +931,8 @@ function composeRows({ stockDays, warningLevel: resolveWarningLevel(stockDays, focusDays, alertDays), isOutbound, + supplierCoop, + supplierCoopLabel: SUPPLIER_COOP_LABELS[supplierCoop] || '', source: 'jst', noInboundRecord: !override.createdAt, }); @@ -1148,7 +1186,7 @@ const buildExportColumns = (warehouse = DEFAULT_WAREHOUSE) => [ { key: 'amount', title: '总金额' }, { key: 'unitPriceLevelLabel', title: '单价预警' }, { key: 'returnDeadlineLabel', title: '退厂时限' }, - { key: 'isOutboundLabel', title: '是否出库' }, + { key: 'supplierCoopLabel', title: '供应商是否合作' }, { key: 'returnQty', title: '退货仓库存(return_qty)' }, { key: 'returnSlotQty', title: `客退临时上架库存(${warehouse.returnSlot})` }, { key: 'sourceLabel', title: '数据来源' }, @@ -1208,7 +1246,7 @@ async function getReturnStockWarningExportRows(options = {}) { warningLabel: WARNING_LABELS[row.warningLevel] || WARNING_LABELS.normal, unitPriceLevelLabel: PRICE_WARNING_LABELS[row.unitPriceLevel] || '', returnDeadlineLabel: formatReturnDeadlineLabel(row.returnDeadlineStatus, row.returnRemainDays), - isOutboundLabel: row.isOutbound ? '是' : '否', + supplierCoopLabel: row.supplierCoopLabel || SUPPLIER_COOP_LABELS.unknown, sourceLabel: row.createdAtSourceLabel || '', createdAtRaw: undefined, })); diff --git a/frontend/src/pages/ReturnsInventoryWarning/StockWarning.jsx b/frontend/src/pages/ReturnsInventoryWarning/StockWarning.jsx index 5562e09..b70077a 100644 --- a/frontend/src/pages/ReturnsInventoryWarning/StockWarning.jsx +++ b/frontend/src/pages/ReturnsInventoryWarning/StockWarning.jsx @@ -71,6 +71,19 @@ const RETURN_DEADLINE_TAGS = { }; const RETURN_DEADLINE_RANK = { overdue: 3, pending: 2, done: 1, none: 0 }; +// 供应商是否合作(后端取自简尚供应商主数据 js_supplier.coopersgn:1=合作 / 2=不合作) +const SUPPLIER_COOP_TAGS = { + cooperative: { color: 'green', label: '合作' }, + ended: { color: 'red', label: '不合作' }, + unknown: { color: 'default', label: '未知' }, +}; +const SUPPLIER_COOP_RANK = { ended: 2, cooperative: 1, unknown: 0 }; +const SUPPLIER_COOP_TIPS = { + cooperative: '该商品编码的供应商在简尚供应商主数据里标记为「合作中」(js_supplier.coopersgn = 1)', + ended: '该商品编码的供应商在简尚供应商主数据里标记为「不合作」(js_supplier.coopersgn = 2),退货处理需另行商定对接人', + unknown: '供应商名在简尚供应商主数据(js_supplier)里没有匹配记录,或该编码没有供应商,无法判断合作状态', +}; + // 预警等级过滤:主视图改成一排状态页签(全部 / 红色预警 / 关注 / 正常 / 未知), // 因此不再需要下拉选项;导出时仍把 warningLevel 原样传给后端(服务端 filter 同口径)。 @@ -348,11 +361,19 @@ const COLUMNS = [ }, }, { - title: '是否出库', - dataIndex: 'isOutbound', - width: 90, + title: '供应商是否合作', + dataIndex: 'supplierCoop', + width: 136, align: 'center', - render: (value) => (value ? 是 : 否), + sorter: (a, b) => (SUPPLIER_COOP_RANK[a.supplierCoop] || 0) - (SUPPLIER_COOP_RANK[b.supplierCoop] || 0), + render: (value) => { + const tag = SUPPLIER_COOP_TAGS[value] || SUPPLIER_COOP_TAGS.unknown; + return ( + + {tag.label} + + ); + }, }, { title: '创建时间来源',