Files
Web_BLV_OA_Exam_Prod/api/models/quiz.ts

371 lines
13 KiB
TypeScript
Raw Normal View History

import { v4 as uuidv4 } from 'uuid';
import { query, run, get } from '../database';
import { Question } from './question';
export interface QuizRecord {
id: string;
userId: string;
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
subjectId?: string;
subjectName?: string;
taskId?: string;
taskName?: string;
}
export interface QuizAnswer {
id: string;
recordId: string;
questionId: string;
userAnswer: string | string[];
score: number;
isCorrect: boolean;
createdAt: string;
questionContent?: string;
questionType?: string;
correctAnswer?: string | string[];
questionScore?: number;
questionAnalysis?: string;
}
export interface SubmitAnswerData {
questionId: string;
userAnswer: string | string[];
score: number;
isCorrect: boolean;
}
export interface SubmitQuizData {
userId: string;
answers: SubmitAnswerData[];
// 试卷总分(题目满分之和)。用于计算得分占比 = totalScore/totalPossibleScore * 100
totalPossibleScore?: number;
}
export class QuizModel {
// 计算考试状态
private static calculateStatus(scorePercentage: number): '不及格' | '合格' | '优秀' {
if (scorePercentage < 60) return '不及格';
if (scorePercentage < 80) return '合格';
return '优秀';
}
// 创建答题记录
static async createRecord(data: { userId: string; totalScore: number; correctCount: number; totalCount: number; totalPossibleScore?: number }): Promise<QuizRecord> {
const id = uuidv4();
const denomRaw = typeof data.totalPossibleScore === 'number' ? data.totalPossibleScore : data.totalCount;
const denom = denomRaw > 0 ? denomRaw : 0;
const scorePercentage = denom > 0 ? (data.totalScore / denom) * 100 : 0;
const status = this.calculateStatus(scorePercentage);
const sql = `
INSERT INTO quiz_records (id, user_id, total_score, correct_count, total_count, score_percentage, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
await run(sql, [id, data.userId, data.totalScore, data.correctCount, data.totalCount, scorePercentage, status]);
return this.findRecordById(id) as Promise<QuizRecord>;
}
// 创建答题答案
static async createAnswer(data: Omit<QuizAnswer, 'id' | 'createdAt'>): Promise<QuizAnswer> {
const id = uuidv4();
const userAnswerStr = Array.isArray(data.userAnswer) ? JSON.stringify(data.userAnswer) : data.userAnswer;
const sql = `
INSERT INTO quiz_answers (id, record_id, question_id, user_answer, score, is_correct)
VALUES (?, ?, ?, ?, ?, ?)
`;
await run(sql, [id, data.recordId, data.questionId, userAnswerStr, data.score, data.isCorrect]);
return {
id,
recordId: data.recordId,
questionId: data.questionId,
userAnswer: data.userAnswer,
score: data.score,
isCorrect: data.isCorrect,
createdAt: new Date().toISOString()
};
}
// 批量创建答题答案
static async createAnswers(recordId: string, answers: SubmitAnswerData[]): Promise<QuizAnswer[]> {
const createdAnswers: QuizAnswer[] = [];
for (const answer of answers) {
const createdAnswer = await this.createAnswer({
recordId,
questionId: answer.questionId,
userAnswer: answer.userAnswer,
score: answer.score,
isCorrect: answer.isCorrect
});
createdAnswers.push(createdAnswer);
}
return createdAnswers;
}
// 提交答题
static async submitQuiz(data: SubmitQuizData): Promise<{ record: QuizRecord; answers: QuizAnswer[] }> {
const totalScore = data.answers.reduce((sum, answer) => sum + answer.score, 0);
const correctCount = data.answers.filter(answer => answer.isCorrect).length;
const totalCount = data.answers.length;
// 创建答题记录
const record = await this.createRecord({
userId: data.userId,
totalScore,
correctCount,
totalCount,
totalPossibleScore: data.totalPossibleScore
});
// 创建答题答案
const answers = await this.createAnswers(record.id, data.answers);
return { record, answers };
}
// 根据ID查找答题记录
static async findRecordById(id: string): Promise<QuizRecord | null> {
const sql = `
SELECT r.id,
r.user_id as userId,
r.total_score as totalScore,
r.correct_count as correctCount,
r.total_count as totalCount,
CASE
WHEN totals.totalPossibleScore > 0 THEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100
ELSE r.score_percentage
END as scorePercentage,
CASE
WHEN totals.totalPossibleScore > 0 THEN
CASE
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 60 THEN '不及格'
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 80 THEN '合格'
ELSE '优秀'
END
ELSE r.status
END as status,
r.created_at as createdAt
FROM quiz_records r
LEFT JOIN (
SELECT a.record_id as recordId, SUM(q.score) as totalPossibleScore
FROM quiz_answers a
JOIN questions q ON a.question_id = q.id
GROUP BY a.record_id
) totals ON totals.recordId = r.id
WHERE r.id = ?
`;
const record = await get(sql, [id]);
return record || null;
}
// 获取用户的答题记录
static async findRecordsByUserId(userId: string, limit = 10, offset = 0): Promise<{ records: QuizRecord[]; total: number }> {
const recordsSql = `
SELECT r.id, r.user_id as userId, r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount,
CASE
WHEN totals.totalPossibleScore > 0 THEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100
ELSE r.score_percentage
END as scorePercentage,
CASE
WHEN totals.totalPossibleScore > 0 THEN
CASE
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 60 THEN '不及格'
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 80 THEN '合格'
ELSE '优秀'
END
ELSE r.status
END as status,
r.created_at as createdAt,
COALESCE(r.subject_id, t.subject_id) as subjectId,
COALESCE(s.name, ts.name) as subjectName,
r.task_id as taskId, t.name as taskName
FROM quiz_records r
LEFT JOIN (
SELECT a.record_id as recordId, SUM(q.score) as totalPossibleScore
FROM quiz_answers a
JOIN questions q ON a.question_id = q.id
GROUP BY a.record_id
) totals ON totals.recordId = r.id
LEFT JOIN exam_tasks t ON r.task_id = t.id
LEFT JOIN exam_subjects s ON r.subject_id = s.id
LEFT JOIN exam_subjects ts ON t.subject_id = ts.id
WHERE r.user_id = ?
ORDER BY r.created_at DESC
LIMIT ? OFFSET ?
`;
const countSql = `SELECT COUNT(*) as total FROM quiz_records WHERE user_id = ?`;
const [records, countResult] = await Promise.all([
query(recordsSql, [userId, limit, offset]),
get(countSql, [userId])
]);
return {
records,
total: countResult.total
};
}
// 获取所有答题记录(管理员用)
static async findAllRecords(limit = 10, offset = 0): Promise<{ records: any[]; total: number }> {
const recordsSql = `
SELECT r.id, r.user_id as userId, u.name as userName, u.phone as userPhone,
r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount,
CASE
WHEN totals.totalPossibleScore > 0 THEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100
ELSE r.score_percentage
END as scorePercentage,
CASE
WHEN totals.totalPossibleScore > 0 THEN
CASE
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 60 THEN '不及格'
WHEN (r.total_score * 1.0 / totals.totalPossibleScore) * 100 < 80 THEN '合格'
ELSE '优秀'
END
ELSE r.status
END as status,
r.created_at as createdAt,
COALESCE(r.subject_id, t.subject_id) as subjectId,
COALESCE(s.name, ts.name) as subjectName,
r.task_id as taskId,
t.name as taskName
FROM quiz_records r
JOIN users u ON r.user_id = u.id
LEFT JOIN (
SELECT a.record_id as recordId, SUM(q.score) as totalPossibleScore
FROM quiz_answers a
JOIN questions q ON a.question_id = q.id
GROUP BY a.record_id
) totals ON totals.recordId = r.id
LEFT JOIN exam_tasks t ON r.task_id = t.id
LEFT JOIN exam_subjects s ON r.subject_id = s.id
LEFT JOIN exam_subjects ts ON t.subject_id = ts.id
ORDER BY r.created_at DESC
LIMIT ? OFFSET ?
`;
const countSql = `SELECT COUNT(*) as total FROM quiz_records`;
const [records, countResult] = await Promise.all([
query(recordsSql, [limit, offset]),
get(countSql)
]);
// 对于每条记录,计算该考试任务的参与人数
const processedRecords = await Promise.all(records.map(async (record) => {
let examCount = 0;
if (record.taskId) {
// 统计该任务的参与人数
const taskCountSql = `SELECT COUNT(DISTINCT user_id) as count FROM quiz_records WHERE task_id = ?`;
const taskCountResult = await get(taskCountSql, [record.taskId]);
examCount = taskCountResult.count || 0;
} else if (record.subjectId) {
// 统计该科目的参与人数
const subjectCountSql = `SELECT COUNT(DISTINCT user_id) as count FROM quiz_records WHERE subject_id = ?`;
const subjectCountResult = await get(subjectCountSql, [record.subjectId]);
examCount = subjectCountResult.count || 0;
}
return {
...record,
examCount
};
}));
return {
records: processedRecords,
total: countResult.total
};
}
// 获取答题答案详情
static async findAnswersByRecordId(recordId: string): Promise<QuizAnswer[]> {
const sql = `
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.analysis as questionAnalysis
FROM quiz_answers a
JOIN questions q ON a.question_id = q.id
WHERE a.record_id = ?
ORDER BY a.created_at ASC
`;
const answers = await query(sql, [recordId]);
return answers.map(row => ({
id: row.id,
recordId: row.recordId,
questionId: row.questionId,
userAnswer: this.parseAnswer(row.userAnswer, row.questionType),
score: row.score,
isCorrect: Boolean(row.isCorrect),
createdAt: row.createdAt,
questionContent: row.questionContent,
questionType: row.questionType,
correctAnswer: this.parseAnswer(row.correctAnswer, row.questionType),
questionScore: row.questionScore,
questionAnalysis: row.questionAnalysis ?? ''
}));
}
// 解析答案
private static parseAnswer(answer: string, type: string): string | string[] {
if (type === 'multiple' || type === 'checkbox') {
try {
return JSON.parse(answer);
} catch (e) {
return answer;
}
}
return answer;
}
// 获取统计数据
static async getStatistics(): Promise<{
totalUsers: number;
totalRecords: number;
averageScore: number;
typeStats: Array<{ type: string; total: number; correct: number; correctRate: number }>;
}> {
const totalUsersSql = `SELECT COUNT(*) as total FROM users`;
const totalRecordsSql = `SELECT COUNT(*) as total FROM quiz_records`;
const averageScoreSql = `SELECT AVG(total_score) as average FROM quiz_records`;
const typeStatsSql = `
SELECT q.type,
COUNT(*) as total,
SUM(CASE WHEN qa.is_correct = 1 THEN 1 ELSE 0 END) as correct,
ROUND(SUM(CASE WHEN qa.is_correct = 1 THEN 1.0 ELSE 0.0 END) * 100 / COUNT(*), 2) as correctRate
FROM quiz_answers qa
JOIN questions q ON qa.question_id = q.id
GROUP BY q.type
`;
const [totalUsers, totalRecords, averageScore, typeStats] = await Promise.all([
get(totalUsersSql),
get(totalRecordsSql),
get(averageScoreSql),
query(typeStatsSql)
]);
return {
totalUsers: totalUsers.total,
totalRecords: totalRecords.total,
averageScore: Math.round(averageScore.average * 100) / 100,
typeStats
};
}
}