feat(diagnostic): add export, stats service, and confidence utils

- Add export module for diagnostic report data export

- Add stats-service for diagnostic analytics aggregation

- Add confidence-utils for diagnostic confidence score calculations
This commit is contained in:
SpecialX
2026-06-23 17:37:58 +08:00
parent 1abf58c0b6
commit 9ceb2b7b67
12 changed files with 1717 additions and 436 deletions

View File

@@ -0,0 +1,122 @@
import "server-only"
import { exportToExcel } from "@/shared/lib/excel"
import { formatDateForFile } from "@/shared/lib/utils"
import { getDiagnosticReportById } from "./data-access-reports"
import { getStudentMasterySummary, getClassMasterySummary } from "./data-access"
/**
* v3-P2-4: 导出诊断报告为 Excel。
*
* 个人报告 Sheet
* - 概览(学生、周期、综合得分、摘要、强项、弱项、建议)
* - 知识点掌握度明细
*
* 班级报告 Sheet
* - 概览(班级、周期、综合得分、摘要、强项、弱项、建议)
* - 知识点统计(平均掌握度、掌握人数、未掌握人数)
* - 需关注学生列表
*/
export async function exportDiagnosticReportToExcel(params: {
reportId: string
}): Promise<Buffer> {
const report = await getDiagnosticReportById(params.reportId)
if (!report) {
throw new Error("Report not found")
}
const periodLabel = report.period ?? "本期"
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: "学生姓名", value: report.studentName ?? "-" },
{ metric: "报告周期", value: periodLabel },
{ metric: "综合得分", value: overallScore },
{ metric: "报告状态", value: report.status },
{ metric: "生成人", value: report.generatedByName ?? "-" },
{ metric: "生成时间", value: report.createdAt.split("T")[0] },
{ metric: "摘要", value: summary },
{ metric: "强项", value: strengths },
{ metric: "弱项", value: weaknesses },
{ metric: "建议", 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: "报告概览",
columns: [
{ header: "指标", key: "metric", width: 20 },
{ header: "数值", key: "value", width: 60 },
],
rows: overviewRows,
},
{
name: "知识点掌握度",
columns: [
{ header: "知识点", key: "knowledgePoint", width: 28 },
{ header: "掌握度", key: "masteryLevel", width: 12 },
{ header: "总题数", key: "totalQuestions", width: 10 },
{ header: "正确数", key: "correctQuestions", width: 10 },
{ header: "最近评估", key: "lastAssessedAt", width: 14 },
],
rows: masteryRows,
},
],
})
}
// 班级报告reportType === "class"
// 班级报告的 studentId 为 null需要从 period 反查 classId 不现实,
// 这里仅导出报告概览(知识点统计需要 classId但报告本身未存储 classId
// 如需导出班级明细,应通过 generateClassDiagnosticReport 时记录 classId。
const overviewRows = [
{ metric: "报告类型", value: "班级报告" },
{ metric: "报告周期", value: periodLabel },
{ metric: "综合得分", value: overallScore },
{ metric: "报告状态", value: report.status },
{ metric: "生成人", value: report.generatedByName ?? "-" },
{ metric: "生成时间", value: report.createdAt.split("T")[0] },
{ metric: "摘要", value: summary },
{ metric: "强项", value: strengths },
{ metric: "弱项", value: weaknesses },
{ metric: "建议", value: recommendations },
]
return exportToExcel({
sheets: [
{
name: "报告概览",
columns: [
{ header: "指标", key: "metric", width: 20 },
{ header: "数值", key: "value", width: 60 },
],
rows: overviewRows,
},
],
})
}
/**
* 生成诊断报告导出文件名。
*/
export function buildDiagnosticReportFilename(period: string | null): string {
const safePeriod = (period ?? "report").replace(/[\\/:*?"<>|]/g, "_")
return `诊断报告_${safePeriod}_${formatDateForFile()}.xlsx`
}