/** * 直接创建 4 个失败模块依赖的数据库表。 * * 背景:drizzle-kit push 因 exam_questions 表的 FK 约束冲突(ER_DROP_INDEX_FK) * 无法执行,导致以下表始终未创建: * - announcements / announcement_reads (公告模块) * - message_notifications (消息模块) * - learning_diagnostic_reports (学情诊断模块) * - error_book_items / error_book_reviews (错题分析模块) * * 本脚本使用 CREATE TABLE IF NOT EXISTS 直接建表,绕过 drizzle-kit 的全量 diff。 * 表结构严格对齐 src/shared/db/schema.ts 中的 Drizzle 定义。 */ require("dotenv/config"); const mysql = require("mysql2/promise"); const TABLES_TO_CHECK = [ "announcements", "announcement_reads", "message_notifications", "learning_diagnostic_reports", "error_book_items", "error_book_reviews", ]; // 按依赖顺序排列:父表在前,子表在后 const CREATE_STATEMENTS = [ // --- 1. announcements --- `CREATE TABLE IF NOT EXISTS \`announcements\` ( \`id\` varchar(128) NOT NULL, \`title\` varchar(255) NOT NULL, \`content\` text NOT NULL, \`type\` enum('school','grade','class') NOT NULL DEFAULT 'school', \`status\` enum('draft','published','archived') NOT NULL DEFAULT 'draft', \`target_grade_id\` varchar(128), \`target_class_id\` varchar(128), \`author_id\` varchar(128) NOT NULL, \`published_at\` datetime, \`is_pinned\` boolean NOT NULL DEFAULT false, \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), INDEX \`announcements_author_idx\` (\`author_id\`), INDEX \`announcements_status_idx\` (\`status\`), INDEX \`announcements_type_idx\` (\`type\`), INDEX \`announcements_target_grade_idx\` (\`target_grade_id\`), INDEX \`announcements_target_class_idx\` (\`target_class_id\`), INDEX \`announcements_status_pinned_idx\` (\`status\`, \`is_pinned\`), CONSTRAINT \`announcements_author_fk\` FOREIGN KEY (\`author_id\`) REFERENCES \`users\` (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, // --- 2. announcement_reads --- `CREATE TABLE IF NOT EXISTS \`announcement_reads\` ( \`id\` varchar(128) NOT NULL, \`announcement_id\` varchar(128) NOT NULL, \`user_id\` varchar(128) NOT NULL, \`read_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), UNIQUE INDEX \`announcement_reads_unique_idx\` (\`announcement_id\`, \`user_id\`), INDEX \`announcement_reads_announcement_idx\` (\`announcement_id\`), INDEX \`announcement_reads_user_idx\` (\`user_id\`), CONSTRAINT \`announcement_reads_announcement_fk\` FOREIGN KEY (\`announcement_id\`) REFERENCES \`announcements\` (\`id\`) ON DELETE CASCADE, CONSTRAINT \`announcement_reads_user_fk\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\` (\`id\`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, // --- 3. message_notifications --- `CREATE TABLE IF NOT EXISTS \`message_notifications\` ( \`id\` varchar(128) NOT NULL, \`user_id\` varchar(128) NOT NULL, \`type\` varchar(128) NOT NULL, \`title\` varchar(255) NOT NULL, \`content\` text, \`link\` varchar(512), \`is_read\` boolean NOT NULL DEFAULT false, \`priority\` varchar(16) NOT NULL DEFAULT 'normal', \`is_archived\` boolean NOT NULL DEFAULT false, \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), INDEX \`message_notifications_user_idx\` (\`user_id\`), INDEX \`message_notifications_is_read_idx\` (\`is_read\`), INDEX \`message_notifications_user_read_idx\` (\`user_id\`, \`is_read\`), INDEX \`message_notifications_created_at_idx\` (\`created_at\`), INDEX \`message_notifications_priority_idx\` (\`priority\`), INDEX \`message_notifications_user_archived_idx\` (\`user_id\`, \`is_archived\`), CONSTRAINT \`message_notifications_user_fk\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\` (\`id\`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, // --- 4. learning_diagnostic_reports --- `CREATE TABLE IF NOT EXISTS \`learning_diagnostic_reports\` ( \`id\` varchar(128) NOT NULL, \`student_id\` varchar(128), \`generated_by\` varchar(128), \`class_id\` varchar(128), \`report_type\` enum('individual','class','grade') NOT NULL DEFAULT 'individual', \`period\` varchar(50), \`summary\` text, \`strengths\` json, \`weaknesses\` json, \`recommendations\` json, \`overall_score\` decimal(5,2), \`status\` enum('draft','published','archived') NOT NULL DEFAULT 'draft', \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), INDEX \`diagnostic_student_idx\` (\`student_id\`), INDEX \`diagnostic_generated_by_idx\` (\`generated_by\`), INDEX \`diagnostic_status_idx\` (\`status\`), INDEX \`diagnostic_report_type_idx\` (\`report_type\`), INDEX \`diagnostic_class_idx\` (\`class_id\`), CONSTRAINT \`diagnostic_student_fk\` FOREIGN KEY (\`student_id\`) REFERENCES \`users\` (\`id\`) ON DELETE CASCADE, CONSTRAINT \`diagnostic_generated_by_fk\` FOREIGN KEY (\`generated_by\`) REFERENCES \`users\` (\`id\`) ON DELETE SET NULL, CONSTRAINT \`diagnostic_class_fk\` FOREIGN KEY (\`class_id\`) REFERENCES \`classes\` (\`id\`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, // --- 5. error_book_items --- `CREATE TABLE IF NOT EXISTS \`error_book_items\` ( \`id\` varchar(128) NOT NULL, \`student_id\` varchar(128) NOT NULL, \`question_id\` varchar(128) NOT NULL, \`source_type\` enum('exam','homework','manual') NOT NULL DEFAULT 'manual', \`source_id\` varchar(128), \`student_answer\` json, \`correct_answer\` json, \`subject_id\` varchar(128), \`knowledge_point_ids\` json, \`status\` enum('new','learning','mastered','archived') NOT NULL DEFAULT 'new', \`mastery_level\` int NOT NULL DEFAULT 0, \`next_review_at\` timestamp NULL, \`review_interval\` int NOT NULL DEFAULT 1, \`review_count\` int NOT NULL DEFAULT 0, \`correct_streak\` int NOT NULL DEFAULT 0, \`note\` text, \`error_tags\` json, \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), INDEX \`eb_item_student_idx\` (\`student_id\`), INDEX \`eb_item_student_status_idx\` (\`student_id\`, \`status\`), INDEX \`eb_item_student_review_idx\` (\`student_id\`, \`next_review_at\`), INDEX \`eb_item_question_idx\` (\`question_id\`), INDEX \`eb_item_subject_idx\` (\`subject_id\`), INDEX \`eb_item_source_idx\` (\`source_type\`, \`source_id\`), CONSTRAINT \`eb_item_student_fk\` FOREIGN KEY (\`student_id\`) REFERENCES \`users\` (\`id\`) ON DELETE CASCADE, CONSTRAINT \`eb_item_question_fk\` FOREIGN KEY (\`question_id\`) REFERENCES \`questions\` (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, // --- 6. error_book_reviews --- `CREATE TABLE IF NOT EXISTS \`error_book_reviews\` ( \`id\` varchar(128) NOT NULL, \`item_id\` varchar(128) NOT NULL, \`student_id\` varchar(128) NOT NULL, \`result\` enum('again','hard','good','easy') NOT NULL, \`reviewed_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, \`new_interval\` int, \`new_mastery_level\` int, \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), INDEX \`eb_review_item_idx\` (\`item_id\`), INDEX \`eb_review_student_idx\` (\`student_id\`), INDEX \`eb_review_student_reviewed_idx\` (\`student_id\`, \`reviewed_at\`), CONSTRAINT \`eb_review_item_fk\` FOREIGN KEY (\`item_id\`) REFERENCES \`error_book_items\` (\`id\`) ON DELETE CASCADE, CONSTRAINT \`eb_review_student_fk\` FOREIGN KEY (\`student_id\`) REFERENCES \`users\` (\`id\`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, ]; async function main() { const url = process.env.DATABASE_URL; if (!url) { console.error("❌ DATABASE_URL 未设置"); process.exit(1); } const conn = await mysql.createConnection({ uri: url }); console.log("✅ 已连接数据库"); // 1. 检查哪些表已存在 const [rows] = await conn.execute( `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${TABLES_TO_CHECK.map(() => "?").join(",")})`, TABLES_TO_CHECK, ); const existing = new Set(rows.map((r) => r.TABLE_NAME)); console.log(`📋 已存在的表: ${[...existing].join(", ") || "(无)"}`); // 2. 按顺序创建缺失的表 for (const sql of CREATE_STATEMENTS) { const match = sql.match(/CREATE TABLE IF NOT EXISTS `(\w+)`/); const tableName = match ? match[1] : "(unknown)"; if (existing.has(tableName)) { console.log(`⏭️ 跳过已存在的表: ${tableName}`); continue; } try { await conn.execute(sql); console.log(`✅ 创建表成功: ${tableName}`); } catch (err) { console.error(`❌ 创建表失败: ${tableName}`); console.error(` 错误: ${err.message}`); console.error(` SQL: ${sql.slice(0, 200)}...`); } } // 3. 最终验证 const [finalRows] = await conn.execute( `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${TABLES_TO_CHECK.map(() => "?").join(",")})`, TABLES_TO_CHECK, ); const finalExisting = new Set(finalRows.map((r) => r.TABLE_NAME)); const missing = TABLES_TO_CHECK.filter((t) => !finalExisting.has(t)); console.log("\n📊 最终状态:"); console.log(` 已存在: ${[...finalExisting].join(", ")}`); if (missing.length > 0) { console.log(` 仍缺失: ${missing.join(", ")}`); process.exitCode = 1; } else { console.log(" ✅ 所有目标表均已就绪"); } await conn.end(); } main().catch((err) => { console.error("致命错误:", err); process.exit(1); });