mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-08 11:07:54 +00:00
222 lines
5.7 KiB
TypeScript
222 lines
5.7 KiB
TypeScript
"use server";
|
||
|
||
import prisma from "@/lib/prisma";
|
||
import { getLocale } from "next-intl/server";
|
||
import { Locale, Status, ProblemLocalization } from "@/generated/client";
|
||
|
||
const getLocalizedTitle = (
|
||
localizations: ProblemLocalization[],
|
||
locale: Locale
|
||
) => {
|
||
if (!localizations || localizations.length === 0) {
|
||
return "Unknown Title";
|
||
}
|
||
|
||
const localization = localizations.find(
|
||
(localization) => localization.locale === locale
|
||
);
|
||
|
||
return localization?.content ?? localizations[0].content ?? "Unknown Title";
|
||
};
|
||
|
||
export interface ProblemCompletionData {
|
||
problemId: string;
|
||
problemDisplayId: number;
|
||
problemTitle: string;
|
||
completed: number;
|
||
uncompleted: number;
|
||
total: number;
|
||
completedPercent: number;
|
||
uncompletedPercent: number;
|
||
}
|
||
|
||
export interface DifficultProblemData {
|
||
id: string;
|
||
className: string;
|
||
problemCount: number;
|
||
problemTitle: string;
|
||
problemDisplayId: number;
|
||
}
|
||
|
||
export async function getProblemCompletionData(): Promise<
|
||
ProblemCompletionData[]
|
||
> {
|
||
// 获取所有提交记录,按题目分组统计
|
||
const submissions = await prisma.submission.findMany({
|
||
include: {
|
||
user: true,
|
||
problem: {
|
||
include: {
|
||
localizations: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
const locale = await getLocale();
|
||
|
||
// 按题目分组统计完成情况(统计独立用户数)
|
||
const problemStats = new Map<
|
||
string,
|
||
{
|
||
completedUsers: Set<string>;
|
||
totalUsers: Set<string>;
|
||
title: string;
|
||
displayId: number;
|
||
}
|
||
>();
|
||
|
||
submissions.forEach((submission) => {
|
||
const localizations = submission.problem.localizations;
|
||
const title = getLocalizedTitle(localizations, locale as Locale);
|
||
const problemId = submission.problemId;
|
||
const problemTitle = title;
|
||
const problemDisplayId = submission.problem.displayId;
|
||
const userId = submission.userId;
|
||
const isCompleted = submission.status === Status.AC; // 只有 Accepted 才算完成
|
||
|
||
if (!problemStats.has(problemId)) {
|
||
problemStats.set(problemId, {
|
||
completedUsers: new Set(),
|
||
totalUsers: new Set(),
|
||
title: problemTitle,
|
||
displayId: problemDisplayId,
|
||
});
|
||
}
|
||
|
||
const stats = problemStats.get(problemId)!;
|
||
stats.totalUsers.add(userId);
|
||
if (isCompleted) {
|
||
stats.completedUsers.add(userId);
|
||
}
|
||
});
|
||
|
||
// 如果没有数据,返回空数组
|
||
if (problemStats.size === 0) {
|
||
return [];
|
||
}
|
||
|
||
// 转换为图表数据格式,按题目displayId排序
|
||
const problemDataArray = Array.from(problemStats.entries()).map(
|
||
([problemId, stats]) => {
|
||
const completed = stats.completedUsers.size;
|
||
const total = stats.totalUsers.size;
|
||
|
||
return {
|
||
problemId: problemId,
|
||
problemDisplayId: stats.displayId,
|
||
problemTitle: stats.title,
|
||
completed: completed,
|
||
uncompleted: total - completed,
|
||
total: total,
|
||
completedPercent: total > 0 ? (completed / total) * 100 : 0,
|
||
uncompletedPercent: total > 0 ? ((total - completed) / total) * 100 : 0,
|
||
};
|
||
}
|
||
);
|
||
|
||
// 按题目编号排序
|
||
return problemDataArray.sort(
|
||
(a, b) => a.problemDisplayId - b.problemDisplayId
|
||
);
|
||
}
|
||
|
||
export async function getDifficultProblemsData(): Promise<
|
||
DifficultProblemData[]
|
||
> {
|
||
// 获取所有测试用例结果
|
||
const testcaseResults = await prisma.testcaseResult.findMany({
|
||
include: {
|
||
testcase: {
|
||
include: {
|
||
problem: {
|
||
include: {
|
||
localizations: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
submission: {
|
||
include: {
|
||
user: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// 按问题分组统计错误率
|
||
const problemStats = new Map<
|
||
string,
|
||
{
|
||
totalAttempts: number;
|
||
wrongAttempts: number;
|
||
title: string;
|
||
displayId: number;
|
||
users: Set<string>;
|
||
}
|
||
>();
|
||
|
||
testcaseResults.forEach((result) => {
|
||
const problemId = result.testcase.problemId;
|
||
const problemTitle =
|
||
result.testcase.problem.localizations?.find((loc) => loc.type === "TITLE")
|
||
?.content || "无标题";
|
||
const problemDisplayId = result.testcase.problem.displayId;
|
||
const userId = result.submission.userId;
|
||
const isWrong = !result.isCorrect;
|
||
|
||
if (!problemStats.has(problemId)) {
|
||
problemStats.set(problemId, {
|
||
totalAttempts: 0,
|
||
wrongAttempts: 0,
|
||
title: problemTitle,
|
||
displayId: problemDisplayId,
|
||
users: new Set(),
|
||
});
|
||
}
|
||
|
||
const stats = problemStats.get(problemId)!;
|
||
stats.totalAttempts++;
|
||
stats.users.add(userId);
|
||
if (isWrong) {
|
||
stats.wrongAttempts++;
|
||
}
|
||
});
|
||
|
||
// 计算错误率并筛选易错题(错误率 > 30% 且至少有3次尝试)
|
||
const difficultProblems = Array.from(problemStats.entries())
|
||
.map(([problemId, stats]) => ({
|
||
id: problemId,
|
||
className: `题目 ${stats.title}`,
|
||
problemCount: stats.wrongAttempts,
|
||
problemTitle: stats.title,
|
||
problemDisplayId: stats.displayId,
|
||
errorRate: (stats.wrongAttempts / stats.totalAttempts) * 100,
|
||
uniqueUsers: stats.users.size,
|
||
totalAttempts: stats.totalAttempts,
|
||
}))
|
||
.filter(
|
||
(problem) =>
|
||
problem.errorRate > 30 && // 错误率超过30%
|
||
problem.totalAttempts >= 3 // 至少有3次尝试
|
||
)
|
||
.sort((a, b) => b.errorRate - a.errorRate) // 按错误率降序排列
|
||
.slice(0, 10); // 取前10个最难的题目
|
||
|
||
return difficultProblems;
|
||
}
|
||
|
||
export async function getDashboardStats() {
|
||
const [problemData, difficultProblems] = await Promise.all([
|
||
getProblemCompletionData(),
|
||
getDifficultProblemsData(),
|
||
]);
|
||
|
||
return {
|
||
problemData,
|
||
difficultProblems,
|
||
totalProblems: problemData.length,
|
||
totalDifficultProblems: difficultProblems.length,
|
||
};
|
||
}
|