Files
NextEdu/scripts/add-missing-columns.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

147 lines
5.4 KiB
JavaScript
Raw 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.
/**
* 为已存在的表添加缺失的列(因 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);
});