/** * iBoard 基础数据初始化脚本 * 用法: npm run db:seed * 幂等: 主键/唯一键冲突跳过, Strategy/Indicator 按 name 存在则更新 */ require('dotenv').config(); const crypto = require('crypto'); const mysql = require('mysql2/promise'); function hashPassword(plain) { const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.scryptSync(plain, salt, 64).toString('hex'); return 'scrypt$' + salt + '$' + hash; } 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(/^\//, ''), }; } async function insertIgnore(conn, sql, params) { try { const [r] = await conn.execute(sql, params); return r.affectedRows; } catch (e) { if (e.code === 'ER_DUP_ENTRY') return 0; throw e; } } async function upsertByName(conn, table, payload, keyField) { const fields = Object.keys(payload); const placeholders = fields.map(() => '?').join(', '); const updateClause = fields .filter((f) => f !== keyField) .map((f) => '`' + f + '` = VALUES(`' + f + '`)') .join(', '); const sql = 'INSERT INTO `' + table + '` (' + fields.map((f) => '`' + f + '`').join(', ') + ') VALUES (' + placeholders + ') ON DUPLICATE KEY UPDATE ' + updateClause; const params = fields.map((f) => payload[f]); const [r] = await conn.execute(sql, params); return r.affectedRows; } async function main() { const cfg = parseUrl(process.env.DATABASE_URL); if (!cfg.host) throw new Error('DATABASE_URL 未配置或解析失败'); const conn = await mysql.createConnection(cfg); console.log('已连接:', cfg.host + ':' + cfg.port + '/' + cfg.database); const adminPwd = hashPassword('admin123'); const n1 = await insertIgnore( conn, 'INSERT INTO `User` (name, email, password, createdAt, updatedAt) VALUES (?, ?, ?, NOW(3), NOW(3))', ['Admin', 'admin@iboard.local', adminPwd], ); console.log('[User] 新增管理员: ' + n1 + ' 行 (admin@iboard.local / admin123)'); const strategies = [ { name: 'us-debt-gdp', description: '美国政府债务/GDP分析', data: { source: 'strategy.js', unit: '万亿美元', rows: [ { year: '2019', debt: 22.7, gdp: 21.4, debtToGdp: 106.1, interestToGdp: 1.8 }, { year: '2020', debt: 26.9, gdp: 20.9, debtToGdp: 128.7, interestToGdp: 1.6 }, { year: '2021', debt: 28.4, gdp: 23.0, debtToGdp: 123.5, interestToGdp: 1.5 }, { year: '2022', debt: 30.9, gdp: 25.4, debtToGdp: 121.6, interestToGdp: 1.6 }, { year: '2023', debt: 33.1, gdp: 26.9, debtToGdp: 123.0, interestToGdp: 1.9 }, { year: '2024', debt: 35.3, gdp: 28.5, debtToGdp: 123.9, interestToGdp: 2.1 }, ]}, }, { name: 'china-debt-gdp', description: '中国政府债务/GDP分析', data: { source: 'strategy.js', unit: '万亿美元', rows: [ { year: '2019', debt: 8.8, gdp: 14.3, debtToGdp: 61.5, interestToGdp: 1.9 }, { year: '2020', debt: 10.3, gdp: 14.7, debtToGdp: 70.1, interestToGdp: 1.8 }, { year: '2021', debt: 11.7, gdp: 16.1, debtToGdp: 72.7, interestToGdp: 1.7 }, { year: '2022', debt: 13.1, gdp: 17.9, debtToGdp: 73.2, interestToGdp: 1.6 }, { year: '2023', debt: 14.5, gdp: 19.4, debtToGdp: 74.7, interestToGdp: 1.6 }, { year: '2024', debt: 15.9, gdp: 21.0, debtToGdp: 75.7, interestToGdp: 1.5 }, ]}, }, ]; let n2 = 0; for (const s of strategies) { n2 += await upsertByName(conn, 'Strategy', { name: s.name, description: s.description, data: JSON.stringify(s.data), updatedAt: new Date() }, 'name'); } console.log('[Strategy] upsert 策略: ' + n2 + ' 行 (1=新增, 2=更新)'); const indicators = [ { name: 'china-bond-1y', groupName: 'china-bond', data: { unit: '%', title: '中国国债1年', rows: [ { month: '2024-01', value: 1.85 }, { month: '2024-02', value: 1.82 }, { month: '2024-03', value: 1.78 }, { month: '2024-04', value: 1.75 }, { month: '2024-05', value: 1.72 }, { month: '2024-06', value: 1.68 }, ]}}, { name: 'china-bond-10y', groupName: 'china-bond', data: { unit: '%', title: '中国国债10年', rows: [ { month: '2024-01', value: 2.65 }, { month: '2024-02', value: 2.62 }, { month: '2024-03', value: 2.58 }, { month: '2024-04', value: 2.55 }, { month: '2024-05', value: 2.52 }, { month: '2024-06', value: 2.48 }, ]}}, { name: 'us-bond-1y', groupName: 'us-bond', data: { unit: '%', title: '美国国债1年', rows: [ { month: '2024-01', value: 4.85 }, { month: '2024-02', value: 4.82 }, { month: '2024-03', value: 4.78 }, { month: '2024-04', value: 4.65 }, { month: '2024-05', value: 4.55 }, { month: '2024-06', value: 4.45 }, ]}}, { name: 'china-lpr-1y', groupName: 'china-lpr', data: { unit: '%', title: '中国LPR 1年期', rows: [ { month: '2024-01', value: 3.45 }, { month: '2024-02', value: 3.45 }, { month: '2024-03', value: 3.45 }, { month: '2024-04', value: 3.40 }, { month: '2024-05', value: 3.40 }, { month: '2024-06', value: 3.40 }, ]}}, ]; let n3 = 0; for (const i of indicators) { n3 += await upsertByName(conn, 'Indicator', { name: i.name, groupName: i.groupName, data: JSON.stringify(i.data), updatedAt: new Date() }, 'name'); } console.log('[Indicator] upsert 指标: ' + n3 + ' 行 (1=新增, 2=更新)'); const article = { title: '欢迎使用 iBoard', slug: 'welcome-to-iboard', content: 'iBoard 用于收集和整理经济数据,以策略为主体重新组织。\n\n本文为示例条目,正式文章仍由 content/articles/*.md 文件承载。', date: '2026-08-03', }; const n4 = await insertIgnore( conn, 'INSERT INTO `Article` (title, slug, content, date, createdAt, updatedAt) VALUES (?, ?, ?, ?, NOW(3), NOW(3))', [article.title, article.slug, article.content, article.date], ); console.log('[Article] 新增示例文章: ' + n4 + ' 行'); const sowRows = [ { month: '2024-01', inventory: 4067 }, { month: '2024-02', inventory: 4042 }, { month: '2024-03', inventory: 3992 }, { month: '2024-04', inventory: 3986 }, { month: '2024-05', inventory: 3996 }, { month: '2024-06', inventory: 4038 }, ]; let n5 = 0; for (const r of sowRows) { n5 += await insertIgnore( conn, 'INSERT INTO `Econ_SowInventory` (month, inventory, createdAt, updatedAt) VALUES (?, ?, NOW(3), NOW(3))', [r.month, r.inventory], ); } console.log('[Econ_SowInventory] 新增: ' + n5 + ' 行'); await conn.end(); console.log('\nSeed 完成.'); } main().catch((e) => { console.error('Seed 失败:', e); process.exit(1); });