'use strict'; /** * 用业务库(JSReportReplica)反算流水,校验已导入的聚水潭官方进销存数据。 * 用法(先起隧道): cd backend && node scripts/validateDailyInventoryFromBiz.js 2026-10-06 * * 注意:业务库 jst_inventoryInfo 是 T-1 同步快照,只能精确校验「昨天」的期末; * 更早的历史日会带自然变动(实测 10-05 差 0.1%~1.2%),属正常。 */ const mysql = require('mysql2/promise'); const sql = require('mssql'); const BIZ_DATE = process.argv[2] || '2026-10-06'; const SYS = { host: '127.0.0.1', port: 13307, user: 'cangku_67c48c6660be787f', password: 'wIJugzi26RQHF4J4BgnwxCxxecp157goR93M_IXEhqc', database: 'shujuku2_system', }; // 业务库能还原的 4 个流水列(与 dailyInventoryFlows.js 口径一致,这里加单日上界) const FLOWS = [ { field: 'purchase_in', label: '采购进仓', sql: `SELECT p.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(pi.qty,0) AS DECIMAL(18,2))) AS qty FROM rpt.jst_purchase p JOIN rpt.jst_purchaseItem pi ON pi.po_id = p.po_id WHERE p.wms_co_id IN (%W%) AND p.po_date >= @from AND p.po_date < @to AND ISNULL(p.status,'') IN ('Confirmed','Finished') GROUP BY p.wms_co_id`, }, { field: 'purchase_return', label: '采购退货', sql: `SELECT o.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(oi.qty,0) AS DECIMAL(18,2))) AS qty FROM rpt.jst_purchaseout o JOIN rpt.jst_purchaseoutItem oi ON oi.io_id = o.io_id WHERE o.wms_co_id IN (%W%) AND o.io_date >= @from AND o.io_date < @to GROUP BY o.wms_co_id`, }, { field: 'sales_out', label: '销售出仓', sql: `SELECT o.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(oi.qty,0) AS DECIMAL(18,2))) AS qty FROM rpt.jst_Orders o JOIN rpt.jst_OrderItem oi ON oi.o_id = o.o_id WHERE o.wms_co_id IN (%W%) AND o.send_date >= @from AND o.send_date < @to AND ISNULL(o.status,'') NOT IN ('Delete','Cancelled') GROUP BY o.wms_co_id`, }, { field: 'sales_return', label: '销售退货', sql: `SELECT r.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(ri.r_qty,0) AS DECIMAL(18,2))) AS qty FROM rpt.jst_returnAndRefundInfo r JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = r.as_id WHERE r.wms_co_id IN (%W%) AND r.as_date >= @from AND r.as_date < @to AND ISNULL(r.type,'') IN (N'普通退货',N'拒收退货',N'换货',N'维修') GROUP BY r.wms_co_id`, }, ]; (async () => { const sys = await mysql.createConnection(SYS); const [official] = await sys.query( 'SELECT wms_co_id, warehouse, opening_qty, closing_qty, purchase_in, purchase_return, sales_out, sales_return FROM daily_inventory_official WHERE DATE(biz_date)=? ORDER BY closing_qty DESC', [BIZ_DATE] ); if (!official.length) { console.log('官方表无 ' + BIZ_DATE + ' 数据'); await sys.end(); return; } const ids = official.map((r) => String(r.wms_co_id)).filter(Boolean); const clause = ids.map((_, i) => '@w' + i).join(','); const wparams = {}; ids.forEach((v, i) => (wparams['w' + i] = v)); // 业务库凭据从系统库 database_configs 现取(不落盘) const [cfgRows] = await sys.query( "SELECT host, port, database_name, username, password FROM database_configs WHERE name='cloud_database_public' LIMIT 1" ); const biz = cfgRows[0]; await sql.connect({ server: biz.host, port: Number(biz.port), database: biz.database_name, user: biz.username, password: biz.password, options: { encrypt: false, trustServerCertificate: true }, connectionTimeout: 20000, requestTimeout: 180000, }); const from = BIZ_DATE + ' 00:00:00'; const d = new Date(BIZ_DATE + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + 1); const to = d.toISOString().slice(0, 10) + ' 00:00:00'; const computed = {}; for (const f of FLOWS) { const req = new sql.Request(); req.input('from', sql.DateTime, new Date(from)); req.input('to', sql.DateTime, new Date(to)); ids.forEach((v, i) => req.input('w' + i, sql.NVarChar, v)); const r = await req.query(f.sql.replace('%W%', clause)); r.recordset.forEach((row) => { computed[String(row.wmsCoId)] = computed[String(row.wmsCoId)] || {}; computed[String(row.wmsCoId)][f.field] = Number(row.qty) || 0; }); } console.log('业务库口径反算 vs 聚水潭官方导出 —— ' + BIZ_DATE); console.log('(业务库仅能还原 4/12 列;采购退货/销售出仓为流出,官方值取绝对值比较)\n'); const pad = (s, n) => String(s).padEnd(n); console.log( pad('仓库', 26) + pad('采购进仓 官方/业务库', 26) + pad('采购退货 官方/业务库', 26) + pad('销售出仓 官方/业务库', 26) + '销售退货 官方/业务库' ); let diffs = { purchase_in: 0, sales_out: 0, purchase_return: 0, sales_return: 0 }; for (const o of official) { const c = computed[String(o.wms_co_id)] || {}; const cells = ['purchase_in', 'purchase_return', 'sales_out', 'sales_return'].map((k) => { const ov = Math.abs(Number(o[k]) || 0); const cv = Math.abs(Number(c[k]) || 0); if (Math.abs(ov - cv) > 0.005) diffs[k]++; return pad(ov + ' / ' + cv, 26); }); console.log(pad(o.warehouse, 26) + cells.join('')); } console.log('\n不一致仓数:' + JSON.stringify(diffs) + ' / 共 ' + official.length + ' 仓'); // 期末 vs 业务库实时库存(仅作量级参考,业务库只有实时库存) const req2 = new sql.Request(); ids.forEach((v, i) => req2.input('w' + i, sql.NVarChar, v)); const inv = await req2.query( `SELECT wms_co_id AS wmsCoId, SUM(CAST(ISNULL(qty,0) AS DECIMAL(18,2))) AS qty FROM rpt.jst_inventoryInfo WHERE wms_co_id IN (${clause}) GROUP BY wms_co_id` ); const invMap = {}; inv.recordset.forEach((r) => (invMap[String(r.wmsCoId)] = Number(r.qty) || 0)); console.log('\n【参考】官方' + BIZ_DATE + '期末 vs 业务库实时库存(业务库无历史快照,仅看量级)'); official .slice() .sort((a, b) => Number(b.closing_qty) - Number(a.closing_qty)) .slice(0, 8) .forEach((o) => { const live = invMap[String(o.wms_co_id)] || 0; const off = Number(o.closing_qty) || 0; const pct = off ? (((live - off) / off) * 100).toFixed(1) : '-'; console.log(pad(o.warehouse, 26) + '期末 ' + off + ' | 实时 ' + live + ' | 差 ' + pct + '%'); }); await sql.close(); await sys.end(); })().catch((e) => { console.log('FAIL', e.message); process.exit(1); });