# 成绩和学情诊断模块审计报告 v2 > 审查日期:2026-06-22 > 审查范围:在 v1 审计(`grades-diagnostic-audit-report.md`)完成所有 P0/P1/P2 改进项之后,对 `src/modules/grades/**`、`src/modules/diagnostic/**`、相关路由层、i18n、架构图进行二次深度审计 > 审查目的:发现 v1 修复后仍存在的代码质量、架构、类型安全、i18n、a11y、错误处理、性能、业务逻辑问题 --- ## 一、v1 完成情况确认 v1 审计报告所有 P0/P1/P2 改进项(共 16 项)均已真实落地,代码验证通过: | v1 编号 | 改进项 | 验证结果 | |---------|--------|----------| | P0-1 | 权限校验缺失 | ✅ 所有页面均调用 `requirePermission()` | | P0-2 | diagnostic 直查 users 表 | ✅ 已改用 `getUserNamesByIds` | | P0-3 | i18n 完全缺失 | ⚠️ 翻译文件已创建,但组件未接入(见 v2 P1-4) | | P0-4 | `/management/grade/page.tsx` 缺失 | ✅ 已补齐 | | P1-1 | 统计业务逻辑抽取 | ✅ `stats-service.ts` 已创建(305 行,8 个纯函数) | | P1-2 | 重复工具函数 | ✅ `lib/grade-utils.ts` 已创建 | | P1-3 | Zod 校验缺失 | ✅ 12 个 Action 已补齐 | | P1-4 | `as` 断言违规 | ✅ 已修复(但 stats-service.ts 新增 1 处,见 v2 P2-2) | | P1-5 | Error Boundary 和 Suspense | ⚠️ `widget-boundary.tsx` 已创建但未被使用(见 v2 P1-1) | | P1-6 | 架构图同步 | ⚠️ 部分同步,行数和路由仍有不一致(见 v2 P2-10) | | P2-1 | a11y 无障碍 | ⚠️ 部分修复,热力图和表单 Label 仍有问题(见 v2 P1-6、P2-7) | | P2-2 | Tailwind 任意值 | ✅ 已修复 | | P2-3 | studentId 字段语义 | ✅ 已修复(schema + types + data-access + components) | | P2-4 | grade_managed scope | ✅ 已修复(子查询过滤) | | P2-5 | parent/diagnostic 页面 | ✅ 已创建 | | P2-6 | SearchParams 统一 | ⚠️ 部分统一,4 个 student 路由仍自定义(见 v2 P2-8) | --- ## 二、v2 新发现问题 ### 2.1 P1 严重问题 #### P1-1 WidgetBoundary 组件已定义但全项目未被使用 | 位置 | 问题 | 违反规则 | |------|------|----------| | [widget-boundary.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/widget-boundary.tsx) L117 | `WidgetBoundary` 组件已导出(139 行),但全项目无任何 import 语句引用它 | "每个独立的数据区块必须用 React Error Boundary 包裹" | | [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) L696 | 声称"已新增 WidgetBoundary 通用组件",但从未被使用 | 架构文档虚假声明 | **后果**:v1 P1-5 改进项仅创建了组件但未实际应用,Error Boundary + Suspense + Skeleton 三件套未生效,单个 Widget 抛错仍会导致整个页面崩溃。 **改进方向**:在 9 个关键组件中应用 `WidgetBoundary`: - grades:`grade-trend-chart`、`grade-distribution-chart`、`class-comparison-chart`、`subject-comparison-chart`、`grade-stats-card`、`class-grade-report` - diagnostic:`mastery-radar-chart`、`class-diagnostic-view`、`student-diagnostic-view` #### P1-2 admin/school/grades/insights 路由完全缺失 loading.tsx 和 error.tsx | 位置 | 问题 | 违反规则 | |------|------|----------| | `src/app/(dashboard)/admin/school/grades/insights/` | **loading.tsx 和 error.tsx 两者都缺失** | "路由级错误边界和加载态" | **后果**:访问 `/admin/school/grades/insights` 时无骨架屏过渡,运行时错误会导致整页崩溃。 #### P1-3 架构数据 JSON 005 权限记录错误 | 位置 | 问题 | 违反规则 | |------|------|----------| | [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) | `/admin/school/grades` 和 `/admin/school/grades/insights` 权限记录为 `grade:manage`,实际代码使用 `school:manage` | "架构图应准确反映代码实际" | **后果**:架构图与代码不一致,权限审计会得出错误结论。 #### P1-4 grades 和 diagnostic 模块 i18n 完全未接入 | 位置 | 问题 | 违反规则 | |------|------|----------| | `src/modules/grades/components/*`(17 个文件) | 翻译文件 `grades.json` 已存在,但**没有任何组件**导入或调用 `useTranslations`,全部硬编码字符串 | "所有用户可见文本必须适配 i18n" | | `src/modules/diagnostic/components/*`(4 个文件) | 翻译文件 `diagnostic.json` 已存在,但 4 个组件全部硬编码英文字符串 | 同上 | **后果**:v1 P0-3 仅创建了翻译文件但未接入组件,i18n 实际仍未生效。多语言用户无法切换语言。 **改进方向**:21 个组件全部接入 `useTranslations("grades")` 或 `useTranslations("diagnostic")`。 #### P1-5 exportGradesAction 安全漏洞 | 位置 | 问题 | 违反规则 | |------|------|----------| | [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L369-380 | `exportGradesAction` 调用 `exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` 时**未传递 `currentUserId: ctx.userId`** | "Server Action 必须传递用户身份到 data-access 层" | | [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L235-239, L303-307, L333 | `getClassGradeStatsAction`、`getClassRankingAction`、`getGradeRecordByIdAction` 均未将 `ctx.dataScope` 传递给 data-access 函数 | 同上 | **后果**:学生(`class_members` scope)调用 `exportGradesAction` 时,`getGradeRecords` 中的 `if (params.scope.type === "class_members" && params.currentUserId)` 条件不成立,不会按 studentId 过滤,**学生可导出全班成绩**。 #### P1-6 diagnostic 缺少 stats-service.ts | 位置 | 问题 | 违反规则 | |------|------|----------| | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L62-90, L146-219, L222-256 | `getStudentMasterySummary`、`getClassMasterySummary`、`getKnowledgePointStats` 包含大量统计计算逻辑(averageMastery、强弱项分类、KP 聚合) | "严格三层架构,统计计算属业务逻辑层" | | [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L46-81, L84-124 | `generateDiagnosticReport`、`generateClassDiagnosticReport` 包含摘要文本生成、强弱项列表构建逻辑 | 同上 | **后果**:diagnostic 模块未遵循 v1 P1-1 为 grades 模块建立的范例,统计逻辑仍混在 data-access 层,难以单独测试。 **改进方向**:抽取 `diagnostic/stats-service.ts`,包含 `classifyStrengthsWeaknesses`、`computeKpStats`、`computeStudentAverage` 等纯函数。 #### P1-7 热力图色块缺少 a11y 支持 | 位置 | 问题 | 违反规则 | |------|------|----------| | [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L128-139 | 热力图色块仅靠 `title` 属性,无 `role="img"` 和 `aria-label`,颜色编码语义无法被辅助技术感知 | "可访问性:ARIA 属性" | **后果**:屏幕阅读器用户无法识别热力图色块的颜色等级含义(绿/黄/橙/红代表掌握度等级)。 #### P1-8 getKnowledgePointStats() 无参调用导致班级平均对比功能失效 | 位置 | 问题 | 违反规则 | |------|------|----------| | [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) L35 | 调用 `getKnowledgePointStats()`(无参数) | "函数调用应正确传参" | | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L222-256 | `getKnowledgePointStats(classId?, gradeId?)` 当两参都为 `undefined` 时,`studentIds` 为 `[]`,直接返回空数组 | 同上 | **后果**:`classStats` 恒为 `[]`,`classAverageMastery` 恒为 `[]`,雷达图中班级平均对比曲线**永不显示**。架构文档标注的"班级平均对比"功能完全失效。 **改进方向**:页面应先查询学生所属班级,再调用 `getKnowledgePointStats(classId)`。 #### P1-9 updateMasteryFromSubmission 覆盖而非累积掌握度 | 位置 | 问题 | 违反规则 | |------|------|----------| | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L93-143 | `onDuplicateKeyUpdate` 将 `totalQuestions`/`correctQuestions`/`masteryLevel` 设为**本次提交的值**,而非累积 | "掌握度应反映学习轨迹" | **后果**:学生上次考 10 题 8 对(mastery=80%),本次考 1 题 1 对(mastery=100%),更新后 mastery 变为 100% 而非累积的 81.8%。掌握度随单次考试剧烈波动,无法反映真实学习轨迹。 **改进方向**:读取已有记录,将 `totalQuestions`/`correctQuestions` 累加后再计算,或采用加权/衰减算法。 ### 2.2 P2 中等问题 #### P2-1 5 个 grades 路由和 1 个 diagnostic 路由缺失 error.tsx | 位置 | 问题 | |------|------| | `src/app/(dashboard)/management/grade/classes/` | 缺失 error.tsx | | `src/app/(dashboard)/management/grade/insights/` | 缺失 error.tsx | | `src/app/(dashboard)/parent/grades/` | 缺失 error.tsx | | `src/app/(dashboard)/student/grades/` | 缺失 error.tsx | | `src/app/(dashboard)/student/diagnostic/` | 缺失 error.tsx | #### P2-2 lib/grade-utils.ts 跨模块直接查询 classes 表 | 位置 | 问题 | 违反规则 | |------|------|----------| | [lib/grade-utils.ts](file:///e:/Desktop/CICD/src/modules/grades/lib/grade-utils.ts) L6, L48-50 | 直接导入并查询 `classes` 表:`db.select({ id: classes.id }).from(classes).where(...)` | "modules 之间通过对方 data-access 通信" | **改进方向**:在 `classes/data-access.ts` 新增 `getClassIdsByGradeIds(gradeIds: string[])` 函数并调用。 #### P2-3 死代码清理 | 位置 | 问题 | |------|------| | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L93 | `updateMasteryFromSubmission` 全局零调用(架构文档标注"待扩展") | | [diagnostic/actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) L133, L154 | `getDiagnosticReportsAction` 和 `getDiagnosticReportByIdAction` 全局零调用,页面直接调用 data-access | **改进方向**:要么删除死代码,要么让页面改为通过 Action 调用(统一权限校验入口)。本报告选择后者,保留 Action 并让页面使用。 #### P2-4 totalStudents 语义错误和班级平均掌握度计算偏差 | 位置 | 问题 | 违反规则 | |------|------|----------| | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L201, L255 | `totalStudents: students.length` 是班级总人数,但 `masteredCount + notMasteredCount` 仅统计有掌握度记录的学生,数据自相矛盾 | "数据模型应语义清晰" | | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L204-205 | `averageMastery` 按记录数而非学生数平均,偏向多 KP 记录的学生 | 同上 | **改进方向**:`totalStudents` 改为实际有掌握度记录的学生数(`levels.length`);`averageMastery` 先算每个学生的个人平均,再对学生平均取平均。 #### P2-5 多 upsert 无事务包裹 | 位置 | 问题 | 违反规则 | |------|------|----------| | [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L119-141 | `Promise.all(Array.from(kpStats.entries()).map(... db.insert(...).onDuplicateKeyUpdate(...)))` 并行执行多个 upsert,无事务包裹 | "多写操作应保证原子性" | **后果**:部分成功部分失败时,掌握度数据将处于不一致状态。 #### P2-6 生成报告未校验掌握度数据 | 位置 | 问题 | 违反规则 | |------|------|----------| | [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L46-81, L84-124 | `generateDiagnosticReport` 只检查 `summary` 是否为 null,不检查 `totalKnowledgePoints === 0` | "应处理空数据边界" | **后果**:学生存在但无任何掌握度数据时,会生成 `overallScore: 0%`、`strengths: []`、`weaknesses: []` 的误导性报告。 #### P2-7 表单 Label 未关联控件 | 位置 | 问题 | |------|------| | [batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L277, L293, L319, L334 | Class、Subject、Type、Semester 的 `