2025-06-21 09:44:14 +00:00
|
|
|
|
"use server";
|
|
|
|
|
|
|
|
|
|
import prisma from "@/lib/prisma";
|
|
|
|
|
import { getLocale } from "next-intl/server";
|
2025-06-21 15:19:49 +00:00
|
|
|
|
import { Locale, Status, ProblemLocalization } from "@/generated/client";
|
2025-06-21 09:44:14 +00:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-21 15:19:49 +00:00
|
|
|
|
export async function getProblemCompletionData(): Promise<
|
|
|
|
|
ProblemCompletionData[]
|
|
|
|
|
> {
|
2025-06-21 09:44:14 +00:00
|
|
|
|
// 获取所有提交记录,按题目分组统计
|
|
|
|
|
const submissions = await prisma.submission.findMany({
|
|
|
|
|
include: {
|
|
|
|
|
user: true,
|
|
|
|
|
problem: {
|
2025-06-21 15:19:49 +00:00
|
|
|
|
include: {
|
|
|
|
|
localizations: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-06-21 09:44:14 +00:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const locale = await getLocale();
|
|
|
|
|
|
|
|
|
|
// 按题目分组统计完成情况(统计独立用户数)
|
2025-06-21 15:19:49 +00:00
|
|
|
|
const problemStats = new Map<
|
|
|
|
|
string,
|
|
|
|
|
{
|
|
|
|
|
completedUsers: Set<string>;
|
|
|
|
|
totalUsers: Set<string>;
|
|
|
|
|
title: string;
|
|
|
|
|
displayId: number;
|
|
|
|
|
}
|
|
|
|
|
>();
|
2025-06-21 09:44:14 +00:00
|
|
|
|
|
|
|
|
|
submissions.forEach((submission) => {
|
2025-06-21 15:19:49 +00:00
|
|
|
|
const localizations = submission.problem.localizations;
|
|
|
|
|
const title = getLocalizedTitle(localizations, locale as Locale);
|
2025-06-21 09:44:14 +00:00
|
|
|
|
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)) {
|
2025-06-21 15:19:49 +00:00
|
|
|
|
problemStats.set(problemId, {
|
|
|
|
|
completedUsers: new Set(),
|
|
|
|
|
totalUsers: new Set(),
|
2025-06-21 09:44:14 +00:00
|
|
|
|
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排序
|
2025-06-21 15:19:49 +00:00
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-06-21 09:44:14 +00:00
|
|
|
|
|
|
|
|
|
// 按题目编号排序
|
2025-06-21 15:19:49 +00:00
|
|
|
|
return problemDataArray.sort(
|
|
|
|
|
(a, b) => a.problemDisplayId - b.problemDisplayId
|
|
|
|
|
);
|
2025-06-21 09:44:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-21 15:19:49 +00:00
|
|
|
|
export async function getDifficultProblemsData(): Promise<
|
|
|
|
|
DifficultProblemData[]
|
|
|
|
|
> {
|
2025-06-21 09:44:14 +00:00
|
|
|
|
// 获取所有测试用例结果
|
|
|
|
|
const testcaseResults = await prisma.testcaseResult.findMany({
|
|
|
|
|
include: {
|
|
|
|
|
testcase: {
|
|
|
|
|
include: {
|
|
|
|
|
problem: {
|
|
|
|
|
include: {
|
2025-06-21 15:19:49 +00:00
|
|
|
|
localizations: true,
|
|
|
|
|
},
|
2025-06-21 09:44:14 +00:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
submission: {
|
|
|
|
|
include: {
|
|
|
|
|
user: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 按问题分组统计错误率
|
2025-06-21 15:19:49 +00:00
|
|
|
|
const problemStats = new Map<
|
|
|
|
|
string,
|
|
|
|
|
{
|
|
|
|
|
totalAttempts: number;
|
|
|
|
|
wrongAttempts: number;
|
|
|
|
|
title: string;
|
|
|
|
|
displayId: number;
|
|
|
|
|
users: Set<string>;
|
|
|
|
|
}
|
|
|
|
|
>();
|
2025-06-21 09:44:14 +00:00
|
|
|
|
|
|
|
|
|
testcaseResults.forEach((result) => {
|
|
|
|
|
const problemId = result.testcase.problemId;
|
2025-06-21 15:19:49 +00:00
|
|
|
|
const problemTitle =
|
|
|
|
|
result.testcase.problem.localizations?.find((loc) => loc.type === "TITLE")
|
|
|
|
|
?.content || "无标题";
|
2025-06-21 09:44:14 +00:00
|
|
|
|
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,
|
|
|
|
|
}))
|
2025-06-21 15:19:49 +00:00
|
|
|
|
.filter(
|
|
|
|
|
(problem) =>
|
|
|
|
|
problem.errorRate > 30 && // 错误率超过30%
|
|
|
|
|
problem.totalAttempts >= 3 // 至少有3次尝试
|
2025-06-21 09:44:14 +00:00
|
|
|
|
)
|
|
|
|
|
.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,
|
|
|
|
|
};
|
2025-06-21 15:19:49 +00:00
|
|
|
|
}
|