import "server-only" import { getTranslations } from "next-intl/server" import { exportToExcel } from "@/shared/lib/excel" import { formatDateForFile } from "@/shared/lib/utils" import { BusinessError } from "@/shared/lib/action-utils" import { getDiagnosticReportById } from "./data-access-reports" import { getStudentMasterySummary, getClassMasterySummary } from "./data-access" /** * v2-P2-3: 导出报告不存在的结构化错误。 */ export class DiagnosticExportError extends BusinessError { constructor(code: "REPORT_NOT_FOUND", message: string) { super(message, code) this.name = "DiagnosticExportError" } } /** * v3-P2-4: 导出诊断报告为 Excel。 * * 个人报告 Sheet: * - 概览(学生、周期、综合得分、摘要、强项、弱项、建议) * - 知识点掌握度明细 * * 班级报告 Sheet: * - 概览(班级、周期、综合得分、摘要、强项、弱项、建议) * - 知识点统计(平均掌握度、掌握人数、未掌握人数) * - 需关注学生列表 */ export async function exportDiagnosticReportToExcel(params: { reportId: string }): Promise { const report = await getDiagnosticReportById(params.reportId) if (!report) { // v2-P2-3: 使用结构化错误码,由调用方 i18n 化 throw new DiagnosticExportError("REPORT_NOT_FOUND", "Report not found") } const t = await getTranslations("diagnostic") const periodLabel = report.period ?? t("parent.selectChild") const overallScore = report.overallScore ?? "-" const strengths = (report.strengths ?? []).join("\n") || "-" const weaknesses = (report.weaknesses ?? []).join("\n") || "-" const recommendations = (report.recommendations ?? []).join("\n") || "-" const summary = report.summary ?? "-" if (report.reportType === "individual" && report.studentId) { // 个人报告 const mastery = await getStudentMasterySummary(report.studentId) const overviewRows = [ { metric: t("exportContent.metricStudent"), value: report.studentName ?? "-" }, { metric: t("exportContent.metricPeriod"), value: periodLabel }, { metric: t("exportContent.metricScore"), value: overallScore }, { metric: t("exportContent.metricStatus"), value: report.status }, { metric: t("exportContent.metricGeneratedBy"), value: report.generatedByName ?? "-" }, { metric: t("exportContent.metricCreatedAt"), value: report.createdAt.split("T")[0] }, { metric: t("exportContent.metricSummary"), value: summary }, { metric: t("exportContent.metricStrengths"), value: strengths }, { metric: t("exportContent.metricWeaknesses"), value: weaknesses }, { metric: t("exportContent.metricRecommendations"), value: recommendations }, ] const masteryRows = (mastery?.allMastery ?? []).map((m) => ({ knowledgePoint: m.knowledgePointName, masteryLevel: m.masteryLevel, totalQuestions: m.totalQuestions, correctQuestions: m.correctQuestions, lastAssessedAt: m.lastAssessedAt.split("T")[0], })) return exportToExcel({ sheets: [ { name: t("exportContent.sheetOverview"), columns: [ { header: t("exportContent.metricStudent"), key: "metric", width: 20 }, { header: "", key: "value", width: 60 }, ], rows: overviewRows, }, { name: t("exportContent.sheetMastery"), columns: [ { header: t("exportContent.colKnowledgePoint"), key: "knowledgePoint", width: 28 }, { header: t("exportContent.colMasteryLevel"), key: "masteryLevel", width: 12 }, { header: t("exportContent.colTotalQuestions"), key: "totalQuestions", width: 10 }, { header: t("exportContent.colCorrectQuestions"), key: "correctQuestions", width: 10 }, { header: t("exportContent.colLastAssessed"), key: "lastAssessedAt", width: 14 }, ], rows: masteryRows, }, ], }) } // 班级报告(reportType === "class") // v4-P2-1: 利用 classId 字段查询班级掌握度,导出知识点统计+需关注学生明细 const classSummary = report.classId ? await getClassMasterySummary(report.classId) : null const overviewRows = [ { metric: t("exportContent.metricReportType"), value: t("type.class") }, ...(classSummary ? [{ metric: t("exportContent.metricClass"), value: classSummary.className }] : []), { metric: t("exportContent.metricPeriod"), value: periodLabel }, { metric: t("exportContent.metricScore"), value: overallScore }, ...(classSummary ? [{ metric: t("exportContent.metricStudentCount"), value: classSummary.studentCount }] : []), ...(classSummary ? [{ metric: t("exportContent.metricAttentionCount"), value: classSummary.studentsNeedingAttention.length }] : []), { metric: t("exportContent.metricStatus"), value: report.status }, { metric: t("exportContent.metricGeneratedBy"), value: report.generatedByName ?? "-" }, { metric: t("exportContent.metricCreatedAt"), value: report.createdAt.split("T")[0] }, { metric: t("exportContent.metricSummary"), value: summary }, { metric: t("exportContent.metricStrengths"), value: strengths }, { metric: t("exportContent.metricWeaknesses"), value: weaknesses }, { metric: t("exportContent.metricRecommendations"), value: recommendations }, ] const sheets: Array<{ name: string columns: Array<{ header: string; key: string; width: number }> rows: Array> }> = [ { name: t("exportContent.sheetOverview"), columns: [ { header: t("exportContent.metricStudent"), key: "metric", width: 20 }, { header: "", key: "value", width: 60 }, ], rows: overviewRows, }, ] // v4-P2-1: 知识点统计 Sheet if (classSummary && classSummary.knowledgePointStats.length > 0) { const classStatsRows = classSummary.knowledgePointStats.map((kp) => ({ knowledgePoint: kp.knowledgePointName, averageMastery: kp.averageMastery.toFixed(1), masteredCount: kp.masteredCount, notMasteredCount: kp.notMasteredCount, totalStudents: kp.totalStudents, })) sheets.push({ name: t("exportContent.sheetClassStats"), columns: [ { header: t("exportContent.colKnowledgePoint"), key: "knowledgePoint", width: 28 }, { header: t("exportContent.colAverageMastery"), key: "averageMastery", width: 14 }, { header: t("exportContent.colMasteredCount"), key: "masteredCount", width: 16 }, { header: t("exportContent.colNotMasteredCount"), key: "notMasteredCount", width: 16 }, { header: t("exportContent.colTotalStudents"), key: "totalStudents", width: 12 }, ], rows: classStatsRows, }) } // v4-P2-1: 需关注学生 Sheet if (classSummary && classSummary.studentsNeedingAttention.length > 0) { const attentionRows = classSummary.studentsNeedingAttention.map((s) => ({ studentName: s.studentName, averageMastery: s.averageMastery.toFixed(1), weakCount: s.weakCount, })) sheets.push({ name: t("exportContent.sheetAttentionStudents"), columns: [ { header: t("exportContent.colStudentName"), key: "studentName", width: 24 }, { header: t("exportContent.colAverageMastery"), key: "averageMastery", width: 14 }, { header: t("exportContent.colWeakCount"), key: "weakCount", width: 12 }, ], rows: attentionRows, }) } return exportToExcel({ sheets }) } /** * 生成诊断报告导出文件名。 */ export async function buildDiagnosticReportFilename(period: string | null): Promise { const t = await getTranslations("diagnostic.exportContent") const safePeriod = (period ?? "report").replace(/[\\/:*?"<>|]/g, "_") const date = formatDateForFile() return t("filename", { period: safePeriod, date }) }