Files
NextEdu/scripts/create-missing-tables.js
SpecialX 9783be58c0 feat(scripts): add diagnostic, seed, and test scripts
- Add add-ai-provider-visibility and add-missing-columns migration scripts

- Add clear-error-book, seed-error-book, diagnose-error-book scripts

- Add diagnose-tables and create-missing-tables scripts

- Add test-failing-modules and test-teacher-pages test scripts
2026-06-24 12:01:54 +08:00

230 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 直接创建 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);
});