- 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
230 lines
10 KiB
JavaScript
230 lines
10 KiB
JavaScript
/**
|
||
* 直接创建 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);
|
||
});
|