feat: 修改部分导入文本的逻辑,添加部署脚本和样式文件,更新数据库迁移逻辑

- 新增部署脚本 `build-deploy-bundle.mjs`,用于构建和部署 web 和 server 目录。
- 新增样式文件 `index-acd65452.css`,包含基础样式和响应式设计。
- 新增脚本 `repro-import-text.mjs`,用于测试文本导入 API。
- 新增测试文件 `db-migration-score-zero.test.ts`,验证历史数据库中 questions.score 约束的迁移逻辑。
- 更新数据库初始化逻辑,允许插入 score=0 的问题。
This commit is contained in:
2026-01-04 09:20:04 +08:00
parent fbfd48e0ca
commit dbf9fdc01c
26 changed files with 1420 additions and 849 deletions

View File

@@ -164,9 +164,84 @@ const ensureUserGroupSchemaAndAllUsersMembership = async () => {
}
};
const getTableCreateSql = async (tableName: string): Promise<string> => {
const row = await get(
`SELECT sql FROM sqlite_master WHERE type='table' AND name=? LIMIT 1`,
[tableName],
);
return (row?.sql as string | undefined) ?? '';
};
const tableSqlHasScoreGreaterThanZeroCheck = (tableSql: string): boolean => {
if (!tableSql) return false;
// 兼容不同空白/大小写写法CHECK(score > 0) / CHECK ( score>0 )
return /check\s*\(\s*score\s*>\s*0\s*\)/i.test(tableSql);
};
const migrateQuestionsScoreCheckToAllowZero = async () => {
const questionsSql = await getTableCreateSql('questions');
if (!tableSqlHasScoreGreaterThanZeroCheck(questionsSql)) return;
console.log('检测到旧表约束questions.score CHECK(score > 0),开始迁移为 >= 0');
// 迁移方式:重建 questions 表SQLite 不支持直接修改 CHECK 约束)
// 注意questions 被 quiz_answers 外键引用,因此迁移期间临时关闭 foreign_keys。
await exec('PRAGMA foreign_keys = OFF;');
try {
await exec('BEGIN TRANSACTION;');
await exec(`
CREATE TABLE IF NOT EXISTS questions_new (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('single', 'multiple', 'judgment', 'text')),
options TEXT,
answer TEXT NOT NULL,
analysis TEXT NOT NULL DEFAULT '',
score INTEGER NOT NULL CHECK(score >= 0),
category TEXT NOT NULL DEFAULT '通用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
await exec(`
INSERT INTO questions_new (id, content, type, options, answer, analysis, score, category, created_at)
SELECT
id,
content,
type,
options,
COALESCE(answer, ''),
COALESCE(analysis, ''),
score,
COALESCE(category, '通用'),
created_at
FROM questions;
`);
await exec('DROP TABLE questions;');
await exec('ALTER TABLE questions_new RENAME TO questions;');
await exec('CREATE INDEX IF NOT EXISTS idx_questions_type ON questions(type);');
await exec('CREATE INDEX IF NOT EXISTS idx_questions_score ON questions(score);');
await exec('CREATE INDEX IF NOT EXISTS idx_questions_category ON questions(category);');
await exec('COMMIT;');
console.log('questions 表迁移完成score 允许 0 分');
} catch (error) {
try {
await exec('ROLLBACK;');
} catch {
// ignore
}
throw error;
} finally {
await exec('PRAGMA foreign_keys = ON;');
}
};
const migrateDatabase = async () => {
// 跳过迁移,因为数据库连接可能未初始化
console.log('跳过数据库迁移');
await migrateQuestionsScoreCheckToAllowZero();
};
// 数据库初始化函数
@@ -194,6 +269,9 @@ export const initDatabase = async () => {
await ensureColumn('quiz_records', "score_percentage REAL", 'score_percentage');
await ensureColumn('quiz_records', "status TEXT", 'status');
// 兼容历史数据库:迁移无法通过 init.sql 修复的约束/结构
await migrateDatabase();
// 用户组(含“全体用户”系统组)
await ensureUserGroupSchemaAndAllUsersMembership();
}

View File

@@ -45,7 +45,7 @@ CREATE TABLE questions (
options TEXT, -- JSON格式存储选项
answer TEXT NOT NULL,
analysis TEXT NOT NULL DEFAULT '',
score INTEGER NOT NULL CHECK(score > 0),
score INTEGER NOT NULL CHECK(score >= 0),
category TEXT NOT NULL DEFAULT '通用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);