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

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

@@ -187,6 +187,55 @@ export class AdminController {
}
}
static async getAllTaskStats(req: Request, res: Response) {
try {
const page = toPositiveInt(req.query.page, 1);
const limit = toPositiveInt(req.query.limit, 5);
const statusRaw = typeof req.query.status === 'string' ? req.query.status : undefined;
const status =
statusRaw === 'completed' || statusRaw === 'ongoing' || statusRaw === 'notStarted'
? statusRaw
: undefined;
const endAtStart = typeof req.query.endAtStart === 'string' ? req.query.endAtStart : undefined;
const endAtEnd = typeof req.query.endAtEnd === 'string' ? req.query.endAtEnd : undefined;
if (endAtStart && !Number.isFinite(Date.parse(endAtStart))) {
return res.status(400).json({ success: false, message: 'endAtStart 参数无效' });
}
if (endAtEnd && !Number.isFinite(Date.parse(endAtEnd))) {
return res.status(400).json({ success: false, message: 'endAtEnd 参数无效' });
}
const { ExamTaskModel } = await import('../models/examTask');
const result = await ExamTaskModel.getAllTasksWithStatsPaged({
page,
limit,
status,
endAtStart,
endAtEnd,
});
res.json({
success: true,
data: result.data,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit),
},
});
} catch (error: any) {
console.error('获取任务统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取任务统计失败',
});
}
}
static async getDashboardOverview(req: Request, res: Response) {
try {
const { QuizModel } = await import('../models');

View File

@@ -66,7 +66,7 @@ export class QuestionController {
// 创建题目
static async createQuestion(req: Request, res: Response) {
try {
const { content, type, category, options, answer, score } = req.body;
const { content, type, category, options, answer, analysis, score } = req.body;
const questionData: CreateQuestionData = {
content,
@@ -74,6 +74,7 @@ export class QuestionController {
category,
options,
answer,
analysis,
score
};
@@ -106,7 +107,7 @@ export class QuestionController {
static async updateQuestion(req: Request, res: Response) {
try {
const { id } = req.params;
const { content, type, category, options, answer, score } = req.body;
const { content, type, category, options, answer, analysis, score } = req.body;
const updateData: Partial<CreateQuestionData> = {};
if (content !== undefined) updateData.content = content;
@@ -114,6 +115,7 @@ export class QuestionController {
if (category !== undefined) updateData.category = category;
if (options !== undefined) updateData.options = options;
if (answer !== undefined) updateData.answer = answer;
if (analysis !== undefined) updateData.analysis = analysis;
if (score !== undefined) updateData.score = score;
const question = await QuestionModel.update(id, updateData);
@@ -181,6 +183,7 @@ export class QuestionController {
type: QuestionController.mapQuestionType(row['题型'] || row['type']),
category: row['题目类别'] || row['category'] || '通用',
answer: row['标准答案'] || row['answer'],
analysis: row['解析'] || row['analysis'] || '',
score: parseInt(row['分值'] || row['score']) || 0,
options: QuestionController.parseOptions(row['选项'] || row['options'])
}));
@@ -215,6 +218,120 @@ export class QuestionController {
}
}
static async importTextQuestions(req: Request, res: Response) {
try {
const mode = req.body?.mode as 'overwrite' | 'incremental';
const rawQuestions = req.body?.questions as any[];
if (mode !== 'overwrite' && mode !== 'incremental') {
return res.status(400).json({
success: false,
message: '导入模式不合法',
});
}
if (!Array.isArray(rawQuestions) || rawQuestions.length === 0) {
return res.status(400).json({
success: false,
message: '题目列表不能为空',
});
}
const errors: string[] = [];
const normalized = new Map<string, CreateQuestionData>();
const normalizeAnswer = (type: string, answer: unknown): string | string[] => {
if (type === 'multiple') {
if (Array.isArray(answer)) {
return answer.map((a) => String(a).trim()).filter(Boolean);
}
return String(answer || '')
.split(/[|,,、\s]+/g)
.map((s) => s.trim())
.filter(Boolean);
}
if (Array.isArray(answer)) {
return String(answer[0] ?? '').trim();
}
return String(answer ?? '').trim();
};
const normalizeJudgment = (answer: string) => {
const v = String(answer || '').trim();
const yes = new Set(['正确', '对', '是', 'true', 'True', 'TRUE', '1', 'Y', 'y', 'yes', 'YES']);
const no = new Set(['错误', '错', '否', '不是', 'false', 'False', 'FALSE', '0', 'N', 'n', 'no', 'NO']);
if (yes.has(v)) return '正确';
if (no.has(v)) return '错误';
return v;
};
for (let i = 0; i < rawQuestions.length; i++) {
const q = rawQuestions[i] || {};
const content = String(q.content ?? '').trim();
if (!content) {
errors.push(`${i + 1}题:题目内容不能为空`);
continue;
}
const type = QuestionController.mapQuestionType(String(q.type ?? '').trim());
const category = String(q.category ?? '通用').trim() || '通用';
const score = Number(q.score);
const options = Array.isArray(q.options) ? q.options.map((o: any) => String(o).trim()).filter(Boolean) : undefined;
let answer = normalizeAnswer(type, q.answer);
const analysis = String(q.analysis ?? '').trim();
if (type === 'judgment' && typeof answer === 'string') {
answer = normalizeJudgment(answer);
}
const questionData: CreateQuestionData = {
content,
type: type as any,
category,
options: type === 'single' || type === 'multiple' ? options : undefined,
answer: answer as any,
analysis,
score,
};
const validationErrors = QuestionModel.validateQuestionData(questionData);
if (validationErrors.length > 0) {
errors.push(`${i + 1}题:${validationErrors.join('')}`);
continue;
}
normalized.set(content, questionData);
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
message: '数据验证失败',
errors,
});
}
const result = await QuestionModel.importFromText(mode, Array.from(normalized.values()));
res.json({
success: true,
data: {
mode,
total: normalized.size,
inserted: result.inserted,
updated: result.updated,
errors: result.errors,
cleared: result.cleared ?? undefined,
},
});
} catch (error: any) {
console.error('文本导入失败:', error);
res.status(500).json({
success: false,
message: error.message || '文本导入失败',
});
}
}
// 映射题型
private static mapQuestionType(type: string): string {
const typeMap: { [key: string]: string } = {
@@ -270,6 +387,7 @@ export class QuestionController {
'题目类别': question.category || '通用',
'选项': question.options ? question.options.join('|') : '',
'标准答案': question.answer,
'解析': question.analysis || '',
'分值': question.score,
'创建时间': new Date(question.createdAt).toLocaleString()
}));