import { promises as fs } from "node:fs"; import path from "node:path"; import { createConnection } from "mysql2/promise"; import { env } from "../config/env"; // 迁移脚本使用单独连接而不是连接池,因为它是一次性命令,不是长时间运行的 HTTP 服务。 async function migrate(): Promise { const connection = await createConnection({ host: env.DB_HOST, port: env.DB_PORT, user: env.DB_USER, password: env.DB_PASSWORD, database: env.DB_NAME, // 迁移文件里可能包含多个 CREATE TABLE / INSERT 语句,所以需要允许多语句执行。 multipleStatements: true, timezone: "+08:00" }); try { // schema_migrations 记录每个已经执行过的 SQL 文件名。 // 之后重复运行 pnpm db:migrate 时,已执行过的迁移会被跳过。 await connection.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( version VARCHAR(255) NOT NULL, applied_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (version) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='数据库迁移记录表'; `); const migrationsDir = path.resolve(process.cwd(), "migrations"); const files = (await fs.readdir(migrationsDir)) .filter((file) => file.endsWith(".sql")) .sort(); for (const file of files) { // 以文件名作为版本号,简单直观,适合这个学习项目。 const [rows] = await connection.query( "SELECT version FROM schema_migrations WHERE version = ? LIMIT 1", [file] ); if (Array.isArray(rows) && rows.length > 0) { console.log(`跳过已执行迁移:${file}`); continue; } const sql = await fs.readFile(path.join(migrationsDir, file), "utf8"); // 迁移文件按文件名顺序执行,便于团队协作时追踪每一次表结构变化。 await connection.query(sql); await connection.query("INSERT INTO schema_migrations (version) VALUES (?)", [file]); console.log(`已执行迁移:${file}`); } } finally { await connection.end(); } } migrate().catch((error: unknown) => { console.error("数据库迁移失败:", error); process.exit(1); });