Files
NextEdu/src/modules/parent/data-access.ts
SpecialX e9a5264fe7 feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates
parent:

- Add parent-student-attendance-detail component

auth:

- Add actions, data-access, schema, services, types

onboarding:

- Add parent-children-form and hooks directory

files:

- Add actions, schema, hooks directory

notifications:

- Add schema and schema test

adaptive-practice:

- Add answer-input, answer-result, practice-result-view, practice-starter-with-nav

- Add question-card, question-content, lib and services directories

ai:

- Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
2026-07-03 10:26:12 +08:00

305 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only"
import { cache } from "react"
import { and, asc, eq, inArray } from "drizzle-orm"
import { db } from "@/shared/db"
import { parentStudentRelations } from "@/shared/db/schema"
import {
getStudentActiveClass,
getStudentClasses,
getStudentSchedule,
} from "@/modules/classes/data-access"
import {
getStudentHomeworkAssignments,
getStudentExamResults,
} from "@/modules/homework/data-access-student"
import { getStudentDashboardGrades } from "@/modules/homework/stats-service"
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<ParentChildRelation[]> => {
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<string | null> => {
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<ChildBasicInfo | null> => {
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<ReturnType<typeof getStudentHomeworkAssignments>>,
): 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<ReturnType<typeof getStudentSchedule>>,
): 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<ReturnType<typeof getStudentSchedule>>,
): 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<ChildDashboardData | null> => {
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<ParentDashboardData> => {
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<Array<{ id: string; name: string | null }>> => {
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,
}))
},
)
/**
* v4-P1-5: 批量查询多个学生的家长 userId 列表。
* 用于通知场景:报告/成绩发布时同步通知所有相关家长。
* 返回去重后的 parentId 数组。
*/
export const getParentIdsByStudentIds = cache(
async (studentIds: string[]): Promise<string[]> => {
if (studentIds.length === 0) return []
const rows = await db
.select({ parentId: parentStudentRelations.parentId })
.from(parentStudentRelations)
.where(inArray(parentStudentRelations.studentId, studentIds))
return Array.from(new Set(rows.map((r) => r.parentId)))
},
)
/**
* L-2: 批量查询学生→家长映射(保留关联关系)。
* 用于考勤通知等需要按家长-学生分组发送的场景,避免 N+1 查询。
* 返回数组形如 [{ parentId, studentId }],同一家长多个学生会出现多条。
*/
export const getParentStudentMapByStudentIds = cache(
async (studentIds: string[]): Promise<Array<{ parentId: string; studentId: string }>> => {
if (studentIds.length === 0) return []
const rows = await db
.select({
parentId: parentStudentRelations.parentId,
studentId: parentStudentRelations.studentId,
})
.from(parentStudentRelations)
.where(inArray(parentStudentRelations.studentId, studentIds))
return rows.map((r) => ({ parentId: r.parentId, studentId: r.studentId }))
},
)