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,11 +1,25 @@
import "server-only"
import { cache } from "react"
import { asc, eq, inArray, or } from "drizzle-orm"
import { and, asc, eq, inArray, or, sql } from "drizzle-orm"
import { db } from "@/shared/db"
import { academicYears, departments, grades, roles, schools, users, usersToRoles } from "@/shared/db/schema"
import type { AcademicYearListItem, DepartmentListItem, GradeListItem, SchoolListItem, StaffOption } from "./types"
import { academicYears, departments, grades, roles, schools, subjects, users, usersToRoles } from "@/shared/db/schema"
import type {
AcademicYearInsertData,
AcademicYearListItem,
AcademicYearUpdateData,
DepartmentInsertData,
DepartmentListItem,
DepartmentUpdateData,
GradeInsertData,
GradeListItem,
GradeUpdateData,
SchoolInsertData,
SchoolListItem,
SchoolUpdateData,
StaffOption,
} from "./types"
const toIso = (d: Date) => d.toISOString()
@@ -19,7 +33,8 @@ export const getDepartments = cache(async (): Promise<DepartmentListItem[]> => {
createdAt: toIso(r.createdAt),
updatedAt: toIso(r.updatedAt),
}))
} catch {
} catch (error) {
console.error("getDepartments failed:", error)
return []
}
})
@@ -36,7 +51,8 @@ export const getAcademicYears = cache(async (): Promise<AcademicYearListItem[]>
createdAt: toIso(r.createdAt),
updatedAt: toIso(r.updatedAt),
}))
} catch {
} catch (error) {
console.error("getAcademicYears failed:", error)
return []
}
})
@@ -51,7 +67,8 @@ export const getSchools = cache(async (): Promise<SchoolListItem[]> => {
createdAt: toIso(r.createdAt),
updatedAt: toIso(r.updatedAt),
}))
} catch {
} catch (error) {
console.error("getSchools failed:", error)
return []
}
})
@@ -104,7 +121,8 @@ export const getGrades = cache(async (): Promise<GradeListItem[]> => {
createdAt: toIso(r.createdAt),
updatedAt: toIso(r.updatedAt),
}))
} catch {
} catch (error) {
console.error("getGrades failed:", error)
return []
}
})
@@ -125,7 +143,8 @@ export const getStaffOptions = cache(async (): Promise<StaffOption[]> => {
name: r.name ?? "Unnamed",
email: r.email,
}))
} catch {
} catch (error) {
console.error("getStaffOptions failed:", error)
return []
}
})
@@ -180,7 +199,272 @@ export const getGradesForStaff = cache(async (staffId: string): Promise<GradeLis
createdAt: toIso(r.createdAt),
updatedAt: toIso(r.updatedAt),
}))
} catch {
} catch (error) {
console.error("getGradesForStaff failed:", error)
return []
}
})
// ---------------------------------------------------------------------------
// Mutations — DB write operations (called only from actions.ts)
// ---------------------------------------------------------------------------
export async function createDepartment(data: DepartmentInsertData): Promise<void> {
await db.insert(departments).values({
id: data.id,
name: data.name,
description: data.description,
})
}
export async function updateDepartment(
id: string,
data: DepartmentUpdateData
): Promise<void> {
await db
.update(departments)
.set({ name: data.name, description: data.description })
.where(eq(departments.id, id))
}
export async function deleteDepartment(id: string): Promise<void> {
await db.delete(departments).where(eq(departments.id, id))
}
export async function createSchool(data: SchoolInsertData): Promise<void> {
await db.insert(schools).values({
id: data.id,
name: data.name,
code: data.code,
})
}
export async function updateSchool(
id: string,
data: SchoolUpdateData
): Promise<void> {
await db
.update(schools)
.set({ name: data.name, code: data.code })
.where(eq(schools.id, id))
}
export async function deleteSchool(id: string): Promise<void> {
await db.delete(schools).where(eq(schools.id, id))
}
export async function createGrade(data: GradeInsertData): Promise<void> {
await db.insert(grades).values({
id: data.id,
schoolId: data.schoolId,
name: data.name,
order: data.order,
gradeHeadId: data.gradeHeadId,
teachingHeadId: data.teachingHeadId,
})
}
export async function updateGrade(
id: string,
data: GradeUpdateData
): Promise<void> {
await db
.update(grades)
.set({
schoolId: data.schoolId,
name: data.name,
order: data.order,
gradeHeadId: data.gradeHeadId,
teachingHeadId: data.teachingHeadId,
})
.where(eq(grades.id, id))
}
export async function deleteGrade(id: string): Promise<void> {
await db.delete(grades).where(eq(grades.id, id))
}
export async function createAcademicYear(
data: AcademicYearInsertData
): Promise<void> {
await db.transaction(async (tx) => {
if (data.isActive) {
await tx.update(academicYears).set({ isActive: false })
}
await tx.insert(academicYears).values({
id: data.id,
name: data.name,
startDate: data.startDate,
endDate: data.endDate,
isActive: data.isActive,
})
})
}
export async function updateAcademicYear(
id: string,
data: AcademicYearUpdateData
): Promise<void> {
await db.transaction(async (tx) => {
if (data.isActive) {
await tx.update(academicYears).set({ isActive: false })
}
await tx
.update(academicYears)
.set({
name: data.name,
startDate: data.startDate,
endDate: data.endDate,
isActive: data.isActive,
})
.where(eq(academicYears.id, id))
})
}
export async function deleteAcademicYear(id: string): Promise<void> {
await db.delete(academicYears).where(eq(academicYears.id, id))
}
// ---------------------------------------------------------------------------
// Cross-module query interfaces — read-only access for other modules
// ---------------------------------------------------------------------------
export type SubjectOption = {
id: string
name: string
code: string | null
order: number
}
export type GradeOption = {
id: string
name: string
schoolId: string
schoolName: string
order: number
}
export const getSubjectOptions = cache(async (): Promise<SubjectOption[]> => {
try {
const rows = await db
.select({
id: subjects.id,
name: subjects.name,
code: subjects.code,
order: subjects.order,
})
.from(subjects)
.orderBy(asc(subjects.order), asc(subjects.name))
return rows.map((r) => ({
id: r.id,
name: r.name,
code: r.code ?? null,
order: Number(r.order ?? 0),
}))
} catch (error) {
console.error("getSubjectOptions failed:", error)
return []
}
})
export const getGradeOptions = cache(async (): Promise<GradeOption[]> => {
try {
const rows = await db
.select({
id: grades.id,
name: grades.name,
order: grades.order,
schoolId: schools.id,
schoolName: schools.name,
})
.from(grades)
.innerJoin(schools, eq(schools.id, grades.schoolId))
.orderBy(asc(schools.name), asc(grades.order), asc(grades.name))
return rows.map((r) => ({
id: r.id,
name: r.name,
schoolId: r.schoolId,
schoolName: r.schoolName,
order: Number(r.order ?? 0),
}))
} catch (error) {
console.error("getGradeOptions failed:", error)
return []
}
})
// ---------------------------------------------------------------------------
// Cross-module query interfaces — grade head/teaching head verification
// ---------------------------------------------------------------------------
/**
* 校验用户是否为指定年级的年级主任。
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
*/
export const isGradeHead = async (
gradeId: string,
userId: string
): Promise<boolean> => {
const trimmedGradeId = gradeId.trim()
const trimmedUserId = userId.trim()
if (!trimmedGradeId || !trimmedUserId) return false
const [row] = await db
.select({ id: grades.id })
.from(grades)
.where(and(eq(grades.id, trimmedGradeId), eq(grades.gradeHeadId, trimmedUserId)))
.limit(1)
return Boolean(row)
}
/**
* 校验用户是否为指定年级的年级主任或教学主任。
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
*/
export const isGradeManager = async (
gradeId: string,
userId: string
): Promise<boolean> => {
const trimmedGradeId = gradeId.trim()
const trimmedUserId = userId.trim()
if (!trimmedGradeId || !trimmedUserId) return false
const [row] = await db
.select({ id: grades.id })
.from(grades)
.where(
and(
eq(grades.id, trimmedGradeId),
or(eq(grades.gradeHeadId, trimmedUserId), eq(grades.teachingHeadId, trimmedUserId))
)
)
.limit(1)
return Boolean(row)
}
/**
* 根据年级名称(大小写不敏感)查找用户担任年级主任的年级 ID。
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
*/
export const findGradeIdByHeadAndName = async (
userId: string,
gradeName: string
): Promise<string | null> => {
const trimmedUserId = userId.trim()
const normalizedGradeName = gradeName.trim().toLowerCase()
if (!trimmedUserId || !normalizedGradeName) return null
const [row] = await db
.select({ id: grades.id })
.from(grades)
.where(
and(
eq(grades.gradeHeadId, trimmedUserId),
sql`LOWER(${grades.name}) = ${normalizedGradeName}`
)
)
.limit(1)
return row?.id ?? null
}