import "server-only" import { cache } from "react" import { and, asc, eq } from "drizzle-orm" import { db } from "@/shared/db" import { parentStudentRelations } from "@/shared/db/schema" import { getStudentActiveClass, getStudentClasses, getStudentSchedule, } from "@/modules/classes/data-access" import { getStudentDashboardGrades, getStudentHomeworkAssignments, getStudentExamResults, } from "@/modules/homework/data-access" import { getStudentGradeSummary } from "@/modules/grades/data-access" import { getGradeNameById } from "@/modules/school/data-access" import { getUserBasicInfo, getUserNamesByIds } from "@/modules/users/data-access" import type { ChildBasicInfo, ChildDashboardData, ChildHomeworkSummaryData, ChildScheduleItem, ChildWeeklyScheduleItem, ParentChildRelation, ParentDashboardData, } from "./types" type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7 const isWeekday = (n: number): n is Weekday => n >= 1 && n <= 7 const toWeekday = (d: Date): Weekday => { const day = d.getDay() // getDay() returns 0 (Sun) - 6 (Sat); normalize Sunday (0) to 7 const normalized = day === 0 ? 7 : day return isWeekday(normalized) ? normalized : 1 } export const getChildren = cache(async (parentId: string): Promise => { const id = parentId.trim() if (!id) return [] const rows = await db .select({ id: parentStudentRelations.id, parentId: parentStudentRelations.parentId, studentId: parentStudentRelations.studentId, relation: parentStudentRelations.relation, createdAt: parentStudentRelations.createdAt, }) .from(parentStudentRelations) .where(eq(parentStudentRelations.parentId, id)) .orderBy(asc(parentStudentRelations.createdAt)) return rows.map((r) => ({ id: r.id, parentId: r.parentId, studentId: r.studentId, relation: r.relation, createdAt: r.createdAt.toISOString(), })) }) /** * 校验家长与子女的关系是否存在,并返回关系类型。 * 同时按 parentId 与 studentId 过滤,防止跨家庭信息泄露。 */ export const verifyParentChildRelation = cache( async (studentId: string, parentId: string): Promise => { const [row] = await db .select({ relation: parentStudentRelations.relation }) .from(parentStudentRelations) .where( and( eq(parentStudentRelations.studentId, studentId), eq(parentStudentRelations.parentId, parentId), ), ) .limit(1) return row?.relation ?? null }, ) export const getChildBasicInfo = cache( async ( studentId: string, relation: string | null = null, ): Promise => { const student = await getUserBasicInfo(studentId) if (!student) return null // gradeName 与 activeClass 相互独立,并行拉取 const [gradeName, activeClass] = await Promise.all([ student.gradeId ? getGradeNameById(student.gradeId) : Promise.resolve(null), getStudentActiveClass(studentId), ]) return { id: student.id, name: student.name, email: student.email, image: student.image, gradeName, className: activeClass?.className ?? null, classId: activeClass?.classId ?? null, relation, } }, ) const buildHomeworkSummary = ( assignments: Awaited>, ): ChildHomeworkSummaryData => { const now = new Date() let pendingCount = 0 let submittedCount = 0 let gradedCount = 0 let overdueCount = 0 for (const a of assignments) { if (a.progressStatus === "graded") { gradedCount += 1 continue } if (a.progressStatus === "submitted") { submittedCount += 1 } if (a.progressStatus === "not_started" || a.progressStatus === "in_progress") { pendingCount += 1 } if (a.dueAt) { const due = new Date(a.dueAt) if (due < now && a.progressStatus !== "submitted") { overdueCount += 1 } } } const recentAssignments = assignments .toSorted((a, b) => { const aDue = a.dueAt ? new Date(a.dueAt).getTime() : Number.POSITIVE_INFINITY const bDue = b.dueAt ? new Date(b.dueAt).getTime() : Number.POSITIVE_INFINITY return aDue - bDue }) .slice(0, 5) return { pendingCount, submittedCount, gradedCount, overdueCount, recentAssignments, } } const buildTodaySchedule = ( schedule: Awaited>, ): ChildScheduleItem[] => { const todayWeekday = toWeekday(new Date()) return schedule .filter((s) => s.weekday === todayWeekday) .map((s) => ({ id: s.id, classId: s.classId, className: s.className, course: s.course, startTime: s.startTime, endTime: s.endTime, location: s.location ?? null, })) .sort((a, b) => a.startTime.localeCompare(b.startTime)) } const buildWeeklySchedule = ( schedule: Awaited>, ): ChildWeeklyScheduleItem[] => { return schedule .map((s) => ({ id: s.id, classId: s.classId, className: s.className, course: s.course, startTime: s.startTime, endTime: s.endTime, location: s.location ?? null, weekday: s.weekday, })) .sort((a, b) => a.weekday === b.weekday ? a.startTime.localeCompare(b.startTime) : a.weekday - b.weekday, ) } export const getChildDashboardData = cache( async (studentId: string, relation: string | null = null): Promise => { const basicInfo = await getChildBasicInfo(studentId, relation) if (!basicInfo) return null const [enrolledClasses, schedule, assignments, gradeTrend, gradeSummary, examResults] = await Promise.all([ getStudentClasses(studentId), getStudentSchedule(studentId), getStudentHomeworkAssignments(studentId), getStudentDashboardGrades(studentId), getStudentGradeSummary(studentId), getStudentExamResults(studentId), ]) return { basicInfo, enrolledClasses, todaySchedule: buildTodaySchedule(schedule), weeklySchedule: buildWeeklySchedule(schedule), homeworkSummary: buildHomeworkSummary(assignments), gradeTrend, gradeSummary, examResults, } }, ) export const getParentDashboardData = cache( async (parentId: string): Promise => { const id = parentId.trim() if (!id) return { parentName: null, children: [] } const [parentInfo, relations] = await Promise.all([ getUserNamesByIds([id]), getChildren(id), ]) const parentName = parentInfo.get(id)?.name ?? null if (relations.length === 0) { return { parentName, children: [] } } const children = await Promise.all( relations.map((r) => getChildDashboardData(r.studentId, r.relation)), ) return { parentName, children: children.filter((c): c is ChildDashboardData => c !== null), } }, ) /** * 获取家长所有子女的轻量列表(id + name),用于详情页头部多子女切换器。 * 一次批量查询,避免 N+1。 */ export const getChildNameList = cache( async (parentId: string): Promise> => { const relations = await getChildren(parentId) if (relations.length === 0) return [] const nameMap = await getUserNamesByIds(relations.map((r) => r.studentId)) return relations.map((r) => ({ id: r.studentId, name: nameMap.get(r.studentId)?.name ?? null, })) }, )