- 期初数量改由本地快照表 daily_inventory_snapshot 提供(上一业务日期末), 每日 01:30 定时任务落库,接口侧缺失时补写兜底,逐日自动滚动 - 新增 dailyInventorySnapshotDb / dailyInventorySnapshotJob 与手动补跑脚本 - dailyInventoryBaseline 瘦身为纯仓库映射,去掉所有硬编码业务数值 - 12 个流水列因业务库缺单据表返回空值,前端灰底显示「—」 - 修复 qty 全为 0 的仓库期末显示 null 的缺陷 - 新增 docs/库存流水单据表需求.md 给 DBA
138 lines
4.8 KiB
JavaScript
138 lines
4.8 KiB
JavaScript
/**
|
||
* 每日进销存 —— 本地库存快照库访问
|
||
*
|
||
* 背景:业务库(MSSQL / JSReportReplica)只有「当前」实时库存 jst_inventoryInfo,
|
||
* 没有按天的历史库存结存表,因此「期初数量」无法回溯。
|
||
* 解决办法:项目本地 MySQL 每天落一份按仓库存的快照,
|
||
* 今日期初 = 上一业务日快照,期末 = 实时库存。
|
||
*
|
||
* 连接复用 LOCAL_DB_* 配置(与项目其它本地表同库)。
|
||
*/
|
||
const path = require('path');
|
||
const dotenv = require('dotenv');
|
||
const mysql = require('mysql2/promise');
|
||
|
||
dotenv.config();
|
||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||
|
||
const TABLE = 'daily_inventory_snapshot';
|
||
|
||
const CREATE_SQL = `
|
||
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
biz_date DATE NOT NULL COMMENT '业务日期(T-1)',
|
||
wms_co_id BIGINT NOT NULL COMMENT '聚水潭仓库 ID',
|
||
warehouse VARCHAR(128) NOT NULL COMMENT '仓库名(主仓)',
|
||
db_warehouse VARCHAR(128) NULL COMMENT '监控表仓库名',
|
||
closing_qty DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '该业务日期末数量',
|
||
sku_count INT NOT NULL DEFAULT 0 COMMENT '有库存 SKU 数',
|
||
taken_at DATETIME NULL COMMENT '源库库存数据更新时间',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uk_date_wh (biz_date, wms_co_id),
|
||
KEY idx_date (biz_date)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日进销存-按仓库存快照';`;
|
||
|
||
let pool = null;
|
||
|
||
function getPool() {
|
||
if (pool) return pool;
|
||
pool = mysql.createPool({
|
||
host: process.env.LOCAL_DB_HOST,
|
||
port: Number(process.env.LOCAL_DB_PORT || 3306),
|
||
user: process.env.LOCAL_DB_USER,
|
||
password: process.env.LOCAL_DB_PASSWORD,
|
||
database: process.env.LOCAL_DB_NAME,
|
||
waitForConnections: true,
|
||
connectionLimit: 5,
|
||
connectTimeout: 10000,
|
||
// DATE/DATETIME 直接返回字符串,避免 JS Date 按本地时区回读时整体偏移一天
|
||
dateStrings: true,
|
||
});
|
||
return pool;
|
||
}
|
||
|
||
async function ensureTable() {
|
||
await getPool().query(CREATE_SQL);
|
||
}
|
||
|
||
/**
|
||
* 写入某业务日的期末快照(幂等,重复执行覆盖)
|
||
* @param {string} bizDate YYYY-MM-DD
|
||
* @param {Array<{wmsCoId:number, warehouse:string, dbWarehouse?:string, closingQty:number, skuCount:number, takenAt?:Date|null}>} rows
|
||
*/
|
||
async function upsertSnapshot(bizDate, rows) {
|
||
if (!bizDate || !rows.length) return 0;
|
||
await ensureTable();
|
||
const conn = await getPool().getConnection();
|
||
try {
|
||
for (const r of rows) {
|
||
await conn.query(
|
||
`INSERT INTO ${TABLE} (biz_date, wms_co_id, warehouse, db_warehouse, closing_qty, sku_count, taken_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE warehouse = VALUES(warehouse), db_warehouse = VALUES(db_warehouse),
|
||
closing_qty = VALUES(closing_qty), sku_count = VALUES(sku_count), taken_at = VALUES(taken_at)`,
|
||
[bizDate, r.wmsCoId, r.warehouse, r.dbWarehouse || null, r.closingQty, r.skuCount, r.takenAt || null]
|
||
);
|
||
}
|
||
return rows.length;
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取某业务日的期末快照
|
||
* @returns {Promise<Map<string, {closingQty:number, skuCount:number, takenAt:Date|null}>>} key = wmsCoId 字符串
|
||
*/
|
||
async function fetchSnapshot(bizDate) {
|
||
const map = new Map();
|
||
if (!bizDate) return map;
|
||
try {
|
||
await ensureTable();
|
||
const [rows] = await getPool().query(
|
||
`SELECT wms_co_id AS wmsCoId, closing_qty AS closingQty, sku_count AS skuCount, taken_at AS takenAt
|
||
FROM ${TABLE} WHERE biz_date = ?`,
|
||
[bizDate]
|
||
);
|
||
rows.forEach((r) => map.set(String(r.wmsCoId), {
|
||
closingQty: Number(r.closingQty),
|
||
skuCount: Number(r.skuCount),
|
||
takenAt: r.takenAt || null,
|
||
}));
|
||
} catch (error) {
|
||
console.error('[daily-inventory-snapshot] 读取快照失败:', error.message);
|
||
}
|
||
return map;
|
||
}
|
||
|
||
/** 已有快照的业务日(倒序,最多 n 条) */
|
||
async function listSnapshotDates(limit = 10) {
|
||
try {
|
||
await ensureTable();
|
||
const [rows] = await getPool().query(
|
||
`SELECT DISTINCT biz_date AS d FROM ${TABLE} ORDER BY biz_date DESC LIMIT ?`,
|
||
[Number(limit) || 10]
|
||
);
|
||
return rows.map((r) => {
|
||
if (typeof r.d === 'string') return r.d.slice(0, 10);
|
||
const v = new Date(r.d);
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
return `${v.getFullYear()}-${pad(v.getMonth() + 1)}-${pad(v.getDate())}`;
|
||
});
|
||
} catch (error) {
|
||
console.error('[daily-inventory-snapshot] 读取快照日期失败:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
TABLE,
|
||
CREATE_SQL,
|
||
getPool,
|
||
ensureTable,
|
||
upsertSnapshot,
|
||
fetchSnapshot,
|
||
listSnapshotDates,
|
||
};
|