/** * 每日进销存 —— 本地库存快照库访问 * * 背景:业务库(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>} 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, };