新增文本题库导入功能,题目新增“解析”字段

This commit is contained in:
2025-12-25 00:15:14 +08:00
parent e2a1555b46
commit dc9fc169ec
30 changed files with 1386 additions and 165 deletions

View File

@@ -318,6 +318,89 @@ export class ExamTaskModel {
return { data, total };
}
static async getAllTasksWithStatsPaged(
input: {
page: number;
limit: number;
status?: 'completed' | 'ongoing' | 'notStarted';
endAtStart?: string;
endAtEnd?: string;
},
): Promise<{ data: Array<ActiveTaskStat & { status: '已完成' | '进行中' | '未开始' }>; total: number }> {
const nowIso = new Date().toISOString();
const nowMs = Date.now();
const offset = (input.page - 1) * input.limit;
const whereParts: string[] = [];
const params: any[] = [];
if (input.status === 'completed') {
whereParts.push('t.end_at < ?');
params.push(nowIso);
} else if (input.status === 'ongoing') {
whereParts.push('t.start_at <= ? AND t.end_at >= ?');
params.push(nowIso, nowIso);
} else if (input.status === 'notStarted') {
whereParts.push('t.start_at > ?');
params.push(nowIso);
}
if (input.endAtStart) {
whereParts.push('t.end_at >= ?');
params.push(input.endAtStart);
}
if (input.endAtEnd) {
whereParts.push('t.end_at <= ?');
params.push(input.endAtEnd);
}
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
const totalRow = await get(`SELECT COUNT(*) as total FROM exam_tasks t ${whereClause}`, params);
const total = Number(totalRow?.total || 0);
const tasks = await all(
`
SELECT
t.id, t.name as taskName, s.name as subjectName, s.total_score as totalScore,
t.start_at as startAt, t.end_at as endAt
FROM exam_tasks t
JOIN exam_subjects s ON t.subject_id = s.id
${whereClause}
ORDER BY t.end_at DESC
LIMIT ? OFFSET ?
`,
[...params, input.limit, offset],
);
const data: Array<ActiveTaskStat & { status: '已完成' | '进行中' | '未开始' }> = [];
for (const task of tasks) {
const report = await this.getReport(task.id);
const stat = this.buildActiveTaskStat({
taskId: task.id,
taskName: task.taskName,
subjectName: task.subjectName,
totalScore: Number(task.totalScore) || 0,
startAt: task.startAt,
endAt: task.endAt,
report,
});
const startMs = new Date(task.startAt).getTime();
const endMs = new Date(task.endAt).getTime();
const status: '已完成' | '进行中' | '未开始' =
Number.isFinite(endMs) && endMs < nowMs
? '已完成'
: Number.isFinite(startMs) && startMs > nowMs
? '未开始'
: '进行中';
data.push({ ...stat, status });
}
return { data, total };
}
static async findById(id: string): Promise<ExamTask | null> {
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt, selection_config as selectionConfig FROM exam_tasks WHERE id = ?`;
const row = await get(sql, [id]);

View File

@@ -8,6 +8,7 @@ export interface Question {
category: string;
options?: string[];
answer: string | string[];
analysis: string;
score: number;
createdAt: string;
}
@@ -18,6 +19,7 @@ export interface CreateQuestionData {
category?: string;
options?: string[];
answer: string | string[];
analysis?: string;
score: number;
}
@@ -26,6 +28,7 @@ export interface ExcelQuestionData {
type: string;
category?: string;
answer: string;
analysis?: string;
score: number;
options?: string[];
}
@@ -37,13 +40,14 @@ export class QuestionModel {
const optionsStr = data.options ? JSON.stringify(data.options) : null;
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
const sql = `
INSERT INTO questions (id, content, type, options, answer, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
await run(sql, [id, data.content, data.type, optionsStr, answerStr, data.score, category]);
await run(sql, [id, data.content, data.type, optionsStr, answerStr, analysis, data.score, category]);
return this.findById(id) as Promise<Question>;
}
@@ -58,8 +62,8 @@ export class QuestionModel {
await run('BEGIN TRANSACTION');
const sql = `
INSERT INTO questions (id, content, type, options, answer, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
for (let i = 0; i < questions.length; i++) {
@@ -69,9 +73,10 @@ export class QuestionModel {
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
// 直接执行插入不调用单个create方法
await run(sql, [id, question.content, question.type, optionsStr, answerStr, question.score, category]);
await run(sql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
success++;
} catch (error: any) {
errors.push(`${i + 1}题: ${error.message}`);
@@ -89,6 +94,77 @@ export class QuestionModel {
return { success, errors };
}
static async importFromText(
mode: 'overwrite' | 'incremental',
questions: CreateQuestionData[],
): Promise<{
inserted: number;
updated: number;
errors: string[];
cleared?: { questions: number; quizRecords: number; quizAnswers: number };
}> {
const errors: string[] = [];
let inserted = 0;
let updated = 0;
let cleared: { questions: number; quizRecords: number; quizAnswers: number } | undefined;
const insertSql = `
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
await run('BEGIN TRANSACTION');
try {
if (mode === 'overwrite') {
const [qCount, rCount, aCount] = await Promise.all([
get(`SELECT COUNT(*) as total FROM questions`),
get(`SELECT COUNT(*) as total FROM quiz_records`),
get(`SELECT COUNT(*) as total FROM quiz_answers`),
]);
cleared = { questions: qCount.total, quizRecords: rCount.total, quizAnswers: aCount.total };
await run(`DELETE FROM quiz_answers`);
await run(`DELETE FROM quiz_records`);
await run(`DELETE FROM questions`);
}
for (let i = 0; i < questions.length; i++) {
const question = questions[i];
try {
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
if (mode === 'incremental') {
const existing = await get(`SELECT id FROM questions WHERE content = ?`, [question.content]);
if (existing?.id) {
await run(
`UPDATE questions SET content = ?, type = ?, options = ?, answer = ?, analysis = ?, score = ?, category = ? WHERE id = ?`,
[question.content, question.type, optionsStr, answerStr, analysis, question.score, category, existing.id],
);
updated++;
continue;
}
}
const id = uuidv4();
await run(insertSql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
inserted++;
} catch (error: any) {
errors.push(`${i + 1}题: ${error.message}`);
}
}
await run('COMMIT');
} catch (error) {
await run('ROLLBACK');
throw error;
}
return { inserted, updated, errors, cleared };
}
// 根据ID查找题目
static async findById(id: string): Promise<Question | null> {
const sql = `SELECT * FROM questions WHERE id = ?`;
@@ -219,6 +295,11 @@ export class QuestionModel {
fields.push('answer = ?');
values.push(answerStr);
}
if (data.analysis !== undefined) {
fields.push('analysis = ?');
values.push(String(data.analysis ?? '').trim().slice(0, 255));
}
if (data.score !== undefined) {
fields.push('score = ?');
@@ -257,6 +338,7 @@ export class QuestionModel {
category: row.category || '通用',
options: row.options ? JSON.parse(row.options) : undefined,
answer: this.parseAnswer(row.answer, row.type),
analysis: String(row.analysis ?? ''),
score: row.score,
createdAt: row.created_at
};
@@ -309,6 +391,10 @@ export class QuestionModel {
if (data.category !== undefined && data.category.trim().length === 0) {
errors.push('题目类别不能为空');
}
if (data.analysis !== undefined && String(data.analysis).length > 255) {
errors.push('解析长度不能超过255个字符');
}
return errors;
}

View File

@@ -23,6 +23,7 @@ export interface QuizAnswer {
questionType?: string;
correctAnswer?: string | string[];
questionScore?: number;
questionAnalysis?: string;
}
export interface SubmitAnswerData {
@@ -195,7 +196,7 @@ export class QuizModel {
SELECT a.id, a.record_id as recordId, a.question_id as questionId,
a.user_answer as userAnswer, a.score, a.is_correct as isCorrect,
a.created_at as createdAt,
q.content as questionContent, q.type as questionType, q.answer as correctAnswer, q.score as questionScore
q.content as questionContent, q.type as questionType, q.answer as correctAnswer, q.score as questionScore, q.analysis as questionAnalysis
FROM quiz_answers a
JOIN questions q ON a.question_id = q.id
WHERE a.record_id = ?
@@ -215,7 +216,8 @@ export class QuizModel {
questionContent: row.questionContent,
questionType: row.questionType,
correctAnswer: this.parseAnswer(row.correctAnswer, row.questionType),
questionScore: row.questionScore
questionScore: row.questionScore,
questionAnalysis: row.questionAnalysis ?? ''
}));
}