Files
Web_BLV_OA_Exam_Prod/api/controllers/quizController.ts
XuJiacheng dbf9fdc01c feat: 修改部分导入文本的逻辑,添加部署脚本和样式文件,更新数据库迁移逻辑
- 新增部署脚本 `build-deploy-bundle.mjs`,用于构建和部署 web 和 server 目录。
- 新增样式文件 `index-acd65452.css`,包含基础样式和响应式设计。
- 新增脚本 `repro-import-text.mjs`,用于测试文本导入 API。
- 新增测试文件 `db-migration-score-zero.test.ts`,验证历史数据库中 questions.score 约束的迁移逻辑。
- 更新数据库初始化逻辑,允许插入 score=0 的问题。
2026-01-04 09:20:04 +08:00

310 lines
9.4 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 (answer.userAnswer === undefined || answer.userAnswer === null) {
answer.userAnswer = '';
}
// 规则分值为0的题目不判定正误不要求答案默认正确
if (Number(question.score) === 0) {
answer.score = 0;
answer.isCorrect = true;
processedAnswers.push(answer);
continue;
}
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 || '获取答题记录失败'
});
}
}
}