Files
NextEdu/src/modules/diagnostic/actions.ts
SpecialX 138b6f1b00 feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components
dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
2026-07-03 10:25:46 +08:00

303 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<string> | null,
formData: FormData
): Promise<ActionState<string>> {
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<string> | null,
formData: FormData
): Promise<ActionState<string>> {
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<string> | null,
formData: FormData
): Promise<ActionState<string>> {
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<string> | null,
formData: FormData
): Promise<ActionState<string>> {
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<string> | null,
formData: FormData
): Promise<ActionState<string>> {
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<ActionState<{ buffer: string; filename: string }>> {
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)
}
}