'use strict'; /** * 退货库存预警(客退数据监控)本地表 * -------------------------------------------------------------------------- * 为什么需要本地表: * 报表副本(JSReportReplica)只有仓库级库存表 jst_inventoryInfo, * 没有聚水潭「箱及仓位库存」的箱/仓位维度,因此: * - 「创建时间」= 聚水潭该箱/仓位记录的上架时间,副本里根本没有这个字段; * - 「件数」也可能与聚水潭口径不同(副本只能按 return_qty + 客退自定义库存位近似)。 * 要跟聚水潭页面 100% 对齐,唯一办法是把聚水潭导出的「箱及仓位库存」导入进来, * 按商品编码覆盖创建时间(可选覆盖件数),并持久化。 * * 按仓库隔离(2026-09-16 新增): * 现在有横林二仓 / 苏北 / 睢宁 / 山东 / 河南 五张看板,每张上传的是各自仓库的聚水潭导出文件, * 同一份文件里同一个商品编码可能同时出现在多个仓;如果不按仓隔离,后导入的会把先导入的覆盖掉。 * 因此两张表都加了 warehouse_key,主键从 (product_code) 改成 (warehouse_key, product_code)。 */ const database = require('../config/database'); const DEFAULT_WAREHOUSE_KEY = 'henlin2'; const SCHEMA_SQL = [ // 1. 按「仓库 + 商品编码」的导入覆盖值(以聚水潭导出为准) `CREATE TABLE IF NOT EXISTS return_stock_warning_import ( warehouse_key VARCHAR(32) NOT NULL DEFAULT '${DEFAULT_WAREHOUSE_KEY}' COMMENT '仓库标识(henlin2/subei/suining/shandong/henan)', product_code VARCHAR(120) NOT NULL COMMENT '商品编码', created_at DATETIME NULL COMMENT '聚水潭创建时间(上架时间)', inbound_qty INT NULL COMMENT '聚水潭件数(导入,可为空=不覆盖)', supplier VARCHAR(255) NULL COMMENT '聚水潭供应商(导入,可为空)', batch_id VARCHAR(64) NOT NULL COMMENT '导入批次', imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '导入时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (warehouse_key, product_code), KEY idx_rswi_batch (batch_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退货库存预警-聚水潭导入覆盖值(按仓库+商品编码)'`, // 2. 导入批次日志 `CREATE TABLE IF NOT EXISTS return_stock_warning_import_batch ( batch_id VARCHAR(64) NOT NULL COMMENT '导入批次', warehouse_key VARCHAR(32) NOT NULL DEFAULT '${DEFAULT_WAREHOUSE_KEY}' COMMENT '仓库标识', file_name VARCHAR(255) NULL COMMENT '文件名', total_rows INT NOT NULL DEFAULT 0 COMMENT '解析出的行数', matched_rows INT NOT NULL DEFAULT 0 COMMENT '成功写入的编码数', with_created_at INT NOT NULL DEFAULT 0 COMMENT '含创建时间的行数', with_qty INT NOT NULL DEFAULT 0 COMMENT '含件数的行数', skipped_rows INT NOT NULL DEFAULT 0 COMMENT '无商品编码被跳过的行数', imported_by VARCHAR(100) NULL COMMENT '导入人', imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '导入时间', PRIMARY KEY (batch_id), KEY idx_rswibatch_time (imported_at), KEY idx_rswibatch_wh (warehouse_key, imported_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退货库存预警-聚水潭导入批次日志'`, ]; /** 某张表上是否存在某列 */ async function hasColumn(table, column) { const rows = await database.local.query( `SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`, [table, column] ); return Number(rows?.[0]?.cnt || 0) > 0; } /** 某张表的主键列(按顺序) */ async function getPrimaryKeyColumns(table) { const rows = await database.local.query( `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY' ORDER BY ORDINAL_POSITION`, [table] ); return (rows || []).map((row) => String(row.COLUMN_NAME || '').toUpperCase()); } /** * 老库升级:给已存在的两张表补 warehouse_key,并把导入表主键改成 (warehouse_key, product_code)。 * 幂等,每次启动都跑一遍,已经是新结构时不做任何事。 */ async function migrateReturnStockWarningSchema() { // 逐列补:两张表都要有 warehouse_key const targets = ['return_stock_warning_import', 'return_stock_warning_import_batch']; for (const table of targets) { // eslint-disable-next-line no-await-in-loop if (!(await hasColumn(table, 'warehouse_key'))) { // 老数据全部归到默认仓(横林二仓)——它本来就是为横林二仓建的 // eslint-disable-next-line no-await-in-loop await database.local.query( `ALTER TABLE ${table} ADD COLUMN warehouse_key VARCHAR(32) NOT NULL DEFAULT '${DEFAULT_WAREHOUSE_KEY}' COMMENT '仓库标识' AFTER ${table === 'return_stock_warning_import' ? 'product_code' : 'batch_id'}` ); } } // 主键改造:老结构是 (product_code),需要换成 (warehouse_key, product_code) const pk = await getPrimaryKeyColumns('return_stock_warning_import'); if (pk.length === 1 && pk[0] === 'PRODUCT_CODE') { await database.local.query( 'ALTER TABLE return_stock_warning_import DROP PRIMARY KEY, ADD PRIMARY KEY (warehouse_key, product_code)' ); } // 批次表的仓库索引 const idxRows = await database.local.query( `SELECT COUNT(*) AS cnt FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'return_stock_warning_import_batch' AND INDEX_NAME = 'idx_rswibatch_wh'` ); if (Number(idxRows?.[0]?.cnt || 0) === 0) { await database.local.query( 'ALTER TABLE return_stock_warning_import_batch ADD KEY idx_rswibatch_wh (warehouse_key, imported_at)' ); } } async function ensureReturnStockWarningSchema() { for (const sql of SCHEMA_SQL) { // eslint-disable-next-line no-await-in-loop await database.local.query(sql); } await migrateReturnStockWarningSchema(); } module.exports = { ensureReturnStockWarningSchema, migrateReturnStockWarningSchema };