feat: 添加得分占比计算功能;优化时间格式化工具函数;更新考试记录和用户任务页面以显示得分占比;新增得分占比测试用例;修复时间解析逻辑
This commit is contained in:
@@ -86,6 +86,7 @@ export class QuizController {
|
||||
}
|
||||
|
||||
const processedAnswers = [];
|
||||
let totalPossibleScore = 0;
|
||||
for (const answer of answers) {
|
||||
const question = await QuestionModel.findById(answer.questionId);
|
||||
if (!question) {
|
||||
@@ -93,6 +94,8 @@ export class QuizController {
|
||||
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;
|
||||
@@ -160,7 +163,7 @@ export class QuizController {
|
||||
processedAnswers.push(answer);
|
||||
}
|
||||
|
||||
const result = await QuizModel.submitQuiz({ userId, answers: processedAnswers });
|
||||
const result = await QuizModel.submitQuiz({ userId, answers: processedAnswers, totalPossibleScore });
|
||||
|
||||
if (subjectId || taskId) {
|
||||
let finalSubjectId: string | null = subjectId || null;
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface TaskReport {
|
||||
userName: string;
|
||||
userPhone: string;
|
||||
score: number | null;
|
||||
scorePercentage: number | null;
|
||||
completedAt: string | null;
|
||||
}>;
|
||||
}
|
||||
@@ -81,16 +82,16 @@ export class ExamTaskModel {
|
||||
: 0;
|
||||
|
||||
const passingUsers = report.details.filter((d) => {
|
||||
if (d.score === null) return false;
|
||||
return d.score / input.totalScore >= 0.6;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 60;
|
||||
}).length;
|
||||
|
||||
const passRate =
|
||||
report.totalUsers > 0 ? Math.round((passingUsers / report.totalUsers) * 100) : 0;
|
||||
|
||||
const excellentUsers = report.details.filter((d) => {
|
||||
if (d.score === null) return false;
|
||||
return d.score / input.totalScore >= 0.8;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 80;
|
||||
}).length;
|
||||
|
||||
const excellentRate =
|
||||
@@ -142,20 +143,20 @@ export class ExamTaskModel {
|
||||
// 获取该任务的详细报表数据
|
||||
const report = await this.getReport(task.id);
|
||||
|
||||
// 计算合格率(得分率60%以上)
|
||||
// 计算合格率(得分占比60%以上)
|
||||
const passingUsers = report.details.filter((d: any) => {
|
||||
if (d.score === null) return false;
|
||||
return (d.score / task.totalScore) >= 0.6;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 60;
|
||||
}).length;
|
||||
|
||||
const passRate = report.totalUsers > 0
|
||||
? Math.round((passingUsers / report.totalUsers) * 100)
|
||||
: 0;
|
||||
|
||||
// 计算优秀率(得分率80%以上)
|
||||
// 计算优秀率(得分占比80%以上)
|
||||
const excellentUsers = report.details.filter((d: any) => {
|
||||
if (d.score === null) return false;
|
||||
return (d.score / task.totalScore) >= 0.8;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 80;
|
||||
}).length;
|
||||
|
||||
const excellentRate = report.totalUsers > 0
|
||||
@@ -198,20 +199,20 @@ export class ExamTaskModel {
|
||||
? Math.round((report.completedUsers / report.totalUsers) * 100)
|
||||
: 0;
|
||||
|
||||
// 4. 计算合格率(得分率60%以上)
|
||||
// 4. 计算合格率(得分占比60%以上)
|
||||
const passingUsers = report.details.filter(d => {
|
||||
if (d.score === null) return false;
|
||||
return (d.score / task.totalScore) >= 0.6;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 60;
|
||||
}).length;
|
||||
|
||||
const passRate = report.totalUsers > 0
|
||||
? Math.round((passingUsers / report.totalUsers) * 100)
|
||||
: 0;
|
||||
|
||||
// 5. 计算优秀率(得分率80%以上)
|
||||
// 5. 计算优秀率(得分占比80%以上)
|
||||
const excellentUsers = report.details.filter(d => {
|
||||
if (d.score === null) return false;
|
||||
return (d.score / task.totalScore) >= 0.8;
|
||||
if (d.scorePercentage === null) return false;
|
||||
return d.scorePercentage >= 80;
|
||||
}).length;
|
||||
|
||||
const excellentRate = report.totalUsers > 0
|
||||
@@ -509,16 +510,39 @@ export class ExamTaskModel {
|
||||
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(task.subjectId));
|
||||
if (!subject) throw new Error('科目不存在');
|
||||
|
||||
// 注意:同一用户可能多次参加同一任务考试,这里取“得分占比最高,其次完成时间最新”的那一次。
|
||||
// 得分占比优先用 quiz_answers/题目分值重算(兼容旧记录),否则回退到 quiz_records.score_percentage。
|
||||
const sqlUsers = `
|
||||
WITH best_records AS (
|
||||
SELECT
|
||||
r.*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY r.user_id, r.task_id
|
||||
ORDER BY COALESCE(r.score_percentage, -1) DESC, r.created_at DESC
|
||||
) as rn
|
||||
FROM quiz_records r
|
||||
WHERE r.task_id = ?
|
||||
),
|
||||
totals AS (
|
||||
SELECT qa.record_id as recordId, SUM(q.score) as totalPossibleScore
|
||||
FROM quiz_answers qa
|
||||
JOIN questions q ON q.id = qa.question_id
|
||||
GROUP BY qa.record_id
|
||||
)
|
||||
SELECT
|
||||
u.id as userId,
|
||||
u.name as userName,
|
||||
u.phone as userPhone,
|
||||
qr.total_score as score,
|
||||
qr.created_at as completedAt
|
||||
br.total_score as score,
|
||||
br.created_at as completedAt,
|
||||
CASE
|
||||
WHEN totals.totalPossibleScore > 0 THEN (br.total_score * 1.0 / totals.totalPossibleScore) * 100
|
||||
ELSE br.score_percentage
|
||||
END as scorePercentage
|
||||
FROM exam_task_users etu
|
||||
JOIN users u ON etu.user_id = u.id
|
||||
LEFT JOIN quiz_records qr ON u.id = qr.user_id AND qr.task_id = ?
|
||||
LEFT JOIN best_records br ON br.user_id = u.id AND br.rn = 1
|
||||
LEFT JOIN totals ON totals.recordId = br.id
|
||||
WHERE etu.task_id = ?
|
||||
`;
|
||||
|
||||
@@ -529,6 +553,7 @@ export class ExamTaskModel {
|
||||
userName: r.userName,
|
||||
userPhone: r.userPhone,
|
||||
score: r.score !== null ? r.score : null,
|
||||
scorePercentage: r.scorePercentage !== null ? Number(r.scorePercentage) : null,
|
||||
completedAt: r.completedAt || null
|
||||
}));
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface SubmitAnswerData {
|
||||
export interface SubmitQuizData {
|
||||
userId: string;
|
||||
answers: SubmitAnswerData[];
|
||||
// 试卷总分(题目满分之和)。用于计算得分占比 = totalScore/totalPossibleScore * 100
|
||||
totalPossibleScore?: number;
|
||||
}
|
||||
|
||||
export class QuizModel {
|
||||
@@ -53,9 +55,11 @@ export class QuizModel {
|
||||
}
|
||||
|
||||
// 创建答题记录
|
||||
static async createRecord(data: { userId: string; totalScore: number; correctCount: number; totalCount: number }): Promise<QuizRecord> {
|
||||
static async createRecord(data: { userId: string; totalScore: number; correctCount: number; totalCount: number; totalPossibleScore?: number }): Promise<QuizRecord> {
|
||||
const id = uuidv4();
|
||||
const scorePercentage = data.totalCount > 0 ? (data.totalScore / data.totalCount) * 100 : 0;
|
||||
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 = `
|
||||
@@ -119,7 +123,8 @@ export class QuizModel {
|
||||
userId: data.userId,
|
||||
totalScore,
|
||||
correctCount,
|
||||
totalCount
|
||||
totalCount,
|
||||
totalPossibleScore: data.totalPossibleScore
|
||||
});
|
||||
|
||||
// 创建答题答案
|
||||
@@ -130,7 +135,35 @@ export class QuizModel {
|
||||
|
||||
// 根据ID查找答题记录
|
||||
static async findRecordById(id: string): Promise<QuizRecord | null> {
|
||||
const sql = `SELECT id, user_id as userId, total_score as totalScore, correct_count as correctCount, total_count as totalCount, score_percentage as scorePercentage, status, created_at as createdAt FROM quiz_records WHERE id = ?`;
|
||||
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;
|
||||
}
|
||||
@@ -139,11 +172,30 @@ export class QuizModel {
|
||||
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,
|
||||
r.score_percentage as scorePercentage, r.status, r.created_at as createdAt,
|
||||
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
|
||||
@@ -170,7 +222,19 @@ export class QuizModel {
|
||||
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,
|
||||
r.score_percentage as scorePercentage, r.status,
|
||||
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,
|
||||
@@ -178,6 +242,12 @@ export class QuizModel {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user