From 0afcb26119f24e4162f7396a1a69f506933100bd Mon Sep 17 00:00:00 2001 From: youruo <15018737611@163.com> Date: Wed, 7 Oct 2026 16:59:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E8=BF=9B=E9=94=80=E5=AD=98=E4=B8=9A=E5=8A=A1=E5=BA=93=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/scripts/validateDailyInventoryFromBiz.js <业务日>... - 直连业务库(凭据经隧道从系统库 database_configs 现取,不落盘): 反算 4 个可还原的流水列(销售出仓/销售退货/采购进仓/采购退货), 与 daily_inventory_official 逐仓比对;期末库存与 jst_inventoryInfo 全仓比对 - 约定:校验类需求默认走业务库,不启浏览器取聚水潭 --- .../scripts/validateDailyInventoryFromBiz.js | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 backend/scripts/validateDailyInventoryFromBiz.js diff --git a/backend/scripts/validateDailyInventoryFromBiz.js b/backend/scripts/validateDailyInventoryFromBiz.js new file mode 100644 index 0000000..14acadf --- /dev/null +++ b/backend/scripts/validateDailyInventoryFromBiz.js @@ -0,0 +1,153 @@ +'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); +});