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,18 +1,15 @@
import "server-only"
import { and, asc, desc, eq, inArray } from "drizzle-orm"
import { cache } from "react"
import { desc, eq, inArray } from "drizzle-orm"
import { db } from "@/shared/db"
import {
classEnrollments,
classes,
examSubmissions,
knowledgePointMastery,
knowledgePoints,
questionsToKnowledgePoints,
submissionAnswers,
users,
} from "@/shared/db/schema"
import { knowledgePointMastery, knowledgePoints } from "@/shared/db/schema"
import { getClassNameById, getActiveStudentIdsByClassId, getClassExists } from "@/modules/classes/data-access"
import { getExamSubmissionWithAnswers } from "@/modules/exams/data-access"
import { getKnowledgePointsForQuestions } from "@/modules/questions/data-access"
import { getUserIdsByGradeId, getUserNamesByIds } from "@/modules/users/data-access"
import type {
ClassMasterySummary,
@@ -42,7 +39,7 @@ const serializeMastery = (r: typeof knowledgePointMastery.$inferSelect): Knowled
})
/** 获取学生在所有知识点的掌握度(含知识点名称) */
export async function getStudentMastery(studentId: string): Promise<MasteryWithKnowledgePoint[]> {
export const getStudentMastery = cache(async (studentId: string): Promise<MasteryWithKnowledgePoint[]> => {
const rows = await db
.select({
mastery: knowledgePointMastery,
@@ -59,11 +56,12 @@ export async function getStudentMastery(studentId: string): Promise<MasteryWithK
knowledgePointName: r.kpName ?? "Unknown",
knowledgePointDescription: r.kpDescription,
}))
}
})
/** 获取学生掌握度摘要(含强项/弱项分析) */
export async function getStudentMasterySummary(studentId: string): Promise<StudentMasterySummary | null> {
const [student] = await db.select({ name: users.name }).from(users).where(eq(users.id, studentId)).limit(1)
export const getStudentMasterySummary = cache(async (studentId: string): Promise<StudentMasterySummary | null> => {
const userMap = await getUserNamesByIds([studentId])
const student = userMap.get(studentId)
if (!student) return null
const allMastery = await getStudentMastery(studentId)
@@ -72,53 +70,49 @@ export async function getStudentMasterySummary(studentId: string): Promise<Stude
? round2(allMastery.reduce((acc, m) => acc + m.masteryLevel, 0) / allMastery.length)
: 0
// Single-pass classification: strengths (>=80) and weaknesses (<60)
const strengths: MasteryWithKnowledgePoint[] = []
const weaknesses: MasteryWithKnowledgePoint[] = []
for (const m of allMastery) {
if (m.masteryLevel >= 80) strengths.push(m)
if (m.masteryLevel < 60) weaknesses.push(m)
}
return {
studentId,
studentName: student.name ?? "Unknown",
averageMastery,
totalKnowledgePoints: allMastery.length,
strengths: allMastery.filter((m) => m.masteryLevel >= 80),
weaknesses: allMastery.filter((m) => m.masteryLevel < 60),
strengths,
weaknesses,
allMastery,
}
}
})
/** 从提交答案更新掌握度(正确率作为掌握度) */
export async function updateMasteryFromSubmission(submissionId: string): Promise<void> {
const [submission] = await db
.select({ studentId: examSubmissions.studentId })
.from(examSubmissions)
.where(eq(examSubmissions.id, submissionId))
.limit(1)
const submission = await getExamSubmissionWithAnswers(submissionId)
if (!submission) return
const answers = await db
.select({
questionId: submissionAnswers.questionId,
score: submissionAnswers.score,
})
.from(submissionAnswers)
.where(eq(submissionAnswers.submissionId, submissionId))
const answers = submission.answers
if (answers.length === 0) return
const questionIds = Array.from(new Set(answers.map((a) => a.questionId)))
const kpLinks = await db
.select({
questionId: questionsToKnowledgePoints.questionId,
knowledgePointId: questionsToKnowledgePoints.knowledgePointId,
})
.from(questionsToKnowledgePoints)
.where(inArray(questionsToKnowledgePoints.questionId, questionIds))
const kpMap = await getKnowledgePointsForQuestions(questionIds)
// Build a Map for O(1) answer lookup instead of find() in loop
const answerByQuestionId = new Map(answers.map((a) => [a.questionId, a]))
const kpStats = new Map<string, { total: number; correct: number }>()
for (const link of kpLinks) {
const answer = answers.find((a) => a.questionId === link.questionId)
for (const [questionId, kpLinks] of kpMap.entries()) {
const answer = answerByQuestionId.get(questionId)
if (!answer) continue
const stat = kpStats.get(link.knowledgePointId) ?? { total: 0, correct: 0 }
stat.total += 1
if ((answer.score ?? 0) > 0) stat.correct += 1
kpStats.set(link.knowledgePointId, stat)
for (const link of kpLinks) {
const stat = kpStats.get(link.knowledgePointId) ?? { total: 0, correct: 0 }
stat.total += 1
if ((answer.score ?? 0) > 0) stat.correct += 1
kpStats.set(link.knowledgePointId, stat)
}
}
const now = new Date()
@@ -147,22 +141,21 @@ export async function updateMasteryFromSubmission(submissionId: string): Promise
}
/** 获取班级掌握度摘要 */
export async function getClassMasterySummary(classId: string): Promise<ClassMasterySummary | null> {
const [classRow] = await db.select({ id: classes.id, name: classes.name }).from(classes).where(eq(classes.id, classId)).limit(1)
if (!classRow) return null
export const getClassMasterySummary = cache(async (classId: string): Promise<ClassMasterySummary | null> => {
const classExists = await getClassExists(classId)
if (!classExists) return null
const className = (await getClassNameById(classId)) ?? "Unknown"
const students = await db
.select({ id: users.id, name: users.name })
.from(classEnrollments)
.innerJoin(users, eq(users.id, classEnrollments.studentId))
.where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.status, "active")))
.orderBy(asc(users.name))
if (students.length === 0) {
return { classId, className: classRow.name, studentCount: 0, averageMastery: 0, knowledgePointStats: [], studentsNeedingAttention: [] }
const studentIds = await getActiveStudentIdsByClassId(classId)
if (studentIds.length === 0) {
return { classId, className, studentCount: 0, averageMastery: 0, knowledgePointStats: [], studentsNeedingAttention: [] }
}
const studentIds = students.map((s) => s.id)
const userMap = await getUserNamesByIds(studentIds)
const students = studentIds
.map((id) => ({ id, name: userMap.get(id)?.name ?? null }))
.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
const masteryRows = await db
.select({ mastery: knowledgePointMastery, kpName: knowledgePoints.name })
.from(knowledgePointMastery)
@@ -203,25 +196,25 @@ export async function getClassMasterySummary(classId: string): Promise<ClassMast
const studentsNeedingAttention = students
.map((s) => {
const e = byStudent.get(s.id)!
const e = byStudent.get(s.id)
if (!e) return null
const avg = e.levels.length > 0 ? round2(e.levels.reduce((a, b) => a + b, 0) / e.levels.length) : 0
return { studentId: s.id, studentName: s.name ?? "Unknown", averageMastery: avg, weakCount: e.weakCount }
})
.filter((s): s is { studentId: string; studentName: string; averageMastery: number; weakCount: number } => s !== null)
.filter((s) => s.averageMastery < 60)
.sort((a, b) => a.averageMastery - b.averageMastery)
return { classId, className: classRow.name, studentCount: students.length, averageMastery, knowledgePointStats, studentsNeedingAttention }
}
return { classId, className, studentCount: students.length, averageMastery, knowledgePointStats, studentsNeedingAttention }
})
/** 获取知识点统计(按班级或年级聚合) */
export async function getKnowledgePointStats(classId?: string, gradeId?: string): Promise<KnowledgePointStat[]> {
export const getKnowledgePointStats = cache(async (classId?: string, gradeId?: string): Promise<KnowledgePointStat[]> => {
let studentIds: string[] = []
if (classId) {
const rows = await db.select({ studentId: classEnrollments.studentId }).from(classEnrollments).where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.status, "active")))
studentIds = rows.map((r) => r.studentId)
studentIds = await getActiveStudentIdsByClassId(classId)
} else if (gradeId) {
const rows = await db.select({ id: users.id }).from(users).where(eq(users.gradeId, gradeId))
studentIds = rows.map((r) => r.id)
studentIds = await getUserIdsByGradeId(gradeId)
}
if (studentIds.length === 0) return []
@@ -251,4 +244,4 @@ export async function getKnowledgePointStats(classId?: string, gradeId?: string)
notMasteredCount: e.notMastered,
totalStudents: studentIds.length,
}))
}
})