refactor: fix all P0/P1/P2 bugs and architecture issues

Bug fixes (from bugs/ directory):

- Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions

- Fix shared/lib <-> auth circular dependency via new session.ts module

- Fix divide-by-zero guard in grades data-access

- Fix audit export data truncation (paginated fetch for full datasets)

- Fix missing transactions in homework grading and elective lottery

- Fix missing revalidatePath in course-plans actions

- Fix frontend permission checks using requirePermission instead of requireAuth

- Fix dashboard role routing using session.user.roles

- Fix student auth pattern (migrate getDemoStudentUser to users module)

- Fix ActionState return type handling in components

Code quality fixes:

- Remove 60+ as type assertions (replace with type guards)

- Remove non-null assertions (use optional chaining or explicit checks)

- Convert dynamic imports to static imports (grades, diagnostic)

- Add React.cache() wrapping for read functions

- Parallelize independent queries with Promise.all

- Add explicit return types to 30+ arrow functions

- Replace any with unknown + type guards

- Fix import type for type-only imports

- Add Zod validation schemas for classes and diagnostic modules

- Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction)

- Add console.error to silent catch blocks

- Fix permission naming consistency (exam:proctor_read -> exam:proctor:read)

Architecture doc sync:

- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Update management-modules-audit.md for P0-7 cross-module fix

Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

View File

@@ -1,23 +1,25 @@
import "server-only"
import { cache } from "react"
import { and, asc, eq } from "drizzle-orm"
import { asc, eq } from "drizzle-orm"
import { db } from "@/shared/db"
import { parentStudentRelations } from "@/shared/db/schema"
import {
classes,
classEnrollments,
grades,
parentStudentRelations,
users,
} from "@/shared/db/schema"
import { getStudentClasses, getStudentSchedule } from "@/modules/classes/data-access"
getClassNameById,
getStudentActiveClassId,
getStudentClasses,
getStudentSchedule,
} from "@/modules/classes/data-access"
import {
getStudentDashboardGrades,
getStudentHomeworkAssignments,
} from "@/modules/homework/data-access"
import { getStudentGradeSummary } from "@/modules/grades/data-access"
import { getGradeOptions } from "@/modules/school/data-access"
import { getUserBasicInfo, getUserNamesByIds } from "@/modules/users/data-access"
import type {
ChildBasicInfo,
ChildDashboardData,
ChildHomeworkSummary,
ChildScheduleItem,
@@ -25,9 +27,15 @@ import type {
ParentDashboardData,
} from "./types"
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
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()
return (day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7
// 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[]> => {
@@ -55,66 +63,44 @@ export const getChildren = cache(async (parentId: string): Promise<ParentChildRe
}))
})
export const getChildBasicInfo = cache(async (studentId: string, relation: string | null = null) => {
const [student] = await db
.select({
id: users.id,
name: users.name,
email: users.email,
image: users.image,
gradeId: users.gradeId,
})
.from(users)
.where(eq(users.id, studentId))
.limit(1)
export const getChildBasicInfo = cache(
async (
studentId: string,
relation: string | null = null,
): Promise<ChildBasicInfo | null> => {
const student = await getUserBasicInfo(studentId)
if (!student) return null
if (!student) return null
let gradeName: string | null = null
if (student.gradeId) {
const [grade] = await db
.select({ name: grades.name })
.from(grades)
.where(eq(grades.id, student.gradeId))
.limit(1)
gradeName = grade?.name ?? null
}
// gradeName 与 classId 相互独立,并行拉取
const [gradeOptions, classId] = await Promise.all([
student.gradeId ? getGradeOptions() : Promise.resolve([]),
getStudentActiveClassId(studentId),
])
const [enrollment] = await db
.select({
classId: classEnrollments.classId,
status: classEnrollments.status,
})
.from(classEnrollments)
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
.orderBy(asc(classEnrollments.createdAt))
.limit(1)
let className: string | null = null
let classId: string | null = null
if (enrollment) {
const [cls] = await db
.select({ id: classes.id, name: classes.name })
.from(classes)
.where(eq(classes.id, enrollment.classId))
.limit(1)
if (cls) {
classId = cls.id
className = cls.name
let gradeName: string | null = null
if (student.gradeId) {
const grade = gradeOptions.find((g) => g.id === student.gradeId)
gradeName = grade?.name ?? null
}
}
return {
id: student.id,
name: student.name,
email: student.email,
image: student.image,
gradeName,
className,
classId,
relation,
}
})
let className: string | null = null
if (classId) {
className = await getClassNameById(classId)
}
return {
id: student.id,
name: student.name,
email: student.email,
image: student.image,
gradeName,
className,
classId,
relation,
}
},
)
const buildHomeworkSummary = (
assignments: Awaited<ReturnType<typeof getStudentHomeworkAssignments>>,
@@ -211,12 +197,12 @@ export const getParentDashboardData = cache(
const id = parentId.trim()
if (!id) return { parentName: null, children: [] }
const [parent, relations] = await Promise.all([
db.select({ name: users.name }).from(users).where(eq(users.id, id)).limit(1),
const [parentInfo, relations] = await Promise.all([
getUserNamesByIds([id]),
getChildren(id),
])
const parentName = parent[0]?.name ?? null
const parentName = parentInfo.get(id)?.name ?? null
if (relations.length === 0) {
return { parentName, children: [] }