diff --git a/prisma/migrations/20260529055652_remove_assignment_problem_max_points/migration.sql b/prisma/migrations/20260529055652_remove_assignment_problem_max_points/migration.sql
new file mode 100644
index 0000000..edd0a13
--- /dev/null
+++ b/prisma/migrations/20260529055652_remove_assignment_problem_max_points/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `maxPoints` on the `AssignmentProblem` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "AssignmentProblem" DROP COLUMN "maxPoints";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2769a32..c37ff4d 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -275,7 +275,6 @@ model Assignment {
model AssignmentProblem {
assignmentId String
problemId String
- maxPoints Int @default(100)
order Int?
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 71eed22..f429eaf 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -3128,7 +3128,6 @@ export async function main() {
data: selectedProblems.map((problem, index) => ({
assignmentId: assignment.id,
problemId: problem.id,
- maxPoints: 100,
order: index + 1,
})),
skipDuplicates: true,
diff --git a/src/app/(protected)/dashboard/actions/assignment-stats.ts b/src/app/(protected)/dashboard/actions/assignment-stats.ts
index 741fdb9..5fd737e 100644
--- a/src/app/(protected)/dashboard/actions/assignment-stats.ts
+++ b/src/app/(protected)/dashboard/actions/assignment-stats.ts
@@ -9,37 +9,44 @@ import {
getAuthenticatedActor,
} from "@/app/(protected)/dashboard/actions/course-auth";
-interface ProblemScoreRow {
+interface ProblemProgressRow {
problemId: string;
displayId: number;
title: string;
- maxPoints: number;
- earnedPoints: number;
+ testcaseCount: number;
+ passedTestcaseCount: number;
solved: boolean;
attempts: number;
bestStatus: string;
}
-function buildProblemScoreRows(
+function buildProblemProgressRows(
assignmentProblems: {
problemId: string;
- maxPoints: number;
problem: {
displayId: number;
+ testcases: { id: string }[];
localizations: { content: string }[];
};
}[],
submissions: {
problemId: string;
status: string;
+ judge: {
+ judgeRuns: {
+ testcaseId: string;
+ status: string;
+ }[];
+ } | null;
}[]
-): ProblemScoreRow[] {
+): ProblemProgressRow[] {
const grouped = new Map<
string,
{
attempts: number;
solved: boolean;
bestStatus: string;
+ passedTestcaseCount: number;
}
>();
@@ -48,12 +55,27 @@ function buildProblemScoreRows(
attempts: 0,
solved: false,
bestStatus: "PD",
+ passedTestcaseCount: 0,
};
+ const passedTestcaseIds = new Set(
+ submission.judge?.judgeRuns
+ .filter((run) => run.status === "ACCEPTED")
+ .map((run) => run.testcaseId) ?? []
+ );
+
current.attempts += 1;
+ if (passedTestcaseIds.size > current.passedTestcaseCount) {
+ current.passedTestcaseCount = passedTestcaseIds.size;
+ current.bestStatus = submission.status;
+ }
if (submission.status === "AC") {
current.solved = true;
current.bestStatus = "AC";
- } else if (!current.solved) {
+ current.passedTestcaseCount = Math.max(
+ current.passedTestcaseCount,
+ passedTestcaseIds.size
+ );
+ } else if (!current.solved && current.bestStatus === "PD") {
current.bestStatus = submission.status;
}
grouped.set(submission.problemId, current);
@@ -61,13 +83,16 @@ function buildProblemScoreRows(
return assignmentProblems.map((item) => {
const progress = grouped.get(item.problemId);
+ const testcaseCount = item.problem.testcases.length;
const solved = Boolean(progress?.solved);
return {
problemId: item.problemId,
displayId: item.problem.displayId,
title: item.problem.localizations[0]?.content ?? `题目${item.problem.displayId}`,
- maxPoints: item.maxPoints,
- earnedPoints: solved ? item.maxPoints : 0,
+ testcaseCount,
+ passedTestcaseCount: solved
+ ? testcaseCount
+ : progress?.passedTestcaseCount ?? 0,
solved,
attempts: progress?.attempts ?? 0,
bestStatus: progress?.bestStatus ?? "-",
@@ -109,10 +134,12 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
select: {
problemId: true,
- maxPoints: true,
problem: {
select: {
displayId: true,
+ testcases: {
+ select: { id: true },
+ },
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
@@ -126,6 +153,16 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
userId: true,
problemId: true,
status: true,
+ judge: {
+ select: {
+ judgeRuns: {
+ select: {
+ testcaseId: true,
+ status: true,
+ },
+ },
+ },
+ },
},
},
},
@@ -138,14 +175,24 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
await assertCourseManagePermission(assignment.courseId, actor);
const problemCount = assignment.problems.length;
- const maxScore = assignment.problems.reduce(
- (total, problem) => total + problem.maxPoints,
+ const totalTestcases = assignment.problems.reduce(
+ (total, problem) => total + problem.problem.testcases.length,
0
);
const submissionGroup = new Map<
string,
- { userId: string; problemId: string; status: string }[]
+ {
+ userId: string;
+ problemId: string;
+ status: string;
+ judge: {
+ judgeRuns: {
+ testcaseId: string;
+ status: string;
+ }[];
+ } | null;
+ }[]
>();
for (const submission of assignment.submissions) {
const bucket = submissionGroup.get(submission.userId) ?? [];
@@ -154,34 +201,41 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
}
const students = assignment.course.enrollments.map((enrollment) => {
- const rows = buildProblemScoreRows(
+ const rows = buildProblemProgressRows(
assignment.problems,
submissionGroup.get(enrollment.userId) ?? []
);
- const score = rows.reduce((sum, row) => sum + row.earnedPoints, 0);
+ const passedTestcaseCount = rows.reduce(
+ (sum, row) => sum + row.passedTestcaseCount,
+ 0
+ );
const solvedCount = rows.filter((row) => row.solved).length;
- const completionPercent =
- problemCount > 0 ? Math.round((solvedCount / problemCount) * 100) : 0;
return {
userId: enrollment.userId,
name: enrollment.user.name,
email: enrollment.user.email,
- totalScore: score,
- maxScore,
+ passedTestcaseCount,
+ totalTestcases,
solvedCount,
problemCount,
- completionPercent,
perProblem: rows,
};
});
const perProblemCoverage = assignment.problems.map((problem) => {
+ const testcaseCount = problem.problem.testcases.length;
const solvedUsers = students.filter((student) =>
student.perProblem.some(
(row) => row.problemId === problem.problemId && row.solved
)
).length;
+ const passedTestcaseCount = students.reduce((total, student) => {
+ const row = student.perProblem.find(
+ (item) => item.problemId === problem.problemId
+ );
+ return total + (row?.passedTestcaseCount ?? 0);
+ }, 0);
const totalUsers = students.length;
return {
problemId: problem.problemId,
@@ -189,10 +243,10 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
title:
problem.problem.localizations[0]?.content ??
`题目${problem.problem.displayId}`,
+ passedTestcaseCount,
+ testcaseCount,
solvedUsers,
totalUsers,
- acCoverage:
- totalUsers > 0 ? Math.round((solvedUsers / totalUsers) * 100) : 0,
};
});
@@ -207,8 +261,8 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
id: assignment.course.id,
title: assignment.course.title,
},
- maxScore,
problemCount,
+ totalTestcases,
students,
perProblemCoverage,
};
@@ -238,10 +292,12 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
select: {
problemId: true,
- maxPoints: true,
problem: {
select: {
displayId: true,
+ testcases: {
+ select: { id: true },
+ },
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
@@ -270,15 +326,26 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
select: {
problemId: true,
status: true,
+ judge: {
+ select: {
+ judgeRuns: {
+ select: {
+ testcaseId: true,
+ status: true,
+ },
+ },
+ },
+ },
},
});
- const rows = buildProblemScoreRows(assignment.problems, submissions);
- const totalScore = rows.reduce((sum, row) => sum + row.earnedPoints, 0);
- const maxScore = rows.reduce((sum, row) => sum + row.maxPoints, 0);
+ const rows = buildProblemProgressRows(assignment.problems, submissions);
+ const passedTestcaseCount = rows.reduce(
+ (sum, row) => sum + row.passedTestcaseCount,
+ 0
+ );
+ const totalTestcases = rows.reduce((sum, row) => sum + row.testcaseCount, 0);
const solvedCount = rows.filter((row) => row.solved).length;
- const completionPercent =
- rows.length > 0 ? Math.round((solvedCount / rows.length) * 100) : 0;
return {
assignment: {
@@ -290,11 +357,10 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
published: assignment.published,
},
course: assignment.course,
- totalScore,
- maxScore,
+ passedTestcaseCount,
+ totalTestcases,
solvedCount,
problemCount: rows.length,
- completionPercent,
rows,
};
}
diff --git a/src/app/(protected)/dashboard/actions/student-dashboard.ts b/src/app/(protected)/dashboard/actions/student-dashboard.ts
index e7f0ecd..cacaba4 100644
--- a/src/app/(protected)/dashboard/actions/student-dashboard.ts
+++ b/src/app/(protected)/dashboard/actions/student-dashboard.ts
@@ -103,7 +103,7 @@ export async function getStudentDashboardData() {
}))
);
- // 计算题目完成情况
+ // 计算题目通过情况
const completedProblems = new Set
- 完成率: {completionRate.toFixed(1)}%
+
+ 已通过 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
+ 道题目
- 总分:{summary.totalScore}/{summary.maxScore}
+ 通过测试点:{summary.passedTestcaseCount}/{summary.totalTestcases}
- 完成:{summary.solvedCount}/{summary.problemCount}
+ 通过题目:{summary.solvedCount}/{summary.problemCount}
- 完成率 {summary.completionPercent}%
{summary.assignment.dueAt
- ? ` · 截止 ${new Date(summary.assignment.dueAt).toLocaleString()}`
- : ""}
+ ? `截止 ${new Date(summary.assignment.dueAt).toLocaleString()}`
+ : "暂无截止时间"}
{error}
- 得分 {row.earnedPoints}/{row.maxPoints} · 状态 {row.bestStatus} · - 提交 {row.attempts} 次 + {row.testcaseCount > 0 + ? `通过测试点 ${row.passedTestcaseCount}/${row.testcaseCount}` + : "未配置测试点"}{" "} + · 状态 {row.bestStatus} · 提交 {row.attempts} 次