# grades(成绩)模块审计报告

> 审计日期:2026-06-25
> 审计范围:`src/modules/grades/**` + `src/app/(dashboard)/**/grades/**` 共 50 个文件
> 架构图定位:`004_architecture_impact_map.md` § 2.6 / `005_architecture_data.json` `modules.grades`
> 总体结论:该模块在架构图中被标记为"标杆模块(拆分范例)",三层架构、跨模块 data-access 通信、Server Action 权限校验、纯函数抽取、a11y 与 i18n 在主要文件中已落实。但仍存在 **3 处 P0 i18n 严重缺陷**、**2 个文件超行数上限**、**多处英文错误消息未 i18n**、**若干 `as` 断言与缺返回值类型**等问题,需按本报告优先级修复。

---

## 一、现有实现概要

### 1.1 文件分布

grades 模块共 50 个文件,按职责划分为四层:

| 层 | 文件数 | 路径 | 职责 |
|----|--------|------|------|
| 类型/校验 | 2 | `modules/grades/types.ts`、`schema.ts` | 类型定义 + Zod 输入校验 |
| 数据访问 | 5 | `data-access.ts`、`data-access-analytics.ts`、`data-access-ranking.ts`、`export.ts`、`stats-service.ts` | DB I/O + Excel 导出 + 统计纯函数 |
| Server Actions | 2 | `actions.ts`、`actions-analytics.ts` | 编排层(权限校验 → Zod → data-access → 通知 → revalidate) |
| Lib 工具 | 4 | `lib/grade-utils.ts`、`scope-filter.ts`、`scope-check.ts`、`type-guards.ts` | 纯工具函数 + 行级权限过滤 + 类型守卫 |
| 组件 | 19 | `modules/grades/components/**` | RSC + 客户端组件混合 |
| 页面 | 18 | `app/(dashboard)/**/grades/**` | 四种角色页面 + loading/error |

### 1.2 主要数据流

```
teacher/grades/page.tsx
  └─▶ requirePermission(GRADE_RECORD_READ)
      └─▶ grades/data-access.getGradeRecords(scope, currentUserId, limit, offset)
          └─▶ lib/scope-filter.buildScopeClassFilter → 行级过滤
          └─▶ db.query.gradeRecords (DB 层分页)
      └─▶ <GradeRecordList> + <GradeFilters> + <ExportButton>

teacher/grades/entry/page.tsx
  └─▶ requirePermission(GRADE_RECORD_MANAGE)
      └─▶ <BatchGradeEntryByExam>
          └─▶ exams/data-access.getExamsForGradeEntry
          └─▶ actions.batchCreateGradeRecordsByExamAction
              └─▶ 单事务写入 grade_records + grade_record_answers
              └─▶ 投影到 exam_submissions(status=graded) + submission_answers
              └─▶ 并行:diagnostic.updateMasteryFromExamScore + error-book.collectFromExamSubmission
              └─▶ notifyGradeEntered(批量通知学生 + 家长)

parent/grades/page.tsx
  └─▶ requirePermission(GRADE_RECORD_READ)
      └─▶ Promise.allSettled(为每个子女并行查询)
          └─▶ grades/data-access.getStudentGradeSummary
          └─▶ grades/data-access-analytics.getClassAverageTrend
          └─▶ <StudentGradeSummary> + <GradeTrendCard> + <ParentExportButton>
```

### 1.3 架构图完整性核对

经核对 `004_architecture_impact_map.md` § 2.6 与 `005_architecture_data.json` `modules.grades`:

- **已记录且准确**:`exports.dataAccess`(15+ 函数)、`exports.actions`(17 个)、`exports.types`(7 类)、`exports.lib`(4 函数)、`exports.statsService`(8 纯函数)、`exports.export`(3 函数)、`exports.components`(WidgetBoundary/SchoolWideSummaryCard/ScoreCell)、`dependencies`(classes/school/users/exams/error-book/diagnostic/notifications/parent)、`knownIssues`(已修复 28 项)。
- **架构图遗漏**:
  - `components/widget-boundary.tsx` 在文件清单中标注为 136 行,但未在 `exports.components` 中列出(仅在 P1-5 修复记录中提及)。
  - `lib/scope-check.ts` 的 `assertClassInScope` 函数未在 `exports.lib` 节点中列出(仅在 v4-P2-6 修复记录中提及)。
  - `parent/grades/page.tsx` 调用的 `getClassAverageTrend` data-access 函数未在架构图 `exports.dataAccess` 中记录。

### 1.4 标杆特性

该模块作为项目标杆,已落实的实践:
- ✅ data-access 按职责拆分为 3 个文件(analytics/ranking/主)
- ✅ Server Actions 全部 `requirePermission()` + Zod 校验
- ✅ 统计逻辑抽取为 `stats-service.ts` 纯函数(8 个),可独立测试
- ✅ 行级权限过滤 `buildScopeClassFilter(scope, currentUserId?)`
- ✅ DB 层分页(`PaginatedGradeRecords`)而非内存分页
- ✅ 事务原子性(`db.transaction`)
- ✅ 跨模块全部通过 data-access 通信,无直查他模块表
- ✅ 类型守卫(`isGradeType`/`isSemester`/`isDistributionTooltipPayload`)替代 `as` 断言
- ✅ `handleActionError` 统一错误处理 + `safeActionCall` 包装器
- ✅ a11y:4 图表 `role="img"` + `aria-label`、2 表格 `<caption>`、色盲友好双重编码
- ✅ RSC 优先 + 客户端组件最小化

---

## 二、现存问题与原因分析

### 2.1 [P0] i18n 严重缺失 — 3 个 error.tsx 硬编码中文

| 文件 | 行号 | 硬编码文本 |
|------|------|-----------|
| [parent/grades/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/grades/error.tsx) | 16-20 | "子女成绩页面加载失败" / "抱歉,页面加载时发生了意外错误..." / "重试" |
| [admin/school/grades/insights/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/error.tsx) | 17-21 | "成绩洞察页面加载失败" / "抱歉..." / "重试" |
| [admin/school/grades/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/error.tsx) | 12-16 | "页面加载失败" / "抱歉..." / "重试" |

- **违反规则**:项目规则"所有用户可见文本必须适配 i18n(next-intl),提取翻译键"。
- **原因**:这三个 error.tsx 与 `teacher/grades/error.tsx`(已 i18n)同期创建,但遗漏未接入 `useTranslations`。
- **直接后果**:非中文用户在页面出错时看到中文错误提示,国际化体验中断;与 teacher/grades/error.tsx 的良好实践形成明显不一致。

### 2.2 [P0] error.tsx 函数缺返回值类型标注

上述 3 个 error.tsx + `student/grades/error.tsx` + 所有 loading.tsx(共 7 个文件)的函数均未显式标注 `: JSX.Element` 返回值类型。

- **违反规则**:项目规则"TypeScript 严格模式:函数返回值必须显式标注,特别是 `Promise<T>`"。
- **原因**:loading/error 文件通常由脚手架生成,未补类型标注。
- **直接后果**:tsc 严格模式下可能漏检类型错误;代码可读性下降。

### 2.3 [P1] `actions.ts` 超行数上限(856 行)

[actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) 当前 856 行,超过 Server Actions 800 行建议上限 56 行。

- **违反规则**:项目规则"Server Actions / Data Access 模块:建议 ≤ 800 行"。
- **原因**:模块职责持续扩张(草稿 CRUD、按试卷录入、撤销、批量删除、通知),所有逻辑集中在单文件。
- **直接后果**:接近 1000 行硬上限,后续新增 Action 将突破硬限;可读性、code review 成本上升。

### 2.4 [P1] `data-access.ts` 接近行数上限(816 行)

[data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) 当前 816 行,超过 800 行建议上限 16 行。

- **违反规则**:同上。
- **原因**:草稿 CRUD(`saveGradeDraft`/`getGradeDraft`/`deleteGradeDraft`)、按试卷录入(`batchCreateGradeRecordsByExam`)混入主文件。
- **直接后果**:与 analytics/ranking 已拆分形成不一致;新增数据访问函数将很快突破硬限。

### 2.5 [P1] `actions.ts` 多处英文错误消息未 i18n

| 行号 | 硬编码英文 |
|------|-----------|
| 148 | "Invalid form data" |
| 180 | "Grade record created" |
| 253 | "Failed to delete grade record" |
| 394-399 | "No records to undo" / "Cannot undo more than 500 records at once" |
| 483-487 | "No records to delete" / "Cannot delete more than 500 records at once" |
| 584-592 | "Can only view your own grades" / "Can only view your children's grades" |
| 654-669 | "Can only view your own grade record" / "Can only view your children's grade records" |

- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **原因**:这些错误消息通过 `ActionState.error` 返回前端展示,早期未统一接入翻译键。
- **直接后果**:非中文用户看到英文错误提示。

### 2.6 [P1] `lib/scope-check.ts` 返回消息未 i18n

[scope-check.ts](file:///e:/Desktop/CICD/src/modules/grades/lib/scope-check.ts) 第 24/29/31 行返回 "You can only access classes you teach" 等英文,这些消息会通过 Action 层直接返回用户。

- **违反规则**:同上。
- **原因**:`assertClassInScope` 是同步函数,无法调用 `getTranslations`(异步),因此返回原始错误消息交由 Action 层包装。
- **直接后果**:Action 层目前未对返回消息做翻译包装,直接透传给用户。

### 2.7 [P1] `actions-analytics.ts` scope 校验消息未 i18n

[actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) 第 176/183 行 "Can only view your own ranking trend" / "Can only view your children's ranking trend"。

- 同 2.5 原因与后果。

### 2.8 [P1] `batch-grade-entry.tsx` 超组件行数上限(604 行)

[batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) 604 行,超过组件 500 行建议上限 104 行(复杂表单可放宽至 800,但仍建议拆分)。

- **违反规则**:项目规则"React 组件:建议 ≤ 500 行(复杂表单/大型表格可放宽至 800)"。
- **原因**:Excel 式表格 + 试卷/班级选择器 + 统计栏 + 撤销机制 + AlertDialog 全部集中在单文件。
- **直接后果**:可读性下降;子组件无法独立复用与测试;状态管理复杂度高,易引入 bug。

### 2.9 [P1] `batch-grade-entry.tsx` 第 341 行 `as` 断言无类型守卫

```ts
JSON.parse(raw) as { ids: string[]; timestamp: number }
```

- **违反规则**:项目规则"禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)"。
- **原因**:sessionStorage 撤销数据反序列化未做类型守卫。
- **直接后果**:数据被篡改时可能运行时报错;类型安全缺失。

### 2.10 [P1] `parent/grades/page.tsx` 第 63 行 `as` 断言

```ts
summary as NonNullable<typeof r.value.summary>
```

- **违反规则**:同 2.9。
- **原因**:虽有前置 `filter(r => r.value.summary)` 保证,但严格违反"禁止 as 断言"规则。
- **直接后果**:类型系统对运行时假设依赖,filter 逻辑变更时可能失效。

### 2.11 [P2] `student-grade-summary.tsx` / `grade-record-list.tsx` 硬编码 "S" 前缀

两文件均使用 `<TableCell>S{r.semester}</TableCell>` 硬编码 "S1"/"S2" 前缀。

- **违反规则**:i18n 规则。
- **原因**:学期显示格式未抽象为翻译键。
- **直接后果**:非英文/中文语境下学期标签不符合本地化习惯。

### 2.12 [P2] 多个组件函数缺返回值类型标注

| 文件 | 函数 |
|------|------|
| `grade-filters.tsx` | `GradeFilters` |
| `grade-query-filters.tsx` | `GradeQueryFilters` |
| `export-button.tsx` | `handleExport` |
| `grade-record-form.tsx` | `handleSubmit` |
| 所有 loading.tsx(7 个) | `Loading` |

- **违反规则**:函数返回值必须显式标注。
- **原因**:脚手架/早期代码遗漏。
- **直接后果**:类型推导依赖,严格模式潜在漏检。

### 2.13 [P2] `data-access.ts` `updateGradeRecord` 类型安全性较弱

第 407 行使用 `Record<string, unknown>` 累积更新字段。

- **违反规则**:TypeScript 严格模式精神(虽未直接违反禁止 any)。
- **原因**:Drizzle 动态更新字段累积时类型推导困难。
- **直接后果**:可能写入非法字段名;类型安全缺失。

### 2.14 [P2] `getStudentGradeSummary` 内部调用 `getClassRanking` 形成 data-access 层内部耦合

[data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) 第 564 行 `getStudentGradeSummary` 内部调用 `getClassRanking`,形成 data-access 层内部依赖。

- **原因**:学生成绩摘要需要排名信息,直接复用排名查询。
- **直接后果**:语义上 `getStudentGradeSummary` 不应依赖 `getClassRanking`;两者缓存参数不完全一致,可能导致重复查询;测试时需 mock 排名逻辑。

### 2.15 [P2] 组件复用抽象不足

| 现状 | 建议 |
|------|------|
| `GradeFilters`(student,nuqs) 与 `GradeQueryFilters`(teacher,URLSearchParams) 两个独立过滤器 | 抽象为统一组件 + props 配置 |
| `ExportButton`(teacher) 与 `ParentExportButton`(parent) | 可统一为带 `variant` prop 的单一组件 |
| `AnalyticsFilters`(teacher RSC) 与 `GradeInsightsFilters`(school ChipNav) | 可考虑统一 |

- **违反规则**:项目规则"识别四个角色共用的 UI 块和业务逻辑块,抽象为泛型组件和 hooks"。
- **原因**:各角色过滤器实现差异较大(nuqs vs URLSearchParams vs ChipNav),未做抽象。
- **直接后果**:新增角色需重写过滤器;维护成本上升。

### 2.16 [P2] 缺少 Error Boundary 包裹的独立数据区块

`teacher/grades/page.tsx` 列表与统计卡片未用 `WidgetBoundary` 包裹(仅 analytics 页面已包裹)。

- **违反规则**:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **原因**:主页面早期未应用 WidgetBoundary。
- **直接后果**:单个数据区块失败会导致整个页面崩溃。

### 2.17 [P2] 撤销机制仅依赖 sessionStorage(5 分钟)

`batch-grade-entry.tsx` 撤销令牌存于 sessionStorage,刷新或跨设备后丢失;`undoBatchCreateGradeRecordsAction` 无时间窗口限制,理论上可撤销任意时间自己录入的记录。

- **违反规则**:项目规则"错误与边界处理:明确处理空数据、无权限、网络异常等边界状态"(隐含)。
- **原因**:撤销机制为 v4 新增,未考虑持久化与时效。
- **直接后果**:误删风险;跨设备无法撤销。

### 2.18 [P2] 通知串行写入

`notifyGradeEntered` 对每个学生/家长串行调用 `createNotification`(actions.ts 第 88-117 行)。

- **违反规则**:性能最佳实践。
- **原因**:通知 data-access 未提供批量接口。
- **直接后果**:批量录入 30+ 学生时通知耗时显著。

### 2.19 [P2] 图表 tooltip a11y 不足

图表组件有 `role="img"` + `aria-label`,但 tooltip(recharts tooltip)的 a11y 支持不足,屏幕阅读器无法读取悬停数据。

### 2.20 [P2] `lib/type-guards.ts` 第 8/12 行 `as` 断言

```ts
(GRADE_TYPES as readonly string[]).includes(v)
```

- **违反规则**:禁止 as 断言(虽属类型收窄,但严格违规)。
- **改进**:改用 `Array.from(GRADE_TYPES).includes(v)` 或 `(GRADE_TYPES as readonly unknown[]).includes(v as unknown)` 后做类型守卫。

### 2.21 [P3] 架构图遗漏

- `components/widget-boundary.tsx` 未在 `exports.components` 节点列出
- `lib/scope-check.ts` 的 `assertClassInScope` 未在 `exports.lib` 节点列出
- `getClassAverageTrend` data-access 函数未记录

---

## 三、行业差距对比

对标 PowerSchool / Gradebook、Google Classroom、Canvas LMS、智学网、校宝在线等 K12 成绩管理产品,本模块差距如下:

### 3.1 缺失:成绩报告卡(Report Card)PDF 导出

- **行业实践**:PowerSchool / Canvas 支持生成单生/班级 PDF 成绩报告卡,含学期汇总、排名、教师评语、签名区。
- **本模块现状**:仅支持 Excel 导出(`exportGradeRecordsToExcel`),无 PDF。
- **影响**:家长会、学期末场景需手动排版打印,效率低。

### 3.2 缺失:成绩变化智能预警

- **行业实践**:智学网提供"成绩大幅波动预警"(分数下降超过 10 分自动通知班主任 + 家长)。
- **本模块现状**:无预警机制,仅静态统计。
- **影响**:教师无法及时发现学习下滑学生,错过干预窗口。

### 3.3 缺失:知识点维度的成绩诊断

- **行业实践**:Canvas LMS 的 MasteryGradebook 按知识点(competency)展示掌握度,而非仅按考试/科目。
- **本模块现状**:成绩按科目/考试维度,虽 `grade_record_answers` 已存储每题得分,但未提供知识点聚合视图(依赖 diagnostic 模块但未集成到 grades 页面)。
- **影响**:教师无法在成绩页直接看到班级在哪个知识点薄弱。

### 3.4 缺失:学生纵向成长档案(Individual Growth Report)

- **行业实践**:PowerSchool 提供单生多年纵向趋势图,含跨学期/跨年级对比。
- **本模块现状**:`GradeTrendCard` 仅展示单学期/单科目趋势,无跨年级纵向档案。
- **影响**:家长无法全面了解学生长期成长轨迹。

### 3.5 缺失:班级横向对比的统计显著性标注

- **行业实践**:专业 BI 工具(如 Tableau 教育版)在班级对比时标注 p 值、效应量(Cohen's d)。
- **本模块现状**:`ClassComparisonChart` 有"显著性分析区域"(v3-P3-5 新增),但仅基于极差经验规则,未使用统计检验。
- **影响**:教师可能过度解读小样本差异。

### 3.6 缺失:成绩录入的草稿协同

- **行业实践**:Google Classroom 支持多位教师协同录入,草稿云端同步,锁定机制防止冲突。
- **本模块现状**:草稿仅单用户单设备(`grade_drafts` 表按 userId 唯一),无协同。
- **影响**:多教师任教同一班级时无法分工录入。

### 3.7 缺失:成绩申诉与复核流程

- **行业实践**:Canvas 支持学生对成绩提出申诉,教师复核后修改并留痕。
- **本模块现状**:仅 `updateGradeRecord`,无申诉状态机与审计轨迹。
- **影响**:成绩修改无留痕,合规性不足。

### 3.8 缺失:移动端适配深度优化

- **行业实践**:校宝在线移动端针对家长提供"成绩单卡片"简化视图,大表格转为卡片堆叠。
- **本模块现状**:`grade-record-list.tsx` 有 `overflow-x-auto`,但未做卡片堆叠适配;图表在移动端可能过小。
- **影响**:家长移动端查看体验一般。

### 3.9 缺失:成绩分布的隐私保护

- **行业实践**:GDPR/FERPA 合规要求学生视角的分布图应隐藏其他学生身份,仅显示相对位置。
- **本模块现状**:学生视角的 `GradeDistributionChart` 显示全班分布,虽未暴露个人身份,但需明确"你在分布中的位置"标注。
- **影响**:隐私合规风险(部分国家/地区)。

### 3.10 缺失:批量导入 Excel 成绩

- **行业实践**:智学网支持下载模板 → 填写 → 上传 Excel 批量导入。
- **本模块现状**:`batch-grade-entry.tsx` 支持下载 CSV 模板 + 粘贴录入,但无完整 Excel 上传解析。
- **影响**:教师需手动粘贴,大批量场景效率低。

---

## 四、改进优先级建议

### P0 — 立即修复(影响用户体验与合规)

| # | 问题 | 改进方向 |
|---|------|---------|
| P0-1 | 3 个 error.tsx 硬编码中文(2.1) | 改为 `useTranslations("grades")` + 新增 `error.*` 翻译键(zh-CN/en 同步) |
| P0-2 | 7 个 loading/error.tsx 函数缺返回值类型(2.2) | 补全 `: JSX.Element` 返回值类型标注 |

### P1 — 本周修复(架构规范与 i18n)

| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | `actions.ts` 超 800 行(2.3) | 拆分草稿相关 3 个 Action 到 `actions-draft.ts`;拆分通知逻辑到 `lib/notify.ts` |
| P1-2 | `data-access.ts` 超 800 行(2.4) | 拆分草稿 CRUD 到 `data-access-drafts.ts`;拆分按试卷录入到 `data-access-exam-entry.ts` |
| P1-3 | `actions.ts` 多处英文错误消息未 i18n(2.5) | 新增 `action.error.*` 翻译键,所有错误消息改用 `t("action.error.*")` |
| P1-4 | `lib/scope-check.ts` 返回消息未 i18n(2.6) | 改为返回错误码枚举,由 Action 层翻译;或返回 `null` + Action 层自行抛错 |
| P1-5 | `actions-analytics.ts` scope 消息未 i18n(2.7) | 同 P1-3 |
| P1-6 | `batch-grade-entry.tsx` 超 500 行(2.8) | 拆分为 `BatchGradeEntryTable` + `BatchGradeEntryStats` + `BatchGradeEntryDialogs` 子组件 |
| P1-7 | `batch-grade-entry.tsx` 第 341 行 as 断言(2.9) | 新增 `isUndoData` 类型守卫函数 |
| P1-8 | `parent/grades/page.tsx` 第 63 行 as 断言(2.10) | 改用类型守卫 `isSummaryFulfilled` 或非空检查 |
| P1-9 | `teacher/grades/page.tsx` 列表与统计未 WidgetBoundary 包裹(2.16) | 用 `<WidgetBoundary>` 包裹独立区块 |

### P2 — 中期改进(质量与一致性)

| # | 问题 | 改进方向 |
|---|------|---------|
| P2-1 | "S" 前缀硬编码(2.11) | 新增 `semester.s1`/`semester.s2` 翻译键 |
| P2-2 | 组件函数缺返回值类型(2.12) | 补全 `: JSX.Element` / `: Promise<void>` |
| P2-3 | `updateGradeRecord` 类型弱(2.13) | 改用 `Partial<typeof gradeRecords.$inferInsert>` |
| P2-4 | `getStudentGradeSummary` 内部耦合 `getClassRanking`(2.14) | 将排名计算抽取到 action 层或 stats-service |
| P2-5 | 过滤器组件未抽象(2.15) | 抽象 `GradeFilterBar` 统一组件 + props 配置 |
| P2-6 | 撤销机制 sessionStorage 依赖(2.17) | 服务端存储撤销令牌 + 24 小时时间窗口 |
| P2-7 | 通知串行写入(2.18) | 新增 `notifications/data-access.bulkCreateNotification` 批量接口 |
| P2-8 | 图表 tooltip a11y(2.19) | 添加 `aria-describedby` + 旁路数据表(sr-only) |
| P2-9 | `lib/type-guards.ts` as 断言(2.20) | 改用 `Array.from(GRADE_TYPES).includes(v)` |

### P3 — 长期改进(行业对齐与企业级)

| # | 问题 | 改进方向 |
|---|------|---------|
| P3-1 | 缺 PDF 成绩报告卡(3.1) | 新增 `lib/report-card.ts`,基于 `@react-pdf/renderer` 生成 PDF |
| P3-2 | 缺成绩变化预警(3.2) | 新增 `stats-service.detectScoreAnomaly` 纯函数 + 通知触发 |
| P3-3 | 缺知识点维度诊断集成(3.3) | 在 analytics 页面集成 diagnostic 模块的知识点掌握度视图 |
| P3-4 | 缺学生纵向档案(3.4) | 新增 `getStudentGrowthArchive` data-access + `GrowthArchiveChart` 组件 |
| P3-5 | 缺统计显著性标注(3.5) | 引入 `simple-statistics` 库计算 p 值 + Cohen's d |
| P3-6 | ✅ 缺协同录入(3.6) | `grade_drafts` 表新增 `lockedBy`/`lockedAt`/`lockToken` 字段 + classSubjectLockIdx 索引，支持锁机制（data-access-drafts.ts 5 个锁函数 + actions-lock.ts 4 个 Server Action + use-draft-lock Hook + DraftLockBanner 组件 + i18n） |
| P3-7 | ✅ 缺申诉流程(3.7) | 新增 `grade_appeals` 表（状态机枚举+4 个索引）+ data-access-appeals.ts（7 个函数）+ actions-appeal.ts（5 个 Server Action）+ schema.ts Zod 校验 + i18n |
| P3-8 | 缺移动端卡片堆叠(3.8) | 新增 `GradeRecordCardList` 移动端组件 |
| P3-9 | 缺隐私保护位置标注(3.9) | 学生视角分布图添加"你的位置"标注 |
| P3-10 | 缺 Excel 上传批量导入(3.10) | 集成 `xlsx` 库,新增 `importGradesFromExcel` Action |
| P3-11 | 单元测试覆盖(2.14 关联) | 为 stats-service.ts 纯函数 + lib/scope-check.ts 补充 vitest 测试 |

### 架构图同步

| # | 改进内容 | 架构图更新 |
|---|---------|-----------|
| A-1 | P1-1 拆分 actions.ts | 004 文件清单 + 005 exports.actions 节点 |
| A-2 | P1-2 拆分 data-access.ts | 004 文件清单 + 005 exports.dataAccess 节点 |
| A-3 | P1-6 拆分 batch-grade-entry | 004 文件清单 + 005 exports.components 节点 |
| A-4 | 补全遗漏 | 004/005 补充 widget-boundary、scope-check.assertClassInScope、getClassAverageTrend |

---

## 五、架构图同步说明

### 5.1 本次审计发现的架构图遗漏(需补充)

1. **`components/widget-boundary.tsx`** — 在 004 § 2.6 文件清单中标注为 136 行,但未在 `exports.components` 节点中列出。需在 005 `modules.grades.exports.components` 数组中新增条目。
2. **`lib/scope-check.ts` 的 `assertClassInScope`** — 仅在 v4-P2-6 修复记录中提及,未在 `exports.lib` 节点列出。需补充。
3. **`getClassAverageTrend` data-access 函数** — `parent/grades/page.tsx` 调用但架构图 `exports.dataAccess` 未记录。需确认其在 `data-access-analytics.ts` 中的位置并补充。

### 5.2 重构后需同步的节点

- 若执行 P1-1(拆分 actions.ts):004 文件清单更新为 `actions.ts`(barrel)+ `actions-draft.ts` + `actions-exam-entry.ts`;005 `exports.actions` 按文件分组。
- 若执行 P1-2(拆分 data-access.ts):004 文件清单更新为 `data-access.ts` + `data-access-drafts.ts` + `data-access-exam-entry.ts`;005 `exports.dataAccess` 按文件分组。
- 若执行 P1-6(拆分 batch-grade-entry):004 文件清单新增 3 个子组件;005 `exports.components` 补充。
- P0/P1 i18n 修复无需更新架构图(翻译键不影响模块导出结构)。

---

## 六、实施计划

> 本节为审计文档的实施清单,按优先级执行,每步完成后运行 `npm run lint && npx tsc --noEmit` 验证零错误。

### 阶段 1:P0 立即修复(本次会话完成)

- [ ] P0-1: 3 个 error.tsx i18n(parent/grades、admin/school/grades、admin/school/grades/insights)
- [ ] P0-2: 7 个 loading/error.tsx 函数补全返回值类型

### 阶段 2:P1 本周修复(本次会话完成)

- [ ] P1-1: 拆分 `actions.ts` → `actions-draft.ts` + 通知抽取到 `lib/notify.ts`
- [ ] P1-2: 拆分 `data-access.ts` → `data-access-drafts.ts` + `data-access-exam-entry.ts`
- [ ] P1-3: `actions.ts` 错误消息 i18n(新增 `action.error.*` 翻译键)
- [ ] P1-4: `lib/scope-check.ts` 改为返回错误码枚举
- [ ] P1-5: `actions-analytics.ts` scope 消息 i18n
- [ ] P1-6: 拆分 `batch-grade-entry.tsx` 为子组件
- [ ] P1-7: `batch-grade-entry.tsx` 第 341 行 as 断言 → 类型守卫
- [ ] P1-8: `parent/grades/page.tsx` 第 63 行 as 断言 → 类型守卫
- [ ] P1-9: `teacher/grades/page.tsx` WidgetBoundary 包裹

### 阶段 3:P2 中期改进(本次会话完成)

- [ ] P2-1: "S" 前缀 i18n
- [ ] P2-2: 组件函数返回值类型补全
- [ ] P2-3: `updateGradeRecord` 类型强化
- [ ] P2-4: 解耦 `getStudentGradeSummary` 与 `getClassRanking`
- [ ] P2-5: 过滤器组件抽象(评估后决定是否实施)
- [ ] P2-6: 撤销机制服务端持久化(评估后决定)
- [ ] P2-7: 通知批量接口(依赖 notifications 模块改造,标注为后续)
- [ ] P2-8: 图表 tooltip a11y
- [ ] P2-9: `lib/type-guards.ts` as 断言修复

### 阶段 4:P3 长期改进(记录待排期,不在本次会话实施)

- [ ] P3-1 ~ P3-10:行业差距对齐(需产品评估与设计)
- [ ] P3-11:单元测试覆盖

### 阶段 5:架构图同步(本次会话完成)

- [ ] A-1 ~ A-4:补充 004/005 遗漏节点 + 重构后节点更新
