Files
NextEdu/docs/architecture/audit/archive/diagnostic-audit-report-v2.md

27 KiB
Raw Blame History

学情诊断Diagnostic模块审计报告 v2

审计日期2026-06-25 审计范围:src/modules/diagnostic/**src/app/(dashboard)/{teacher,student,parent}/diagnostic/** 参照规则:.trae/rules/project_rules.mddocs/architecture/004_architecture_impact_map.md §2.22、docs/architecture/005_architecture_data.json 前置文档:


一、现有实现概要

1.1 v1/v4 已完成项回顾

v1 与 v4 审计共完成 25 项改进3 P0 + 5 P1 + 5 P2 + 12 v4-P1涵盖跨模块 WidgetBoundary 提升到 shared、教师页面标题 i18n 化、教师 error.tsx i18n 化、as 断言消除、分享按钮移除、报告内容 i18n 驱动、Excel 导出 i18n 化、班级报告导出明细、角色配置驱动role-config.ts、年级诊断报告纵向切片、热力图键盘导航、DataScope 行级权限、师生关系校验、草稿隔离、通知机制、热力图图例、移动端表格滚动等。

1.2 当前文件分布v2 实测)

文件 行数 职责
类型 types.ts 126 DiagnosticReport / Mastery / Summary 类型定义(含 v4-P2-3 GradeMasterySummary
数据访问 data-access.ts 519 掌握度查询 + 从提交/作业/成绩更新掌握度(含事务)
数据访问 data-access-reports.ts 323 诊断报告 CRUD + DataScope 过滤 + 结构化错误码
统计服务 stats-service.ts 506 14 个纯统计函数(含年级聚合)
Server Actions actions.ts 302 6 个 Action含年级生成 + 通知)
校验 schema.ts 39 5 个 Zod schema
导出 export.ts 175 Excel 导出(含班级明细 3 Sheet
角色配置 role-config.ts 40 角色配置驱动v4-P2-2
组件 components/student-diagnostic-view.tsx 299 学生诊断视图(概览+雷达+强弱项+报告+历史)
组件 components/class-diagnostic-view.tsx 449 班级诊断视图(热力图+筛选+排名+关注列表+生成)
组件 components/report-list.tsx 373 报告列表(过滤+表格+发布/删除/导出)
组件 components/mastery-radar-chart.tsx 85 雷达图封装
组件 components/confidence-utils.ts 31 置信度计算
页面 4 个 page.tsx + 5 个 loading.tsx + 5 个 error.tsx teacher/student/parent 三角色路由
i18n zh-CN/diagnostic.json + en/diagnostic.json 252 / 同步 翻译文件

1.3 数据流

page.tsx (RSC)
  ├─ getStudentMasterySummary / getClassMasterySummary / getGradeMasterySummary / getKnowledgePointStats  (data-access)
  │    └─ db (drizzle) → knowledgePointMastery / knowledgePoints 表
  │    └─ 跨模块 data-accessclasses / users / school / exams / homework / questions
  ├─ getDiagnosticReports (data-access-reports, 含 DataScope 过滤)
  │    └─ db → learningDiagnosticReports 表
  └─ <StudentDiagnosticView> / <ClassDiagnosticView> / <ReportList>  (client)
       └─ generateStudentReportAction / generateClassReportAction / generateGradeReportAction
          / publishReportAction / deleteReportAction / exportDiagnosticReportAction
          / getClassStudentsByKnowledgePointAction

1.4 架构图记录完整性

经核对 004_architecture_impact_map.md §2.22 与 005_architecture_data.json modules.diagnostic 节点,架构图对诊断模块的记录基本完整,已涵盖 v1/v4 全部修复。但本次 v2 审计发现以下偏差需后续同步:

  • 架构图未记录 getDiagnosticReportsgrade_managed scope 未过滤的已知缺陷v2-P1-1
  • 架构图未记录 confidence-utils.ts 的置信度计算逻辑过于简化v2-P1-5
  • 架构图未记录 3 个子路由 error.tsx 仍存在硬编码中文v2-P0-1

二、现存问题与原因分析

2.1 国际化

问题 2.1.1 3 个子路由 error.tsx 硬编码中文P0

  • 位置
  • 现象:三个 error.tsx 客户端组件未使用 useTranslations,全部硬编码中文文案。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
  • 原因v1 审计 P0-3 仅修复了 teacher/diagnostic/error.tsx(教师报告列表页),遗漏了教师子路由和学生/家长子路由的 error.tsx。
  • 后果:英文环境下这三个错误页显示中文,与系统其他已 i18n 化的错误页风格不一致。

问题 2.1.2 i18n 标签与代码逻辑不一致P1

  • 位置
  • 现象i18n 标签显示"弱项(<60%",但代码实际将掌握度 < 80 的知识点都归类为弱项。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"——文本需与逻辑一致。
  • 原因P3-16 修复弱项分类阈值时未同步更新 i18n 标签。
  • 后果:用户看到"弱项(<60%"标签,但实际列表包含 60-79% 的知识点,造成认知混乱。

问题 2.1.3 死 i18n 键未清理P2

  • 位置zh-CN/diagnostic.json#L157-165
  • 现象reportList.sharereportList.shareAriaLabelreportList.shareTitlereportList.shareDescriptionreportList.shareLinkLabelreportList.copyLinkreportList.copyLinkSuccessreportList.copyLinkFailedreportList.shareLinkAriaLabel 共 9 个键仍保留在翻译文件中,但 v1-P1-3 已移除分享按钮,这些键不再被引用。
  • 违反规则:项目规则精神——保持代码与配置一致,避免死代码。
  • 原因:移除分享按钮时未清理对应的 i18n 键。
  • 后果:翻译文件臃肿,维护成本增加;新增语言时需翻译无用的键。

问题 2.1.4 热力图 aria-label 硬编码中文标点格式P1

  • 位置class-diagnostic-view.tsx#L195
  • 现象aria-label={${kp.knowledgePointName}${kp.averageMastery.toFixed(1)}%${levelLabel}${kp.masteredCount}/${kp.totalStudents}} 使用硬编码中文全角冒号""和逗号""。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 原因aria-label 拼接时未使用 i18n 模板。
  • 后果:英文环境下屏幕阅读器读出中文标点,影响无障碍体验。

问题 2.1.5 export.ts 错误消息硬编码英文P2

  • 位置export.ts#L28throw new Error("Report not found")
  • 现象:导出报告不存在时抛出硬编码英文错误。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果:中文环境下用户看到英文错误消息。

2.2 权限与安全

问题 2.2.1 grade_managed DataScope 未过滤P1安全漏洞

  • 位置data-access-reports.ts#L220-238
  • 现象getDiagnosticReports 仅处理 childrenclass_taught 两种 DataScopegrade_managed(年级主任)和 class_members学生scope 不做任何过滤。代码注释明确写道:// grade_managed 需要跨模块查询年级学生,由调用方自行过滤
  • 违反规则:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"。
  • 原因grade_managed scope 需要跨模块查询年级学生 ID通过 getUserIdsByGradeId),实现时为避免跨模块依赖未在 data-access 层完成过滤。
  • 后果年级主任grade_head / teaching_head角色调用时getDiagnosticReports 返回全校所有学生的诊断报告,存在数据越权风险。虽然当前 teacher/diagnostic/page.tsx 在客户端对 class_members 做了二次过滤,但 grade_managed 完全未过滤。

问题 2.2.2 教师报告列表页客户端过滤 class_membersP2

  • 位置teacher/diagnostic/page.tsx#L56-59
  • 现象const visibleReports = ctx.dataScope.type === "class_members" ? reports.reports.filter((r) => r.studentId === ctx.userId) : reports.reports
  • 违反规则:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"——客户端过滤不安全。
  • 原因:教师页面理论上不应被学生角色访问,但代码保留了 class_members 分支作为防御性过滤。这种过滤应在 data-access 层完成。
  • 后果:虽然不影响功能(学生不会访问教师路由),但违背了"数据过滤在 data-access 层"的原则,且 data-access 已返回了不该返回的数据。

2.3 架构解耦

问题 2.3.1 组件直接 import actions无服务接口抽象P1

  • 位置
    • report-list.tsx#L41import { publishReportAction, deleteReportAction, exportDiagnosticReportAction } from "../actions"
    • class-diagnostic-view.tsx#L33import { generateClassReportAction, getClassStudentsByKnowledgePointAction } from "../actions"
  • 现象:客户端组件直接 import 并调用 Server Actions未通过接口抽象或依赖注入。
  • 违反规则:项目规则"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"。
  • 原因v1 审计将此项列为"后续建议"未实施,但用户在本次审计中将其升级为强制要求。
  • 后果:组件无法独立测试(测试时必须 mock 整个 actions 模块);无法在不修改组件代码的情况下替换 actions 实现;组件与 Server Action 实现紧耦合。

2.4 错误处理与边界

问题 2.4.1 无 Error Boundary 包裹独立数据区块P1

  • 位置
    • student-diagnostic-view.tsx:概览卡片、雷达图、强弱项、报告、历史列表均在同一组件内,无 Error Boundary 隔离。
    • class-diagnostic-view.tsx:概览、热力图、筛选、排名、关注列表、生成报告均在同一组件内。
  • 现象:仅页面级有 error.tsx 错误边界,组件内部各数据区块无独立 Error Boundary。
  • 违反规则:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
  • 原因v4-P2 已将 WidgetBoundary 提升到 shared 层并用于页面级包裹,但未下沉到组件内部各数据区块。
  • 后果:雷达图渲染失败会导致整个诊断页面崩溃;热力图数据异常会波及排名表和关注列表。

问题 2.4.2 异步数据无 Suspense + 骨架屏P2

  • 位置:所有页面均使用 await Promise.all 一次性获取所有数据后传给客户端组件。
  • 现象:未使用 React Suspense 流式渲染,数据获取完成前整个页面阻塞。
  • 违反规则:项目规则"异步数据使用 React Suspense + 骨架屏"。
  • 后果:首屏白屏时间长;无法渐进式展示数据区块。

2.5 可测试性与置信度

问题 2.5.1 置信度计算过于简化P1

  • 位置confidence-utils.ts#L18-21
  • 现象getConfidenceLevel 仅判断 overallScore === null 返回 "insufficient",否则一律返回 "high"。注释承认"后续可扩展为基于 totalQuestions 等数据量字段的多级判断",但未实施。
  • 违反规则:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离"——置信度计算虽已提取为纯函数,但逻辑不完整。
  • 原因v4-P3-7 引入置信度时为简化首版,未实现多级判断。
  • 后果:仅做了 1 道题的报告也显示"高置信度",误导教师判断报告可信度。

问题 2.5.2 stats-service 14 个纯函数无单测P2

  • 位置stats-service.ts
  • 现象14 个纯函数(computeAverageMasteryclassifyStrengthsWeaknessesaggregateClassMastery 等)已正确提取为纯函数,但无对应的单元测试文件。
  • 违反规则:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
  • 后果统计数据计算逻辑变更无回归保障P3-16 弱项阈值修复这类边界 case 无法自动验证。

2.6 可复用性与扩展性

问题 2.6.1 雷达图知识点名称截断无 tooltipP2

  • 位置mastery-radar-chart.tsx#L22-26
  • 现象shortName: d.knowledgePoint.length > 8 ? ${d.knowledgePoint.slice(0, 8)}... : d.knowledgePoint 截断超过 8 字符的知识点名称,但未提供 tooltip 显示完整名称。
  • 违反规则:项目规则"明确处理空数据、无权限、网络异常等边界状态"——信息截断是边界状态。
  • 后果:长名称知识点被截断,用户无法查看完整名称,影响数据理解。

问题 2.6.2 通知类型使用 "grade" 而非专用类型P2

  • 位置actions.ts#L157, L173
  • 现象createNotification({ type: "grade", ... }) 诊断报告发布通知复用了成绩通知类型 "grade"。
  • 违反规则:项目规则精神——类型应语义准确,便于分类与过滤。
  • 后果:学生无法区分"成绩通知"和"诊断报告通知";通知筛选功能无法按类型精确过滤诊断报告。

问题 2.6.3 无监控埋点接口P2

  • 位置:整个模块无任何监控/埋点代码。
  • 现象:报告生成、发布、删除、导出等关键操作无埋点。
  • 违反规则:项目规则"监控:方案中预留关键操作埋点接口"。
  • 后果:无法追踪诊断报告的使用情况;无法度量报告生成/发布转化率;异常无法主动发现。

2.7 parent 页面细节

问题 2.7.1 parent/diagnostic/page.tsx noRecordsTitle 与 noRecordsDescription 重复P2

  • 位置parent/diagnostic/page.tsx#L97-98
  • 现象noRecordsTitle={t("parent.noReports")}noRecordsDescription={t("parent.noReports")} 使用同一个 i18n 键。
  • 原因:复制粘贴时未区分标题和描述。
  • 后果空状态时标题和描述显示相同文本UI 不够精细。

三、行业差距对比

对标 PowerSchool、Infinite Campus、Skyward、Alma、智学网、班级小管家、超星学习通、ClassIn 等 K12 系统,本模块在 v1/v4 修复后仍有以下差距:

维度 优秀实践 本模块现状 影响
掌握度时间线 PowerSchool/智学网支持掌握度时间线,展示知识点掌握度随时间的变化趋势,支持按知识点钻取历史 仅展示当前快照,knowledgePointMastery 表无历史版本,lastAssessedAt 仅记录最近一次 教师无法判断学生是否在进步或退步;无法评估教学干预效果
个性化学习路径 Alma/智学网基于弱项推荐具体学习资源(题目、视频、文档),形成学习路径 仅提供练习按钮跳转题目库,无资源推荐算法 推荐不够精准,学生需自行筛选练习内容
学生×知识点掌握度矩阵 Infinite Campus/Alma 提供学生×知识点矩阵热力图,支持点击单元格查看明细 班级视图仅有知识点聚合热力图,无学生维度矩阵 教师无法快速定位"哪个学生在哪个知识点上薄弱"
预测性分析at-risk 预警) PowerSchool/Infinite Campus 基于历史数据预测 at-risk 学生,提前干预 无预测模型,仅基于当前掌握度 <60 判定需关注 无法主动预警,错失早期干预窗口
报告模板自定义 PowerSchool 支持学校自定义报告模板推荐话术、评分区间、logo 报告内容固定由 stats-service 生成,仅 i18n 可切换语言 无法按学校需求定制报告风格
多维度诊断 Infinite Campus 结合成绩+出勤+行为做多维综合诊断 仅基于知识点掌握度单一维度 诊断维度单一,无法反映学生综合学习状态
PDF 导出 所有对标系统支持 PDF 导出(便于打印分发) 仅支持 Excel 导出 无法满足打印分发场景
数据置信度可视化 Alma 在报告上标注数据量与置信度,帮助教师判断结论可靠性 置信度计算过于简化(仅 null/非 null 两级) 教师无法判断报告可信度
流式渲染 现代 K12 系统使用 React Suspense 流式渲染,首屏快速可见 全部 await Promise.all 阻塞渲染 首屏白屏时间长

四、改进优先级建议

P0紧急影响核心规范或安全

编号 问题 改进方向
v2-P0-1 3 个子路由 error.tsx 硬编码中文 接入 useTranslations("diagnostic"),新增对应 i18n 键

P1重要影响安全/解耦/一致性)

编号 问题 改进方向
v2-P1-1 grade_managed DataScope 未过滤 在 data-access-reports.ts 中处理 grade_managed scope调用 getUserIdsByGradeId 过滤
v2-P1-2 i18n 弱项标签与代码逻辑不一致 更新 i18n 标签为"弱项(<80%"
v2-P1-3 热力图 aria-label 硬编码中文标点 使用 i18n 模板键 heatmapCellAriaLabel
v2-P1-4 组件直接 import actions 无接口抽象 定义 DiagnosticService 接口,通过 React Context 注入;组件通过 useDiagnosticService() 获取
v2-P1-5 置信度计算过于简化 基于 totalQuestions 实现多级置信度(<5 insufficient / 5-15 low / 16-30 medium / >30 high
v2-P1-6 无 Error Boundary 包裹独立数据区块 在概览、雷达图、强弱项、报告、历史等区块外包裹 WidgetBoundary

P2增强提升完整性与企业级能力

编号 问题 改进方向
v2-P2-1 死 i18n 键未清理 移除 9 个 share 相关 i18n 键
v2-P2-2 教师页面客户端过滤 class_members 在 data-access 层过滤,移除客户端 filter
v2-P2-3 export.ts 错误消息硬编码 改用 DiagnosticReportError 结构化错误码
v2-P2-4 异步数据无 Suspense 流式渲染 拆分数据获取为独立 async 组件,使用 Suspense 包裹(中长期)
v2-P2-5 雷达图名称截断无 tooltip 为截断的名称添加 Tooltip 显示完整名称
v2-P2-6 通知类型使用 "grade" 新增 "diagnostic" 通知类型(需 notifications 模块配合)
v2-P2-7 无监控埋点接口 定义 DiagnosticMonitor 接口,在关键操作处调用
v2-P2-8 stats-service 无单测 为 14 个纯函数补充单元测试
v2-P2-9 parent noRecordsTitle/Description 重复 区分标题和描述 i18n 键

P3长期需较大投入

编号 问题 改进方向
v2-P3-1 掌握度时间线 新增 knowledgePointMasteryHistory 表记录历史版本,前端展示时间线图表
v2-P3-2 个性化学习路径推荐 基于弱项推荐具体学习资源
v2-P3-3 学生×知识点掌握度矩阵 新增矩阵视图组件
v2-P3-4 预测性分析 基于 historical mastery 训练 at-risk 预测模型
v2-P3-5 报告模板自定义 允许学校配置报告模板
v2-P3-6 PDF 导出 新增 PDF 导出能力

五、架构图同步说明

本次 v2 审计发现架构图需同步以下内容:

004_architecture_impact_map.md §2.22

  1. 已知问题新增
    • 记录 v2-P0-1 三个子路由 error.tsx 硬编码中文及修复
    • 记录 v2-P1-1 grade_managed DataScope 未过滤及修复
    • 记录 v2-P1-4 组件解耦 Context 注入
    • 记录 v2-P1-5 置信度计算改进
    • 记录 v2-P1-6 数据区块 Error Boundary 包裹
  2. 依赖关系更新:标注 diagnostic 模块新增 DiagnosticServiceContext 依赖注入机制

005_architecture_data.json

  1. modules.diagnostic.exports 补充 DiagnosticService 接口、DiagnosticServiceProvideruseDiagnosticService hook
  2. modules.diagnostic.knownIssues 新增 v2 系列问题记录
  3. modules.diagnostic.dependencies 标注 grade_managed scope 现已调用 getUserIdsByGradeId 过滤

六、实施计划

6.1 本次实施范围P0 + P1 + 可快速完成的 P2

本次实施 P0 全部 + P1 全部 + 6 项 P2P3 长期项记录备查不实施。

6.2 重构方案设计(满足强制原则)

完全解耦

定义 TypeScript 接口 DiagnosticService 抽象所有数据依赖:

// src/modules/diagnostic/services/diagnostic-service.ts
export interface DiagnosticService {
  generateStudentReport(studentId: string, period: string): Promise<string>
  generateClassReport(classId: string, period: string): Promise<string>
  generateGradeReport(gradeId: string, period: string): Promise<string>
  publishReport(id: string): Promise<void>
  deleteReport(id: string): Promise<void>
  exportReport(reportId: string): Promise<{ buffer: string; filename: string }>
  getClassStudentsByKp(classId: string, kpId: string, threshold?: number): Promise<...>
}

通过 React Context 注入:

// src/modules/diagnostic/services/diagnostic-service-context.tsx
const DiagnosticServiceContext = createContext<DiagnosticService | null>(null)
export function DiagnosticServiceProvider({ service, children }) { ... }
export function useDiagnosticService(): DiagnosticService { ... }

默认实现绑定现有 Server Actions测试时可注入 mock 实现。

组合优先

  • StudentDiagnosticViewClassDiagnosticViewReportList 通过 children / slots 组合子区块。
  • 逻辑复用提取为 useDiagnosticActions hook内部调用 useDiagnosticService())。

国际化就绪

  • 所有新增文本使用 diagnostic.* 命名空间翻译键。
  • aria-label 模板使用 i18n 占位符:heatmapCellAriaLabel: "{name}{level}%{label}{mastered}/{total}"

最大化复用

  • DiagnosticSection 通用区块组件(含 WidgetBoundary + Suspense + 标题 + 内容 slot
  • useDiagnosticActions hook 统一封装 actions 调用 + toast + router.refresh。

错误与边界处理

  • 每个数据区块外包裹 WidgetBoundary
  • 异步子区块使用 Suspense + 骨架屏(本次仅在页面级流式拆分,组件级 Suspense 列入 P2-4 长期)。

可测试性

  • DiagnosticService 接口清晰,可 mock。
  • confidence-utils 置信度计算改为基于 totalQuestions 的多级判断。

可扩展性

  • 角色差异通过 role-config.ts 配置驱动v4-P2-2 已实现)。
  • 通知类型、监控埋点通过接口预留扩展点。

企业级补充

  • a11y热力图 aria-label i18n 化;雷达图 tooltip。
  • 性能:保持 RSC 获取初始数据。
  • 安全grade_managed scope 在 data-access 层过滤。
  • 监控:定义 DiagnosticMonitor 接口预留埋点。

6.3 翻译文件结构示例

{
  "diagnostic": {
    "error": {
      "classLoadFailed": "班级学情诊断加载失败",
      "classLoadFailedDesc": "抱歉,加载班级诊断数据时发生了意外错误。请稍后重试。",
      "studentLoadFailed": "学生学情诊断加载失败",
      "studentLoadFailedDesc": "抱歉,加载学生诊断数据时发生了意外错误。请稍后重试。",
      "parentLoadFailed": "子女学情诊断加载失败",
      "parentLoadFailedDesc": "抱歉,加载子女诊断数据时发生了意外错误。请稍后重试。",
      "retry": "重试"
    },
    "weaknesses": {
      "title": "弱项(<80%"
    },
    "classDiagnostic": {
      "heatmapCellAriaLabel": "{name}{level}%{label}{mastered}/{total}"
    },
    "reportList": {
      "radarPointTooltip": "{fullName}"
    }
  }
}