cangku/backend/src/services/supplierExitDateProvider.js
lunanting 30554720b2 feat: 客退看板新增「最晚退厂时间」列(工作平台数据,先占位待接入)
- 新增 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 补占位符样式。
2026-09-28 16:06:40 +08:00

141 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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,
};