refactor: P0-1/2/4 解耦修复 - 拆分过耦合文件 + dashboard 解耦

This commit is contained in:
SpecialX
2026-06-18 01:45:55 +08:00
parent 220061d62e
commit 62be0b9404
18 changed files with 2534 additions and 2130 deletions

View File

@@ -26,86 +26,20 @@ import type {
HomeworkAssignmentStatus,
HomeworkSubmissionDetails,
HomeworkSubmissionListItem,
HomeworkAssignmentAnalytics,
HomeworkAssignmentQuestionAnalytics,
StudentHomeworkAssignmentListItem,
StudentHomeworkProgressStatus,
StudentHomeworkTakeData,
StudentDashboardGradeProps,
StudentHomeworkScoreAnalytics,
StudentRanking,
TeacherGradeTrendItem,
} from "./types"
import type { DataScope } from "@/shared/types/permissions"
export const getTeacherGradeTrends = cache(async (teacherId: string, limit: number = 5): Promise<TeacherGradeTrendItem[]> => {
const recentAssignments = await db.query.homeworkAssignments.findMany({
where: and(
eq(homeworkAssignments.creatorId, teacherId),
or(eq(homeworkAssignments.status, "published"), eq(homeworkAssignments.status, "archived"))
),
orderBy: [desc(homeworkAssignments.createdAt)],
limit: limit,
})
export const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null
if (recentAssignments.length === 0) return []
const assignmentIds = recentAssignments.map((a) => a.id)
const [maxScoreMap, targetCountRows, submissionStats] = await Promise.all([
getAssignmentMaxScoreById(assignmentIds),
db
.select({
assignmentId: homeworkAssignmentTargets.assignmentId,
count: count(homeworkAssignmentTargets.studentId),
})
.from(homeworkAssignmentTargets)
.where(inArray(homeworkAssignmentTargets.assignmentId, assignmentIds))
.groupBy(homeworkAssignmentTargets.assignmentId),
db
.select({
assignmentId: homeworkSubmissions.assignmentId,
avgScore: sql<number>`AVG(${homeworkSubmissions.score})`,
count: count(homeworkSubmissions.id),
})
.from(homeworkSubmissions)
.where(
and(
inArray(homeworkSubmissions.assignmentId, assignmentIds),
eq(homeworkSubmissions.status, "graded")
)
)
.groupBy(homeworkSubmissions.assignmentId),
])
const targetCountMap = new Map<string, number>()
for (const r of targetCountRows) targetCountMap.set(r.assignmentId, r.count)
const statsMap = new Map<string, { avg: number; count: number }>()
for (const r of submissionStats) statsMap.set(r.assignmentId, { avg: Number(r.avgScore), count: Number(r.count) })
return recentAssignments.map((a) => {
const stats = statsMap.get(a.id) ?? { avg: 0, count: 0 }
return {
id: a.id,
title: a.title,
averageScore: stats.avg,
maxScore: maxScoreMap.get(a.id) ?? 0,
submissionCount: stats.count,
totalStudents: targetCountMap.get(a.id) ?? 0,
createdAt: a.createdAt.toISOString(),
}
}).reverse() // Reverse to show trend from left (older) to right (newer)
})
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null
const toQuestionContent = (v: unknown): HomeworkQuestionContent | null => {
export const toQuestionContent = (v: unknown): HomeworkQuestionContent | null => {
if (!isRecord(v)) return null
return v as HomeworkQuestionContent
}
const getAssignmentMaxScoreById = async (assignmentIds: string[]): Promise<Map<string, number>> => {
export const getAssignmentMaxScoreById = async (assignmentIds: string[]): Promise<Map<string, number>> => {
const ids = assignmentIds.filter((v) => v.trim().length > 0)
if (ids.length === 0) return new Map()
@@ -473,152 +407,6 @@ export const getHomeworkAssignmentById = cache(async (id: string, scope?: DataSc
}
})
export const getHomeworkAssignmentAnalytics = cache(
async (assignmentId: string): Promise<HomeworkAssignmentAnalytics | null> => {
const assignment = await db.query.homeworkAssignments.findFirst({
where: eq(homeworkAssignments.id, assignmentId),
with: {
sourceExam: true,
},
})
if (!assignment) return null
const [targetsRow] = await db
.select({ c: count() })
.from(homeworkAssignmentTargets)
.where(eq(homeworkAssignmentTargets.assignmentId, assignmentId))
const [submissionsRow] = await db
.select({ c: count() })
.from(homeworkSubmissions)
.where(eq(homeworkSubmissions.assignmentId, assignmentId))
const [submittedRow] = await db
.select({ c: sql<number>`COUNT(DISTINCT ${homeworkSubmissions.studentId})` })
.from(homeworkSubmissions)
.where(
and(
eq(homeworkSubmissions.assignmentId, assignmentId),
inArray(homeworkSubmissions.status, ["submitted", "graded"])
)
)
const [gradedRow] = await db
.select({ c: sql<number>`COUNT(DISTINCT ${homeworkSubmissions.studentId})` })
.from(homeworkSubmissions)
.where(and(eq(homeworkSubmissions.assignmentId, assignmentId), eq(homeworkSubmissions.status, "graded")))
const assignmentQuestions = await db.query.homeworkAssignmentQuestions.findMany({
where: eq(homeworkAssignmentQuestions.assignmentId, assignmentId),
with: { question: true },
orderBy: (q, { asc }) => [asc(q.order)],
})
const statsByQuestionId = new Map<string, HomeworkAssignmentQuestionAnalytics>()
for (const aq of assignmentQuestions) {
statsByQuestionId.set(aq.questionId, {
questionId: aq.questionId,
questionType: aq.question.type,
questionContent: toQuestionContent(aq.question.content),
maxScore: aq.score ?? 0,
order: aq.order ?? 0,
errorCount: 0,
errorRate: 0,
})
}
const gradedSubmissionsAll = await db.query.homeworkSubmissions.findMany({
where: and(
eq(homeworkSubmissions.assignmentId, assignmentId),
eq(homeworkSubmissions.status, "graded")
),
orderBy: (s, { desc }) => [desc(s.updatedAt)],
with: {
answers: true,
student: true,
},
})
const latestByStudentId = new Map<string, (typeof gradedSubmissionsAll)[number]>()
for (const s of gradedSubmissionsAll) {
if (!latestByStudentId.has(s.studentId)) latestByStudentId.set(s.studentId, s)
}
const gradedSubmissions = Array.from(latestByStudentId.values())
const scoreBySubmissionQuestion = new Map<string, number>()
const answerBySubmissionQuestion = new Map<string, unknown>()
for (const sub of gradedSubmissions) {
for (const ans of sub.answers) {
const key = `${sub.id}|${ans.questionId}`
if (scoreBySubmissionQuestion.has(key)) continue
scoreBySubmissionQuestion.set(key, ans.score ?? 0)
const raw = ans.answerContent
if (isRecord(raw) && "answer" in raw) {
answerBySubmissionQuestion.set(key, raw.answer)
} else {
answerBySubmissionQuestion.set(key, raw)
}
}
}
const denom = gradedSubmissions.length
if (denom > 0) {
for (const q of statsByQuestionId.values()) {
if (q.maxScore <= 0) continue
let errors = 0
const wrongAnswers: Array<{ studentId: string; studentName: string; answerContent: unknown }> = []
for (const sub of gradedSubmissions) {
const key = `${sub.id}|${q.questionId}`
const score = scoreBySubmissionQuestion.get(key) ?? 0
if (score < q.maxScore) {
errors += 1
wrongAnswers.push({
studentId: sub.studentId,
studentName: sub.student.name || "Unknown",
answerContent: answerBySubmissionQuestion.get(key),
})
}
}
q.errorCount = errors
q.errorRate = errors / denom
q.wrongAnswers = wrongAnswers.slice(0, 500)
}
}
const questions: HomeworkAssignmentQuestionAnalytics[] = Array.from(statsByQuestionId.values())
.sort((a, b) => a.order - b.order)
const analytics: HomeworkAssignmentAnalytics = {
assignment: {
id: assignment.id,
title: assignment.title,
description: assignment.description,
status: (assignment.status as HomeworkAssignmentStatus) ?? "draft",
sourceExamId: assignment.sourceExamId,
sourceExamTitle: assignment.sourceExam.title,
structure: assignment.structure as unknown,
availableAt: assignment.availableAt ? assignment.availableAt.toISOString() : null,
dueAt: assignment.dueAt ? assignment.dueAt.toISOString() : null,
allowLate: assignment.allowLate,
lateDueAt: assignment.lateDueAt ? assignment.lateDueAt.toISOString() : null,
maxAttempts: assignment.maxAttempts,
targetCount: targetsRow?.c ?? 0,
submissionCount: submissionsRow?.c ?? 0,
submittedCount: submittedRow?.c ?? 0,
gradedCount: gradedRow?.c ?? 0,
createdAt: assignment.createdAt.toISOString(),
updatedAt: assignment.updatedAt.toISOString(),
},
gradedSampleCount: gradedSubmissions.length,
questions,
}
return analytics
}
)
export const getHomeworkSubmissionDetails = cache(async (submissionId: string): Promise<HomeworkSubmissionDetails | null> => {
const submission = await db.query.homeworkSubmissions.findFirst({
where: eq(homeworkSubmissions.id, submissionId),
@@ -882,157 +670,12 @@ export const getStudentHomeworkTakeData = cache(async (assignmentId: string, stu
}
})
export const getStudentDashboardGrades = cache(async (studentId: string): Promise<StudentDashboardGradeProps> => {
const id = studentId.trim()
if (!id) return { trend: [], recent: [], ranking: null }
const targetAssignmentIdsRows = await db
.select({ assignmentId: homeworkAssignmentTargets.assignmentId })
.from(homeworkAssignmentTargets)
.where(eq(homeworkAssignmentTargets.studentId, id))
const targetAssignmentIds = Array.from(new Set(targetAssignmentIdsRows.map((r) => r.assignmentId)))
if (targetAssignmentIds.length === 0) return { trend: [], recent: [], ranking: null }
const gradedSubmissions = await db.query.homeworkSubmissions.findMany({
where: and(
eq(homeworkSubmissions.studentId, id),
inArray(homeworkSubmissions.assignmentId, targetAssignmentIds),
eq(homeworkSubmissions.status, "graded")
),
orderBy: (s, { desc }) => [desc(s.updatedAt)],
limit: 200,
})
const latestByAssignmentId = new Map<string, (typeof gradedSubmissions)[number]>()
for (const s of gradedSubmissions) {
if (!latestByAssignmentId.has(s.assignmentId)) latestByAssignmentId.set(s.assignmentId, s)
}
const unique = Array.from(latestByAssignmentId.values()).sort((a, b) => {
const aTime = (a.submittedAt ?? a.updatedAt).getTime()
const bTime = (b.submittedAt ?? b.updatedAt).getTime()
return aTime - bTime
})
const trendSubmissions = unique.slice(-10)
const recentSubmissions = [...trendSubmissions].sort((a, b) => {
const aTime = (a.submittedAt ?? a.updatedAt).getTime()
const bTime = (b.submittedAt ?? b.updatedAt).getTime()
return bTime - aTime
})
const assignmentIds = Array.from(new Set(trendSubmissions.map((s) => s.assignmentId)))
const assignments = await db.query.homeworkAssignments.findMany({
where: inArray(homeworkAssignments.id, assignmentIds),
})
const titleByAssignmentId = new Map(assignments.map((a) => [a.id, a.title] as const))
const maxScoreByAssignmentId = await getAssignmentMaxScoreById(assignmentIds)
const toAnalytics = (s: (typeof trendSubmissions)[number]): StudentHomeworkScoreAnalytics => {
const maxScore = maxScoreByAssignmentId.get(s.assignmentId) ?? 0
const score = s.score ?? 0
const percentage = maxScore > 0 ? (score / maxScore) * 100 : 0
return {
assignmentId: s.assignmentId,
assignmentTitle: titleByAssignmentId.get(s.assignmentId) ?? "Untitled",
score,
maxScore,
percentage,
submittedAt: (s.submittedAt ?? s.updatedAt).toISOString(),
}
}
const trend = trendSubmissions.map(toAnalytics)
const recent = recentSubmissions.map(toAnalytics).slice(0, 5)
const enrollment = await db.query.classEnrollments.findFirst({
where: and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active")),
orderBy: (e, { asc }) => [asc(e.createdAt)],
})
if (!enrollment) return { trend, recent, ranking: null }
const classStudents = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.where(and(eq(classEnrollments.classId, enrollment.classId), eq(classEnrollments.status, "active")))
const classStudentIds = Array.from(new Set(classStudents.map((r) => r.studentId)))
const classSize = classStudentIds.length
if (classSize === 0) return { trend, recent, ranking: null }
const classAssignmentIdsRows = await db
.selectDistinct({ assignmentId: homeworkAssignmentTargets.assignmentId })
.from(homeworkAssignmentTargets)
.where(inArray(homeworkAssignmentTargets.studentId, classStudentIds))
const classAssignmentIds = Array.from(new Set(classAssignmentIdsRows.map((r) => r.assignmentId)))
if (classAssignmentIds.length === 0) return { trend, recent, ranking: null }
const classMaxScoreByAssignmentId = await getAssignmentMaxScoreById(classAssignmentIds)
const classGradedSubmissions = await db.query.homeworkSubmissions.findMany({
where: and(
inArray(homeworkSubmissions.studentId, classStudentIds),
inArray(homeworkSubmissions.assignmentId, classAssignmentIds),
eq(homeworkSubmissions.status, "graded")
),
orderBy: (s, { desc }) => [desc(s.updatedAt)],
limit: 5000,
})
const latestByStudentAssignment = new Map<string, (typeof classGradedSubmissions)[number]>()
for (const s of classGradedSubmissions) {
const key = `${s.studentId}|${s.assignmentId}`
if (!latestByStudentAssignment.has(key)) latestByStudentAssignment.set(key, s)
}
const totalsByStudentId = new Map<string, { score: number; maxScore: number }>()
for (const sub of latestByStudentAssignment.values()) {
const maxScore = classMaxScoreByAssignmentId.get(sub.assignmentId) ?? 0
const score = sub.score ?? 0
const prev = totalsByStudentId.get(sub.studentId) ?? { score: 0, maxScore: 0 }
totalsByStudentId.set(sub.studentId, {
score: prev.score + score,
maxScore: prev.maxScore + maxScore,
})
}
const classUsers = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(inArray(users.id, classStudentIds))
const nameByStudentId = new Map(classUsers.map((u) => [u.id, u.name ?? "Student"] as const))
const myName = nameByStudentId.get(id) ?? "Student"
const ranked = classStudentIds
.map((studentId) => {
const totals = totalsByStudentId.get(studentId) ?? { score: 0, maxScore: 0 }
const percentage = totals.maxScore > 0 ? (totals.score / totals.maxScore) * 100 : 0
return { studentId, percentage, totals }
})
.sort((a, b) => {
if (b.percentage !== a.percentage) return b.percentage - a.percentage
return a.studentId.localeCompare(b.studentId)
})
const myIndex = ranked.findIndex((r) => r.studentId === id)
if (myIndex < 0) return { trend, recent, ranking: null }
const myTotals = ranked[myIndex]?.totals ?? { score: 0, maxScore: 0 }
const myPercentage = myTotals.maxScore > 0 ? (myTotals.score / myTotals.maxScore) * 100 : 0
const ranking: StudentRanking = {
studentId: id,
studentName: myName,
rank: myIndex + 1,
classSize,
totalScore: myTotals.score,
totalMaxScore: myTotals.maxScore,
percentage: myPercentage,
}
return { trend, recent, ranking }
})
// Re-export stats functions for backward compatibility
// New code should import directly from "./stats-service"
export {
getTeacherGradeTrends,
getHomeworkAssignmentAnalytics,
getStudentDashboardGrades,
getHomeworkDashboardStats,
} from "./stats-service"
export type { HomeworkDashboardStats } from "./stats-service"