"use server" import { revalidatePath } from "next/cache" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import type { ActionState } from "@/shared/types/action-state" import { handleActionError } from "@/shared/lib/action-utils" import { createNotification } from "@/modules/notifications/data-access" import { getStudentIdsByClassId } from "@/modules/classes/data-access" import { getParentIdsByStudentIds } from "@/modules/parent/data-access" import { generateDiagnosticReport, generateClassDiagnosticReport, generateGradeDiagnosticReport, publishDiagnosticReport, deleteDiagnosticReport, getDiagnosticReportById, } from "./data-access-reports" import { getClassStudentsByKnowledgePoint } from "./data-access" import { exportDiagnosticReportToExcel, buildDiagnosticReportFilename, } from "./export" import { GenerateStudentReportSchema, GenerateClassReportSchema, GenerateGradeReportSchema, PublishReportSchema, DeleteReportSchema, } from "./schema" /** 生成学生个人诊断报告 */ export async function generateStudentReportAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.DIAGNOSTIC_MANAGE) 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}`) return { success: true, message: "Diagnostic report generated", data: id } } catch (e) { return handleActionError(e) } } /** 生成班级诊断报告 */ export async function generateClassReportAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.DIAGNOSTIC_MANAGE) 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}`) return { success: true, message: "Class diagnostic report generated", data: id } } catch (e) { return handleActionError(e) } } /** v4-P2-3: 生成年级诊断报告 */ export async function generateGradeReportAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.DIAGNOSTIC_MANAGE) const parsed = GenerateGradeReportSchema.safeParse({ gradeId: formData.get("gradeId"), period: formData.get("period"), }) if (!parsed.success) { return { success: false, message: "Missing gradeId or period" } } const { gradeId, period } = parsed.data const id = await generateGradeDiagnosticReport(gradeId, period, ctx.userId) revalidatePath("/teacher/diagnostic") revalidatePath("/admin/diagnostic") return { success: true, message: "Grade diagnostic report generated", data: id } } catch (e) { return handleActionError(e) } } /** 发布诊断报告 */ export async function publishReportAction( prevState: ActionState | null, formData: FormData ): Promise> { try { await requirePermission(Permissions.DIAGNOSTIC_MANAGE) const parsed = PublishReportSchema.safeParse({ id: formData.get("id"), }) if (!parsed.success) { return { success: false, message: "Missing report id" } } await publishDiagnosticReport(parsed.data.id) // v3-P1-4 + v4-P1-4 + v4-P1-5:发布报告后发送通知 // - 个人报告:通知学生本人 + 其家长 // - 班级报告:通知全班学生 + 全班学生家长 const report = await getDiagnosticReportById(parsed.data.id) if (!report) { revalidatePath("/teacher/diagnostic") return { success: true, message: "Report published" } } const title = `诊断报告已发布:${report.period ?? "本期"}` const content = report.summary ?? "您有一份新的学情诊断报告,请查看详情。" const link = "/student/diagnostic" // 收集需要通知的学生 ID 列表 const studentIdsToNotify: string[] = [] if (report.studentId) { // 个人报告:通知单个学生 studentIdsToNotify.push(report.studentId) } else if (report.classId) { // v4-P1-4: 班级报告(有 classId):通知全班学生 const classStudentIds = await getStudentIdsByClassId(report.classId) studentIdsToNotify.push(...classStudentIds) } // 通知学生本人 for (const studentId of studentIdsToNotify) { try { await createNotification({ userId: studentId, type: "diagnostic", title, content, link, }) } catch { // 单条通知失败不阻断整体流程 } } // v4-P1-5: 通知所有相关家长 if (studentIdsToNotify.length > 0) { try { const parentIds = await getParentIdsByStudentIds(studentIdsToNotify) for (const parentId of parentIds) { try { await createNotification({ userId: parentId, type: "diagnostic", title, content: report.summary ?? "您的孩子有一份新的学情诊断报告,请查看详情。", link: "/parent/diagnostic", }) } catch { // 单条通知失败不阻断整体流程 } } } catch { // 家长查询失败不阻断整体流程 } } revalidatePath("/teacher/diagnostic") return { success: true, message: "Report published" } } catch (e) { return handleActionError(e) } } /** 删除诊断报告 */ export async function deleteReportAction( prevState: ActionState | null, formData: FormData ): Promise> { try { await requirePermission(Permissions.DIAGNOSTIC_MANAGE) const parsed = DeleteReportSchema.safeParse({ id: formData.get("id"), }) if (!parsed.success) { return { success: false, message: "Missing report id" } } await deleteDiagnosticReport(parsed.data.id) revalidatePath("/teacher/diagnostic") return { success: true, message: "Report deleted" } } catch (e) { return handleActionError(e) } } /** * v3-P2-4: 导出诊断报告为 Excel。 * 返回 base64 编码的 buffer 和文件名,前端通过 Blob 下载。 */ export async function exportDiagnosticReportAction( reportId: string ): Promise> { try { await requirePermission(Permissions.DIAGNOSTIC_READ) if (!reportId || typeof reportId !== "string") { return { success: false, message: "Missing report id" } } const report = await getDiagnosticReportById(reportId) if (!report) { return { success: false, message: "Report not found" } } const buffer = await exportDiagnosticReportToExcel({ reportId }) const filename = await buildDiagnosticReportFilename(report.period) return { success: true, data: { buffer: buffer.toString("base64"), filename, }, } } catch (e) { return handleActionError(e) } } /** * v3-P2-5: 获取班级学生在指定知识点上的掌握度列表。 * 用于班级诊断页面的"按知识点筛选学生"功能。 */ export async function getClassStudentsByKnowledgePointAction(params: { classId: string knowledgePointId: string threshold?: number }): Promise< ActionState< Array<{ studentId: string studentName: string masteryLevel: number totalQuestions: number correctQuestions: number lastAssessedAt: string | null needsAttention: boolean }> > > { try { const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ) if (!params.classId || !params.knowledgePointId) { return { success: false, message: "Missing classId or knowledgePointId" } } // 教师只能查看所教班级 if ( ctx.dataScope.type === "class_taught" && !ctx.dataScope.classIds.includes(params.classId) ) { return { success: false, message: "You can only access classes you teach" } } // 学生/家长不可访问 if (ctx.dataScope.type === "class_members" || ctx.dataScope.type === "children") { return { success: false, message: "Access denied" } } const result = await getClassStudentsByKnowledgePoint( params.classId, params.knowledgePointId, { threshold: params.threshold } ) return { success: true, data: result } } catch (e) { return handleActionError(e) } }