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,6 +1,8 @@
import "server-only"
import { createId } from "@paralleldrive/cuid2"
import { and, desc, eq, inArray } from "drizzle-orm"
import { cache } from "react"
import { db } from "@/shared/db"
import { learningDiagnosticReports, users } from "@/shared/db/schema"
@@ -19,6 +21,12 @@ const toNumber = (v: unknown): number => {
const round2 = (n: number): number => Math.round(n * 100) / 100
const isStringArray = (v: unknown): v is string[] =>
Array.isArray(v) && v.every((item) => typeof item === "string")
const toStringArrayNullable = (v: unknown): string[] | null =>
isStringArray(v) ? v : null
const serializeReport = (r: typeof learningDiagnosticReports.$inferSelect): DiagnosticReport => ({
id: r.id,
studentId: r.studentId,
@@ -26,9 +34,9 @@ const serializeReport = (r: typeof learningDiagnosticReports.$inferSelect): Diag
reportType: r.reportType,
period: r.period,
summary: r.summary,
strengths: (r.strengths as string[] | null) ?? null,
weaknesses: (r.weaknesses as string[] | null) ?? null,
recommendations: (r.recommendations as string[] | null) ?? null,
strengths: toStringArrayNullable(r.strengths),
weaknesses: toStringArrayNullable(r.weaknesses),
recommendations: toStringArrayNullable(r.recommendations),
overallScore: r.overallScore !== null ? toNumber(r.overallScore) : null,
status: r.status,
createdAt: r.createdAt.toISOString(),
@@ -56,7 +64,6 @@ export async function generateDiagnosticReport(
const summaryText = `学生 ${summary.studentName}${period} 期间整体掌握度 ${overallScore.toFixed(1)}%,共评估 ${summary.totalKnowledgePoints} 个知识点,强项 ${strengths.length} 个,弱项 ${weaknesses.length} 个。`
const { createId } = await import("@paralleldrive/cuid2")
const id = createId()
await db.insert(learningDiagnosticReports).values({
id,
@@ -100,7 +107,6 @@ export async function generateClassDiagnosticReport(
const summaryText = `班级 ${summary.className}${period} 期间整体掌握度 ${summary.averageMastery.toFixed(1)}%,学生 ${summary.studentCount} 人,需重点关注 ${summary.studentsNeedingAttention.length} 人。`
const { createId } = await import("@paralleldrive/cuid2")
const id = createId()
await db.insert(learningDiagnosticReports).values({
id,
@@ -119,71 +125,71 @@ export async function generateClassDiagnosticReport(
}
/** 查询诊断报告列表 */
export async function getDiagnosticReports(
filters: DiagnosticReportQueryParams
): Promise<DiagnosticReportWithDetails[]> {
const conditions = []
if (filters.studentId) conditions.push(eq(learningDiagnosticReports.studentId, filters.studentId))
if (filters.reportType) conditions.push(eq(learningDiagnosticReports.reportType, filters.reportType))
if (filters.status) conditions.push(eq(learningDiagnosticReports.status, filters.status))
if (filters.period) conditions.push(eq(learningDiagnosticReports.period, filters.period))
export const getDiagnosticReports = cache(
async (filters: DiagnosticReportQueryParams): Promise<DiagnosticReportWithDetails[]> => {
const conditions = []
if (filters.studentId) conditions.push(eq(learningDiagnosticReports.studentId, filters.studentId))
if (filters.reportType) conditions.push(eq(learningDiagnosticReports.reportType, filters.reportType))
if (filters.status) conditions.push(eq(learningDiagnosticReports.status, filters.status))
if (filters.period) conditions.push(eq(learningDiagnosticReports.period, filters.period))
const rows = await db
.select({
report: learningDiagnosticReports,
studentName: users.name,
})
.from(learningDiagnosticReports)
.leftJoin(users, eq(users.id, learningDiagnosticReports.studentId))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(learningDiagnosticReports.createdAt))
const rows = await db
.select({
report: learningDiagnosticReports,
studentName: users.name,
})
.from(learningDiagnosticReports)
.leftJoin(users, eq(users.id, learningDiagnosticReports.studentId))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(learningDiagnosticReports.createdAt))
const generatorIds = Array.from(
new Set(rows.map((r) => r.report.generatedBy).filter((id): id is string => id !== null))
)
const generatorMap = new Map<string, string>()
if (generatorIds.length > 0) {
const generators = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(inArray(users.id, generatorIds))
for (const g of generators) generatorMap.set(g.id, g.name ?? "Unknown")
}
const generatorIds = Array.from(
new Set(rows.map((r) => r.report.generatedBy).filter((id): id is string => id !== null))
)
const generatorMap = new Map<string, string>()
if (generatorIds.length > 0) {
const generators = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(inArray(users.id, generatorIds))
for (const g of generators) generatorMap.set(g.id, g.name ?? "Unknown")
}
return rows.map((r) => ({
...serializeReport(r.report),
studentName: r.studentName ?? "Unknown",
generatedByName: r.report.generatedBy ? generatorMap.get(r.report.generatedBy) ?? "Unknown" : null,
}))
}
return rows.map((r) => ({
...serializeReport(r.report),
studentName: r.studentName ?? "Unknown",
generatedByName: r.report.generatedBy ? generatorMap.get(r.report.generatedBy) ?? "Unknown" : null,
}))
},
)
/** 获取报告详情 */
export async function getDiagnosticReportById(
id: string
): Promise<DiagnosticReportWithDetails | null> {
const [row] = await db
.select({ report: learningDiagnosticReports, studentName: users.name })
.from(learningDiagnosticReports)
.leftJoin(users, eq(users.id, learningDiagnosticReports.studentId))
.where(eq(learningDiagnosticReports.id, id))
.limit(1)
if (!row) return null
let generatedByName: string | null = null
if (row.report.generatedBy) {
const [gen] = await db
.select({ name: users.name })
.from(users)
.where(eq(users.id, row.report.generatedBy))
export const getDiagnosticReportById = cache(
async (id: string): Promise<DiagnosticReportWithDetails | null> => {
const [row] = await db
.select({ report: learningDiagnosticReports, studentName: users.name })
.from(learningDiagnosticReports)
.leftJoin(users, eq(users.id, learningDiagnosticReports.studentId))
.where(eq(learningDiagnosticReports.id, id))
.limit(1)
generatedByName = gen?.name ?? null
}
return {
...serializeReport(row.report),
studentName: row.studentName ?? "Unknown",
generatedByName,
}
}
if (!row) return null
let generatedByName: string | null = null
if (row.report.generatedBy) {
const [gen] = await db
.select({ name: users.name })
.from(users)
.where(eq(users.id, row.report.generatedBy))
.limit(1)
generatedByName = gen?.name ?? null
}
return {
...serializeReport(row.report),
studentName: row.studentName ?? "Unknown",
generatedByName,
}
},
)
/** 发布诊断报告 */
export async function publishDiagnosticReport(id: string): Promise<void> {