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

@@ -51,11 +51,12 @@ export class QuestionModel {
static async create(data: CreateQuestionData): Promise<Question> {
const id = uuidv4();
const optionsStr = data.options ? JSON.stringify(data.options) : null;
const rawAnswer = (data as any).answer;
const normalizedAnswer =
data.type === 'judgment' && !Array.isArray(data.answer)
? this.normalizeJudgmentAnswer(data.answer)
: data.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
data.type === 'judgment' && !Array.isArray(rawAnswer)
? this.normalizeJudgmentAnswer(rawAnswer)
: rawAnswer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : String(normalizedAnswer ?? '');
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
@@ -88,11 +89,12 @@ export class QuestionModel {
const question = questions[i];
const id = uuidv4();
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const rawAnswer = (question as any).answer;
const normalizedAnswer =
question.type === 'judgment' && !Array.isArray(question.answer)
? this.normalizeJudgmentAnswer(question.answer)
: question.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
question.type === 'judgment' && !Array.isArray(rawAnswer)
? this.normalizeJudgmentAnswer(rawAnswer)
: rawAnswer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : String(normalizedAnswer ?? '');
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
@@ -153,11 +155,12 @@ export class QuestionModel {
const question = questions[i];
try {
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const rawAnswer = (question as any).answer;
const normalizedAnswer =
question.type === 'judgment' && !Array.isArray(question.answer)
? this.normalizeJudgmentAnswer(question.answer)
: question.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
question.type === 'judgment' && !Array.isArray(rawAnswer)
? this.normalizeJudgmentAnswer(rawAnswer)
: rawAnswer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : String(normalizedAnswer ?? '');
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
@@ -316,11 +319,12 @@ export class QuestionModel {
}
if (data.answer !== undefined) {
const rawAnswer = (data as any).answer;
const normalizedAnswer =
data.type === 'judgment' && !Array.isArray(data.answer)
? this.normalizeJudgmentAnswer(data.answer)
: data.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
data.type === 'judgment' && !Array.isArray(rawAnswer)
? this.normalizeJudgmentAnswer(rawAnswer)
: rawAnswer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : String(normalizedAnswer ?? '');
fields.push('answer = ?');
values.push(answerStr);
}
@@ -411,13 +415,20 @@ export class QuestionModel {
}
// 验证答案
if (!data.answer) {
errors.push('答案不能为空');
const score = Number((data as any).score);
const allowEmptyAnswer = Number.isFinite(score) && score === 0;
if (!allowEmptyAnswer) {
const ans = (data as any).answer;
const isEmptyArray = Array.isArray(ans) && ans.length === 0;
const isEmptyString = typeof ans === 'string' && ans.trim().length === 0;
if (ans === undefined || ans === null || isEmptyArray || isEmptyString) {
errors.push('答案不能为空');
}
}
// 验证分值
if (!data.score || data.score <= 0) {
errors.push('分值必须是数');
if (!Number.isFinite(score) || score < 0) {
errors.push('分值必须是非负数');
}
if (data.category !== undefined && data.category.trim().length === 0) {
@@ -445,12 +456,16 @@ export class QuestionModel {
errors.push(`${index + 1}行:题型必须是 single、multiple、judgment 或 text`);
}
if (!row.answer) {
const score = Number((row as any).score);
const allowEmptyAnswer = Number.isFinite(score) && score === 0;
const ans = (row as any).answer;
const isEmptyString = typeof ans === 'string' && ans.trim().length === 0;
if (!allowEmptyAnswer && (ans === undefined || ans === null || isEmptyString)) {
errors.push(`${index + 1}行:答案不能为空`);
}
if (!row.score || row.score <= 0) {
errors.push(`${index + 1}行:分值必须是`);
if (!Number.isFinite(score) || score < 0) {
errors.push(`${index + 1}行:分值必须是非负`);
}
});