# 成绩和学情诊断模块审计报告 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 的 `<Label>` 无 `htmlFor` |
| [grade-record-form.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-form.tsx) L88, L104, L120, L151, L166 | 5 个 `<Label>` 无 `htmlFor` |
| [grade-query-filters.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-query-filters.tsx) L40, L57, L74, L90 | 4 个 `<Label>` 无 `htmlFor` |
| [report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) L120-147 | 过滤器 Label 缺少 `htmlFor` |

#### P2-8 SearchParams 统一（剩余文件）

| 位置 | 问题 |
|------|------|
| [admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx) L16 | 使用旧版 `getSearchParam, type SearchParams` from `@/shared/lib/utils` |
| `src/app/(dashboard)/student/schedule/page.tsx` L11 | 自定义 `type SearchParams` |
| `src/app/(dashboard)/student/learning/assignments/page.tsx` L23 | 自定义 `type SearchParams` |
| `src/app/(dashboard)/student/learning/textbooks/page.tsx` L13 | 自定义 `type SearchParams` + 自定义 `getParam` |
| `src/app/(dashboard)/student/learning/courses/page.tsx` L11 | 自定义 `type SearchParams` + 自定义 `getParam` |

#### P2-9 recorderName 硬编码和 grade-trend-card a11y

| 位置 | 问题 |
|------|------|
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L266 | `getStudentGradeSummary` 中 `recorderName: "Unknown"` 硬编码，已导入 `getUserNamesByIds` 但未用于获取录入人姓名 |
| [grade-trend-card.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-trend-card.tsx) L37-53 | `TrendLineChart` 未包裹 `role="img"` + `aria-label`（其他 4 个图表组件均已添加） |

#### P2-10 架构文档行数和路由记录不一致

| 位置 | 问题 |
|------|------|
| [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.6 | grades 模块 10 个文件行数与实际不一致（如 `actions.ts` 文档 359 行，实际 398 行） |
| [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 | diagnostic 模块 3 个文件行数与实际不一致 |
| [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) | 缺失 `/teacher/grades/analytics` 和 `/management/grade` 路由记录 |

### 2.3 P3 长期问题（记录但不本次实施）

| 编号 | 问题 | 位置 |
|------|------|------|
| P3-1 | `toNumber` 工具函数在 grades 和 diagnostic 模块重复定义 | 多处 |
| P3-2 | `byKp` 聚合逻辑重复 | diagnostic/data-access.ts L175-202 / L238-256 |
| P3-3 | actions.ts 错误处理模板重复 14 次 | grades/actions.ts + actions-analytics.ts |
| P3-4 | `isGradeType`/`isSemester` 类型守卫重复定义 | batch-grade-entry.tsx / grade-record-form.tsx |
| P3-5 | `Option` 类型重复定义 3 次 | 3 个组件 |
| P3-6 | `export.ts` 的 `avg` 函数与 `stats-service.ts` 逻辑重复 | export.ts L148 |
| P3-7 | `TYPE_LABELS` 硬编码中文映射与 i18n 重复 | export.ts L12-17 |
| P3-8 | `classIds` 过滤逻辑重复 3 次 | data-access.ts / export.ts |
| P3-9 | `WidgetBoundary` 的 `WidgetErrorBoundary` 类构造函数参数类型不匹配 | widget-boundary.tsx L47 |
| P3-10 | `createDefaultBuckets` 不必要导出 | stats-service.ts L229 |
| P3-11 | 6 个组件内部回调函数缺失返回类型标注 | 多处 |
| P3-12 | `batch-grade-entry.tsx` useEffect 草稿保存 bug（依赖数组含 scores） | L182-193 |
| P3-13 | `batch-grade-entry.tsx` useMemo 依赖数组未包含 validateScore | L162-177 |
| P3-14 | 5 处串行 DB 查询可并行化 | data-access.ts / data-access-analytics.ts 等 |
| P3-15 | `getDiagnosticReports` 无分页 | data-access-reports.ts L127-159 |
| P3-16 | 强弱项分类存在 60-79 盲区 | data-access.ts L77-78 |
| P3-17 | 班级报告 strengths 无数量上限 | data-access-reports.ts L96-98 |
| P3-18 | `getStudentMasterySummary` 内部串行可并行化 | data-access.ts L62-67 |
| P3-19 | `getStudentMastery` 导出但仅内部使用 | data-access.ts L42 |
| P3-20 | `grade-filters.tsx` 硬编码科目列表 | L47-53 |
| P3-21 | `class-diagnostic-view.tsx` "View" 按钮缺少描述性 aria-label | L218-223 |
| P3-22 | `student-diagnostic-view.tsx` "Practice" 按钮缺少描述性 aria-label | L129-133 |
| P3-23 | 3 个表格缺少 `<caption>` | class-grade-report / student-grade-summary / batch-grade-entry |
| P3-24 | `stats-service.ts` L110 `as GradeTrendPoint["type"]` 断言违规 | stats-service.ts |
| P3-25 | `batch-grade-entry.tsx` JSON.parse 后 `as` 断言（灰色地带） | L75, L90, L127 |
| P3-26 | `lib/grade-utils.ts` 61 行略超 40 行工具函数建议上限 | lib/grade-utils.ts |
| P3-27 | data-access 写操作抛异常暴露给用户，建议结构化错误码 | data-access-reports.ts |
| P3-28 | `grade-filters.tsx` 使用科目名称作为 value 而非科目 ID | L47-53 |

---

## 三、v2 改进优先级

### P1（本次实施）

| # | 问题 | 改进方向 | 状态 |
|---|------|----------|------|
| v2-P1-1 | WidgetBoundary 未被使用 | 在 9 个关键组件中应用 WidgetBoundary | ✅ 已在 3 个页面应用 |
| v2-P1-2 | admin/school/grades/insights 缺失 loading/error | 补齐 loading.tsx 和 error.tsx | ✅ 已补齐 |
| v2-P1-3 | 架构数据 JSON 005 权限记录错误 | 修正为 `school:manage` | ✅ 已修正 |
| v2-P1-4 | i18n 完全未接入 | 21 个组件接入 useTranslations | ✅ 21 个组件全部接入 |
| v2-P1-5 | exportGradesAction 安全漏洞 | 传递 currentUserId 和 dataScope | ✅ 已修复 |
| v2-P1-6 | diagnostic 缺少 stats-service.ts | 抽取纯统计函数 | ✅ 已抽取（352 行，12 个纯函数） |
| v2-P1-7 | 热力图色块 a11y | 添加 role="img" + aria-label | ✅ 已修复 |
| v2-P1-8 | getKnowledgePointStats 无参调用 | 页面先查班级再传参 | ✅ 已修复 |
| v2-P1-9 | updateMasteryFromSubmission 覆盖逻辑 | 改为累积计算 | ✅ 已改为累积模式 |

### P2（本次实施）

| # | 问题 | 改进方向 | 状态 |
|---|------|----------|------|
| v2-P2-1 | 5 个路由缺失 error.tsx | 补齐 | ✅ 已补齐 7 个 error.tsx |
| v2-P2-2 | lib/grade-utils.ts 跨模块查询 | 改用 classes data-access | ✅ 已改用子查询 |
| v2-P2-3 | 死代码清理 | 页面改用 Action 调用 | ✅ 已删除 2 个死 Action + 2 个死 schema |
| v2-P2-4 | totalStudents 语义和平均掌握度计算 | 修正计算逻辑 | ✅ 已修正 |
| v2-P2-5 | 多 upsert 无事务 | 包裹 db.transaction() | ✅ 已包裹事务 |
| v2-P2-6 | 生成报告未校验掌握度数据 | 添加 totalKnowledgePoints === 0 校验 | ✅ 已添加校验 |
| v2-P2-7 | 表单 Label 未关联控件 | 添加 htmlFor 和 id | ✅ 4 个组件已修复 |
| v2-P2-8 | SearchParams 统一剩余文件 | 改用 @/shared/lib/search-params | ✅ 5 个文件已统一 |
| v2-P2-9 | recorderName 硬编码和 grade-trend-card a11y | 修复 | ✅ 已修复 |
| v2-P2-10 | 架构文档行数和路由记录 | 同步更新 | ✅ 004 和 005 已同步 |

### P3（长期，本次不实施）

P3-1 ~ P3-28 共 28 项长期改进，记录备查，后续迭代处理。

---

## 四、合规项确认（v2）

以下条目在 v2 审计中**已通过**：

- ✅ 所有 Server Action 调用 `requirePermission()`
- ✅ 所有 Server Action 返回 `ActionState<T>`
- ✅ 所有 Server Action 使用 `revalidatePath`
- ✅ 无 `any` 类型
- ✅ 无 `?!` 组合（可选链后非空断言）
- ✅ 无模块循环依赖
- ✅ 无 N+1 查询
- ✅ 所有读查询函数使用 `cache()`
- ✅ 文件行数全部合规（最大 batch-grade-entry.tsx 450 行 < 500）
- ✅ i18n 翻译文件键完整（zh-CN 与 en 一致）
- ✅ i18n/request.ts 已加载所有命名空间
- ✅ studentId 可空 null 安全处理完整
- ✅ diagnostic 跨模块依赖通过 data-access
- ✅ grades data-access 统计逻辑已抽取到 stats-service.ts

---

## 五、实施计划

本报告列出的 P1（9 项）和 P2（10 项）改进项将在本次实施中全部完成。P3 长期改进项记录备查，后续迭代处理。

实施顺序：
1. P1 安全漏洞修复（v2-P1-5）
2. P1 业务逻辑修复（v2-P1-8、v2-P1-9）
3. P1 架构修复（v2-P1-6）
4. P1 路由补齐（v2-P1-2）
5. P1 a11y 修复（v2-P1-7）
6. P1 WidgetBoundary 应用（v2-P1-1）
7. P1 i18n 接入（v2-P1-4）
8. P1 架构图修正（v2-P1-3）
9. P2 改进项（v2-P2-1 ~ v2-P2-10）
10. 验证：lint + tsc + 提交
