新增文本题库导入功能,题目新增“解析”字段
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ?? ''
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user