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