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

@@ -0,0 +1,280 @@
import "server-only";
import { cache } from "react"
import { and, asc, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
import { db } from "@/shared/db"
import {
classes,
classEnrollments,
homeworkAssignmentTargets,
homeworkAssignments,
homeworkSubmissions,
subjects,
exams,
users,
} from "@/shared/db/schema"
import type {
ClassStudent,
StudentEnrolledClass,
} from "./types"
import {
compareClassLike,
getAccessibleClassIdsForTeacher,
getSessionTeacherId,
getTeacherSubjectIdsForClass,
} from "./data-access"
export const getStudentsSubjectScores = cache(
async (studentIds: string[]): Promise<Map<string, Record<string, number | null>>> => {
if (studentIds.length === 0) return new Map()
// 1. Find assignments targeted at these students
const assignmentTargets = await db
.select({ assignmentId: homeworkAssignmentTargets.assignmentId })
.from(homeworkAssignmentTargets)
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
const assignmentIds = Array.from(new Set(assignmentTargets.map(t => t.assignmentId)))
if (assignmentIds.length === 0) return new Map()
// 2. Get assignment details including subject from linked exam
const assignments = await db
.select({
id: homeworkAssignments.id,
createdAt: homeworkAssignments.createdAt,
subjectId: exams.subjectId,
subjectName: subjects.name
})
.from(homeworkAssignments)
.innerJoin(exams, eq(homeworkAssignments.sourceExamId, exams.id))
.leftJoin(subjects, eq(exams.subjectId, subjects.id))
.where(and(
inArray(homeworkAssignments.id, assignmentIds),
eq(homeworkAssignments.status, "published")
))
.orderBy(desc(homeworkAssignments.createdAt))
// 3. Filter subjects (exclude PE, Music, Art)
const excludeSubjects = ["体育", "音乐", "美术"]
const subjectAssignments = new Map<string, string>() // subject -> assignmentId (latest)
for (const a of assignments) {
if (!a.subjectName) continue
if (excludeSubjects.includes(a.subjectName)) continue
if (!subjectAssignments.has(a.subjectName)) {
subjectAssignments.set(a.subjectName, a.id)
}
}
const targetAssignmentIds = Array.from(subjectAssignments.values())
if (targetAssignmentIds.length === 0) return new Map()
// 4. Get submissions for these assignments
const submissions = await db
.select({
studentId: homeworkSubmissions.studentId,
assignmentId: homeworkSubmissions.assignmentId,
score: homeworkSubmissions.score,
createdAt: homeworkSubmissions.createdAt,
})
.from(homeworkSubmissions)
.where(inArray(homeworkSubmissions.assignmentId, targetAssignmentIds))
.orderBy(desc(homeworkSubmissions.createdAt))
// 5. Map back to subject scores per student
const studentScores = new Map<string, Record<string, number | null>>()
// Create reverse map for assignment -> subject
const assignmentSubjectMap = new Map<string, string>()
for (const [subject, id] of subjectAssignments.entries()) {
assignmentSubjectMap.set(id, subject)
}
for (const s of submissions) {
const subject = assignmentSubjectMap.get(s.assignmentId)
if (!subject) continue
if (!studentScores.has(s.studentId)) {
studentScores.set(s.studentId, {})
}
const scores = studentScores.get(s.studentId)!
// Only set if not already set (since we ordered by desc createdAt, first one is latest)
if (scores[subject] === undefined) {
scores[subject] = s.score
}
}
return studentScores
}
)
export const getClassStudentSubjectScoresV2 = cache(
async (params: { classId: string; teacherId?: string }): Promise<Map<string, Record<string, number | null>>> => {
const teacherId = params.teacherId ?? (await getSessionTeacherId())
if (!teacherId) return new Map()
const classId = params.classId.trim()
if (!classId) return new Map()
const accessibleIds = await getAccessibleClassIdsForTeacher(teacherId)
if (accessibleIds.length === 0 || !accessibleIds.includes(classId)) return new Map()
const [classRow] = await db
.select({ id: classes.id, teacherId: classes.teacherId })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
if (!classRow) return new Map()
const isHomeroomTeacher = classRow.teacherId === teacherId
const subjectIds = isHomeroomTeacher ? [] : await getTeacherSubjectIdsForClass(teacherId, classId)
if (!isHomeroomTeacher && subjectIds.length === 0) return new Map()
const enrollments = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.where(and(
eq(classEnrollments.classId, classId),
eq(classEnrollments.status, "active")
))
const studentIds = enrollments.map((e) => e.studentId)
const studentScores = await getStudentsSubjectScores(studentIds)
if (subjectIds.length === 0) return studentScores
// Map subjectIds to names for filtering
const subjectRows = await db
.select({ id: subjects.id, name: subjects.name })
.from(subjects)
.where(inArray(subjects.id, subjectIds))
const allowed = new Set(subjectRows.map((s) => s.name))
const filtered = new Map<string, Record<string, number | null>>()
for (const [studentId, scores] of studentScores.entries()) {
const nextScores: Record<string, number | null> = {}
for (const [subject, score] of Object.entries(scores)) {
if (allowed.has(subject)) nextScores[subject] = score
}
filtered.set(studentId, nextScores)
}
return filtered
}
)
export const getStudentClasses = cache(async (studentId: string): Promise<StudentEnrolledClass[]> => {
const id = studentId.trim()
if (!id) return []
const rows = await (async () => {
try {
return await db
.select({
id: classes.id,
schoolName: classes.schoolName,
name: classes.name,
grade: classes.grade,
homeroom: classes.homeroom,
room: classes.room,
teacherName: users.name,
teacherEmail: users.email,
})
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.leftJoin(users, eq(users.id, classes.teacherId))
.where(and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active")))
.orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
} catch {
return await db
.select({
id: classes.id,
schoolName: sql<string | null>`NULL`.as("schoolName"),
name: classes.name,
grade: classes.grade,
homeroom: classes.homeroom,
room: classes.room,
teacherName: users.name,
teacherEmail: users.email,
})
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.leftJoin(users, eq(users.id, classes.teacherId))
.where(and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active")))
.orderBy(asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
}
})()
const list = rows.map((r) => ({
id: r.id,
schoolName: r.schoolName,
name: r.name,
grade: r.grade,
homeroom: r.homeroom,
room: r.room,
teacherName: r.teacherName,
teacherEmail: r.teacherEmail,
}))
list.sort(compareClassLike)
return list
})
export const getClassStudents = cache(
async (params?: { classId?: string; q?: string; status?: string; teacherId?: string }): Promise<ClassStudent[]> => {
const teacherId = params?.teacherId ?? (await getSessionTeacherId())
if (!teacherId) return []
const classId = params?.classId?.trim()
const q = params?.q?.trim().toLowerCase()
const status = params?.status?.trim().toLowerCase()
const accessibleIds = await getAccessibleClassIdsForTeacher(teacherId)
if (accessibleIds.length === 0) return []
const conditions: SQL[] = [inArray(classes.id, accessibleIds)]
if (classId) {
conditions.push(eq(classes.id, classId))
}
if (status === "active" || status === "inactive") {
conditions.push(eq(classEnrollments.status, status))
}
if (q && q.length > 0) {
const needle = `%${q}%`
conditions.push(
sql`(LOWER(COALESCE(${users.name}, '')) LIKE ${needle} OR LOWER(${users.email}) LIKE ${needle})`
)
}
const rows = await db
.select({
id: users.id,
name: users.name,
email: users.email,
image: users.image,
gender: users.gender,
classId: classes.id,
className: classes.name,
status: classEnrollments.status,
joinedAt: classEnrollments.createdAt,
})
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.innerJoin(users, eq(users.id, classEnrollments.studentId))
.where(and(...conditions))
.orderBy(asc(users.name), asc(users.email))
return rows.map((r) => ({
id: r.id,
name: r.name ?? "Unnamed",
email: r.email,
image: r.image,
gender: r.gender,
classId: r.classId,
className: r.className,
status: r.status,
joinedAt: r.joinedAt,
}))
}
)