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
This commit is contained in:
146
scripts/add-missing-columns.js
Normal file
146
scripts/add-missing-columns.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 为已存在的表添加缺失的列(因 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);
|
||||
});
|
||||
Reference in New Issue
Block a user