Files
NextEdu/docs/architecture/audit/grades-diagnostic-audit-report.md
SpecialX 5f3a1a4662 refactor(grades,diagnostic): 完成成绩和学情诊断模块审计 P1+P2 改进项
P1-1: 抽取 stats-service.ts,将 8 个统计计算纯函数从 data-access 层分离
P1-5: 创建 WidgetBoundary 组件 + 补齐 teacher 路由 loading.tsx/error.tsx (14 文件)
P1-6: 同步架构图文档 004/005,新增 stats-service 与 widget-boundary 节点
P2-1: 补充 a11y ARIA 属性(5 图表 role=img + aria-label,2 表格 caption,3 列表 role=list,3 按钮 aria-label)
P2-3: 修复班级报告 studentId 字段语义错误(schema 改为可空 + 迁移 + 代码适配)
P2-4: 修复 grade_managed scope 返回空数据(改为子查询 classes 表按 gradeId 过滤)
P2-5: 新增 /parent/diagnostic/ 页面(多子女学情诊断聚合 + loading + error)
P2-6: 统一 SearchParams 工具(student/grades 和 management/grade/insights 改用 @/shared/lib/search-params)
2026-06-22 17:07:32 +08:00

673 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 成绩和学情诊断模块审计报告
> 审查日期2026-06-22
> 审查范围:`src/modules/grades/**`(成绩模块)、`src/modules/diagnostic/**`(学情诊断模块)、`src/app/(dashboard)/{admin,teacher,student,parent}/grades/**`、`src/app/(dashboard)/{teacher,student}/diagnostic/**`、`src/app/(dashboard)/management/grade/**`、相关 i18n 翻译文件
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.6grades、§2.22diagnostic、`docs/architecture/005_architecture_data.json` L7362grades、L10927diagnostic
---
## 一、现有实现概要
### 1.1 文件分布
#### grades 模块(成绩分析)
| 层 | 路径 | 文件数 | 行数 | 说明 |
|----|------|--------|------|------|
| Actions | `src/modules/grades/actions.ts` | 1 | 312 | 10 个 Server ActionCRUD + 查询 + 导出) |
| Actions | `src/modules/grades/actions-analytics.ts` | 1 | 133 | 5 个分析 Server Action趋势/对比/分布/排名) |
| Data-access | `src/modules/grades/data-access.ts` | 1 | 433 | 成绩 CRUD + 统计(含统计业务逻辑) |
| Data-access | `src/modules/grades/data-access-analytics.ts` | 1 | 337 | 趋势/对比/分布分析(含统计业务逻辑) |
| Data-access | `src/modules/grades/data-access-ranking.ts` | 1 | 119 | 排名查询(含 normalize 逻辑) |
| Export | `src/modules/grades/export.ts` | 1 | 200 | Excel 导出(明细 + 班级汇总) |
| Schema | `src/modules/grades/schema.ts` | 1 | 52 | 4 个 Zod schema |
| Types | `src/modules/grades/types.ts` | 1 | 186 | 14 个类型定义 |
| Components | `src/modules/grades/components/*` | 16 | 41~442 | 16 个组件(含 batch-grade-entry 442 行) |
#### diagnostic 模块(学情诊断)
| 层 | 路径 | 文件数 | 行数 | 说明 |
|----|------|--------|------|------|
| Actions | `src/modules/diagnostic/actions.ts` | 1 | 172 | 6 个 Server Action生成/发布/删除/查询) |
| Data-access | `src/modules/diagnostic/data-access.ts` | 1 | 257 | 知识点掌握度查询 + 更新 |
| Data-access | `src/modules/diagnostic/data-access-reports.ts` | 1 | 203 | 诊断报告 CRUD**直查 users 表** |
| Schema | `src/modules/diagnostic/schema.ts` | 1 | 48 | 6 个 Zod schema |
| Types | `src/modules/diagnostic/types.ts` | 1 | 97 | 11 个类型定义 |
| Components | `src/modules/diagnostic/components/*` | 4 | 69~267 | 4 个组件(含 class-diagnostic-view 267 行) |
#### 路由层
| 角色 | 路由 | 文件数 | 说明 |
|------|------|--------|------|
| admin | `/admin/school/grades/``/admin/school/grades/insights/` | 4 | 含 loading.tsx + error.tsx |
| teacher | `/teacher/grades/``/teacher/grades/analytics/``/teacher/grades/entry/``/teacher/grades/stats/` | 4 | **无 loading.tsx / error.tsx** |
| teacher | `/teacher/diagnostic/``/teacher/diagnostic/class/[classId]/``/teacher/diagnostic/student/[studentId]/` | 3 | **无 loading.tsx / error.tsx** |
| student | `/student/grades/``/student/diagnostic/` | 4 | 含 loading.tsx**无 error.tsx** |
| parent | `/parent/grades/` | 2 | 含 loading.tsx**无 error.tsx** |
| management | `/management/grade/``/management/grade/classes/``/management/grade/insights/` | 5 | **`/management/grade/page.tsx` 缺失**(孤儿 loading/error |
### 1.2 主要数据流
```
[成绩录入] teacher/grades/entry
└─▶ grades/actions.batchCreateGradeRecordsAction
├─▶ requirePermission(GRADE_RECORD_MANAGE)
└─▶ data-access.batchCreateGradeRecords → db.insert(gradeRecords)
[成绩查询] teacher/grades / student/grades / parent/grades
└─▶ grades/actions.getGradeRecordsAction
├─▶ requirePermission(GRADE_RECORD_READ)
├─▶ data-access.getGradeRecords含 scope 行级过滤)
└─▶ 跨模块classes/school/users data-access
[成绩分析] teacher/grades/analytics
└─▶ grades/actions-analytics.getGradeTrendAction / getClassComparisonAction / ...
├─▶ requirePermission(GRADE_RECORD_READ)
└─▶ data-access-analytics含统计计算逻辑
[学情诊断-学生] teacher/diagnostic/student/[id] / student/diagnostic
└─▶ diagnostic/data-access.getStudentMasterySummary
└─▶ 跨模块users data-accessgetUserNamesByIds
[学情诊断-班级] teacher/diagnostic/class/[id]
└─▶ diagnostic/data-access.getClassMasterySummary
└─▶ 跨模块classes/exams/questions/users data-access
[诊断报告生成] teacher/diagnostic
└─▶ diagnostic/actions.generateStudentReportAction / generateClassReportAction
├─▶ requirePermission(DIAGNOSTIC_MANAGE)
└─▶ data-access-reports.createDiagnosticReport
└─▶ ⚠️ 直查 users 表(违反三层架构)
```
### 1.3 架构图记录情况
`004_architecture_impact_map.md` §2.6grades和 §2.22diagnostic已记录两个模块的导出函数、依赖关系、已知问题和文件清单。架构图信息基本完整但存在以下遗漏
- **grades 模块行数过时**:架构图 L681 标注 `data-access.ts` 419 行(实际 433 行、L682 `data-access-analytics.ts` 293 行(实际 337 行)
- **diagnostic 模块 deps 过时**`005_architecture_data.json` L10922/L10937-10941/L10954-10958/L10972-10975 仍记录 diagnostic 直查对方表,实际代码已通过 data-access 接口访问P1-1 已修复但文档未同步)
- **diagnostic `data-access-reports.ts` 直查 users 表未记录**:架构图未标注此违规
- **grades 模块 actions-analytics.ts 的 5 个 Action 未完整列入 exports 清单**
- **`/management/grade/page.tsx` 缺失**未在路由清单中标注
- **teacher 端 grades/diagnostic 路由普遍缺少 loading.tsx/error.tsx** 未标注
---
## 二、现存问题与原因分析
### 2.1 安全性权限校验缺失或不一致P0
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [teacher/grades/entry/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/entry/page.tsx) | **无任何权限校验**(既无 `requirePermission` 也无 `getAuthContext` | "所有 Server Action 必须调用 `requirePermission()` 进行权限校验" |
| [teacher/grades/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/stats/page.tsx) | **无任何权限校验** | 同上 |
| [teacher/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 |
| [teacher/grades/analytics/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/analytics/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 |
| [teacher/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(DIAGNOSTIC_READ)` | 同上 |
| [teacher/diagnostic/class/[classId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(DIAGNOSTIC_READ)` | 同上 |
| [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(DIAGNOSTIC_READ)` | 同上 |
| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 |
| [student/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/diagnostic/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(DIAGNOSTIC_READ)` | 同上 |
| [parent/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/grades/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(GRADE_RECORD_READ)` | 同上 |
**后果**:成绩录入页面(`/teacher/grades/entry`)和成绩统计页面(`/teacher/grades/stats`)完全无权限校验,依赖路由中间件做粗粒度角色路由。若中间件配置错误或绕过,任意已登录用户可访问成绩录入页面并调用 `batchCreateGradeRecordsAction`(虽然 Action 层有 `requirePermission`,但页面层缺少二次校验不符合"Server Action 二次校验"要求)。
### 2.2 架构分层:跨模块直接查询 users 表P0
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L8 | `import { learningDiagnosticReports, users } from "@/shared/db/schema"` | "modules/ 之间通过对方 data-access 通信,不直接查询对方 DB 表" |
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L137-140 | `getDiagnosticReports` 直接 `leftJoin(users, ...)` 查询学生姓名 | 同上 |
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L149-153 | 直接 `db.select({ id: users.id, name: users.name }).from(users)` 查询生成者姓名 | 同上 |
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L168-170 | `getDiagnosticReportById` 直接 `leftJoin(users, ...)` | 同上 |
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L177-182 | 直接 `db.select({ name: users.name }).from(users)` | 同上 |
**后果**`diagnostic` 模块绕过 `users` 模块的 data-access 层直接查询 `users` 表,破坏模块封装性。`users` 表 schema 变更将直接影响 diagnostic 模块。同模块的 `data-access.ts` 已正确通过 `getUserNamesByIds` 访问,但 `data-access-reports.ts` 却绕过,存在不一致。
### 2.3 架构分层:统计业务逻辑混入 data-accessP1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L217-270 | `getClassGradeStats` 包含 average/median/max/min/variance/stdDev/passRate/excellentRate 计算53 行统计逻辑) | "严格三层架构,依赖方向单向" — 统计计算属业务逻辑层 |
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L272-337 | `getStudentGradeSummary` 包含 averageScore 计算 | 同上 |
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L339-373 | `getClassRanking` 包含 rank 计算 | 同上 |
| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L59-119 | `getGradeTrend` 包含 normalized/avg 计算 | 同上 |
| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L128-218 | `getClassComparison` 包含 normalized/median/avg/passCount/excellentCount 计算90 行) | 同上 |
| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L226-289 | `getSubjectComparison` 包含 median/avg/passRate/excellentRate 计算 | 同上 |
| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L299-336 | `getGradeDistribution` 包含 bucket 分类逻辑 | 同上 |
| [grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L31-118 | `getRankingTrend` 包含 normalize/rank 计算逻辑 | 同上 |
**后果**data-access 层职责混乱,既负责数据读取又负责业务计算,难以单独测试统计逻辑。架构图 L671 已标记此 P2 问题。应抽取到独立的 `stats-service.ts`(参考 homework 模块的 `stats-service.ts` 范例)。
### 2.4 重复代码工具函数多处重复P1
| 重复函数 | 出现位置 | 违反规则 |
|----------|----------|----------|
| `buildScopeClassFilter` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L57-75、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L34-48 | "工具函数:建议 ≤ 40 行" + DRY 原则 |
| `toNumber` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L34-37、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L24-27、[grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L16-19 | 同上 |
| `normalize` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts)、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L29-32、[grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L21-24 | 同上 |
**后果**3 个文件重复实现相同工具函数,修改时需同步多处,易遗漏导致行为不一致。应抽取到 `grades/lib/stats-utils.ts``shared/lib/grade-utils.ts`
### 2.5 国际化完全缺失P0
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `src/modules/grades/components/*`16 个文件) | 全部使用硬编码英文字符串0 处 `useTranslations` 调用 | "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键" |
| `src/modules/diagnostic/components/*`4 个文件) | 全部使用硬编码英文字符串0 处 `useTranslations` 调用 | 同上 |
| [grades/export.ts](file:///e:/Desktop/CICD/src/modules/grades/export.ts) L12-17, L54-61, L68-80, L287, L295 | Excel 导出表头、指标名、文件名硬编码中文 | 同上 |
| `src/shared/i18n/messages/{zh-CN,en}/` | **不存在 `grades.json` 和 `diagnostic.json` 翻译文件** | 同上 |
| [i18n/request.ts](file:///e:/Desktop/CICD/src/i18n/request.ts) L22-28 | 仅加载 5 个命名空间common/auth/onboarding/classes/errors未加载 grades/diagnostic | 同上 |
| `src/modules/grade-management/components/*`7 个文件12 处) | 调用 `useTranslations("grade")``grade.json` 翻译文件不存在,**运行时会报 `MISSING_MESSAGE` 错误** | 同上 |
**后果**
1. grades 和 diagnostic 模块完全无法国际化,所有用户可见文本固定为英文(部分中文混合),无法支持多语言。
2. grade-management 模块(年级管理,与成绩模块不同)调用未加载的 `grade` 命名空间,访问 `/management/grade/``/admin/school/grades/insights` 等页面会因找不到翻译键而**运行时报错**。
### 2.6 前端规范Error Boundary 和 Suspense 缺失P1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `src/modules/grades/components/*`16 个文件) | 全部无 Error Boundary | "每个独立的数据区块必须用 React Error Boundary 包裹" |
| `src/modules/diagnostic/components/*`4 个文件) | 全部无 Error Boundary | 同上 |
| `src/modules/grades/components/*`16 个文件) | 全部无 Suspense + 骨架屏 | "异步数据使用 React Suspense + 骨架屏" |
| `src/modules/diagnostic/components/*`4 个文件) | 全部无 Suspense + 骨架屏 | 同上 |
| `src/app/(dashboard)/teacher/grades/` | **无 loading.tsx / error.tsx** | 路由级错误边界和加载态缺失 |
| `src/app/(dashboard)/teacher/diagnostic/` | **无 loading.tsx / error.tsx** | 同上 |
**后果**:单个组件抛错会导致整个页面崩溃;异步加载无骨架屏过渡,用户体验差(白屏等待)。
### 2.7 前端规范a11y 无障碍缺失P2
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `src/modules/grades/components/*`15/16 个文件) | 无 ARIA 属性(仅 batch-grade-entry.tsx 有 `aria-hidden``aria-invalid` | "可访问性a11y语义化标签、ARIA 属性、键盘导航" |
| `src/modules/diagnostic/components/*`4 个文件) | 无 ARIA 属性 | 同上 |
| [grades/components/grade-record-list.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-list.tsx) L93-100 | 删除按钮无 `aria-label` | 同上 |
| [diagnostic/components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L128-139 | 热力图色块仅靠 `title` 属性,无 `role="img"``aria-label` | 同上 |
| [diagnostic/components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) L38-66 | 雷达图无 `aria-label` / `role="img"` 描述 | 同上 |
| [diagnostic/components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) L192-200, L202-210 | 发布/删除按钮仅 `title`,无 `aria-label` | 同上 |
**后果**:屏幕阅读器用户无法识别图表内容、按钮用途,不符合 WCAG 2.1 AA 标准。
### 2.8 TypeScript 规范:`as` 断言违规P1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [grades/components/batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L221 | `remark: undefined as string \| undefined` | "禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)" |
| [grades/components/batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L312 | `setType(v as typeof type)` | 同上 |
| [grades/components/grade-record-form.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-form.tsx) L142 | `setType(v as typeof type)` | 同上 |
| [grades/components/grade-distribution-chart.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-distribution-chart.tsx) L66-67 | `payload as { payload?: {...} }`(从 unknown 转换但未使用类型守卫) | 同上 |
**后果**`as` 断言绕过 TypeScript 类型检查,可能隐藏运行时类型错误。应使用类型守卫或 Zod 运行时校验。
### 2.9 Tailwind 规范任意值违规P2
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [diagnostic/components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L255 | `className="w-[180px]"` | "禁止使用任意值(`w-[137px]`),除非有充分理由并注释" |
| [diagnostic/components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) L45 | `className="mx-auto h-[360px] w-full max-w-[520px]"` | 同上 |
**后果**:绕过设计令牌系统,无法统一调整尺寸主题。
### 2.10 数据模型缺陷:班级报告 studentId 字段语义错误P2
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L111-114 | 班级报告 `studentId: generatedBy` 将生成者 ID 写入 studentId 字段 | "数据模型设计应语义清晰" |
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L111 | 同上 | 同上 |
**后果**`report-list.tsx` L178 显示 `r.studentName` 时,班级报告会显示生成者(教师)姓名而非学生姓名,存在数据语义错误。架构图 L1245 已标记此 P2 问题。
### 2.11 Server Action 规范Zod 校验缺失P1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L154-170 | `deleteGradeRecordAction` 无 Zod 校验(仅 id 字符串) | "输入使用 Zod 验证,验证失败返回结构化错误" |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L171-190 | `getGradeRecordsAction` 无 Zod 校验(使用 `GradeQueryParams` 类型) | 同上 |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L191-208 | `getClassGradeStatsAction` 无 Zod 校验 | 同上 |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L209-232 | `getStudentGradeSummaryAction` 无 Zod 校验 | 同上 |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L233-250 | `getClassRankingAction` 无 Zod 校验 | 同上 |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L251-269 | `getGradeRecordByIdAction` 无 Zod 校验 | 同上 |
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L270-312 | `exportGradesAction` 无 Zod 校验params 为内联对象类型) | 同上 |
| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L26-45 | `getGradeTrendAction` 无 Zod 校验 | 同上 |
| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L46-64 | `getClassComparisonAction` 无 Zod 校验 | 同上 |
| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L65-83 | `getSubjectComparisonAction` 无 Zod 校验 | 同上 |
| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L84-103 | `getGradeDistributionAction` 无 Zod 校验 | 同上 |
| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L104-133 | `getRankingTrendAction` 无 Zod 校验 | 同上 |
**后果**12 个 Action 缺失 Zod 校验,客户端可传入任意类型参数,可能导致运行时错误或 SQL 注入风险。diagnostic 模块的 6 个 Action 全部使用 Zod 校验,是标杆范例。
### 2.12 业务逻辑漏洞grade_managed scope 返回空数据P2
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L62-64 | `grade_managed` scope 返回 `sql\`1=0\``(始终无数据) | "权限过滤应正确反映角色数据范围" |
| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L39 | 同上 | 同上 |
**后果**年级管理员grade_managed scope无法查看任何成绩数据可能是业务逻辑漏洞。年级管理员应能查看所管年级的所有班级成绩。
### 2.13 路由缺陷page.tsx 缺失P1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `src/app/(dashboard)/management/grade/page.tsx` | **文件缺失**,但有 loading.tsx/error.tsx 孤儿文件 | "路由页面应完整" |
**后果**:访问 `/management/grade` 会 404但 loading.tsx 和 error.tsx 仍存在,造成混乱。
### 2.14 角色覆盖不一致admin/parent 无 diagnostic UIP2
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `005_architecture_data.json` L174-175, L214-215 | admin 和 parent 都有 `DIAGNOSTIC_MANAGE`/`DIAGNOSTIC_READ` 权限 | "权限点应有对应 UI" |
| `src/app/(dashboard)/admin/` | **无 diagnostic 页面** | 同上 |
| `src/app/(dashboard)/parent/` | **无 diagnostic 页面** | 同上 |
**后果**admin 和 parent 拥有 diagnostic 权限但无对应 UI权限与 UI 覆盖不一致。家长无法查看子女的学情诊断报告。
### 2.15 SearchParams 工具未统一P3
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) | 自定义 `SearchParams` 类型和 `getParam` 函数 | "最大化复用" |
| [management/grade/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/management/grade/insights/page.tsx) | 自定义 `SearchParams` 类型和 `getParam` 函数 | 同上 |
**后果**:与 teacher 端 grades 页面已复用 `@/shared/lib/search-params` 的做法不一致,存在重复代码。
---
## 三、行业差距对比
### 3.1 成绩模块grades行业对标
| 功能维度 | 行业优秀实践K12 成绩管理系统) | 当前实现 | 差距影响 |
|----------|-------------------------------|----------|----------|
| **成绩录入** | 支持Excel批量导入、扫码录入、语音录入录入时实时校验分数范围自动计算总分、平均分 | 仅支持单条录入 + 批量录入(表单式);有分数范围校验;无 Excel 导入 | 教师录入效率低,大班级成绩录入耗时 |
| **成绩分析** | 多维度分析(班级/年级/个人/科目);支持自定义分析维度;提供归因分析(哪些题目失分多) | 5 种分析(趋势/班级对比/科目对比/分布/排名);无归因分析;无自定义维度 | 教师无法定位失分原因,难以针对性教学 |
| **可视化** | 交互式图表hover 显示详情、点击下钻);支持图表下载为图片;支持自定义图表配置 | 静态图表TrendLineChart/SimpleBarChart无 hover 详情;无下载功能 | 数据呈现不够直观,教师难以深入分析 |
| **报告导出** | 支持 PDF/Excel/CSV 多格式;支持自定义报告模板;支持批量导出(按班级/年级) | 仅 Excel 导出(明细 + 班级汇总);无 PDF无自定义模板 | 无法满足学校正式报告需求(如家长会报告需 PDF |
| **预警机制** | 成绩异常预警(突然下降/持续低迷);及格率预警;班级对比异常预警 | 无预警机制 | 教师无法及时发现学生成绩异常 |
| **多角色视图** | 学生看自己 + 班级平均;家长看子女 + 班级排名;教师看所教班级;管理员看全校 | 4 角色都有基本视图;但 parent 无 diagnosticadmin 无 diagnostic | 家长无法全面了解子女学情 |
| **空状态/加载态** | 完善的空状态插画 + 引导操作;骨架屏过渡 | 仅部分页面有 loading.tsx组件无 Suspense | 用户体验差,白屏等待 |
| **数据联动** | 成绩 → 学情诊断 → 推荐练习;成绩 → 作业 → 知识点掌握度 | grades 与 diagnostic 无数据联动;无推荐练习 | 无法形成"诊断-练习-反馈"闭环 |
### 3.2 学情诊断模块diagnostic行业对标
| 功能维度 | 行业优秀实践K12 学情诊断系统) | 当前实现 | 差距影响 |
|----------|-------------------------------|----------|----------|
| **知识点掌握度** | 基于IRT项目反应理论计算支持知识点权重支持时间衰减近期表现权重更高 | 基于正确率简单计算;无权重;无时间衰减 | 掌握度计算不够精准 |
| **诊断报告** | 自动生成 PDF 报告;支持自定义模板;含学习建议、练习推荐、进步轨迹 | 生成 draft 报告JSON 存储);无 PDF建议为静态文本 | 报告不够专业,无法直接发给家长 |
| **可视化** | 雷达图 + 热力图 + 知识图谱;支持知识点下钻;支持时间对比 | 雷达图 + 热力图;无知识图谱;无下钻 | 知识结构呈现不够清晰 |
| **个性化推荐** | 基于弱项推荐练习题/微课;支持难度自适应;支持学习路径规划 | 仅列出弱项知识点 + "Practice" 链接(跳转到作业列表) | 无法精准推荐练习内容 |
| **班级诊断** | 班级整体掌握度 + 重点关注学生列表 + 教学建议;支持按知识点筛选学生 | 班级掌握度摘要 + 需关注学生列表;无教学建议 | 教师难以根据诊断调整教学 |
| **历史趋势** | 掌握度随时间变化曲线;支持对比多个时间段 | 无历史趋势(仅当前快照) | 无法评估学习进步情况 |
| **多角色覆盖** | 学生/家长/教师/管理员都能查看;家长看子女诊断报告 | 仅 teacher + student 有 UIparent/admin 无 UI | 家长无法了解子女学情 |
### 3.3 关键差距总结
1. **数据孤岛**grades 和 diagnostic 模块无数据联动,无法形成"成绩 → 诊断 → 练习 → 反馈"闭环。行业优秀产品(如猿题库、作业帮)已实现完整学习闭环。
2. **家长端缺失**parent 无 diagnostic UI家长无法查看子女学情诊断报告。K12 场景下家长是重要决策者,缺失影响家校沟通。
3. **报告专业度不足**diagnostic 报告为 JSON 存储,无 PDF 导出,无法直接用于家长会。行业产品普遍支持专业 PDF 报告。
4. **预警机制空白**:成绩异常、掌握度低迷无预警,教师无法主动干预。
5. **可视化深度不足**:无知识图谱、无下钻分析、无时间对比,数据呈现停留在表层。
---
## 四、改进优先级建议
### P0紧急 — 安全与合规)
| # | 问题 | 改进方向 |
|---|------|----------|
| P0-1 | 权限校验缺失10 个页面) | 所有页面调用 `requirePermission()`teacher/grades 用 `GRADE_RECORD_READ`/`GRADE_RECORD_MANAGE`teacher/diagnostic 用 `DIAGNOSTIC_READ`/`DIAGNOSTIC_MANAGE`student/parent 用对应 READ 权限 |
| P0-2 | diagnostic/data-access-reports.ts 直查 users 表 | 改为调用 `@/modules/users/data-access` 的 `getUserNamesByIds`,删除 `users` 表 import |
| P0-3 | i18n 完全缺失 + grade-management 运行时报错 | 创建 `grades.json` 和 `diagnostic.json` 翻译文件zh-CN + en修复 `grade-management` 模块的 `grade` 命名空间(创建 `grade.json` 或改用 `gradeManagement`);在 `i18n/request.ts` 注册新命名空间 |
| P0-4 | `/management/grade/page.tsx` 缺失 | 补齐 page.tsx 或删除孤儿 loading.tsx/error.tsx |
### P1较严重 — 架构与质量)
| # | 问题 | 改进方向 | 状态 |
|---|------|----------|------|
| P1-1 | 统计业务逻辑混入 data-access | 抽取 `grades/stats-service.ts`,将 `getClassGradeStats`/`getClassComparison`/`getSubjectComparison`/`getGradeDistribution`/`getRankingTrend` 的统计计算迁移至纯函数(参考 homework/stats-service.ts 范例) | ✅ 已完成 |
| P1-2 | 重复工具函数 | 抽取 `grades/lib/scope-filter.ts``buildScopeClassFilter`)和 `grades/lib/stats-utils.ts``toNumber`/`normalize` | ✅ 已完成 |
| P1-3 | 12 个 Action 缺失 Zod 校验 | 为 `deleteGradeRecordAction`/`getGradeRecordsAction`/`getClassGradeStatsAction`/`getStudentGradeSummaryAction`/`getClassRankingAction`/`getGradeRecordByIdAction`/`exportGradesAction` + 5 个 analytics Action 创建对应 Zod schema | ✅ 已完成 |
| P1-4 | `as` 断言违规4 处) | 使用类型守卫或 Zod 运行时校验替代 | ✅ 已完成 |
| P1-5 | Error Boundary 和 Suspense 缺失 | 创建 `grades/components/widget-boundary.tsx`Error Boundary + Suspense + Skeleton 组合每个数据区块独立包裹teacher/grades 和 teacher/diagnostic 路由补齐 loading.tsx/error.tsx | ✅ 已完成 |
| P1-6 | 架构图同步 | 更新 `004` 和 `005` 文档grades 行数、diagnostic deps、新增 stats-service.ts、新增 lib/、补齐 actions-analytics exports | ✅ 已完成 |
### P2优化 — 体验与扩展)
| # | 问题 | 改进方向 | 状态 |
|---|------|----------|------|
| P2-1 | a11y 无障碍缺失 | 补充 ARIA 属性:图表 `role="img"` + `aria-label`;按钮 `aria-label`;表格 `caption`;列表 `role="list"` | ✅ 已完成 |
| P2-2 | Tailwind 任意值 | 移除 `w-[180px]`/`h-[360px]`/`max-w-[520px]`,改用设计令牌或注释说明 | ✅ 已完成 |
| P2-3 | 班级报告 studentId 字段语义错误 | 修改 `learningDiagnosticReports` schema将 `studentId` 改为可空,或增加 `classId`/`generatedBy` 字段 | ✅ 已完成 |
| P2-4 | grade_managed scope 返回空数据 | 修复 `buildScopeClassFilter`grade_managed scope 应返回所管年级的班级过滤条件 | ✅ 已完成 |
| P2-5 | admin/parent 无 diagnostic UI | 新增 `/parent/diagnostic/` 页面家长查看子女诊断报告admin 可复用 teacher 视图 | ✅ 已完成 |
| P2-6 | SearchParams 工具未统一 | student/grades 和 management/grade/insights 改用 `@/shared/lib/search-params` | ✅ 已完成 |
### P3长期 — 行业对标)
| # | 问题 | 改进方向 |
|---|------|----------|
| P3-1 | grades 与 diagnostic 无数据联动 | 设计联动接口:成绩录入后触发掌握度更新;诊断报告含成绩趋势 |
| P3-2 | 无预警机制 | 新增 `grades/alerts-service.ts`:成绩下降预警、及格率预警、掌握度低迷预警 |
| P3-3 | 诊断报告无 PDF 导出 | 集成 PDF 生成库(如 @react-pdf/renderer支持专业报告模板 |
| P3-4 | 无知识图谱可视化 | 引入知识图谱组件(如 react-flow展示知识点关系与掌握度 |
| P3-5 | 无个性化练习推荐 | 基于弱项推荐练习题,对接 questions 模块 |
| P3-6 | Widget 配置系统 | 设计 `GradesWidgetConfig`/`DiagnosticWidgetConfig` 类型,按角色配置渲染哪些 Widget |
---
## 五、架构图同步说明
本次审计发现架构图存在以下遗漏或不一致,需在实现后同步更新:
### 5.1 `004_architecture_impact_map.md` 需补充
1. **§2.6 grades 模块**
- 更新文件清单行数:`data-access.ts` 419→433、`data-access-analytics.ts` 293→337
- 补充 `actions-analytics.ts` 的 5 个 Action 到 exports 清单(当前仅列 11 个,实际 15 个)
- 新增 `stats-service.ts`P1-1 抽取后)
- 新增 `lib/scope-filter.ts`、`lib/stats-utils.ts`P1-2 抽取后)
- 新增 `components/widget-boundary.tsx`P1-5 新增)
2. **§2.22 diagnostic 模块**
- 更新已知问题:标注 `data-access-reports.ts` 直查 users 表P0-2 修复前)
- 更新文件清单行数(如有变化)
3. **路由清单**
- 标注 `/management/grade/page.tsx` 缺失P0-4 修复前)
- 标注 teacher/grades 和 teacher/diagnostic 路由缺少 loading.tsx/error.tsx
- 新增 `/parent/diagnostic/` 路由P2-5 实现后)
### 5.2 `005_architecture_data.json` 需修改
1. `modules.grades` 节点L7362
- 更新 `dataAccess` 中各函数的 `deps`:移除直查 `classes`/`classEnrollments`/`subjects`/`users`,改为 `classes/data-access.*`/`school/data-access.*`/`users/data-access.*`
- 新增 `stats-service.ts` 的 exports
- 新增 `lib/scope-filter.ts`、`lib/stats-utils.ts` 的 exports
- 补充 `actions-analytics.ts` 的 5 个 Action 到 `actions` 数组
2. `modules.diagnostic` 节点L10927
- 更新 `dataAccess` 中各函数的 `deps`:移除直查 `users`/`classes`/`classEnrollments`/`examSubmissions`/`submissionAnswers`/`questionsToKnowledgePoints`,改为对应模块 data-access
- 标注 `data-access-reports.ts` 的 `getDiagnosticReports`/`getDiagnosticReportById` 依赖 `users/data-access.getUserNamesByIds`P0-2 修复后)
3. `permissions` 节点:
- 确认 `GRADE_RECORD_READ`/`GRADE_RECORD_MANAGE`/`DIAGNOSTIC_READ`/`DIAGNOSTIC_MANAGE` 权限点已定义(已存在 ✓)
4. `routes` 节点:
- 补充 teacher/grades/entry、teacher/grades/stats、teacher/diagnostic/class/[classId]、teacher/diagnostic/student/[studentId] 路由
- 标注 `/management/grade/page.tsx` 缺失
- 新增 `/parent/diagnostic/` 路由P2-5 实现后)
5. `dependencyMatrix`
- 更新 grades → classes/school/users 的依赖关系(通过 data-access已正确
- 更新 diagnostic → classes/exams/questions/users 的依赖关系(通过 data-accessP0-2 修复后完全正确)
### 5.3 翻译文件结构示例
```
src/shared/i18n/messages/
├─ zh-CN/
│ ├─ grades.json # 新增(成绩模块)
│ ├─ diagnostic.json # 新增(学情诊断模块)
│ └─ grade.json # 新增grade-management 模块,修复运行时报错)
└─ en/
├─ grades.json # 新增
├─ diagnostic.json # 新增
└─ grade.json # 新增
```
`grades.json` 结构示例zh-CN
```json
{
"title": {
"list": "成绩查询",
"entry": "成绩录入",
"analytics": "成绩分析",
"stats": "成绩统计"
},
"filters": {
"class": "班级",
"subject": "科目",
"type": "类型",
"semester": "学期",
"allClasses": "全部班级",
"allSubjects": "全部科目",
"allTypes": "全部类型",
"allSemesters": "全部学期",
"searchPlaceholder": "按标题搜索..."
},
"type": {
"exam": "考试",
"quiz": "测验",
"homework": "作业",
"other": "其他"
},
"semester": {
"s1": "第一学期",
"s2": "第二学期"
},
"list": {
"empty": "暂无成绩记录",
"columns": {
"student": "学生",
"class": "班级",
"subject": "科目",
"title": "标题",
"score": "分数",
"type": "类型",
"semester": "学期",
"recordedBy": "录入人",
"date": "日期"
}
},
"form": {
"title": "录入成绩",
"save": "保存",
"saving": "保存中...",
"cancel": "取消",
"selectClass": "选择班级",
"selectSubject": "选择科目",
"selectStudent": "选择学生",
"titlePlaceholder": "如期中考试",
"score": "分数",
"fullScore": "满分",
"remark": "备注(可选)",
"remarkPlaceholder": "关于此成绩的备注..."
},
"delete": {
"title": "删除成绩记录",
"confirmation": "确定要删除此成绩记录吗?此操作不可撤销。",
"confirm": "删除",
"cancel": "取消",
"deleting": "删除中..."
},
"export": {
"detail": "导出成绩明细",
"classReport": "导出班级成绩总表",
"success": "导出成功",
"failed": "导出失败"
},
"stats": {
"title": "统计",
"average": "平均分",
"median": "中位数",
"max": "最高分",
"min": "最低分",
"stdDev": "标准差",
"variance": "方差",
"passRate": "及格率",
"excellentRate": "优秀率",
"count": "人数"
},
"analytics": {
"trend": "成绩趋势",
"classComparison": "班级对比",
"subjectComparison": "科目对比",
"distribution": "分数分布",
"ranking": "排名",
"rankingTrend": "排名趋势"
},
"batch": {
"title": "批量录入",
"saving": "保存中...",
"restored": "已恢复未保存的成绩草稿",
"invalidScores": "存在无效分数",
"fullScoreRequired": "满分必填",
"saved": "已录入"
},
"empty": {
"noRecords": "暂无成绩记录",
"noData": "暂无数据"
},
"error": {
"loadFailed": "加载失败",
"saveFailed": "保存失败",
"deleteFailed": "删除失败",
"retry": "重试"
}
}
```
`diagnostic.json` 结构示例zh-CN
```json
{
"title": {
"student": "学生学情诊断",
"class": "班级学情诊断",
"reportList": "诊断报告"
},
"type": {
"individual": "个人",
"class": "班级",
"grade": "年级"
},
"status": {
"draft": "草稿",
"published": "已发布",
"archived": "已归档"
},
"filters": {
"reportType": "报告类型",
"status": "状态",
"allTypes": "全部类型",
"allStatuses": "全部状态"
},
"summary": {
"overallMastery": "总体掌握度",
"strengths": "强项",
"weaknesses": "弱项",
"students": "学生数",
"avgMastery": "平均掌握度",
"needAttention": "需重点关注"
},
"chart": {
"radarTitle": "知识点掌握度",
"radarDescription": "掌握度雷达图",
"heatmapTitle": "知识点掌握度热力图",
"rankingTitle": "知识点排名"
},
"report": {
"generate": "生成诊断报告",
"generateStudent": "生成学生诊断报告",
"generateClass": "生成班级诊断报告",
"publish": "发布",
"delete": "删除",
"publishTitle": "发布报告",
"deleteTitle": "删除报告",
"recommendations": "学习建议",
"history": "报告历史"
},
"strengths": {
"title": "强项≥80%",
"practice": "练习"
},
"weaknesses": {
"title": "弱项(<60%",
"practice": "练习"
},
"empty": {
"noData": "暂无诊断数据",
"noClassData": "无法加载班级掌握度摘要",
"noMastery": "暂无知识点掌握度记录",
"noReports": "暂无诊断报告"
},
"error": {
"generateFailed": "生成报告失败",
"publishFailed": "发布失败",
"deleteFailed": "删除失败",
"loadFailed": "加载失败",
"retry": "重试"
}
}
```
---
## 六、合规项确认
以下条目**已通过审计**
- ✅ **grades 模块跨模块依赖全部通过 data-access**所有跨模块访问classes/school/users均通过对方 data-access 函数
- ✅ **diagnostic 模块 data-access.ts 跨模块依赖通过 data-access**(仅 data-access-reports.ts 违规)
- ✅ **所有 Server Action 调用 `requirePermission()`**grades 15 个 + diagnostic 6 个 = 21 个 Action 全部合规
- ✅ **所有 Server Action 返回 `ActionState<T>`**
- ✅ **所有 Server Action 使用 `revalidatePath` 精确刷新**
- ✅ **无 `role === "xxx"` 硬编码**:全模块无
- ✅ **diagnostic 组件使用 `usePermission().hasPermission()`**class-diagnostic-view.tsx 和 report-list.tsx 已使用
- ✅ **无 `dangerouslySetInnerHTML`**
- ✅ **无 `any` 类型**
- ✅ **文件行数全部合规**:最大为 grades/components/batch-grade-entry.tsx 442 行 < 500 行组件建议上限
- ✅ **`"use client"` / `"use server"` / `"server-only"` 正确放置**
- ✅ **`import type` 使用规范**
- ✅ **diagnostic schema.ts 枚举与 types.ts 联合类型一致**
- ✅ **接口命名规范**(无 I 前缀PascalCase
---
## 七、重构方案设计要点(供后续实现参考)
### 7.1 完全解耦
- 定义 `GradesDataService` 接口抽象数据依赖,使用 React Context 注入
- 模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access
- 不同角色差异通过接口不同实现隔离(如 `TeacherGradesService`/`StudentGradesService`/`ParentGradesService`
### 7.2 组合优先
- 所有 UI 通过组件组合children、slots、render props实现灵活性
- 逻辑复用抽取为自定义 hooks如 `useGradeRecords`/`useGradeTrend`/`useMasterySummary`
- 严禁继承或深层嵌套 HOC
### 7.3 最大化复用
- 识别四角色共用 UI 块:`GradeTrendChart`/`GradeStatsCard`/`MasteryRadarChart`/`WidgetBoundary`
- 抽象泛型组件:`<DataTable<T>>`/`<FilterBar>`/`<EmptyState>`/`<ErrorState>`
- 各角色模块仅组合复用单元,可配置化显示内容
### 7.4 配置驱动
- 设计 `GradesWidgetConfig` 类型,按角色配置渲染哪些 Widget
- 示例teacher 看 [录入, 查询, 分析, 统计]student 看 [我的成绩, 趋势]parent 看 [子女成绩, 趋势]
### 7.5 错误与边界处理
- 每个独立数据区块用 `<WidgetBoundary>`Error Boundary + Suspense + Skeleton 组合)包裹
- 明确处理空数据、无权限、网络异常等边界状态
- 支持流式渲染React Server Components 获取初始数据)
### 7.6 可测试性
- 数据获取、计算、格式化等纯逻辑放入 `stats-service.ts` 或 hooks
- 导出清晰接口类型以便 mock
- 统计函数为纯函数,易于单测
### 7.7 监控埋点
- 预留关键操作埋点接口:成绩录入、报告生成、报告发布、导出操作
- 通过 `shared/lib/analytics` 统一上报