Files
Web_BLV_OA_Exam_Prod/api/controllers/quizController.ts
XuJiacheng eb4504960e feat: 更新构建流程,添加 API 构建脚本和 SQL 文件复制脚本
- 修改 package.json,更新构建命令,添加 postbuild 脚本以复制 init.sql 文件。
- 新增 scripts/build-api.mjs,使用 esbuild 构建 API 代码。
- 新增 scripts/copy-init-sql.mjs,复制数据库初始化 SQL 文件到构建输出目录。
- 在 SubjectSelectionPage 组件中添加 totalScore 属性,增加历史最高分状态显示功能。
- 在 ExamSubjectPage 和 QuestionManagePage 中优化判断题答案处理逻辑。
- 在 OptionList 组件中将判断题选项文本从 'T' 和 'F' 改为 '对' 和 '错'。
- 在 QuizFooter 组件中调整样式,增加按钮和文本的可读性。
- 新增用户默认组测试用例,验证新用户创建后自动加入“全体用户”系统组。
- 新增 tsconfig.api.json,配置 API 相关 TypeScript 编译选项。
- 移除 vite.config.ts 中的 global 定义。
2025-12-30 20:33:14 +08:00

298 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Request, Response } from 'express';
import { QuestionModel, QuizModel, SystemConfigModel } from '../models';
import { Question } from '../models/question';
export class QuizController {
static async generateQuiz(req: Request, res: Response) {
try {
const { userId, subjectId, taskId } = req.body;
if (!userId) {
return res.status(400).json({
success: false,
message: '用户ID不能为空'
});
}
if (taskId) {
const { ExamTaskModel } = await import('../models/examTask');
const result = await ExamTaskModel.generateQuizQuestions(taskId, userId);
res.json({
success: true,
data: {
questions: result.questions,
totalScore: result.totalScore,
timeLimit: result.timeLimitMinutes
}
});
return;
}
if (!subjectId) {
return res.status(400).json({
success: false,
message: 'subjectId或taskId必须提供其一'
});
}
const { ExamSubjectModel } = await import('../models/examSubject');
const subject = await ExamSubjectModel.findById(subjectId);
if (!subject) {
return res.status(404).json({
success: false,
message: '考试科目不存在'
});
}
const result = await ExamSubjectModel.generateQuizQuestions(subject);
res.json({
success: true,
data: {
questions: result.questions,
totalScore: result.totalScore,
timeLimit: result.timeLimitMinutes
}
});
} catch (error: any) {
const message = error?.message || '生成试卷失败';
const status = message.includes('不存在')
? 404
: [
'用户ID不能为空',
'subjectId或taskId必须提供其一',
'当前时间不在任务有效范围内',
'用户未被分派到此任务',
'考试次数已用尽',
].some((m) => message.includes(m))
? 400
: 500;
res.status(status).json({
success: false,
message,
});
}
}
static async submitQuiz(req: Request, res: Response) {
try {
const { userId, subjectId, taskId, answers } = req.body;
if (!userId || !answers || !Array.isArray(answers)) {
return res.status(400).json({
success: false,
message: '参数不完整'
});
}
const processedAnswers = [];
let totalPossibleScore = 0;
for (const answer of answers) {
const question = await QuestionModel.findById(answer.questionId);
if (!question) {
processedAnswers.push(answer);
continue;
}
totalPossibleScore += Number(question.score) || 0;
if (question.type === 'multiple') {
const optionCount = question.options ? question.options.length : 0;
const unitScore = optionCount > 0 ? question.score / optionCount : 0;
let userAnsList: string[] = [];
if (Array.isArray(answer.userAnswer)) {
userAnsList = answer.userAnswer;
} else if (typeof answer.userAnswer === 'string') {
try {
userAnsList = JSON.parse(answer.userAnswer);
} catch (e) {
userAnsList = [answer.userAnswer];
}
}
let correctAnsList: string[] = [];
if (Array.isArray(question.answer)) {
correctAnsList = question.answer;
} else if (typeof question.answer === 'string') {
try {
const parsed = JSON.parse(question.answer);
if (Array.isArray(parsed)) correctAnsList = parsed;
else correctAnsList = [question.answer];
} catch {
correctAnsList = [question.answer];
}
}
const userSet = new Set(userAnsList);
const correctSet = new Set(correctAnsList);
let isFullCorrect = true;
if (userSet.size !== correctSet.size) {
isFullCorrect = false;
} else {
for (const a of userSet) {
if (!correctSet.has(a)) {
isFullCorrect = false;
break;
}
}
}
if (isFullCorrect) {
answer.score = question.score;
answer.isCorrect = true;
} else {
let tempScore = 0;
for (const uAns of userAnsList) {
if (correctSet.has(uAns)) {
tempScore += unitScore;
} else {
tempScore -= unitScore;
}
}
let finalScore = Math.max(0, tempScore);
finalScore = Math.round(finalScore * 10) / 10;
answer.score = finalScore;
answer.isCorrect = false;
}
} else if (question.type === 'single' || question.type === 'judgment') {
const normalizeJudgment = (raw: unknown) => {
const v = String(raw ?? '').trim();
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
if (yes.has(v)) return '正确';
if (no.has(v)) return '错误';
return v;
};
const isCorrect =
question.type === 'judgment'
? normalizeJudgment(answer.userAnswer) === normalizeJudgment(question.answer)
: answer.userAnswer === question.answer;
answer.score = isCorrect ? question.score : 0;
answer.isCorrect = isCorrect;
}
processedAnswers.push(answer);
}
const result = await QuizModel.submitQuiz({ userId, answers: processedAnswers, totalPossibleScore });
if (subjectId || taskId) {
let finalSubjectId: string | null = subjectId || null;
const finalTaskId: string | null = taskId || null;
// 任务考试场景下如果前端未传subjectId则从任务中反查
if (!finalSubjectId && finalTaskId) {
const { ExamTaskModel } = await import('../models/examTask');
const task = await ExamTaskModel.findById(finalTaskId);
if (task?.subjectId) {
finalSubjectId = task.subjectId;
}
}
const sql = `
UPDATE quiz_records
SET subject_id = ?, task_id = ?
WHERE id = ?
`;
await import('../database').then(({ run }) => run(sql, [finalSubjectId, finalTaskId, result.record.id]));
}
res.json({
success: true,
data: {
recordId: result.record.id,
totalScore: result.record.totalScore,
correctCount: result.record.correctCount,
totalCount: result.record.totalCount
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || '提交答题失败'
});
}
}
static async getUserRecords(req: Request, res: Response) {
try {
const { userId } = req.params;
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const result = await QuizModel.findRecordsByUserId(userId, limit, (page - 1) * limit);
res.json({
success: true,
data: result.records,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit)
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || '获取答题记录失败'
});
}
}
static async getRecordDetail(req: Request, res: Response) {
try {
const { recordId } = req.params;
const record = await QuizModel.findRecordById(recordId);
if (!record) {
return res.status(404).json({
success: false,
message: '答题记录不存在'
});
}
const answers = await QuizModel.findAnswersByRecordId(recordId);
res.json({
success: true,
data: {
record,
answers
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || '获取答题记录详情失败'
});
}
}
static async getAllRecords(req: Request, res: Response) {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const result = await QuizModel.findAllRecords(limit, (page - 1) * limit);
res.json({
success: true,
data: result.records,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit)
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || '获取答题记录失败'
});
}
}
}