/** * 为已存在的表添加缺失的列(因 drizzle-kit push 阻塞未同步的 schema 变更) * * 缺失列清单: * - learning_diagnostic_reports.class_id (v4-P1-4 班级报告关联) * - announcements.is_pinned (V2-P2-13d 公告置顶) * - messages.is_starred (V2-P2-13c 消息星标) * - message_notifications.priority (V2-P2-13b 通知优先级) * - message_notifications.is_archived (V2-P2-13b 通知归档) */ require("dotenv/config"); const mysql = require("mysql2/promise"); // 每个 ALTER 操作:[表名, 描述, SQL语句数组] const ALTER_OPERATIONS = [ { table: "learning_diagnostic_reports", description: "添加 class_id 列(班级报告关联)", sql: [ "ALTER TABLE `learning_diagnostic_reports` ADD COLUMN `class_id` varchar(128) NULL AFTER `generated_by`", "ALTER TABLE `learning_diagnostic_reports` ADD INDEX `diagnostic_class_idx` (`class_id`)", "ALTER TABLE `learning_diagnostic_reports` ADD CONSTRAINT `diagnostic_class_fk` FOREIGN KEY (`class_id`) REFERENCES `classes` (`id`) ON DELETE SET NULL", ], }, { table: "announcements", description: "添加 is_pinned 列(公告置顶)", sql: [ "ALTER TABLE `announcements` ADD COLUMN `is_pinned` boolean NOT NULL DEFAULT false AFTER `published_at`", "ALTER TABLE `announcements` ADD INDEX `announcements_status_pinned_idx` (`status`, `is_pinned`)", ], }, { table: "messages", description: "添加 is_starred 列(消息星标)", sql: [ "ALTER TABLE `messages` ADD COLUMN `is_starred` boolean NOT NULL DEFAULT false AFTER `receiver_deleted_at`", "ALTER TABLE `messages` ADD INDEX `messages_receiver_starred_idx` (`receiver_id`, `is_starred`)", ], }, { table: "message_notifications", description: "添加 priority 和 is_archived 列(通知优先级与归档)", sql: [ "ALTER TABLE `message_notifications` ADD COLUMN `priority` varchar(16) NOT NULL DEFAULT 'normal' AFTER `is_read`", "ALTER TABLE `message_notifications` ADD COLUMN `is_archived` boolean NOT NULL DEFAULT false AFTER `priority`", "ALTER TABLE `message_notifications` ADD INDEX `message_notifications_priority_idx` (`priority`)", "ALTER TABLE `message_notifications` ADD INDEX `message_notifications_user_archived_idx` (`user_id`, `is_archived`)", ], }, ]; async function columnExists(conn, table, column) { const [rows] = await conn.execute( `SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`, [table, column], ); return rows.length > 0; } async function indexExists(conn, table, indexName) { const [rows] = await conn.execute( `SELECT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ? LIMIT 1`, [table, indexName], ); return rows.length > 0; } async function main() { const conn = await mysql.createConnection({ uri: process.env.DATABASE_URL }); console.log("✅ 已连接数据库\n"); for (const op of ALTER_OPERATIONS) { console.log(`${"=".repeat(60)}`); console.log(`表: ${op.table} — ${op.description}`); console.log("=".repeat(60)); for (const sql of op.sql) { try { // 检查是否已存在(ADD COLUMN / ADD INDEX / ADD CONSTRAINT) const isColumn = sql.includes("ADD COLUMN"); const isIndex = sql.includes("ADD INDEX"); const isConstraint = sql.includes("ADD CONSTRAINT"); if (isColumn) { const match = sql.match(/ADD COLUMN `(\w+)`/); if (match && await columnExists(conn, op.table, match[1])) { console.log(` ⏭️ 跳过已存在的列: ${match[1]}`); continue; } } else if (isIndex) { const match = sql.match(/ADD INDEX `(\w+)`/); if (match && await indexExists(conn, op.table, match[1])) { console.log(` ⏭️ 跳过已存在的索引: ${match[1]}`); continue; } } await conn.execute(sql); console.log(` ✅ 执行成功: ${sql.slice(0, 100)}...`); } catch (err) { if (err.code === "ER_DUP_FIELDNAME" || err.code === "ER_DUP_KEYNAME" || err.errno === 1060 || err.errno === 1061) { console.log(` ⏭️ 已存在,跳过: ${err.message}`); } else { console.error(` ❌ 执行失败: ${err.message}`); console.error(` SQL: ${sql}`); } } } console.log(""); } // 验证最终列结构 console.log(`${"=".repeat(60)}`); console.log("最终验证 — 检查缺失列是否已添加"); console.log("=".repeat(60)); const checks = [ ["learning_diagnostic_reports", "class_id"], ["announcements", "is_pinned"], ["messages", "is_starred"], ["message_notifications", "priority"], ["message_notifications", "is_archived"], ]; let allOk = true; for (const [table, col] of checks) { const exists = await columnExists(conn, table, col); const status = exists ? "✅" : "❌"; if (!exists) allOk = false; console.log(` ${status} ${table}.${col}`); } console.log(allOk ? "\n✅ 所有缺失列已就绪" : "\n❌ 仍有缺失列,请检查错误"); await conn.end(); process.exitCode = allOk ? 0 : 1; } main().catch((err) => { console.error("致命错误:", err); process.exit(1); });