#!/usr/bin/env node /** * exportHistoricalSwapEvents.js — 一次性历史数据导出(开发期工具,⚠ 需要数据库连接) * ============================================================================ * 用途:把生产/测试库里真实的 swap_event 记录导出成 * fe-tests/fixtures/historical_swap_events.json, * 供 historicalReconciliation.test.js 离线对账。 * * 数据库边界:本脚本【是唯一】触碰数据库的地方,但它【不属于 npm test 路径】 * (文件名非 *.test.js,jest testMatch 不会执行它)。CI 跑测试时零 DB 依赖。 * * 运行(在 YLErpWeb/fe-tests 下): * npm i mysql2 # 仅开发期依赖,不进生产 * MYSQL_HOST=... MYSQL_USER=... MYSQL_PASS=... MYSQL_DB=... \ * node tools/exportHistoricalSwapEvents.js [--from 2026-01-01] [--to 2026-08-08] [--limit 200] * * 导出后请把结果提交进仓库(fixture 文件本身不含任何凭据)。 */ 'use strict'; const fs = require('fs'); const path = require('path'); async function main() { let mysql; try { mysql = require('mysql2/promise'); } catch (e) { console.error('缺少 mysql2,请先 `npm i mysql2`(仅开发期依赖)'); process.exit(2); } const cfg = { host: process.env.MYSQL_HOST, user: process.env.MYSQL_USER, password: process.env.MYSQL_PASS, database: process.env.MYSQL_DB, }; if (!cfg.host || !cfg.user || !cfg.database) { console.error('请提供环境变量 MYSQL_HOST / MYSQL_USER / MYSQL_PASS / MYSQL_DB'); process.exit(2); } const args = process.argv.slice(2); const getArg = (name, dflt) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] ? args[i + 1] : dflt; }; const from = getArg('--from', '1970-01-01'); const to = getArg('--to', '9999-12-31'); const limit = parseInt(getArg('--limit', '200'), 10); const conn = await mysql.createConnection(cfg); // event_type: 2=平仓(unwind) 3=结息(income) 4=多空平仓 5=多空互换(按实际枚举调整) const [rows] = await conn.execute( `SELECT id, event_type, data, create_time FROM swap_event WHERE event_type IN (2,3,4,5) AND create_time BETWEEN ? AND ? ORDER BY create_time DESC LIMIT ?`, [from, to, limit] ); await conn.end(); const records = []; for (const row of rows) { let data = {}; try { data = JSON.parse(row.data || '{}'); } catch (e) { continue; } // swap_event.data 是后端序列化的 UnwindData,字段需按实际结构映射。 // 下面仅抽取对账所需的最小字段;不同 event_type 的字段名以实际库结构为准。 const deal = data.deal || data.Deal || {}; const floatPosition = data.floatPosition || data.FloatPosition || {}; const interestList = data.interestList || data.InterestList || []; const marginList = data.marginList || data.MarginList || []; records.push({ id: `EVT_${row.id}`, desc: `导出记录 event_type=${row.event_type} @ ${row.create_time}`, eventType: row.event_type, deal, floatPosition, interestList, marginList, expected: { SwapCloseAmount: Number(deal.SwapCloseAmount != null ? deal.SwapCloseAmount : floatPosition.SwapCloseAmount), SwapRealizedPnL: Number(deal.SwapRealizedPnL != null ? deal.SwapRealizedPnL : floatPosition.SwapRealizedPnL), SwapMarginAmount: Number(deal.SwapMarginAmount != null ? deal.SwapMarginAmount : floatPosition.SwapMarginAmount), }, }); } const out = { meta: { schemaVersion: 1, source: `exported from ${cfg.database} (${from}..${to}, limit ${limit})`, note: '由 tools/exportHistoricalSwapEvents.js 生成;运行时无需 DB。', }, records, }; const outPath = path.join(__dirname, '..', 'fixtures', 'historical_swap_events.json'); fs.writeFileSync(outPath, JSON.stringify(out, null, 2)); console.log(`已导出 ${records.length} 条记录 → ${outPath}`); } main().catch(e => { console.error(e); process.exit(1); });