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

@@ -26,6 +26,12 @@ import type {
import { getClassHomeworkInsights } from "./data-access-stats"
import { getClassSchedule } from "./data-access-schedule"
const isClassSubject = (v: unknown): v is ClassSubject =>
typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v)
const toClassSubject = (v: string): ClassSubject | null =>
isClassSubject(v) ? v : null
export const getSessionTeacherId = async (): Promise<string | null> => {
const { auth } = await import("@/auth")
const session = await auth()
@@ -118,14 +124,44 @@ export const compareClassLike = (
}
export const getAccessibleClassIdsForTeacher = async (teacherId: string): Promise<string[]> => {
const ownedIds = await db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId))
const assignedIds = await db
.select({ id: classSubjectTeachers.classId })
.from(classSubjectTeachers)
.where(eq(classSubjectTeachers.teacherId, teacherId))
const [ownedIds, assignedIds] = await Promise.all([
db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId)),
db
.select({ id: classSubjectTeachers.classId })
.from(classSubjectTeachers)
.where(eq(classSubjectTeachers.teacherId, teacherId)),
])
return Array.from(new Set([...ownedIds.map((x) => x.id), ...assignedIds.map((x) => x.id)]))
}
/**
* Verify that a teacher owns a class (teacherId match on classes row).
* Used by scheduling module to gate classSchedule writes.
*/
export async function verifyTeacherOwnsClass(classId: string, teacherId: string): Promise<boolean> {
const [owned] = await db
.select({ id: classes.id })
.from(classes)
.where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
.limit(1)
return Boolean(owned)
}
export const getClassGradeIdsByClassIds = async (classIds: string[]): Promise<Map<string, string>> => {
if (classIds.length === 0) return new Map()
const rows = await db
.select({ id: classes.id, gradeId: classes.gradeId })
.from(classes)
.where(inArray(classes.id, classIds))
const map = new Map<string, string>()
for (const row of rows) {
if (typeof row.gradeId === "string" && row.gradeId.trim().length > 0) {
map.set(row.id, row.gradeId)
}
}
return map
}
export const getTeacherSubjectIdsForClass = async (teacherId: string, classId: string): Promise<string[]> => {
const rows = await db
.select({ subjectId: classSubjectTeachers.subjectId })
@@ -134,6 +170,178 @@ export const getTeacherSubjectIdsForClass = async (teacherId: string, classId: s
return Array.from(new Set(rows.map((r) => String(r.subjectId))))
}
/**
* 获取班级的教师 ID班主任
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassTeacherById = async (classId: string): Promise<string | null> => {
const [row] = await db
.select({ teacherId: classes.teacherId })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return row?.teacherId ?? null
}
/**
* 获取班级所有学生 ID不限状态
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
*/
export const getStudentIdsByClassId = async (classId: string): Promise<string[]> => {
const rows = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.where(eq(classEnrollments.classId, classId))
return rows.map((r) => r.studentId)
}
/**
* 获取多个班级的所有学生 ID不限状态
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
*/
export const getStudentIdsByClassIds = async (classIds: string[]): Promise<string[]> => {
if (classIds.length === 0) return []
const rows = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.where(inArray(classEnrollments.classId, classIds))
return Array.from(new Set(rows.map((r) => r.studentId)))
}
/**
* 获取班级所有活跃学生 IDstatus = 'active')。
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
*/
export const getActiveStudentIdsByClassId = async (classId: string): Promise<string[]> => {
const rows = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.status, "active")))
return rows.map((r) => r.studentId)
}
/**
* 获取教师在一个班级所教的科目 ID 列表。
* 参数顺序为 (classId, teacherId),供跨模块调用使用。
*/
export const getTeacherSubjectIdsByClass = async (classId: string, teacherId: string): Promise<string[]> => {
return getTeacherSubjectIdsForClass(teacherId, classId)
}
/**
* 获取学生当前活跃班级的 ID。
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
*/
export const getStudentActiveClassId = async (studentId: string): Promise<string | null> => {
const [row] = await db
.select({ classId: classEnrollments.classId })
.from(classEnrollments)
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
.orderBy(asc(classEnrollments.createdAt))
.limit(1)
return row?.classId ?? null
}
/**
* 获取学生当前活跃班级对应的年级 ID。
* 供跨模块调用使用,避免直接查询 classEnrollments/classes 表。
*/
export const getStudentActiveGradeId = async (studentId: string): Promise<string | null> => {
const [row] = await db
.select({ gradeId: classes.gradeId })
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
.orderBy(asc(classEnrollments.createdAt))
.limit(1)
return row?.gradeId ?? null
}
/**
* 校验班级是否存在。
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassExists = async (classId: string): Promise<boolean> => {
const [row] = await db
.select({ id: classes.id })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return Boolean(row)
}
/**
* 获取班级名称。
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassNameById = async (classId: string): Promise<string | null> => {
const [row] = await db
.select({ name: classes.name })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return row?.name ?? null
}
/**
* 获取班级关联的年级 ID。
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassGradeId = async (classId: string): Promise<string | null> => {
const [row] = await db
.select({ gradeId: classes.gradeId })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return row?.gradeId ?? null
}
/**
* 获取多个班级关联的年级 ID 列表(去重,过滤空值)。
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getGradeIdsByClassIds = async (classIds: string[]): Promise<string[]> => {
if (classIds.length === 0) return []
const rows = await db
.selectDistinct({ gradeId: classes.gradeId })
.from(classes)
.where(inArray(classes.id, classIds))
return rows
.map((r) => r.gradeId)
.filter((id): id is string => typeof id === "string" && id.length > 0)
}
/**
* 批量获取班级名称Map<classId, name>)。
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassNamesByIds = async (classIds: string[]): Promise<Map<string, string>> => {
const result = new Map<string, string>()
const uniqueIds = Array.from(new Set(classIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
if (uniqueIds.length === 0) return result
const rows = await db
.select({ id: classes.id, name: classes.name })
.from(classes)
.where(inArray(classes.id, uniqueIds))
for (const r of rows) result.set(r.id, r.name)
return result
}
/**
* 获取指定年级下的所有班级id + name
* 供跨模块调用使用,避免直接查询 classes 表。
*/
export const getClassesByGradeId = async (gradeId: string): Promise<Array<{ id: string; name: string }>> => {
if (!gradeId) return []
const rows = await db
.select({ id: classes.id, name: classes.name })
.from(classes)
.where(eq(classes.gradeId, gradeId))
return rows.map((r) => ({ id: r.id, name: r.name }))
}
export const getTeacherClasses = cache(async (params?: { teacherId?: string }): Promise<TeacherClass[]> => {
const teacherId = params?.teacherId ?? (await getSessionTeacherId())
if (!teacherId) return []
@@ -237,8 +445,8 @@ export const getTeacherTeachingSubjects = cache(async (): Promise<ClassSubject[]
.orderBy(asc(subjects.name))
return rows
.map((r) => r.subject as ClassSubject)
.filter((s) => DEFAULT_CLASS_SUBJECTS.includes(s))
.map((r) => toClassSubject(r.subject))
.filter((s): s is ClassSubject => s !== null)
})
export async function createTeacherClass(data: CreateTeacherClassInput): Promise<string> {
@@ -263,7 +471,11 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
.select({ id: subjects.id, name: subjects.name })
.from(subjects)
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
const idByName = new Map<ClassSubject, string>()
for (const r of subjectRows) {
const subject = toClassSubject(r.name)
if (subject) idByName.set(subject, r.id)
}
await db.transaction(async (tx) => {
await tx.insert(classes).values({
@@ -279,13 +491,11 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
teacherId,
})
const values = DEFAULT_CLASS_SUBJECTS
.filter((name) => idByName.has(name))
.map((name) => ({
classId: id,
subjectId: idByName.get(name)!,
teacherId: null,
}))
const values = DEFAULT_CLASS_SUBJECTS.flatMap((name) => {
const subjectId = idByName.get(name)
if (!subjectId) return []
return [{ classId: id, subjectId, teacherId: null }]
})
await tx.insert(classSubjectTeachers).values(values)
})
return id
@@ -295,8 +505,6 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
}
}
throw new Error("Failed to create class")
return id
}
export async function ensureClassInvitationCode(classId: string): Promise<string> {
@@ -558,15 +766,17 @@ export async function setClassSubjectTeachers(params: {
.select({ id: subjects.id, name: subjects.name })
.from(subjects)
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
const idByName = new Map<ClassSubject, string>()
for (const r of subjectRows) {
const subject = toClassSubject(r.name)
if (subject) idByName.set(subject, r.id)
}
const values = DEFAULT_CLASS_SUBJECTS
.filter((name) => idByName.has(name))
.map((name) => ({
classId,
subjectId: idByName.get(name)!,
teacherId: teacherBySubject.get(name) ?? null,
}))
const values = DEFAULT_CLASS_SUBJECTS.flatMap((name) => {
const subjectId = idByName.get(name)
if (!subjectId) return []
return [{ classId, subjectId, teacherId: teacherBySubject.get(name) ?? null }]
})
await db
.insert(classSubjectTeachers)