- 新增 supplierExitDateProvider.js:按供应商名匹配「最晚退厂时间」, 数据源为工作平台库 WorkbenchDataShared(同实例,另有 WorkbenchIdentity / DataPlatformControl)。当前副本账号 cangku 无该库访问权限、系统设置里没有它的 连接配置、.env 的 JIAN_SUN_DB_* 与 WORK_ORDER_* 也为空,因此先返回不可用。 接入方式:IT 开通后配 SUPPLIER_EXIT_DATE_ENABLED=true + SUPPLIER_EXIT_DATE_SQL (查询需返回 supplierName / exitDate 两列),或直接写死 CODE_* 常量,无需改其他文件。 - returnStockWarningService.js:composeRows 增加 latestExitDate / latestExitDateLabel / latestExitDateAvailable;列表响应新增 exitDateMeta; 导出列在「供应商是否合作」后新增「最晚退厂时间」(未接入时导出显示「待接入」); 口径说明补一条(含完整取证:工作平台库不可达、副本无任何退厂字段、 fys_supplier.NoCoopTime 是不可用的停更快照)。 - StockWarning.jsx:表格新增「最晚退厂时间」列,有值显示日期、未接入显示「待接入」 (Tooltip 说明数据来源与开通方式);monitor.css 补占位符样式。
141 lines
6.6 KiB
JavaScript
141 lines
6.6 KiB
JavaScript
'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<string, string> }
|
||
*/
|
||
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,
|
||
};
|