/** * 数据库迁移执行脚本 * 用法: node database/migrate.js [migration_file] * 示例: node database/migrate.js 002_add_merchant_cover_image.sql */ const mysql = require('mysql2/promise'); const fs = require('fs'); const path = require('path'); const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m', bold: '\x1b[1m', }; function log(msg, color = '') { console.log(color ? `${color}${msg}${colors.reset}` : msg); } function loadEnv() { const envPaths = [ path.resolve(__dirname, '../apps/server/.env.local'), path.resolve(__dirname, '../apps/server/.env'), ]; let envFile = null; for (const p of envPaths) { if (fs.existsSync(p)) { envFile = p; break; } } if (!envFile) { log('错误: 未找到 .env.local 或 .env 文件', colors.red); process.exit(1); } const env = {}; for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const i = trimmed.indexOf('='); if (i === -1) continue; env[trimmed.slice(0, i).trim()] = trimmed.slice(i + 1).trim(); } return { host: env.DB_HOST || 'localhost', port: parseInt(env.DB_PORT || '3306', 10), user: env.DB_USERNAME || 'root', password: env.DB_PASSWORD || '', database: env.DB_DATABASE || 'rent_platform', }; } async function runSqlFile(conn, filePath) { let raw = fs.readFileSync(filePath, 'utf-8'); // 移除注释行 raw = raw.replace(/--.*$/gm, ''); // 移除 USE 语句(已在代码中处理) raw = raw.replace(/USE\s+\S+\s*;/gi, ''); // 按 ; 分割,过滤空语句 const stmts = raw .split(';') .map((s) => s.trim()) .filter((s) => s.length > 0); let ok = 0; let skip = 0; let errors = []; for (const stmt of stmts) { try { await conn.execute(stmt); ok++; } catch (err) { if (err.code === 'ER_DUP_FIELDNAME' || err.code === 'ER_DUP_KEYNAME') { skip++; log(` ⚠ 跳过: ${err.message.slice(0, 80)}`, colors.yellow); } else { errors.push({ code: err.code, message: err.message, stmt: stmt.slice(0, 100) }); } } } return { ok, skip, errors }; } async function main() { const args = process.argv.slice(2); if (args.length === 0) { log(''); log('用法: node database/migrate.js ', colors.cyan); log('示例: node database/migrate.js 002_add_merchant_cover_image.sql', colors.cyan); log(''); log('可用的迁移文件:', colors.yellow); const migrationsDir = path.resolve(__dirname, 'migrations'); const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')); files.forEach(f => log(` - ${f}`, colors.cyan)); log(''); process.exit(0); } const migrationFile = args[0]; const migrationPath = path.resolve(__dirname, 'migrations', migrationFile); if (!fs.existsSync(migrationPath)) { log(`错误: 迁移文件不存在: ${migrationPath}`, colors.red); process.exit(1); } const config = loadEnv(); log(''); log('==========================================', colors.cyan); log(' 数据库迁移执行', colors.bold + colors.cyan); log('==========================================', colors.cyan); log(` 主机: ${config.host}:${config.port}`); log(` 用户: ${config.user}`); log(` 数据库: ${config.database}`); log(` 迁移文件: ${migrationFile}`); log('==========================================', colors.cyan); log(''); let conn; try { conn = await mysql.createConnection({ host: config.host, port: config.port, user: config.user, password: config.password, database: config.database, }); log('✓ 数据库连接成功', colors.green); } catch (err) { log(`连接失败: ${err.message}`, colors.red); log('请检查 apps/server/.env.local 中的数据库配置', colors.red); process.exit(1); } log(''); log('执行迁移...', colors.yellow); const result = await runSqlFile(conn, migrationPath); log(''); if (result.errors.length > 0) { log('==========================================', colors.red); log(` ✗ 迁移失败`, colors.bold + colors.red); log('==========================================', colors.red); log(''); log('错误详情:', colors.red); result.errors.forEach((err, i) => { log(` ${i + 1}. [${err.code}] ${err.message}`, colors.red); log(` SQL: ${err.stmt}...`, colors.yellow); }); log(''); } else { log('==========================================', colors.green); log(' ✓ 迁移完成!', colors.bold + colors.green); log('==========================================', colors.green); log(''); log(` 成功: ${result.ok} 条语句`, colors.green); if (result.skip > 0) { log(` 跳过: ${result.skip} 条语句 (已存在)`, colors.yellow); } log(''); } await conn.end(); if (result.errors.length > 0) { process.exit(1); } } main().catch((err) => { log(`\n迁移失败: ${err.message}`, colors.red); console.error(err); process.exit(1); });