/** * 一次性数据迁移: 把 Indicator.data (JSON) 拆分到 IndicatorPoint 明细表 * 用法: 在迁移 A (建表+加列) 之后、迁移 B (删 data 列) 之前运行 * 幂等: 同 (indicatorId, period) 重复插入会更新 value, 可安全重跑 */ require('dotenv').config() const mysql = require('mysql2/promise') function parseUrl(url) { const u = new URL(url) return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, ''), } } // 根据 row 的时间字段推断频率: year | quarter | month | day function inferFrequency(row) { if (row && row.month !== undefined) return 'month' if (row && row.year !== undefined) return 'year' const p = String((row && row.period) || '') if (/^\d{4}$/.test(p)) return 'year' if (/^\d{4}-Q[1-4]$/.test(p)) return 'quarter' if (/^\d{4}-\d{2}$/.test(p)) return 'month' if (/^\d{4}-\d{2}-\d{2}$/.test(p)) return 'day' return 'month' } // 统一 row 的时间字段为 period function periodOf(row) { if (!row) return '' return String(row.period !== undefined ? row.period : (row.month !== undefined ? row.month : row.year)) } async function main() { const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL)) try { const [rows] = await conn.execute('SELECT id, name, groupName, data FROM Indicator') console.log('[Migrate] 读取', rows.length, '个 Indicator') let points = 0, updated = 0 for (const ind of rows) { // mysql2 对 JSON 列已自动解析为对象, 兼容字符串兜底 let data = ind.data if (typeof data === 'string') { try { data = JSON.parse(data) } catch { data = null } } const title = (data && data.title) || ind.name const unit = (data && data.unit) || '' const source = (data && data.source) || '' const rowsArr = (data && Array.isArray(data.rows)) ? data.rows : [] const freq = rowsArr.length ? inferFrequency(rowsArr[0]) : 'month' // 更新 Indicator 元数据列 await conn.execute( 'UPDATE Indicator SET title=?, unit=?, source=?, frequency=?, updatedAt=NOW(3) WHERE id=?', [title, unit, source, freq, ind.id], ) updated++ // 写入明细 for (const r of rowsArr) { const period = periodOf(r) const value = Number(r.value) if (!period || Number.isNaN(value)) continue const [res] = await conn.execute( 'INSERT INTO IndicatorPoint (indicatorId, period, value, createdAt, updatedAt) VALUES (?, ?, ?, NOW(3), NOW(3)) ' + 'ON DUPLICATE KEY UPDATE value=VALUES(value), updatedAt=NOW(3)', [ind.id, period, value], ) points += (res.affectedRows > 0 ? 1 : 0) } console.log('[Migrate]', ind.name, '->', rowsArr.length, '个点, 频率', freq) } console.log('[Migrate] 完成: 更新', updated, '个 Indicator, 写入/更新', points, '个明细点') } finally { await conn.end() } } main().catch((e) => { console.error('[Migrate] 失败:', e); process.exit(1) })