'use strict'; /** * 「最晚退厂时间」取数(2026-09-28 新增) * * 业务口径:供应商不再合作时,工作平台会给该供应商一个「最晚退厂时间」, * 即它放在仓里的货最后必须拉走的日期,看板上跟「供应商是否合作」列并排展示。 * * ── 数据在哪 ────────────────────────────────────────────────────────────── * 工作平台库。副本实例(218.93.9.21:1433)上共有 9 个库: * master / model / msdb / tempdb / SSISDB / JSReportReplica(副本,我们只读它)/ * DataPlatformControl / WorkbenchDataShared / WorkbenchIdentity。 * 「工作平台」= WorkbenchDataShared(配套身份库 WorkbenchIdentity,控制库 DataPlatformControl)。 * * ── 当前为什么是空的(2026-09-28 已逐条取证,别再重复排查)─────────────── * ① 副本账号 cangku 访问 WorkbenchDataShared / WorkbenchIdentity / DataPlatformControl * **一律被拒**:The server principal "cangku" is not able to access the database * "WorkbenchDataShared" under the current security context. * ② 系统设置(database_configs)里只有 cloud_database_public(JSReportReplica)与 * product_information_database(PostgreSQL),没有工作平台库。 * ③ 本机 .env 与生产 /opt/cangku/shared/backend.env 里的 JIAN_SUN_DB_HOST/USER/PASSWORD、 * WORK_ORDER_* 全部为空(只留了端口/库名占位)。 * ④ 副本库内部没有任何「退厂」字段:5,496 个字段全量搜过,COLUMN_NAME 含「退厂」的为 0; * 最接近的 fys_supplier.NoCoopTime(不合作时间)**不能拿来顶替** —— 它是停更快照 * (DownloadTime 多在 2023~2024),崔桥仓 8 个不合作供应商里只有 1 个有值, * 而且那 1 条自身还标着 IsCoop='合作',自相矛盾。 * * ── IT 开通后怎么接(只需改本文件)──────────────────────────────────────── * 二选一,改完即生效,页面与导出都会自动出数: * A. 给副本账号授只读:让 IT 执行 `USE WorkbenchDataShared; GRANT SELECT TO [cangku];` * (或建同义词/视图),然后把下面 EXIT_DATE_QUERY 填成跨库查询,例如: * SELECT SupplierName AS supplierName, CONVERT(varchar(10), LatestExitDate, 120) AS exitDate * FROM WorkbenchDataShared.dbo.<供应商不合作表> * B. 让 IT 把「供应商不合作」表同步进 JSReportReplica(像 js_supplier / fys_supplier 那样), * 再把 EXIT_DATE_QUERY 指向副本里的表名。 * 查询只需要返回两列:supplierName(供应商名,口径同 js_supplier.scmname / jst_sku.supplier_name, * 即简称)与 exitDate(最晚退厂日期,字符串 'YYYY-MM-DD' 或 datetime 均可)。 * * 未接入时本模块恒返回 available=false,页面该列显示「待接入」,不会报错、不影响其他列。 */ const cloudDb = require('../config/cloudDatabase'); const SOURCE_LABEL = '工作平台(WorkbenchDataShared)· 供应商不合作'; const UNAVAILABLE_MESSAGE = '工作平台库尚未授权,待 IT 开通后自动出数'; // ---- 接入点(IT 开通后配这两个环境变量即可,不必改代码)-------------------- // SUPPLIER_EXIT_DATE_ENABLED=true // SUPPLIER_EXIT_DATE_SQL=<查询语句,需返回 supplierName / exitDate 两列> // 也可以在下面 CODE_* 常量里直接写死(env 优先)。 const CODE_EXIT_DATE_ENABLED = false; const CODE_EXIT_DATE_QUERY = ''; const ENABLED = process.env.SUPPLIER_EXIT_DATE_ENABLED === 'true' || CODE_EXIT_DATE_ENABLED; const EXIT_DATE_QUERY = process.env.SUPPLIER_EXIT_DATE_SQL || CODE_EXIT_DATE_QUERY; // --------------------------------------------------------------------------- const CACHE_TTL_MS = Number.parseInt(process.env.SUPPLIER_EXIT_DATE_CACHE_TTL_MS, 10) >= 0 ? Number.parseInt(process.env.SUPPLIER_EXIT_DATE_CACHE_TTL_MS, 10) : 30 * 60 * 1000; let cache = null; // { at, payload } const formatExitDate = (value) => { if (value === null || value === undefined || value === '') return ''; if (value instanceof Date) { if (Number.isNaN(value.getTime())) return ''; const y = value.getFullYear(); const m = String(value.getMonth() + 1).padStart(2, '0'); const d = String(value.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } const text = String(value).trim(); if (!text) return ''; const match = text.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/); if (!match) return text; return `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`; }; /** * 取「供应商 → 最晚退厂时间」映射。 * 返回 { available, sourceLabel, message, bySupplier: Map } */ async function loadSupplierExitDates() { const describe = () => ({ available: false, sourceLabel: SOURCE_LABEL, message: UNAVAILABLE_MESSAGE, bySupplier: new Map(), }); if (!ENABLED || !EXIT_DATE_QUERY) return describe(); if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.payload; try { const rows = await cloudDb.query(EXIT_DATE_QUERY, {}, { requestTimeout: 120000 }); const bySupplier = new Map(); (rows || []).forEach((row) => { const name = String(row.supplierName || '').trim(); const date = formatExitDate(row.exitDate); if (!name || !date) return; const prev = bySupplier.get(name); // 同一供应商多条时取最晚的日期 if (!prev || date > prev) bySupplier.set(name, date); }); const payload = { available: true, sourceLabel: SOURCE_LABEL, message: '', bySupplier, }; cache = { at: Date.now(), payload }; return payload; } catch (error) { // 取不到不算致命:该列留空、页面提示待接入,其余列照常 console.warn('[SupplierExitDate] 最晚退厂时间取数失败:', error.message); const payload = { available: false, sourceLabel: SOURCE_LABEL, message: `工作平台库读取失败(${error.message}),该列暂空`, bySupplier: new Map(), }; cache = { at: Date.now(), payload }; return payload; } } const EMPTY_EXIT_DATES = Object.freeze({ available: false, sourceLabel: SOURCE_LABEL, message: UNAVAILABLE_MESSAGE, bySupplier: new Map(), }); module.exports = { loadSupplierExitDates, formatExitDate, EMPTY_EXIT_DATES, EXIT_DATE_SOURCE_LABEL: SOURCE_LABEL, EXIT_DATE_UNAVAILABLE_MESSAGE: UNAVAILABLE_MESSAGE, };