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:
@@ -13,6 +13,14 @@ import {
|
||||
publishDiagnosticReport,
|
||||
deleteDiagnosticReport,
|
||||
} from "./data-access-reports"
|
||||
import {
|
||||
GenerateStudentReportSchema,
|
||||
GenerateClassReportSchema,
|
||||
PublishReportSchema,
|
||||
DeleteReportSchema,
|
||||
GetDiagnosticReportsSchema,
|
||||
GetDiagnosticReportByIdSchema,
|
||||
} from "./schema"
|
||||
import type { DiagnosticReportQueryParams } from "./types"
|
||||
|
||||
/** 生成学生个人诊断报告 */
|
||||
@@ -23,15 +31,15 @@ export async function generateStudentReportAction(
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.DIAGNOSTIC_MANAGE)
|
||||
|
||||
const studentId = formData.get("studentId")
|
||||
const period = formData.get("period")
|
||||
if (typeof studentId !== "string" || studentId.length === 0) {
|
||||
return { success: false, message: "Missing studentId" }
|
||||
}
|
||||
if (typeof period !== "string" || period.length === 0) {
|
||||
return { success: false, message: "Missing period" }
|
||||
const parsed = GenerateStudentReportSchema.safeParse({
|
||||
studentId: formData.get("studentId"),
|
||||
period: formData.get("period"),
|
||||
})
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Missing studentId or period" }
|
||||
}
|
||||
|
||||
const { studentId, period } = parsed.data
|
||||
const id = await generateDiagnosticReport(studentId, period, ctx.userId)
|
||||
revalidatePath("/teacher/diagnostic")
|
||||
revalidatePath(`/teacher/diagnostic/student/${studentId}`)
|
||||
@@ -51,15 +59,15 @@ export async function generateClassReportAction(
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.DIAGNOSTIC_MANAGE)
|
||||
|
||||
const classId = formData.get("classId")
|
||||
const period = formData.get("period")
|
||||
if (typeof classId !== "string" || classId.length === 0) {
|
||||
return { success: false, message: "Missing classId" }
|
||||
}
|
||||
if (typeof period !== "string" || period.length === 0) {
|
||||
return { success: false, message: "Missing period" }
|
||||
const parsed = GenerateClassReportSchema.safeParse({
|
||||
classId: formData.get("classId"),
|
||||
period: formData.get("period"),
|
||||
})
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Missing classId or period" }
|
||||
}
|
||||
|
||||
const { classId, period } = parsed.data
|
||||
const id = await generateClassDiagnosticReport(classId, period, ctx.userId)
|
||||
revalidatePath("/teacher/diagnostic")
|
||||
revalidatePath(`/teacher/diagnostic/class/${classId}`)
|
||||
@@ -79,12 +87,14 @@ export async function publishReportAction(
|
||||
try {
|
||||
await requirePermission(Permissions.DIAGNOSTIC_MANAGE)
|
||||
|
||||
const id = formData.get("id")
|
||||
if (typeof id !== "string" || id.length === 0) {
|
||||
const parsed = PublishReportSchema.safeParse({
|
||||
id: formData.get("id"),
|
||||
})
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Missing report id" }
|
||||
}
|
||||
|
||||
await publishDiagnosticReport(id)
|
||||
await publishDiagnosticReport(parsed.data.id)
|
||||
revalidatePath("/teacher/diagnostic")
|
||||
return { success: true, message: "Report published" }
|
||||
} catch (e) {
|
||||
@@ -102,12 +112,14 @@ export async function deleteReportAction(
|
||||
try {
|
||||
await requirePermission(Permissions.DIAGNOSTIC_MANAGE)
|
||||
|
||||
const id = formData.get("id")
|
||||
if (typeof id !== "string" || id.length === 0) {
|
||||
const parsed = DeleteReportSchema.safeParse({
|
||||
id: formData.get("id"),
|
||||
})
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Missing report id" }
|
||||
}
|
||||
|
||||
await deleteDiagnosticReport(id)
|
||||
await deleteDiagnosticReport(parsed.data.id)
|
||||
revalidatePath("/teacher/diagnostic")
|
||||
return { success: true, message: "Report deleted" }
|
||||
} catch (e) {
|
||||
@@ -123,7 +135,13 @@ export async function getDiagnosticReportsAction(
|
||||
): Promise<ActionState<Awaited<ReturnType<typeof getDiagnosticReports>>>> {
|
||||
try {
|
||||
await requirePermission(Permissions.DIAGNOSTIC_READ)
|
||||
const reports = await getDiagnosticReports(params)
|
||||
|
||||
const parsed = GetDiagnosticReportsSchema.safeParse(params)
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid query params" }
|
||||
}
|
||||
|
||||
const reports = await getDiagnosticReports(parsed.data)
|
||||
return { success: true, data: reports }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
@@ -138,7 +156,13 @@ export async function getDiagnosticReportByIdAction(
|
||||
): Promise<ActionState<Awaited<ReturnType<typeof getDiagnosticReportById>>>> {
|
||||
try {
|
||||
await requirePermission(Permissions.DIAGNOSTIC_READ)
|
||||
const report = await getDiagnosticReportById(id)
|
||||
|
||||
const parsed = GetDiagnosticReportByIdSchema.safeParse({ id })
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Missing report id" }
|
||||
}
|
||||
|
||||
const report = await getDiagnosticReportById(parsed.data.id)
|
||||
return { success: true, data: report }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
48
src/modules/diagnostic/schema.ts
Normal file
48
src/modules/diagnostic/schema.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod"
|
||||
|
||||
/** 生成学生个人诊断报告 */
|
||||
export const GenerateStudentReportSchema = z.object({
|
||||
studentId: z.string().min(1),
|
||||
period: z.string().min(1),
|
||||
})
|
||||
|
||||
export type GenerateStudentReportInput = z.infer<typeof GenerateStudentReportSchema>
|
||||
|
||||
/** 生成班级诊断报告 */
|
||||
export const GenerateClassReportSchema = z.object({
|
||||
classId: z.string().min(1),
|
||||
period: z.string().min(1),
|
||||
})
|
||||
|
||||
export type GenerateClassReportInput = z.infer<typeof GenerateClassReportSchema>
|
||||
|
||||
/** 发布诊断报告 */
|
||||
export const PublishReportSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export type PublishReportInput = z.infer<typeof PublishReportSchema>
|
||||
|
||||
/** 删除诊断报告 */
|
||||
export const DeleteReportSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export type DeleteReportInput = z.infer<typeof DeleteReportSchema>
|
||||
|
||||
/** 查询诊断报告列表 */
|
||||
export const GetDiagnosticReportsSchema = z.object({
|
||||
studentId: z.string().optional(),
|
||||
reportType: z.enum(["individual", "class", "grade"]).optional(),
|
||||
status: z.enum(["draft", "published", "archived"]).optional(),
|
||||
period: z.string().optional(),
|
||||
})
|
||||
|
||||
export type GetDiagnosticReportsInput = z.infer<typeof GetDiagnosticReportsSchema>
|
||||
|
||||
/** 获取诊断报告详情 */
|
||||
export const GetDiagnosticReportByIdSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export type GetDiagnosticReportByIdInput = z.infer<typeof GetDiagnosticReportByIdSchema>
|
||||
Reference in New Issue
Block a user