# Next_Edu 架构影响地图

> 重写日期：2026-06-17
> 目标：一次阅读即可直观理解整个项目架构
> 规则：源码修改后须同步更新本文档与 `005_architecture_data.json`
> 审查依据：`docs/architecture/audit/` 下 4 份审查报告

---

## 目录

- [第一部分：全局架构概览](#第一部分全局架构概览)
  - [1.1 分层架构图](#11-分层架构图)
  - [1.2 模块依赖关系图](#12-模块依赖关系图)
  - [1.3 数据流向图（考试流程）](#13-数据流向图考试流程)
  - [1.4 核心调用链路](#14-核心调用链路)
- [第二部分：模块清单](#第二部分模块清单)
- [第三部分：已知架构问题和技术债](#第三部分已知架构问题和技术债)
- [附录 A：模块间依赖矩阵](#附录-a模块间依赖矩阵)
- [附录 B：关键参数影响链](#附录-b关键参数影响链)
- [附录 C：核心函数签名索引](#附录-c核心函数签名索引)
- [相关文档](#相关文档)

---

# 第一部分：全局架构概览

## 1.1 分层架构图

项目采用**严格三层架构**，依赖方向必须单向：`app → modules → shared`。

```
┌─────────────────────────────────────────────────────────────────────┐
│  app/ (路由层)                                                       │
│  ─────────────────────────────────────────────────────────────────  │
│  (auth)/          login / register / privacy / terms                │
│  (dashboard)/     admin / teacher / student / parent / management   │
│  api/             REST API (auth, ai, upload, files, search, ...)   │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ 调用 Server Actions / data-access
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  modules/ (业务模块层)                                               │
│  ─────────────────────────────────────────────────────────────────  │
│  核心教学：  exams · homework · questions · textbooks · grades       │
│              · lesson-preparation                                     │
│  教学管理：  classes · school · scheduling · attendance · course-plans│
│  用户与沟通：users · messaging · notifications · parent · audit      │
│  扩展功能：  elective · proctoring · diagnostic · dashboard          │
│  权限管理：  rbac                                                     │
│  其他：      announcements · files · settings · auth · layout · student│
│                                                                     │
│  每个模块标准结构：actions.ts (编排) → data-access.ts (DB) → schema.ts│
└──────────────────────────────┬──────────────────────────────────────┘
                               │ 依赖
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  shared/ (基础设施层)                                                │
│  ─────────────────────────────────────────────────────────────────  │
│  db/              schema.ts (54 张表) + relations.ts + index.ts     │
│  lib/             auth-guard · permissions · ai · audit-logger ·    │
│                   change-logger · login-logger · password-policy ·  │
│                   rate-limit · excel · file-storage · ...           │
│  hooks/           use-permission · use-aria-live · ...              │
│  components/      ui/ (shadcn) · a11y/ · global-search · ...        │
│  types/           permissions · action-state                        │
└─────────────────────────────────────────────────────────────────────┘
                               ▲
                               │ 反向依赖（违规，见 1.2）
┌──────────────────────────────┴──────────────────────────────────────┐
│  根模块：src/auth.ts (NextAuth 配置) · src/proxy.ts (中间件)         │
│  ✅ shared/lib/{audit-logger, change-logger, auth-guard} 已通过       │
│     shared/lib/session.ts 单一入口获取 session，不再直依赖 @/auth     │
│     （详见第三部分 P0-2）                                              │
└─────────────────────────────────────────────────────────────────────┘
```

**分层规则**：
- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access，不直接访问 DB
- `modules/` 之间通过对方 data-access 通信，不直接查询对方表
- `shared/` 是被依赖方，不应反向依赖 `app/` 或 `modules/`
- `src/auth.ts` 和 `src/proxy.ts` 位于根目录，属于应用层

---

## 1.1.1 M7 移动端 PWA 支持（2026-07-01 新增）

- **PWA 清单**：新增 `src/app/manifest.ts`（Next.js App Router 元数据路由），生成 `/manifest.webmanifest`，声明 `name`/`short_name=NextEdu`/`start_url=/`/`display=standalone`/`theme_color=#1976d2`/`icons=/icon.svg`。
- **根布局**：`src/app/layout.tsx` 新增 `viewport` 导出（`width=device-width`、`initialScale=1`、`maximumScale=5`、`userScalable=true`、`themeColor` 按明暗主题切换）+ `metadata` 新增 `manifest`/`appleWebApp` 配置，支持添加到主屏幕与独立显示。
- **触控友好**：`lesson-preparation` 模块 `node-editor.tsx` 与 `lesson-plan-readonly-view.tsx` 的 `<ReactFlow>` 组件新增 `panOnDrag`/`zoomOnPinch`/`zoomOnDoubleClick={false}`/`selectionOnDrag={false}` 属性，防止移动端双击误缩放、优化触控拖拽体验。

---

## 1.2 模块依赖关系图

下图展示模块间的实际依赖关系，**标注依赖类型与合规性**。

### 图例

- `───▶` 合理依赖（通过 data-access 或类型导入）
- `═══▶` 违规依赖（直接查询对方 DB 表）
- `⟳  ` 循环依赖
- `──▷` UI 组件组合（合理）

### 1.2.1 核心业务模块依赖

```
                    ┌──────────────┐
                    │  textbooks   │ ◀── 标杆模块（无跨模块 DB 访问）
                    └──────┬───────┘
                           │ ──▷ UI 组合（knowledge-point-dialogs）
                           │
            ┌──────────────┼──────────────┐
            │              │              │
            ▼              ▼              ▼
     ┌────────────┐  ┌──────────┐  ┌────────────┐
     │ questions  │  │  exams   │  │  homework  │
     └─────┬──────┘  └────┬─────┘  └─────┬──────┘
           │ ✅ P1-1      │ ✅ P1-1      │ ✅ P1-1 已修复
           │ 通过 textbooks│ 通过 questions│ 通过 exams/classes/
           │ data-access  │ data-access  │   school/users data-access
           │              │ 通过 classes  │
           │              │ data-access   │
           ┌─┴────────┐     │              │
           │ grades   │◀────┘ 仅外键引用（合理）
           │ (成绩)   │◀──── exams data-access（✅ 2026-06-24 新增：getExamsForGradeEntry/getExamForGradeEntry 按试卷录入成绩）
           └────┬─────┘
                │ ✅ P1-1 已修复
                │ 通过 classes/school/users data-access
                ▼
           ┌──────────┐
           │ classes  │ ✅ P1-1 已修复：通过 homework/data-access-classes
           └────┬─────┘   获取作业数据，不再直查 homework/exams 表
                │ ✅ P0-1 已修复：data-access.ts 已拆分为 5 文件
                │ ✅ P0-5 已修复：classSchedule 写入口统一到 scheduling
                │
                    ┌─────────┼─────────┐
                    │         │         │
                    ▼         ▼         ▼
              ┌──────────┐ ┌──────┐ ┌──────────┐
              │scheduling│ │school│ │ attendance│
              └────┬─────┘ └──────┘ └──────────┘
                   │ ✅ P0-5 已修复
                   │ classSchedule 写入口统一到 scheduling
                   │ 通过 classes/data-access 校验归属
                   │ ✅ P1-1 已修复：通过 users data-access
```

### 1.2.2 扩展模块依赖

```
┌─────────────┐  ✅ P0-3 已修复：通过各模块 data-access    ┌──────────────┐
│ dashboard   │  获取数据（getUsersDashboardStats 等）       │ users/classes│
│ (聚合层)    │──────────────────────────────────────▶│ /exams/...   │
└─────────────┘  ✅ P0-4 已修复：Promise.all 并行调用     └──────────────┘

┌─────────────┐  ─── 调用 data-access（合理）   ┌──────────────┐
│ parent      │──────────────────────────────▶│ classes/      │
│ (聚合层)    │  ✅ P1-1 已修复：通过各模块     │ homework/grades│
└─────────────┘   data-access 获取数据          └──────────────┘

┌─────────────┐  ✅ P1-1 已修复：通过 exams/questions/      ┌──────────────┐
│ diagnostic  │  classes/users data-access 获取数据          │ exams/questions│
│             │──────────────────────────────────────▶│ /classes/users│
└─────────────┘                                       └──────────────┘

┌─────────────┐  ✅ P1-1 已修复：通过 exams/users           ┌──────────────┐
│ proctoring  │  data-access 获取数据                       │ exams/users   │
└─────────────┘──────────────────────────────────────▶└──────────────┘

┌─────────────┐  ─── 调用 notifications dispatcher     ┌──────────────┐
│ messaging   │  （通知偏好/CRUD 已迁移至 notifications）│ notifications │
│             │──────────────────────────────────────▶│ (拥有         │
│             │  ✅ P0-4 / P1-5 已修复：单向依赖       │  messageNotif│
└─────────────┘                                       │  ications +  │
                                                      │  preferences)│
                                                      └──────────────┘

┌─────────────┐  ─── 调用 messaging Action       ┌──────────────┐
│ settings    │  （通知偏好表单）                 │ messaging     │
└─────────────┘──────────────────────────────▶└──────────────┘

┌─────────────────┐  ───▶ data-access（合理）       ┌──────────────┐
│ lesson-prep     │──────────────────────────────▶│ textbooks    │
│ (备课聚合层)    │  只读章节/知识点树              │ (章节/KP 树) │
│                 │──────────────────────────────▶└──────────────┘
│                 │──────────────────────────────▶┌──────────────┐
│                 │  创建/查询题目                  │ questions    │
│                 │──────────────────────────────▶└──────────────┘
│                 │──────────────────────────────▶┌──────────────┐
│                 │  创建 exam 草稿                │ exams        │
│                 │──────────────────────────────▶└──────────────┘
│                 │──────────────────────────────▶┌──────────────┐
│                 │  创建作业下发                  │ homework     │
│                 │──────────────────────────────▶└──────────────┘
│                 │──────────────────────────────▶┌──────────────┐
│                 │  查询教师班级                  │ classes      │
│                 │──────────────────────────────▶└──────────────┘
│                 │──────────────────────────────▶┌──────────────┐
│                 │  附件                          │ files        │
└─────────────────┘──────────────────────────────▶└──────────────┘
```

### 1.2.3 循环依赖详情 ✅ 已修复

```
shared/lib/audit-logger.ts   ──┐
shared/lib/change-logger.ts  ──┼──▶ shared/lib/session.ts ──▶ (dynamic import) @/auth
shared/lib/auth-guard.ts     ──┘

src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
              ──▶ import { ... } from "@/shared/lib/login-logger"
              ──▶ import { ... } from "@/shared/lib/password-policy"
              ──▶ import { ... } from "@/shared/lib/rate-limit"
              ──▶ import { ... } from "@/shared/lib/role-utils"        # P1-3 拆出
              ──▶ import { ... } from "@/shared/lib/bcrypt-utils"      # P1-3 拆出
              ──▶ import { ... } from "@/shared/lib/http-utils"        # P1-3 拆出
              ──▶ import { ... } from "@/shared/lib/password-security-service"  # P1-3 拆出
              ──▶ import { db, schema } from "@/shared/db"

  ✅ 修复：shared/lib/* 不再静态 import @/auth，统一通过 session.ts 单一入口
     session.ts 内部使用 dynamic import("@/auth") 打破模块级静态循环
     运行时调用链保持不变，模块加载图无环
```

---

## 1.3 数据流向图（考试流程）

以"考试流程"为例，展示数据从创建到成绩统计的完整流向。

```
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 1：教师创建考试                                                 │
│ ─────────────────────────────────────────────────────────────────  │
│ teacher/exams/create/page.tsx                                       │
│   └─▶ exams/actions.createExamAction                                │
│         ├─▶ requirePermission(EXAM_CREATE)  [shared/auth-guard]     │
│         ├─▶ persistExamDraft()              [exams/data-access]     │
│         │     └─▶ db.insert(exams)           [shared/db]            │
│         │     └─▶ db.insert(examQuestions)   [shared/db]            │
│         └─▶ revalidatePath("/teacher/exams")                        │
│                                                                     │
│ 数据写入：exams 表 + examQuestions 表                                │
│ ✅ P0-1 已修复：persistAiGeneratedExamDraft 改为调用 questions/data-access.createQuestionWithRelations，不再直查 questions 表 │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 2：学生作答（作业化考试）                                        │
│ ─────────────────────────────────────────────────────────────────  │
│ student/learning/assignments/[assignmentId]/page.tsx                │
│   └─▶ homework/actions.startHomeworkSubmissionAction                │
│         ├─▶ requirePermission(HOMEWORK_SUBMIT)                      │
│         ├─▶ data-access-write.startHomeworkSubmission  ✅ P1-2 已修复 │
│         │     └─▶ db.insert(homeworkSubmissions)  [shared/db]       │
│         └─▶ 返回 submissionId                                       │
│                                                                     │
│   └─▶ homework/actions.saveHomeworkAnswerAction                     │
│         └─▶ data-access-write.saveHomeworkAnswer  ✅ P1-2 已修复     │
│              └─▶ db.transaction(insert homeworkAnswers) [shared/db] │
│                                                                     │
│   └─▶ homework/actions.submitHomeworkAction                         │
│         └─▶ data-access-write.submitHomework  ✅ P1-2 已修复         │
│              └─▶ db.update(homeworkSubmissions.status="submitted")  │
│                                                                     │
│ 数据写入：homeworkSubmissions 表 + homeworkAnswers 表                │
│ ✅ P1-2 已修复：actions 层不再直接 DB 操作，已下沉到 data-access-write│
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 3：教师批改                                                     │
│ ─────────────────────────────────────────────────────────────────  │
│ teacher/homework/submissions/[submissionId]/page.tsx                │
│   └─▶ homework/actions.gradeHomeworkSubmissionAction                │
│         ├─▶ requirePermission(HOMEWORK_GRADE)                       │
│         └─▶ data-access-write.gradeHomeworkSubmission  ✅ P1-2 已修复│
│              └─▶ db.update(homeworkAnswers) 循环  [shared/db]       │
│                                                                     │
│ 数据更新：homeworkAnswers.isCorrect / score / feedback               │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 4：成绩统计与诊断                                               │
│ ─────────────────────────────────────────────────────────────────  │
│ teacher/grades/page.tsx                                             │
│   └─▶ grades/data-access.getGradeRecords                            │
│         └─▶ ✅ P1-1 已修复：通过 classes/school/users data-access    │
│                                                                     │
│ teacher/diagnostic/page.tsx                                         │
│   └─▶ diagnostic/data-access.updateMasteryFromSubmission            │
│         ├─▶ ✅ P1-1 已修复：通过 exams data-access 获取提交          │
│         ├─▶ ✅ P1-1 已修复：通过 questions data-access 获取知识点    │
│         └─▶ 更新 knowledgePointMastery                              │
│                                                                     │
│ 数据读取：homeworkSubmissions → grades → knowledgePointMastery        │
└─────────────────────────────────────────────────────────────────────┘
```

**关键观察**：考试流程横跨 4 个模块（exams → homework → grades → diagnostic），✅ P1-1 已修复：所有跨模块查询已改为通过对方 data-access 接口，模块封装性已恢复。

---

## 1.4 核心调用链路

### 1.4.1 调用链路：创建考试（含 AI 出题）

```
[Client] exam-form.tsx
   │ FormData
   ▼
[Route]  POST /teacher/exams/create (Server Action)
   │
   ▼
[Action] exams/actions.createAiExamAction
   │
   ├─▶ requirePermission(EXAM_CREATE)
   │     └─▶ shared/lib/auth-guard.getAuthContext()
   │           ├─▶ auth()  [src/auth.ts]
   │           ├─▶ db.query.usersToRoles  [shared/db]
   │           ├─▶ db.query.classSubjectTeachers
   │           └─▶ 返回 { userId, roles, permissions, dataScope }
   │
   ├─▶ generateAiCreateDraftFromSource()
   │     └─▶ exams/ai-pipeline.ts (912 行)
   │           ├─▶ shared/lib/ai.createAiChatCompletion()
   │           │     └─▶ OpenAI SDK + db.query.aiProviders
   │           └─▶ JSON 解析 + Zod 校验
   │
   ├─▶ persistAiGeneratedExamDraft()
   │     └─▶ exams/data-access.ts
   │           ├─▶ db.insert(exams)              ✅ 合理
   │           ├─▶ db.insert(examQuestions)      ✅ 合理
   │           └─▶ questions/data-access.createQuestionWithRelations  ✅ P0-1 已修复：通过 data-access
   │
   └─▶ revalidatePath("/teacher/exams")
```

### 1.4.2 调用链路：学生提交作业

```
[Client] homework-take-view.tsx
   │
   ▼
[Action] homework/actions.submitHomeworkAction
   │
   ├─▶ requirePermission(HOMEWORK_SUBMIT)
   │
   ├─▶ data-access-write.submitHomework  ✅ P1-2 已修复
   │     （校验 submission 归属 + 更新状态）
   │     └─▶ db.update(homeworkSubmissions)
   │           SET status = "submitted", submittedAt = now()
   │
   └─▶ 返回 ActionState<{ submissionId }>
```

### 1.4.3 调用链路：管理员仪表盘聚合

```
[Route]  /admin/dashboard/page.tsx (Server Component)
   │
   ▼
[DataAccess] dashboard/data-access.getAdminDashboardData
   │
   ├─▶ users/data-access.getUsersDashboardStats()           ✅ 通过模块 data-access
   │     ├─ userCount / activeSessionsCount / userRoleCounts
   │     └─ recentUsers (含角色解析)
   ├─▶ classes/data-access.getClassesDashboardStats()       ✅ 通过模块 data-access
   │     └─ classCount
   ├─▶ textbooks/data-access.getTextbooksDashboardStats()   ✅ 通过模块 data-access
   │     └─ textbookCount / chapterCount
   ├─▶ questions/data-access.getQuestionsDashboardStats()   ✅ 通过模块 data-access
   │     └─ questionCount
   ├─▶ exams/data-access.getExamsDashboardStats(scope?)     ✅ 通过模块 data-access
   │     └─ examCount (含 scope 过滤)
   └─▶ homework/stats-service.getHomeworkDashboardStats(scope?)  ✅ 通过模块 data-access
         ├─ homeworkAssignmentCount / homeworkAssignmentPublishedCount
         └─ homeworkSubmissionCount / homeworkSubmissionToGradeCount

   ✅ P0-4 已修复：dashboard 改为并行调用各模块 dashboard stats 函数，不再直查跨模块表
```

### 1.4.4 调用链路：admin 路由组统一权限守卫

```
[Layout]  app/(dashboard)/admin/layout.tsx (Server Component)
   │
   ▼
[AuthGuard] shared/lib/auth-guard.getAuthContext()
   │
   └─▶ getSession() → 校验已登录（未登录抛 PermissionDeniedError）
   │
   ▼
[Children] 各 admin/* 页面（page.tsx）在函数体首行调用 requirePermission(XXX)
   │
   ├─▶ /admin/school/schools         → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/school/academic-year   → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/school/classes         → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/school/departments     → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/school/grades          → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/school/grades/insights → requirePermission(SCHOOL_MANAGE)
   ├─▶ /admin/users/import           → requirePermission(USER_MANAGE)
   ├─▶ /admin/users                  → requirePermission(USER_MANAGE)
   ├─▶ /admin/roles                  → requirePermission(ROLE_READ)
   ├─▶ /admin/roles/[id]             → requirePermission(ROLE_READ)
   ├─▶ /admin/permissions            → requirePermission(PERMISSION_READ)
   ├─▶ /admin/scheduling/auto        → requirePermission(SCHEDULE_AUTO)
   ├─▶ /admin/scheduling/changes     → requirePermission(SCHEDULE_ADJUST)
   ├─▶ /admin/scheduling/rules       → requirePermission(SCHEDULE_ADJUST)
   ├─▶ /admin/announcements          → requirePermission(ANNOUNCEMENT_MANAGE)
   ├─▶ /admin/announcements/[id]     → requirePermission(ANNOUNCEMENT_MANAGE)
   ├─▶ /admin/audit-logs             → requirePermission(AUDIT_LOG_READ)
   └─▶ /admin/invitation-codes       → requirePermission(USER_MANAGE)  ✅ audit-P2-3 新增

   ✅ P0 安全修复：admin/layout.tsx 提供登录态统一守卫，
      各页面 requirePermission() 提供细粒度权限校验

   teacher/classes/* 路由权限校验（2026-06-22 审计修复）：
   ├─▶ /teacher/classes/my           → requirePermission(CLASS_READ)  ✅ 已修复
   ├─▶ /teacher/classes/my/[id]      → requirePermission(CLASS_READ)  ✅ 已修复
   ├─▶ /teacher/classes/schedule     → requirePermission(CLASS_READ)  ✅ 已修复
   └─▶ /teacher/classes/students     → requirePermission(CLASS_READ)  ✅ 已修复

   student/learning + student/schedule 路由权限校验（2026-06-26 审计修复）：
   ├─▶ /student/learning                  → requirePermission(CLASS_READ)  ✅ 已修复（原 homework:submit）
   ├─▶ /student/learning/courses          → requirePermission(CLASS_READ)  ✅ 已修复（原 homework:submit）
   ├─▶ /student/learning/courses/[classId]→ requirePermission(CLASS_READ)  ✅ 已修复（原 homework:submit）
   └─▶ /student/schedule                  → requirePermission(CLASS_READ)  ✅ 已修复（原 homework:submit）
```

---

# 第二部分：模块清单

> 每个模块包含：职责 · 导出函数 · 依赖关系 · 已知问题 · 文件清单

## 2.1 shared（基础设施层）

**职责**：提供全项目共享的 DB Schema、工具函数、权限系统、UI 基础组件、通用 Hooks。

**导出函数**（核心）：
- `getAuthContext()` / `requirePermission(p)` / `requireAuth()` — 认证与权限
- `resolvePermissions(roles)` / `resolveDataScope(userId, roles)` — 权限解析
- `logAudit()` / `logLoginEvent()` / `logDataChange()` — 日志记录
- `createAiChatCompletion()` / `parseAiChatPayload()` — AI 调用
- `validatePassword()` / `isAccountLocked()` / `rateLimit()` — 安全策略
- `exportToExcel()` / `parseExcel()` / `generateTemplate()` — Excel 工具
- `cn()` / `formatDate()` / `formatFileSize()` — 通用工具
- `getInitials(name)` / `formatDateForFile(d?)` — 通用工具（P1-c / P1-a 重构新增：从 parent/lib/utils.ts、grades/export-button.tsx 等多处重复实现抽取）
- `downloadBase64File(base64, filename, mimeType?)` / `downloadBlob(blob, filename)` — 客户端文件下载（P1-c 重构新增：从 grades/export-button、users/user-import-dialog、audit/audit-log-export-button 三处重复实现抽取，位于 `lib/download.ts`）
- `handleActionError(e)` / `safeActionCall(action, options?)` / `safeJsonParse(json, msg)` / `safeParseDate(v, field)` / `safeParseNumber(v, field)` / `escapeLikePattern(input)` — Server Action 错误处理与客户端调用包装（2026-06-23 审计修复新增：位于 `lib/action-utils.ts`，统一所有 Server Action catch 块错误处理，避免内部错误消息暴露给客户端）
- `BusinessError` / `NotFoundError` / `ValidationError` — 错误类（2026-06-23 审计修复新增：位于 `lib/action-utils.ts`，已知业务错误基类，message 可安全返回客户端）
- `extractQuestionPreview(content, fallback?, maxLength?)` — 题目内容纯文本预览提取（✅ 2026-06-24 新增：位于 `lib/question-content.ts`，从 `unknown` 类型题目内容递归提取纯文本，支持字符串/数组/对象格式，可选截断；被 error-book、homework 等模块共享使用，避免各模块重复实现题目内容解析逻辑）
- `isRecord(v)` — 通用类型守卫（P1-6 代码去重新增：位于 `lib/type-guards.ts`，判断未知值是否为非空对象 `v is Record<string, unknown>`；消除 homework/stats-service、lesson-preparation/node-summary、lesson-preparation/ai-suggest 等 12+ 个文件中重复定义的 `isRecord` 局部实现，从 `unknown` 安全收窄类型，替代 `as Record<string, unknown>` 断言）
- `trackEvent(name, payload)` / `trackExamEvent(name, payload)` / `trackAuthEvent(name, payload)` — 监控埋点接口（位于 `lib/track-event.ts`，server-only，当前输出到 console.info，预留接入外部监控服务；EventName 联合类型覆盖 announcement/message/notification/attendance/elective/exam/homework/ai/audit/files/auth 模块事件；✅ P3 新增事件名 `homework.excellent_viewed`、`homework.remind_unsubmitted`；✅ audit-P1-9 新增 auth 模块 8 个事件 `auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`，并新增 `trackAuthEvent` 便捷函数自动设置 targetType="user"）
- `ROLE_NAMES` — 角色名常量对象（✅ 2026-06-24 新增：位于 `types/permissions.ts`，包含 ADMIN/TEACHER/STUDENT/PARENT/GRADE_HEAD/TEACHING_HEAD 键，替代 DB 查询中硬编码的角色名字符串；被 error-book/data-access-analytics.getAllStudentIds 使用 `ROLE_NAMES.STUDENT`）

**共享组件导出**（P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d / 第二轮 P0-1/P0-2/P0-3/P1-1/P1-2/P1-3/P1-4 重构新增，按类别组织）：

| 类别 | 组件 | 文件 | 用途 | 消费方数量 |
|------|------|------|------|-----------|
| **UI 组件** | `StatCard` | `components/ui/stat-card.tsx` | 统计卡片（标题+数值+图标+描述+跳转+骨架屏） | 8 个（P1-a） |
| **UI 组件** | `StatItem` | `components/ui/stat-item.tsx` | 紧凑统计项（label+icon+value+hint，用于统计面板网格） | 8 个（P1-a） |
| **UI 组件** | `ChipNav` | `components/ui/chip-nav.tsx` | 芯片导航组（通过 URL search params 切换筛选维度，Link 跳转） | 3 个（P1-b） |
| **UI 组件** | `PageHeader` | `components/ui/page-header.tsx` | 页面头部（标题+描述+icon+actions，响应式布局） | 2 个（P2-b: profile/page.tsx, settings/security/page.tsx） |
| **UI 组件** | `FilterBar` / `FilterSearchInput` / `FilterResetButton` | `components/ui/filter-bar.tsx` | 筛选栏容器+搜索框+重置按钮（统一布局壳，URL 状态由各模块处理） | 5 个（P3-b: exam/textbook/question/audit-log/login-log filters） |
| **UI 组件** | `ConfirmDeleteDialog` | `components/ui/confirm-delete-dialog.tsx` | 通用删除确认对话框（AlertDialog 包装，支持自定义 confirmText/cancelText） | 5 个（P0-1: announcement-detail, message-detail, course-plan-detail, grade-classes-view, students-table） |
| **UI 组件** | `Pagination` | `components/ui/pagination.tsx` | 通用分页 UI（Showing X-Y of Z + Page X of Y + 上一页/下一页按钮） | 3 个（P0-2: audit-log-table, login-log-table, data-change-log-table） |
| **UI 组件** | `EmptyTableRow` | `components/ui/empty-table-row.tsx` | 表格空状态行（TableRow + TableCell 居中显示空状态文案） | 3 个（P0-3: audit-log-table, login-log-table, data-change-log-table） |
| **UI 组件** | `ResizablePanel` | `components/ui/resizable-panel.tsx` | 自实现可拖拽分栏容器（左右两栏 + 中间分隔条 pointer 拖拽调整宽度，无新依赖；用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景） | 2 个（exams/components/exam-rich-form, homework/components/homework-scan-grading-view） |
| **UI 组件** | `ErrorState` | `components/error-state.tsx` | 通用错误边界展示组件（"use client"，统一 error.tsx 渲染逻辑：AlertTriangle 图标 + i18n title/description + retry 按钮；props：`{ error, reset, namespace }`；翻译键查找规则：在传入 namespace 下查找 `error.title`/`error.description`/`error.retry`） | 10+ 个（2026-06-26 审计修复新增：classes/student 等模块的 error.tsx） |
| **UI 组件** | `StatusBadge` | `components/ui/status-badge.tsx` | 通用状态徽章（Badge + 状态→variant/label/className 映射表，修复 in_progress 颜色不一致 bug） | 9+ 个（P1-1: audit 3 文件, grades 2 文件, student/learning/assignments, parent/child-homework-summary, student-upcoming-assignments-card, question-columns） |
| **表单字段** | `TextField` | `components/form-fields/text-field.tsx` | 通用文本字段（FormField + Input 包装，支持 text/number/password/datetime-local 类型 + value 转换器） | 3 个文件 16 处（P1-2: profile-settings-form 6, exam-basic-info-form 4, ai-provider-settings-card 4） |
| **表单字段** | `SelectField` | `components/form-fields/select-field.tsx` | 通用选择字段（FormField + Select 包装，支持 toSelectValue/fromSelectValue 处理 number↔string） | 4 个文件 8 处（P1-2: exam-basic-info-form 3, ai-provider-settings-card 1, create-question-dialog 2, profile-settings-form 1） |
| **表单字段** | `TextareaField` | `components/form-fields/textarea-field.tsx` | 通用多行文本字段（FormField + Textarea 包装） | 1 个（P1-2: create-question-dialog） |
| **图表组件** | `ChartCardShell` | `components/charts/chart-card-shell.tsx` | 图表卡片外壳（Card+Header+EmptyState+Content 统一结构） | 8 个（P3-c） |
| **图表组件** | `TrendLineChart` | `components/charts/trend-line-chart.tsx` | 趋势折线图（LineChart 统一配置，支持单/多系列） | 8 个（P3-c: grade-trend-chart 等） |
| **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图（BarChart 统一配置，支持单/多 Bar + Cell 分桶着色 + defs 自定义 SVG 图案） | 8 个（P3-c: grade-distribution-chart 等） |
| **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图（RadarChart 统一配置，支持双 Radar 对比） | 8 个（P3-c: subject-comparison-chart, mastery-radar-chart 等） |
| **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项（课程+时间+地点+班级徽章，separator/card 两种变体） | 3 个（P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view） |
| **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏（搜索+题型+难度，default/compact 两种布局） | 2 个（P3-d: exam-assembly, question-bank-picker） |
| **题库组件** | `QuestionCascadeFilter` | `modules/questions/components/question-cascade-filter.tsx` | ✅ 审计新增：教材→章节→知识点三级级联筛选（nuqs URL 状态同步，清除上级自动清除下级） | 1 个（question-filters） |
| **题库组件** | `KnowledgePointSelector` | `modules/questions/components/knowledge-point-selector.tsx` | ✅ 审计新增：知识点选择器（支持搜索过滤，从 create-question-dialog 拆分） | 1 个（create-question-dialog） |
| **题库组件** | `OptionsEditor` | `modules/questions/components/options-editor.tsx` | ✅ 审计新增：选项编辑器（单选/多选模式，增删改选项，从 create-question-dialog 拆分） | 1 个（create-question-dialog） |
| **题库组件** | `QuestionBankResultsClient` | `modules/questions/components/question-bank-results-client.tsx` | ✅ 审计新增：RSC 与客户端 Hook 桥接（解决 useQuestionColumns Hook 在客户端组件中使用） | 1 个（teacher/questions/page） |
| **题库组件** | `QuestionContentRenderer` | `modules/questions/components/question-content-renderer.tsx` | ✅ 审计新增：题目结构化渲染（题干/选项/答案/解析，单选圆点/多选方框，正确答案高亮） | 1 个（question-actions 查看详情） |
| **题库组件** | `BatchOperations` | `modules/questions/components/batch-operations.tsx` | ✅ 审计新增：批量操作工具栏（批量删除，权限感知，事务原子性） | 1 个（question-data-table） |
| **题库组件** | `ImportExportButtons` | `modules/questions/components/import-export-buttons.tsx` | ✅ 审计新增：导入导出组件（JSON 格式，导出下载/上传导入，导入前预览确认） | 2 个（teacher/admin questions page） |
| **设置组件** | `SettingsView` | `modules/settings/components/settings-view.tsx` | 统一设置页布局（5 标签页：General/Notifications/Appearance/Security/AI，角色差异通过 props 注入，Tab URL 持久化，登出二次确认） | 4 个（P2-a: admin/teacher/student/parent 设置页） |

**共享 Hooks 导出**（第二轮 P1-4 重构新增）：

| Hook | 文件 | 签名 | 用途 | 消费方 |
|------|------|------|------|--------|
| `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation<T>(options?): { isWorking, mutate }` | 通用 Server Action mutation Hook，替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式 | 1 个示范（P1-4: schools-view），潜在影响 50+ 文件 |
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }` | 通用 Server Action 查询 Hook，替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式，内置竞态防护 | 1 个示范（P1-4: create-question-dialog），潜在影响 11 个文件 |

**共享工具函数导出**（第二轮 P1-3 重构新增）：

| 函数 | 文件 | 签名 | 用途 | 消费方 |
|------|------|------|------|--------|
| `formatDateTime` | `lib/utils.ts` | `formatDateTime(date, locale?): string` | 国际化日期+时间格式化（含小时、分钟） | 4 个（P1-3: lesson-plan-card, version-history-drawer, proctoring-dashboard, exam-ai-generator） |
| `formatLongDate` | `lib/utils.ts` | `formatLongDate(date, locale?): string` | 国际化长日期格式化（含星期、完整月份名），默认 zh-CN，weekday=short | 1 个（P1-3: teacher-dashboard-header） |

> 注：`SettingsView` 位于 `modules/settings/components/`（非 shared 层），因仅被 settings 模块消费，未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。

**依赖关系**：
- 被依赖方：**所有模块**依赖 shared
- ✅ 反向依赖已修复：`shared/lib/{audit-logger, change-logger, auth-guard}` 通过 `shared/lib/session.ts` 单一入口获取 session，不再直接依赖 `@/auth`

**已知问题**：
- ✅ P0：~~`shared/lib/*` ↔ `@/auth` 循环依赖~~ 已修复（新增 `shared/lib/session.ts` 封装 session 获取，3 个文件改为 `import { getSession } from "@/shared/lib/session"`）
- ⚠️ P1：`schema.ts` 1111 行（54 张表混合，超 1000 硬上限）
- ✅ P1：~~`auth.ts` 293 行混合 5 类职责~~ 已拆分（4 个辅助函数组迁移至 `shared/lib/{role-utils,bcrypt-utils,http-utils,password-security-service}`，auth.ts 仅保留 NextAuth 配置）
- ✅ P2-2 已修复：~~`ai.ts` 218 行混合 5 类职责~~ 已拆分为 `ai/` 目录（payload-parser.ts/api-key-crypto.ts/provider-config.ts/client.ts/errors.ts/index.ts），原 `ai.ts` 保留为向后兼容的重导出文件（9 行）
- ✅ P2-4 已修复：~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已迁移至 `modules/onboarding/`（actions/data-access/schema/types/components），引导流程改为独立路由 `/onboarding` + middleware 重定向 + Server Action
- ✅ P0-2/P0-3/P0-4/P0-5/P1-1/P1-2/P1-4/P1-5 已修复（v3 对标 PowerSchool/Veracross/Auth0）：家长绑定三因子验证（邮箱+生日+手机号后4位）、教师多科目循环绑定、审计日志、服务端幂等、URL query 持久化步骤、局部错误收集、家长多子女动态行、跳过机制明确化
- ✅ v3 i18n 体系引入：采用 next-intl 4.x（without i18n routing 模式），cookie 驱动 locale 切换，字典放在 `shared/i18n/messages/{locale}/`，支持 zh-CN/en 两种语言，不破坏现有路由组结构
- ✅ v3 班级邀请码体系引入（对标 Google Classroom / 钉钉教育 / 智学网）：新增 `class_invitation_codes` 表（独立表，支持有效期/次数限制/审计/多码并存），6 位字母数字（剔除歧义字符 0/O/1/I/L，空间 1.13 亿），rate limit 防爆破（10 次/5 分钟），审计日志全链路记录，懒清理过期码

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `db/schema.ts` | 1111 | 54 张表定义（超硬上限） |
| `db/relations.ts` | - | 表关系定义 |
| `db/index.ts` | - | Drizzle 客户端 |
| `lib/auth-guard.ts` | - | 认证上下文 + 权限校验 + DataScope |
| `lib/permissions.ts` | - | 角色-权限映射 |
| `lib/session.ts` | 38 | session 获取单一入口（getSession，server-only，dynamic import 打破循环） |
| `lib/ai.ts` | 9 | 向后兼容重导出（P2-2 已拆分到 `ai/` 目录） |
| `lib/ai/payload-parser.ts` | 78 | 请求负载解析 |
| `lib/ai/api-key-crypto.ts` | 28 | API Key 加密/解密 |
| `lib/ai/provider-config.ts` | 132 | Provider 配置查询（V3.1 增强：基于 session 用户的可见性/所有权校验，public + own private 过滤） |
| `lib/ai/client.ts` | 58 | AI 客户端创建与调用 |
| `lib/ai/errors.ts` | 8 | 错误格式化 |
| `lib/ai/index.ts` | 5 | 聚合导出 |
| `lib/audit-logger.ts` | - | 操作日志（通过 session.ts 获取 session，http-utils 获取 IP/UA） |
| `lib/change-logger.ts` | - | 数据变更日志（通过 session.ts 获取 session，http-utils 获取 IP） |
| `lib/login-logger.ts` | - | 登录日志（通过 http-utils 获取 IP/UA） |
| `lib/password-policy.ts` | - | 密码策略纯函数 |
| `lib/rate-limit/` | - | 速率限制器抽象（audit-P1-6 新增：抽象 `RateLimiter` 接口；默认内存实现 `MemoryRateLimiter`；可选 Redis 实现 `RedisRateLimiter` 基于 @upstash/ratelimit；通过 `RATE_LIMIT_DRIVER=memory\|redis` 环境变量切换；`rateLimit()` / `resetRateLimit()` 改为 Promise 返回以支持异步后端；`rateLimitKey` / `RATE_LIMIT_RULES` / `rateLimitHeaders` 保持同步纯函数） |
| `lib/role-utils.ts` | 51 | 角色规范化纯函数（normalizeRole / resolvePrimaryRole；✅ P2-8 修改：保留 grade_head/teaching_head 独立性，不再映射为 teacher；优先级 admin>grade_head>teaching_head>teacher>student>parent） |
| `lib/bcrypt-utils.ts` | 18 | bcrypt 哈希前缀规范化纯函数 |
| `lib/breached-password.ts` | 125 | 已泄露密码检测（✅ audit-P2-4 新增：HIBP k-anonymity API 范围查询；仅发送 SHA-1 前 5 字符到 `api.pwnedpasswords.com`，本地比对完整哈希后缀，不泄露用户密码；3 秒超时 + AbortController；fail-open 策略：API 不可用时返回 `checkSkipped=true` 不阻断注册/改密；server-only） |
| `lib/http-utils.ts` | 44 | 请求头解析（resolveClientIp / getUserAgent，server-only） |
| `lib/password-security-service.ts` | 84 | 密码安全 DB 操作（账户锁定/失败登录追踪，server-only） |
| `lib/route-resolver.ts` | 21 | 角色默认路径解析纯函数 `resolveDefaultPath(roles: string[]): string`（✅ audit-P0-6 新增：合并 `proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现；纯函数无 `server-only`，可在 edge/proxy 运行时使用；优先级 admin>grade_head/teaching_head>teacher>student>parent>fallback `/dashboard`） |
| `lib/route-permissions.ts` | 82 | 路由权限配置常量（✅ audit-P1-10 + audit-P1-12 新增：从 proxy.ts 抽取，配置驱动设计；导出 4 个 `Record<string, Permission>` 常量：`SPECIFIC_ROUTE_PERMISSIONS`（15 个精确路由权限，audit-P1-10 扩展自 3 个至 15 个覆盖 `/admin/*` 子路径）/ `ROUTE_PREFIX_PERMISSIONS`（5 个角色路由前缀权限）/ `DASHBOARD_ROUTE_PERMISSIONS`（4 个仪表盘细粒度权限防跨角色访问）/ `API_ROUTE_PERMISSIONS`（1 个 API 路由权限）；纯常量无 `server-only`，新增路由权限只需修改此文件无需改 proxy.ts 逻辑） |
| `lib/permission-bitmap.ts` | 222 | 权限位图编解码（✅ audit-P1-7 新增：将 67 个权限点字符串数组约 1.1KB 压缩为约 13 字符 base36 字符串，体积减少 99%；`PERMISSION_BITMAP_ORDER` 顺序固定不可变，新增权限只能追加末尾；纯函数无 `server-only`，可在 edge runtime 使用；导出 `encodePermissionsBitmap(permissions)` / `decodePermissionsBitmap(bitmap)` / `hasPermissionInBitmap(bitmap, permission)`；BigInt 精度安全：手工按位累加 base36 → BigInt，避免 `parseInt(bitmap, 36)` 的 Number 53 bit 精度丢失） |
| `lib/excel.ts` | - | Excel 导入导出 |
| `lib/file-storage.ts` | - | 文件存储抽象 |
| `hooks/use-permission.ts` | - | 客户端权限 Hook |
| `components/ui/*` | 35 文件 | shadcn/ui 标准组件（✅ P2-3 新增 alert.tsx） |
| `components/ui/stat-card.tsx` | 95 | StatCard 统计卡片（P1-a 新增） |
| `components/ui/stat-item.tsx` | 38 | StatItem 紧凑统计项（P1-a 新增） |
| `components/ui/chip-nav.tsx` | 78 | ChipNav 芯片导航（P1-b 新增） |
| `components/ui/page-header.tsx` | 44 | PageHeader 页面头部（P2-b 新增，含 icon 属性） |
| `components/ui/filter-bar.tsx` | 124 | FilterBar + FilterSearchInput + FilterResetButton（P3-b 新增） |
| `components/ui/resizable-panel.tsx` | - | ResizablePanel 可拖拽分栏容器（自实现，无新依赖；用于富文本编辑器与阅卷式批改的左右分栏布局） |
| `components/error-state.tsx` | 35 | ErrorState 通用错误边界展示组件（"use client"；2026-06-26 审计修复新增：统一 30+ error.tsx 渲染逻辑；props `{ error, reset, namespace }`，基于 EmptyState + AlertTriangle + next-intl useTranslations） |
| `components/charts/chart-card-shell.tsx` | 90 | ChartCardShell 图表卡片外壳（P3-c 新增） |
| `components/charts/trend-line-chart.tsx` | 153 | TrendLineChart 趋势折线图（P3-c 新增） |
| `components/charts/simple-bar-chart.tsx` | 162 | SimpleBarChart 柱状图（P3-c 新增） |
| `components/charts/comparison-radar-chart.tsx` | 143 | ComparisonRadarChart 对比雷达图（P3-c 新增） |
| `components/schedule/schedule-list.tsx` | 112 | ScheduleList + ScheduleListItem 课表列表（P3-a 新增） |
| `components/question/question-bank-filters.tsx` | 137 | QuestionBankFilters 题库筛选栏（P3-d 新增） |
| `modules/questions/components/question-cascade-filter.tsx` | 130+ | QuestionCascadeFilter 三级级联筛选（✅ 审计新增：教材→章节→知识点，nuqs URL 状态） |
| `modules/questions/components/knowledge-point-selector.tsx` | 80+ | KnowledgePointSelector 知识点选择器（✅ 审计新增：从 create-question-dialog 拆分） |
| `modules/questions/components/options-editor.tsx` | 120+ | OptionsEditor 选项编辑器（✅ 审计新增：从 create-question-dialog 拆分） |
| `modules/questions/components/question-bank-results-client.tsx` | 30+ | QuestionBankResultsClient RSC 桥接（✅ 审计新增） |
| `modules/questions/utils/parse-content.ts` | 60+ | parseQuestionContent 类型守卫（✅ 审计新增：替代 `as` 断言） |
| `modules/questions/utils/track-event.ts` | 30+ | trackQuestionEvent 埋点接口（✅ 审计新增：no-op 实现） |
| `modules/questions/components/question-content-renderer.tsx` | 110+ | QuestionContentRenderer 题目结构化渲染（✅ 审计新增） |
| `modules/questions/components/batch-operations.tsx` | 110+ | BatchOperations 批量操作工具栏（✅ 审计新增） |
| `modules/questions/components/import-export-buttons.tsx` | 180+ | ImportExportButtons 导入导出组件（✅ 审计新增） |
| `lib/download.ts` | 47 | downloadBase64File + downloadBlob 客户端下载工具（P1-c 新增） |
| `lib/type-guards.ts` | - | 通用类型守卫集合（P1-6 新增 `isRecord(v): v is Record<string, unknown>`，消除 12+ 个文件重复定义；含 grades/lesson-preparation 等模块的类型守卫 `isGradeDraftData`/`isExerciseBlockData`/`isPermission` 等） |
| `lib/track-event.ts` | - | 监控埋点接口（server-only，no-op console.info 实现；EventName 联合类型覆盖 11 个模块事件；P3 新增 `homework.excellent_viewed`、`homework.remind_unsubmitted`；audit-P1-9 新增 auth 模块 8 个事件 + `trackAuthEvent` 便捷函数） |
| `lib/utils.ts` | - | 通用工具（P1-a/P1-c 新增 getInitials + formatDateForFile） |
| ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除（引导流程业务逻辑已迁移至 `modules/onboarding/`，原文件无任何 import 引用，删除以避免文件膨胀） |
| `components/global-search.tsx` | 221 | 全局搜索（业务泄漏） |
| `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型（✅ rbac 新增：`Role` 类型从固定联合类型改为 `string` 支持动态角色；新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫；`isRole()` 标记为 deprecated） |

---

## 2.2 exams（考试模块）

**职责**：考试全生命周期管理（创建/编辑/预览/发布/删除/复制）+ AI 辅助出题。

**导出函数**：
- Actions：`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`（✅ v4-P2-7 新增：年级仪表盘维度3，按 gradeId 查询年级下所有考试 + 提交统计，EXAM_READ 权限）/ `autoMarkExamAction`（✅ 2026-06-24 新增：AI 自动标记试卷文本，将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档，EXAM_AI_GENERATE 权限，返回 `{ doc, title }` 供富文本编辑器载入。✅ 2026-06-24 拆分：迁移至 `ai-pipeline/auto-mark.ts`，actions.ts re-export 保持向后兼容）/ `createExamFromRichEditorAction`（✅ 2026-06-24 新增：从富文本编辑器保存试卷草稿，将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化，EXAM_CREATE 权限；✅ 2026-06-24 升级：递归 `convertStructureNode` 支持 section/group/question 三级层级，section 数据不再丢失。✅ 2026-06-24 拆分：迁移至 `actions-rich-editor.ts`，actions.ts re-export 保持向后兼容）（✅ P1-2 已修复：actions 层不再直接访问 DB，全部下沉到 data-access） / `updateExamFromRichEditorAction`（✅ 2026-06-24 新增：从富文本编辑器更新已有试卷，校验所有权后递归转换 EditorDoc→structure，为无 questionId 的题目创建 questions 记录，调用 `data-access.updateExamWithRichEditorData` 持久化，EXAM_UPDATE 权限，revalidatePath 列表页与组卷页。✅ P0-4 修复（2026-06-24）：原直接 db.transaction 写 exams/examQuestions 表，现已下沉到 data-access.updateExamWithRichEditorData。✅ 2026-06-24 拆分：迁移至 `actions-rich-editor.ts`，actions.ts re-export 保持向后兼容）
- Data-access：`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`（✅ v4-P2-7 新增：年级仪表盘维度3，exams 表有直接 gradeId 字段，配合 examSubmissions 聚合提交数/已评分数/平均分，支持 scope 行级过滤）（后 8 个为 P1-2 新增）/ `getExamsForGradeEntry`（✅ 2026-06-24 新增：按 scope 过滤试卷列表，只返回有题目的试卷，供成绩录入页试卷选择器使用，返回 id/title/subjectName/gradeName/questionCount/totalScore）/ `getExamForGradeEntry`（✅ 2026-06-24 新增：获取单个试卷详情含题目列表，innerJoin questions 获取 type，含 scope 校验，返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}]，供 grades 模块按试卷录入成绩使用）/ `updateExamWithRichEditorData`（✅ 2026-06-24 新增，P0-4 修复：从富文本编辑器更新已有试卷的 DB 事务，单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions，原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此）
- AI Pipeline：`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` / `loadAiDraftQuestionsAndStructure`（✅ 2026-06-24 新增：从 actions.ts 下沉，加载 AI 草稿题目与结构，优先从 rawAiQuestions JSON 解析，否则从源文本生成）
- Utils：`normalizeStructure`（v3 新增：将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`，类型守卫模式无 `as` 断言，从 `teacher/exams/[id]/build/page.tsx` 提取；✅ 2026-06-24 升级：支持 section/group/question 三级层级递归校验，section 节点含 title/level/children，group 节点含 title/instruction/children）
- Preview Utils（`components/exam-preview-utils.ts`，✅ 2026-06-24 补充记录）：`buildPreviewNodes` / `parseEditableContent` / `flattenPreviewQuestions` / `findPreviewQuestionNode` / `updatePreviewQuestionNodeInList` / `buildPreviewSignature` / `buildPreviewPayload` / `buildPreviewRequestData`（以上 8 个为预览纯函数，供 Hook 与组件复用）+ ✅ 2026-06-24 新增 5 个（从 use-exam-preview-tasks Hook 提取以控制行数）：`buildTaskFormValues`（提取表单字段子集供任务回填）/ `isPreviewBackgroundTask`（类型守卫，校验 localStorage 恢复数据）/ `persistPreviewTasksToStorage`（持久化任务到 localStorage）/ `parseStoredPreviewTasks`（解析 localStorage 任务，queued/running 标记为 failed 防僵尸）/ `executeBackgroundPreviewTask`（执行单个后台预览任务的纯函数）
- Hooks（✅ 2026-06-24 拆分记录）：`useExamPreview`（主组合器，68 行，组合三个子 Hook + handlePreview 编排，返回 API 与拆分前一致）/ `usePreviewState`（状态子 Hook，67 行，10 个 useState + 3 个更新函数）/ `usePreviewBackgroundTasks`（后台任务子 Hook，80 行，PQueue 队列 + localStorage 持久化 + 入队/打开任务）/ `usePreviewRewrite`（AI 重写子 Hook，79 行，handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction）
- Stats-service（V3-8 新增）：`getExamAnalytics`（cache 包装，聚合考试所有作业的已批改提交，计算平均分/及格率/分数段分布/逐题错误率与难度等级，对标智学网考试分析）+ `ExamAnalyticsSummary` 类型
- Types（✅ 2026-06-24 新增成绩录入相关类型）：`ExamQuestionItem`（试卷中单个题目的精简结构 { id, order, score, type }）/ `ExamForGradeEntry`（成绩录入用的试卷详情，含题目列表）/ `ExamOptionForEntry`（成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore }）
- Editor 子模块（✅ 2026-06-24 新增 `editor/` 目录，基于 Tiptap 的试卷富文本编辑器）：
  - 组件：`ExamRichEditor`（富文本编辑器主体，forwardRef 暴露 `ExamRichEditorHandle` 以便父组件获取 JSONContent）/ `SelectionToolbar`（浮动选择工具栏，对选中文本应用 DottedMark 等标记）
  - 自定义 Tiptap 扩展（`editor/extensions/`）：`DottedMark`（加点字标记，拼音注音题）/ `BlankNode`（填空空位节点，原子节点）/ `ImageNode`（图片节点，含 fileId/url/alt 属性）/ `QuestionBlock`（题目块节点，含 type/score 等 attrs）/ `GroupBlock`（大题分组节点，含 title）/ `SectionBlock`（✅ 2026-06-24 新增：分卷节点，含 title/level attrs，isolating: true，对应第Ⅰ卷/第Ⅱ卷层级）
  - 类型（`editor/exam-rich-editor-types.ts`）：`RichQuestionType`（`single_choice | multiple_choice | judgment | text | composite`）/ `RichQuestionContent`（题干文本+选项+填空+图片+子题+正确答案）/ `EditorQuestion` / `EditorStructureNode`（✅ 2026-06-24 升级：section|group|question 三级递归，section 含 title/level/children，group 含 title/instruction/children）/ `EditorDoc`（{ title, questions, structure }）/ `EditorJSONContent`（JSONContent 别名）
  - 转换工具：`editorDocToStructure`（`editor/editor-to-structure.ts`，Tiptap JSONContent → EditorDoc，供 `createExamFromRichEditorAction`/`updateExamFromRichEditorAction` 持久化使用）/ `structureToEditorDoc`（`editor/structure-to-editor.ts`，EditorDoc → Tiptap JSONContent，✅ 2026-06-24 升级支持 section 节点回填，供编辑器回填已保存试卷使用）/ `examNodesToEditorDoc`（✅ 2026-06-24 新增：`editor/exam-nodes-to-editor-doc.ts`，BuildExam 的 `ExamNode[]` + `Question[]` → EditorDoc 桥梁函数，递归转换 section/group/question，供 edit-rich 页面加载已有试卷到富文本编辑器）
  - Barrel 导出：`editor/index.ts` 聚合导出上述所有组件、扩展、类型与转换函数
- Components（V3-8 新增）：`ExamAnalyticsDashboard`（考试分析仪表盘：汇总卡片+分数段分布+逐题分析表）
- Components（✅ 2026-06-24 新增）：`ExamRichForm`（富文本编辑器试卷创建/编辑表单，使用 `ResizablePanel` 左编辑右预览布局，集成 `autoMarkExamAction` 一键 AI 标记；✅ 2026-06-24 升级支持 `mode="create"|"edit"` 双模式：create 模式调用 `createExamFromRichEditorAction` 保存草稿，edit 模式调用 `updateExamFromRichEditorAction` 更新已有试卷，编辑模式隐藏科目/年级/难度等字段并显示"Back to Build"按钮）
- Components（✅ P3 新增记录）：`ExamPreview`（试卷结构预览组件，从 exam-rich-form.tsx 提取，渲染 section/group/question 三级嵌套；P3 修改：题目图片 `<img>` → `next/image` `<Image>`，新增 `next/image` 导入；`useTranslations('examHomework')` 国际化）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`questions`（✅ P0-1 已修复：通过 data-access.createQuestionWithRelations）、`classes`（✅ P0-2 已修复：通过 data-access.getClassGradeIdsByClassIds）、`school`（✅ P1-1 已修复：通过 school data-access.getSubjectOptions/getGradeOptions）、`homework`（V3-8 新增：stats-service 通过 `homework/data-access.getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId` 获取作业与提交数据，合理跨模块调用）
- 被依赖：`homework`（通过 sourceExamId 外键，合理）、`dashboard`（通过 data-access，P0-4 已修复）、`proctoring`（✅ P1-1 已修复：通过 exams data-access）、`diagnostic`（✅ P1-1 已修复：通过 exams data-access）、`grades`（✅ 2026-06-24 新增：通过 data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用）

**已知问题**：
- ✅ P0-1 已修复：~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`，通过 ID 映射保持 structure 引用一致
- ✅ P0-2 已修复：~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds`
- ✅ P1-1 已修复：~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions`
- ✅ P1-2 已修复：~~`actions.ts` 832 行（超 800 建议），多处直接 DB 操作~~ DB 操作已下沉到 data-access，actions.ts 现 691 行
- ✅ P0-4 已修复（2026-06-24）：~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData
- ✅ actions.ts 拆分（2026-06-24）：~~`actions.ts` 1525 行（超 1000 硬上限）~~ 拆分为 4 个文件：actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action)，actions.ts re-export 保持向后兼容
- ✅ P0-8 已修复：~~`data-access.ts` 1036 行（超 1000 硬上限）~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口)，文末 re-export 保持向后兼容
- ⚠️ P1：`ai-pipeline.ts` 857 行（超 800 建议），混合 4 类职责
- ✅ P2 已修复：`ai-pipeline.ts` 中 3 处非空断言清理（`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`）
- ✅ V3-5/V3-8/V3-12：`exam-actions.tsx` 增强：V3-5 角色化菜单（按角色显示不同操作项）、V3-8 新增 analytics 菜单项（BarChart3 图标，跳转 `/teacher/exams/[id]/analytics`）、V3-12 移动端触摸目标尺寸优化

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 633 | 10 个核心 Server Action（✅ 2026-06-24 拆分：从 1525 行降至 633 行；re-export autoMarkExamAction/createExamFromRichEditorAction/updateExamFromRichEditorAction 保持向后兼容；P0-4 已修复：无直接 DB 操作） |
| `actions-helpers.ts` | 96 | ✅ 2026-06-24 新增：跨文件共享辅助函数（getStringValue/getBoolValue/parseExamModeConfig/failState/successState/invalidFormState/prepareExamCreateContext） |
| `actions-rich-editor.ts` | 250 | ✅ 2026-06-24 新增：富文本编辑器 Server Action（createExamFromRichEditorAction/updateExamFromRichEditorAction）+ 共享辅助 convertStructureNode + RichExamCreateSchema/RichExamUpdateSchema。P0-4 修复：updateExamFromRichEditorAction 调用 data-access.updateExamWithRichEditorData 不再直访 DB |
| `ai-pipeline/auto-mark.ts` | 356 | ✅ 2026-06-24 新增：autoMarkExamAction Server Action + 纯转换辅助（buildTiptapDocFromAiResponse/splitByDottedTexts/extractTitleFromAiResponse/AutoMarkSchema/AutoMarkResult）。从 actions.ts 迁出 |
| `ai-pipeline.ts` | 857 | AI 出题管线（超限） |
| `data-access.ts` | 542 | 考试 CRUD（✅ 2026-06-24 拆分：跨模块接口迁移至 data-access-cross-module.ts，原 1036 行降至 542 行；含 P1-2 写/查询函数，P0-1/P0-2 已修复：通过 questions/classes data-access 跨模块通信；✅ P0-4 修复：新增 updateExamWithRichEditorData 将富文本编辑器更新事务下沉；导出 parseExamMeta/getString/getNumber/toExamStatus 供 cross-module 复用；文末 re-export 跨模块接口保持向后兼容） |
| `data-access-cross-module.ts` | 462 | ✅ 2026-06-24 新增：跨模块查询接口（供 homework/grades/proctoring/diagnostic/dashboard/lesson-preparation 等模块调用，避免直查 exams/examSubmissions 表）。包含 getExamsDashboardStats/getExamIdsByGradeIds/getExamWithQuestionsForHomework/getExamSubjectIdMap/getExamTitleById/getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionWithAnswers/getExamSubmissionsForExam/addExamQuestions/getExamsByGradeId/getExamsForGradeEntry/getExamForGradeEntry，标记 `server-only`，从 data-access.ts 导入 parseExamMeta/getString/getNumber/toExamStatus 复用 |
| `stats-service.ts` | - | V3-8 新增：考试分析数据聚合（`getExamAnalytics` + `ExamAnalyticsSummary` 类型） |
| `types.ts` | 50+ | 类型定义（2026-06-24 新增：ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用。注：`AutoMarkResult`/`AutoMarkSchema` 已迁移至 `ai-pipeline/auto-mark.ts`，`RichExamCreateSchema`/`RichExamUpdateSchema` 已迁移至 `actions-rich-editor.ts`） |
| `editor/index.ts` | 23 | 2026-06-24 新增：富文本编辑器 barrel 导出（聚合组件/扩展/类型/转换函数） |
| `editor/exam-rich-editor.tsx` | - | 2026-06-24 新增：ExamRichEditor 富文本编辑器主体（forwardRef + ExamRichEditorHandle） |
| `editor/selection-toolbar.tsx` | - | 2026-06-24 新增：SelectionToolbar 浮动选择工具栏 |
| `editor/exam-rich-editor-types.ts` | 42 | 2026-06-24 新增：富文本编辑器类型定义（RichQuestionType/RichQuestionContent/EditorQuestion/EditorStructureNode/EditorDoc/EditorJSONContent） |
| `editor/editor-to-structure.ts` | - | 2026-06-24 新增：editorDocToStructure（Tiptap JSONContent → EditorDoc） |
| `editor/structure-to-editor.ts` | - | 2026-06-24 新增：structureToEditorDoc（EditorDoc → Tiptap JSONContent） |
| `editor/extensions/index.ts` | 5 | 2026-06-24 新增：Tiptap 扩展 barrel 导出 |
| `editor/extensions/dotted-mark.ts` | - | 2026-06-24 新增：DottedMark 加点字标记扩展 |
| `editor/extensions/blank-node.tsx` | - | 2026-06-24 新增：BlankNode 填空空位节点扩展 |
| `editor/extensions/image-node.ts` | - | 2026-06-24 新增：ImageNode 图片节点扩展 |
| `editor/extensions/group-block.tsx` | - | 2026-06-24 新增：GroupBlock 大题分组节点扩展 |
| `editor/extensions/question-block.tsx` | - | 2026-06-24 新增：QuestionBlock 题目块节点扩展 |
| `hooks/use-exam-preview.ts` | 68 | 预览 Hook 主组合器（✅ 2026-06-24 拆分：从 318 行降至 68 行，组合 state/tasks/rewrite 三个子 Hook，保留 handlePreview 编排逻辑，返回 API 与拆分前完全一致，调用方 exam-form.tsx 无需修改） |
| `hooks/use-exam-preview-state.ts` | 67 | ✅ 2026-06-24 新增：预览状态子 Hook（10 个 useState + updatePreviewQuestionNode/updateSelectedQuestionFromAi/applyPreviewResult 三个更新函数） |
| `hooks/use-exam-preview-tasks.ts` | 80 | ✅ 2026-06-24 新增：预览后台任务子 Hook（PQueue 并发队列 + localStorage 持久化恢复 + handleBackgroundPreview/handleOpenPreviewTask） |
| `hooks/use-exam-preview-rewrite.ts` | 79 | ✅ 2026-06-24 新增：预览 AI 重写子 Hook（handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction） |
| `components/exam-preview-utils.ts` | 293 | 预览纯函数工具集（buildPreviewNodes/parseEditableContent/flattenPreviewQuestions/findPreviewQuestionNode/updatePreviewQuestionNodeInList/buildPreviewSignature/buildPreviewPayload/buildPreviewRequestData + ✅ 2026-06-24 新增 5 个：buildTaskFormValues/isPreviewBackgroundTask/persistPreviewTasksToStorage/parseStoredPreviewTasks/executeBackgroundPreviewTask，从 use-exam-preview-tasks Hook 提取以控制行数） |
| `components/exam-preview.tsx` | - | 试卷结构预览组件（use client）：从 exam-rich-form.tsx 提取，渲染试卷富文本编辑器的结构预览，支持分卷(section)/大题分组(group)/题目(question)三级嵌套递归渲染。✅ P3 修改：题目块中图片将原生 `<img>` 替换为 `next/image` 的 `<Image>` 组件（自动优化、懒加载、防布局偏移），新增导入 `next/image`；使用 `useTranslations('examHomework')` 国际化 section/group 标题与统计文案 |
| `utils/normalize-structure.ts` | 57 | v3 新增：exam.structure 运行时校验与归一化（从 build/page.tsx 提取） |
| `components/exam-analytics-dashboard.tsx` | - | V3-8 新增：考试分析仪表盘组件 |
| `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强：角色化菜单+analytics 链接+移动端触摸优化 |
| `components/exam-rich-form.tsx` | - | 2026-06-24 新增：富文本编辑器试卷创建/编辑表单（ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿）。✅ 2026-06-24 拆分后：import 路径直连 `ai-pipeline/auto-mark.ts`（autoMarkExamAction）和 `actions-rich-editor.ts`（createExamFromRichEditorAction/updateExamFromRichEditorAction），支持 create/edit 双模式 |
| `components/exam-boundaries.tsx` | 189 | ✅ 2026-06-25 新增（P2-3）：模块统一边界组件。导出 `ExamErrorBoundary`（薄包装 SectionErrorBoundary，固定 examHomework 命名空间）、`ExamEmptyState`（i18n 键驱动的空状态）、`ExamSkeleton`（list/card/detail/analytics/editor 五种变体）。复用 shared 层 EmptyState/Skeleton/SectionErrorBoundary，避免每页重复实现。新增 i18n 键 `exam.error.boundaryTitle/boundaryDescription/retry` |
| `components/*` | 19+ 文件 | 考试表单/组卷/预览/分析组件 |
| `services/exam-service-port.ts` | 95 | ✅ 2026-06-25 新增（P2-4）：模块对内的服务端口契约。导出 `ExamServicePort` 接口（listExams/getExam/createExam/updateExam/deleteExam/duplicateExam/autoMarkExam/getExamPreview/getAnalytics/getSubjects/getGrades 共 12 个方法）+ `ExamDetail`/`ExamCreateInput`/`ExamUpdateInput` 类型。区别于 shared 层的 ExamHomeworkServicePort（跨模块调用方契约，单例注册），本接口面向模块内部组件，通过 React Context 注入 |
| `services/exam-service-context.tsx` | 72 | ✅ 2026-06-25 新增（P2-4）：ExamServicePort 的 React Context 注入点。导出 `ExamServiceProvider`（Provider 组件）、`useExamService`（消费者 Hook，未注入时抛错）、`useTryExamService`（测试用，未注入时返回 null）。当前为骨架，具体实现将在 P2-6+ 落地 |
| `services/index.ts` | 23 | ✅ 2026-06-25 新增：services 子模块 barrel 导出 |
| `config/exam-widgets.ts` | 192 | ✅ 2026-06-25 新增（P2-5）：配置驱动的 Widget 布局。导出 `ExamWidgetConfig`/`ExamWidgetLayoutConfig` 类型、四个角色的默认配置（TEACHER_EXAM_WIDGET_CONFIG/ADMIN_EXAM_WIDGET_CONFIG/STUDENT_EXAM_WIDGET_CONFIG/PARENT_EXAM_WIDGET_CONFIG）、`getExamWidgetConfig(role)` 与 `getWidgetsBySlot(config, slot)` 工具函数。与 dashboard/config/widget-configs.ts 模式对齐 |

**已知问题**：
- ✅ P0-1 已修复：~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`，通过 ID 映射保持 structure 引用一致
- ✅ P0-2 已修复：~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds`
- ✅ P1-1 已修复：~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions`
- ✅ P1-2 已修复：~~`actions.ts` 832 行（超 800 建议），多处直接 DB 操作~~ DB 操作已下沉到 data-access，actions.ts 现 691 行
- ✅ P0-4 已修复（2026-06-24）：~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData
- ✅ actions.ts 拆分（2026-06-24）：~~`actions.ts` 1525 行（超 1000 硬上限）~~ 拆分为 4 个文件：actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action)，actions.ts re-export 保持向后兼容
- ✅ P0-8 已修复：~~`data-access.ts` 1036 行（超 1000 硬上限）~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口)，文末 re-export 保持向后兼容
- ⚠️ P1：`ai-pipeline.ts` 857 行（超 800 建议），混合 4 类职责
- ✅ P2 已修复：`ai-pipeline.ts` 中 3 处非空断言清理（`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`）
- ✅ V3-5/V3-8/V3-12：`exam-actions.tsx` 增强：V3-5 角色化菜单（按角色显示不同操作项）、V3-8 新增 analytics 菜单项（BarChart3 图标，跳转 `/teacher/exams/[id]/analytics`）、V3-12 移动端触摸目标尺寸优化
- ✅ 2026-06-25 P2 落地（exams 审计报告实施）：
  - ✅ P2-1：在 `editor/exam-rich-editor-types.ts` 新增 `isRichQuestionType`/`isStandaloneQuestionType`/`toRichQuestionType`/`toStandaloneQuestionType` 类型守卫；`editor/editor-to-structure.ts` 与 `editor/exam-nodes-to-editor-doc.ts` 改用 `toRichQuestionType`；`actions-rich-editor.ts` 两处 `as "single_choice"|"multiple_choice"|"text"|"judgment"` 断言改为 `toStandaloneQuestionType` 安全收窄（消除 4 处 `as` 违规）
  - ✅ P2-3：新增 `components/exam-boundaries.tsx`（ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元，5 种骨架变体）
  - ✅ P2-4：新增 `services/exam-service-port.ts` + `services/exam-service-context.tsx`（ExamServicePort 接口 + Context 注入骨架，与 shared 层 ExamHomeworkServicePort 区分定位）
  - ✅ P2-5：新增 `config/exam-widgets.ts`（四角色 Widget 配置 + getExamWidgetConfig/getWidgetsBySlot）

---

## 2.3 homework（作业模块）

**职责**：作业全生命周期（创建/发布/作答/批改/分析）。

**导出函数**：
- Actions：`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`（V3-7 新增：批量自动批改，HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业）/ `getScansAction`（✅ 2026-06-24 新增：获取某次提交的所有答题扫描图，扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId；支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交，学生 HOMEWORK_SUBMIT 仅可访问自己的提交）/ `deleteScanAction`（✅ 2026-06-24 新增：删除答题扫描图，仅允许提交者本人删除且仅在提交状态为 started 时允许，调用 `files/data-access.deleteFileAttachment` 删除 DB 记录）/ `getExcellentSubmissionsAction`（✅ P3-1 新增：查询优秀作业列表，HOMEWORK_GRADE 权限，按 ctx.dataScope 过滤，调用前埋点 `homework.excellent_viewed`）/ `remindUnsubmittedAction`（✅ P3-2 新增：催交提醒，HOMEWORK_GRADE 权限，查询未提交学生后通过动态 import 调用 `notifications/data-access.createNotification` 创建 type=homework/priority=high 站内通知，Promise.allSettled 容错统计，埋点 `homework.remind_unsubmitted`）（✅ P1-2 已修复：actions 层不再直接访问 DB，全部下沉到 data-access/data-access-write）
- Data-access：`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`（已迁移至 users 模块 `getCurrentStudentUser`，此处为 re-export 向后兼容）/ `toQuestionContent` / `getAssignmentMaxScoreById`（后两者供 stats-service 使用；`isRecord` 已于 2026-06-24 迁移至 `@/shared/lib/type-guards` 统一复用）/ `getHomeworkAssignmentsByExamId`（V3-8 新增：按考试 ID 查作业+目标/提交/批改计数）/ `getGradedSubmissionsByExamId`（V3-8 新增：按考试 ID 查已批改提交，按学生去重）/ `getStudentSubmissionResult`（V3-9 新增：查学生指定作业最新提交，用于结果页）/ `getStudentExamResults`（V3-11 新增：查学生考试结果列表，供家长端展示）/ `getExcellentSubmissions`（✅ P3-1 新增：查优秀作业列表，按得分率过滤 minPercentage 默认 80，同一学生取最高分，按得分率降序，支持 scope 过滤）/ `getUnsubmittedStudents`（✅ P3-2 新增：查未提交作业的学生列表，对比 targets 与已提交学生集合返回差集，供催交提醒使用）
- Data-access-classes：`getAssignmentIdsForStudents` / `getHomeworkAssignmentsWithSubject` / `getHomeworkAssignmentsByIds` / `getAssignmentTargetCounts` / `getHomeworkSubmissionsForStudents` / `getPublishedHomeworkAssignmentsWithSubject` / `getHomeworkSubmissionsForAssignments`（P0-7 新增，供 classes 模块跨模块调用；✅ P1-1 已修复：内部不再直查 exams/subjects 表，改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`）
- Data-access-write：11 个写操作函数（P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`）
- Stats-service：`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`（从 data-access.ts re-export 以保持向后兼容）
- Components（V3-7/V3-9 新增）：`HomeworkBatchGradingView`（批量批改视图：勾选+一键批改+toast 反馈）/ `HomeworkSubmissionResult`（提交后即时反馈：分数汇总+对错分布+错题预览）
- Components（✅ 2026-06-24 新增扫描图相关）：`ScanUploader`（学生扫描图上传组件，调用 /api/upload 上传图片，targetType="homework"，支持增删/排序，导出 `ScanImage` 类型）/ `ScanImageViewer`（扫描图查看器，支持翻页/缩放/旋转/全屏，用于阅卷式批改时查看学生答题图片）/ `HomeworkScanGradingView`（教师阅卷式批改视图，使用 `ResizablePanel` 左侧题目+批改表单、右侧扫描图查看器，集成 `getScansAction` 拉取扫描图、`gradeHomeworkSubmissionAction` 批改，支持上一份/下一份提交快速切换）
- Components（✅ P3-1 新增）：`ExcellentSubmissions`（优秀作业展示 async 服务端组件，按得分率降序，前三名金/银/铜徽章，迟交 Clock 标注，仅展示姓名不展示学号保护隐私，直接调用 `getExcellentSubmissions` 避免多余 action 往返）/ `ExcellentSubmissionsWithBoundary`（带 `SectionErrorBoundary` + `Suspense` 包裹版本，提供 Skeleton 骨架屏与局部错误降级，避免本区块故障影响整个作业详情页）
- Types（✅ 2026-06-24 新增）：`ScanAttachment`（扫描图附件结构 { fileId, url, filename, originalName, page }，由 `getScansAction` 返回）/ `ScanImage`（扫描图 UI 结构，由 `ScanUploader` 导出供 `ScanImageViewer` 复用）
- Types（✅ P3-1 新增）：`ExcellentSubmissionItem`（优秀作业展示项 { submissionId, assignmentId, assignmentTitle, studentName, totalScore, maxScore, percentage, submittedAt, isLate }，percentage 为得分率 0-100，isLate 标注迟交但质量优秀样例）/ `ExcellentSubmissionQuery`（优秀作业查询参数 { assignmentId, minPercentage?, limit? }，minPercentage 默认 80，limit 默认 10）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`exams`（✅ P1-1 已修复：通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule）、`classes`（✅ P1-1 已修复：通过 classes data-access.getStudentIdsByClassId 等 7 个函数）、`school`（✅ P1-1 已修复：通过 school data-access.getSubjectOptions/getSubjectNameMapByIds）、`users`（✅ P1-1 已修复：通过 users data-access.getUserWithRole/getUserNamesByIds）、`files`（✅ 2026-06-24 新增：`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录，合理跨模块调用）、`notifications`（✅ P3-2 新增：`remindUnsubmittedAction` 通过动态 import 调用 `notifications/data-access.createNotification` 为未提交学生创建催交通知，合理跨模块调用）
- 被依赖：`dashboard`（通过 data-access，合理）、`parent`（通过 data-access，合理；V3-11 新增 `getStudentExamResults` 供 parent 调用）、`classes`（✅ P0-7 已修复：classes 通过 `homework/data-access-classes` 获取作业数据，不再反向直查 homework/exams 表）、`exams`（V3-8 新增：`exams/stats-service.getExamAnalytics` 调用 `getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId`，合理跨模块调用）

**已知问题**：
- ✅ P0 已解决：`data-access.ts` 已拆分至 598 行（原 1038 行超 1000 硬上限），统计函数迁移至 `stats-service.ts`
- ✅ P0 已解决：`getStudentDashboardGrades` 排名计算逻辑迁移至 `stats-service.ts`
- ✅ P0 已解决：`getHomeworkAssignmentAnalytics` 错误率统计逻辑迁移至 `stats-service.ts`
- ✅ P0-7 已修复：新增 `data-access-classes.ts`，将 classes 模块对 homework/exams 表的直查封装为 homework 模块的导出函数，恢复三层架构
- ✅ P1-1 已修复：~~5 处直查 `exams` 表~~ 改为调用 `exams/data-access.getExamIdsByGradeIds` / `getExamSubjectIdMap` / `getExamWithQuestionsForHomework`；`data-access-classes.ts` 内部 JOIN exams+subjects 也改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`，彻底消除跨模块直查
- ✅ P0-竞品已修复：新增 `hooks/use-exam-countdown.ts` 倒计时 Hook（对标智学网/猿题库），`homework-take-view.tsx` 集成限时/监考模式倒计时显示与到时自动提交；`data-access.ts` 的 `getStudentHomeworkTakeData` 新增 `examModeConfig` + `startedAt` 字段供客户端计算截止时间
- ✅ P1-2 已修复：~~`actions.ts` 多处直接 DB 操作（`createHomeworkAssignmentAction` 157 行）~~ DB 操作已下沉到 `data-access-write.ts`，actions.ts 现 239 行
- ✅ V3-7：新增 `batchAutoGradeSubmissionsAction` + `batchAutoGradeSubmissions` + `HomeworkBatchGradingView`，提交列表页接入批量批改
- ✅ V3-8：新增 `getHomeworkAssignmentsByExamId` + `getGradedSubmissionsByExamId`，供 exams 模块跨模块调用
- ✅ V3-9：新增 `getStudentSubmissionResult` + `HomeworkSubmissionResult` + 路由 `/student/learning/assignments/[assignmentId]/result`，`homework-take-view.tsx` 提交后跳转结果页
- ✅ V3-12：`homework-take-view.tsx` 移动端触摸目标尺寸优化
- ✅ 2026-06-23 审计修复：data-access 层 4 个查询函数新增 `scope?: DataScope` 参数（`getHomeworkAssignments`/`getHomeworkAssignmentById`/`getHomeworkSubmissions`/`getHomeworkAssignmentReviewList`），教师仅查看自己创建的作业，学生/家长仅查看相关作业；Server Action catch 块改用 `handleActionError` 统一错误处理
- ✅ 2026-06-24 i18n 完成：11 个文件（6 个客户端组件 + 3 个服务端页面 + 2 个已有 `t` 的页面）全部接入 `next-intl`，硬编码中文/英文文案替换为 `examHomework.homework.*` 翻译键（zh-CN + en 双语）；客户端组件使用 `useTranslations("examHomework")`，服务端页面使用 `getTranslations("examHomework")`
- ✅ 2026-06-25 审计修复 P0：13 个缺失的 `loading.tsx`/`error.tsx` 路由文件全部补全（teacher/homework 根 + assignments + submissions + submissions/[submissionId] + scan-grading；student/learning/assignments + result）；`homework-take-view.tsx` 拆分为 5 个组件（`HomeworkTakeView` 主容器 396 行 + `HomeworkTakeSidebar` 侧边栏 + `HomeworkTakeConfirmDialog` 提交确认对话框）；`schema.ts` 错误消息改为 i18n 键 `homework.form.error.*`，Server Action 通过 `translateZodErrors` 翻译后返回 `fieldErrors`；`ScanAttachment` 类型从 `actions.ts` 迁移至 `types.ts`
- ✅ 2026-06-25 审计修复 P1：`homework-grading-view.tsx` 删除本地 wrapper 函数（`isAutoGradable`/`applyAutoGrades`/`getCorrectnessState`/`extractQuestionText`），直接调用 `lib/question-content-utils`；`data-access.ts` 末尾 re-export（`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`/`getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`）已删除，调用方改为直接 import 子模块；`teacher/homework/assignments/[id]/page.tsx` Analytics 区块接入 `SectionErrorBoundary + Suspense + Skeleton`；`getScansAction` 中的 DB 查询下沉到新建的 `data-access-scans.ts::getScansBySubmissionId`
- ✅ 2026-06-25 审计修复 P2：`startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `deleteScanAction` 补全 `trackExamEvent` 埋点（`homework.started` / `homework.answer_saved` / `homework.scan_deleted`）；`extractQuestionText` 从 `homework-grading-view.tsx` 迁移到 `lib/question-content-utils.ts`

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 716 | 作业 CRUD + 批改（✅ 2026-06-25：删除末尾 re-export，调用方改为直接 import 子模块；✅ P3-1 新增 `getExcellentSubmissions`：按得分率过滤优秀作业，同一学生取最高分；✅ P3-2 新增 `getUnsubmittedStudents`：查询未提交作业学生供催交提醒使用） |
| `data-access-write.ts` | 455 | 作业写操作（P1-2 新增 10 个写函数从 actions 下沉；V3-7 新增 `batchAutoGradeSubmissions`） |
| `data-access-classes.ts` | 285 | 跨模块查询封装（P0-7 新增；✅ P1-1 已修复：内部通过 exams/school data-access 获取考试科目信息，不再直查 exams/subjects 表） |
| `data-access-student.ts` | 293 | 学生端只读查询（✅ 2026-06-25：从 data-access.ts 拆出 `getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`） |
| `data-access-exam-cross.ts` | 119 | 考试跨模块查询（`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`） |
| `data-access-scans.ts` | 44 | ✅ 2026-06-25 审计新增：扫描图查询（`getScansBySubmissionId`，从 actions.ts 下沉的 DB 查询） |
| `data-access-error-collection.ts` | 121 | 错题采集查询 |
| `data-access-utils.ts` | 8 | 内部工具函数 |
| `stats-service.ts` | 414 | 统计分析（教师趋势/作业分析/学生仪表盘成绩） |
| `actions.ts` | 688 | 10 个 Server Action（P1-2 已修复，无直接 DB 操作；✅ 2026-06-25：`ScanAttachment` 类型迁移至 types.ts；schema.ts 错误消息改为 i18n 键，新增 `translateZodErrors` 翻译辅助函数；补全 `trackExamEvent` 埋点；✅ P3-1 新增 `getExcellentSubmissionsAction`：查询优秀作业列表，HOMEWORK_GRADE 权限 + scope 过滤 + 埋点 `homework.excellent_viewed`；✅ P3-2 新增 `remindUnsubmittedAction`：催交提醒，调用 `notifications/data-access.createNotification` 创建站内通知，埋点 `homework.remind_unsubmitted`） |
| `types.ts` | 282 | 类型定义（✅ 2026-06-25：新增 `ScanAttachment` 接口，从 actions.ts 迁入；✅ P3-1 新增 `ExcellentSubmissionItem`/`ExcellentSubmissionQuery` 类型） |
| `schema.ts` | 63 | Zod 校验（✅ 2026-06-25：错误消息改为 i18n 键 `homework.form.error.*`） |
| `lib/question-content-utils.ts` | 321 | 题目内容工具函数（✅ 2026-06-25：`extractQuestionText` 从 grading-view 迁入） |
| `lib/question-content-utils.test.ts` | 415 | 工具函数单测 |
| `hooks/use-exam-countdown.ts` | 106 | P0-竞品新增：考试倒计时 Hook（每秒更新、紧急状态高亮、到时回调自动提交） |
| `hooks/use-debounced-auto-save.ts` | 182 | 答案防抖自动保存 Hook（localStorage 离线缓存） |
| `components/homework-take-view.tsx` | 396 | ✅ 2026-06-25 重构：从 520 行拆分为 3 个组件（主容器 + Sidebar + ConfirmDialog）；V3-9/V3-12：提交后跳转结果页+移动端触摸优化；P0-竞品：集成限时/监考模式倒计时（useExamCountdown）+ 到时自动提交 |
| `components/homework-take-sidebar.tsx` | 169 | ✅ 2026-06-25 新增：从 homework-take-view 拆出（自动保存状态/作业信息/截止时间/尝试次数/描述/进度网格/提交按钮） |
| `components/homework-take-confirm-dialog.tsx` | 62 | ✅ 2026-06-25 新增：从 homework-take-view 拆出（提交确认对话框，含未作答数量警告） |
| `components/homework-grading-view.tsx` | 472 | ✅ 2026-06-25：删除 4 个本地 wrapper 函数，直接调用 lib/question-content-utils |
| `components/homework-batch-grading-view.tsx` | 165 | V3-7 新增：批量批改视图（use client） |
| `components/homework-submission-result.tsx` | 224 | V3-9 新增：提交后即时反馈页 |
| `components/homework-scan-grading-view.tsx` | 262 | 2026-06-24 新增：教师阅卷式批改视图（ResizablePanel 左题目右扫描图 + 上一份/下一份切换） |
| `components/scan-uploader.tsx` | 254 | 2026-06-24 新增：学生扫描图上传组件（调用 /api/upload，targetType="homework"，导出 `ScanImage` 类型） |
| `components/scan-image-viewer.tsx` | 194 | 2026-06-24 新增：扫描图查看器（翻页/缩放/旋转/全屏） |
| `components/question-renderer.tsx` | 339 | 题目渲染器（支持单选/多选/判断/文本题型） |
| `components/excellent-submissions.tsx` | 206 | P3-1：优秀作业展示（服务端组件 + SectionErrorBoundary + Suspense + 骨架屏；✅ P3-5 修复：`getTranslations` 从 `next-intl/server` 导入） |
| `components/student-homework-review-view.tsx` | 206 | 学生作业回顾视图 |
| `components/homework-assignment-form.tsx` | 219 | 作业创建/编辑表单 |
| `components/assignment-filters.tsx` | 46 | 作业列表筛选器（use client）：基于 nuqs 的 URL 状态管理（✅ 2026-06-24 i18n：接入 `useTranslations`） |
| `components/homework-assignment-exam-content-card.tsx` | 29 | 作业考试内容卡片（✅ 2026-06-24 i18n：接入 `useTranslations`） |
| `components/homework-assignment-exam-preview-pane.tsx` | 39 | 作业考试预览面板（✅ 2026-06-24 i18n：接入 `useTranslations`） |
| `components/homework-assignment-exam-error-explorer.tsx` | 39 | 作业错误分析主组件 |
| `components/homework-assignment-exam-error-explorer-lazy.tsx` | 76 | 作业错误分析懒加载组件（✅ 2026-06-24 i18n：Fallback 接入 `useTranslations`） |
| `components/homework-assignment-question-error-detail-panel.tsx` | 132 | 题目错误详情面板（✅ 2026-06-24 i18n：接入 `useTranslations`，`formatAnswer` 移入组件内闭包以访问 `t`） |
| `components/homework-assignment-question-error-overview-card.tsx` | 89 | 题目错误概览卡片（✅ 2026-06-24 i18n：接入 `useTranslations`） |

---

## 2.4 questions（题库模块）

**职责**：题库管理（题目 CRUD、知识点关联、题型支持、教材→章节→知识点级联筛选）。

**导出函数**：
- Actions（11 个，全部调用 `requirePermission()`）：`getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` / `getTextbookOptionsAction`（✅ 审计新增：级联第一级）/ `getChapterOptionsAction(textbookId)`（✅ 审计新增：级联第二级）/ `getKnowledgePointOptionsByChapterAction(chapterId)`（✅ 审计新增：级联第三级）/ `deleteQuestionsBatchAction`（✅ 审计新增：批量删除）/ `exportQuestionsAction`（✅ 审计新增：导出）/ `importQuestionsAction`（✅ 审计新增：导入）
- Data-access：`getQuestions`（✅ 审计升级：支持 `textbookId`/`chapterId` 级联筛选参数，通过 textbooks data-access 获取知识点 ID 集合）/ `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions` / `getTextbookOptions`（✅ 审计新增）/ `getChapterOptions(textbookId)`（✅ 审计新增）/ `getKnowledgePointOptionsByChapter(chapterId)`（✅ 审计新增）/ `deleteQuestionsBatch`（✅ 审计新增：批量删除，事务原子性）/ `exportQuestions`（✅ 审计新增：导出，权限范围过滤）/ `importQuestions`（✅ 审计新增：批量导入，事务原子性）
- Types：`Question` / `QuestionWithRelations` / `GetQuestionsParams`（✅ 审计升级：新增 `textbookId`/`chapterId` 字段）/ `CreateQuestionInput` / `KnowledgePointOption` / `TextbookOption`（✅ 审计新增）/ `ChapterOption`（✅ 审计新增）/ `QuestionContent`（✅ 审计新增：类型守卫接口）/ `QuestionOption`（✅ 审计新增）/ `QUESTION_TYPE_I18N_KEY`（✅ 审计新增：题型 i18n 键映射）/ `DIFFICULTY_I18N_KEY`（✅ 审计新增：难度 i18n 键映射）
- Utils：`parseQuestionContent(raw)`（✅ 审计新增：类型守卫，替代 `as` 断言）/ `getQuestionPreview` / `getQuestionOptions` / `getQuestionText` / `trackQuestionEvent`（✅ 审计新增：埋点接口 no-op）/ `trackQuestionCreated` / `trackQuestionUpdated` / `trackQuestionDeleted` / `trackQuestionSearched`
- Components：`QuestionFilters`（✅ 审计重写：组合 QuestionCascadeFilter）/ `QuestionCascadeFilter`（✅ 审计新增：三级级联筛选，nuqs 管理 URL 状态）/ `QuestionDataTable`（✅ 审计重写：i18n + 集成 BatchOperations）/ `useQuestionColumns`（✅ 审计改为 Hook：因 useTranslations 是 Hook）/ `CreateQuestionButton`（✅ 审计重写：权限感知）/ `QuestionActions`（✅ 审计重写：权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览）/ `CreateQuestionDialog`（✅ 审计重写：拆分为 281 行）/ `KnowledgePointSelector`（✅ 审计新增：从 dialog 拆分）/ `OptionsEditor`（✅ 审计新增：从 dialog 拆分）/ `QuestionBankResultsClient`（✅ 审计新增：RSC 与客户端 Hook 桥接）/ `QuestionContentRenderer`（✅ 审计新增：题目结构化渲染）/ `BatchOperations`（✅ 审计新增：批量删除工具栏）/ `ImportExportButtons`（✅ 审计新增：JSON 导入导出）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`textbooks`（✅ 审计升级：通过 textbooks data-access 的 `getTextbooks` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` 实现级联筛选）
- 被依赖：`exams`（通过类型导入，合理）、`textbooks`（UI 组合，合理）、`error-book`（通过 data-access，合理）、`adaptive-practice`（通过 data-access，合理）

**已知问题**：
- ✅ P1-2 已修复：~~写操作函数错放在 `actions.ts`~~ 已下沉到 data-access
- ✅ P1-1 已修复：~~`getKnowledgePointOptionsAction` 直查 textbooks 模块表~~ 改为调用 textbooks data-access
- ✅ P2 已解决：~~`data-access.ts` 仅 129 行~~ 扩充至 260+ 行
- ✅ 审计 P0 已修复：~~按名字搜索等于无用~~ 新增教材→章节→知识点三级级联筛选（QuestionCascadeFilter + URL 状态同步）
- ✅ 审计 P0 已修复：~~前端权限硬编码~~ 全部改用 `usePermission().hasPermission()`
- ✅ 审计 P0 已修复：~~`as` 断言违反 TypeScript 严格模式~~ 提取 `parseQuestionContent` 类型守卫函数
- ✅ 审计 P0 已修复：~~i18n 完全缺失~~ 新增 `questions.json` 翻译文件（zh-CN + en），所有展示文本使用 i18n 键
- ✅ 审计 P0 已修复：~~缺 Error Boundary~~ 新增 `app/(dashboard)/teacher/questions/error.tsx`
- ✅ 审计 P1 已修复：~~create-question-dialog.tsx 453 行超规范~~ 拆分为 KnowledgePointSelector + OptionsEditor + 主对话框（281 行）
- ✅ 审计 P1 已修复：~~列定义为纯函数无法使用 useTranslations~~ 改为 `useQuestionColumns` Hook，通过 `QuestionBankResultsClient` 客户端桥接
- ✅ 审计 P1 已修复：~~缺监控埋点~~ 预留 `trackQuestionEvent` no-op 接口
- ✅ 审计 P2 已修复：~~Server Action 缺权限校验~~ 全部 8 个 Action 均调用 `requirePermission()`
- ✅ 审计 P2 已实施：~~仅支持教师角色~~ 新增 admin/questions 页面，复用全部组件，权限感知自动适配
- ✅ 审计 P2 已实施：~~无题目结构化预览~~ 新增 QuestionContentRenderer 组件（题干/选项/答案/解析，单选圆点/多选方框，正确答案高亮）
- ✅ 审计 P2 已实施：~~无批量操作~~ 新增 deleteQuestionsBatch data-access + deleteQuestionsBatchAction + BatchOperations 组件（事务原子性，权限感知）
- ✅ 审计 P2 已实施：~~无导入导出~~ 新增 exportQuestions/importQuestions data-access + exportQuestionsAction/importQuestionsAction + ImportExportButtons 组件（JSON 格式，权限范围过滤，导入预览确认）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 380+ | 11 个 Server Action（全部 requirePermission，含 3 个级联筛选 + 批量删除 + 导入导出） |
| `data-access.ts` | 600+ | 题目 CRUD + 知识点选项 + 级联筛选 + 批量删除 + 导入导出 |
| `schema.ts` | 18 | Zod 校验 |
| `types.ts` | 60+ | 类型定义（含 TextbookOption/ChapterOption/QuestionContent/i18n 键映射） |
| `utils/parse-content.ts` | 60+ | 类型守卫（parseQuestionContent + 便捷方法） |
| `utils/track-event.ts` | 30+ | 埋点接口（no-op 实现） |
| `components/question-cascade-filter.tsx` | 130+ | 三级级联筛选（nuqs URL 状态） |
| `components/question-filters.tsx` | 40+ | 筛选栏（组合 QuestionCascadeFilter） |
| `components/question-data-table.tsx` | 155+ | 数据表格（i18n + 集成 BatchOperations） |
| `components/question-columns.tsx` | 100+ | 列定义 Hook（useQuestionColumns） |
| `components/create-question-button.tsx` | 30+ | 创建按钮（权限感知） |
| `components/question-actions.tsx` | 200+ | 行操作（权限感知 + 类型守卫 + QuestionContentRenderer） |
| `components/create-question-dialog.tsx` | 281 | 创建对话框（组合 KnowledgePointSelector + OptionsEditor） |
| `components/knowledge-point-selector.tsx` | 80+ | 知识点选择器（从 dialog 拆分） |
| `components/options-editor.tsx` | 120+ | 选项编辑器（从 dialog 拆分） |
| `components/question-bank-results-client.tsx` | 30+ | RSC 与客户端 Hook 桥接 |
| `components/question-content-renderer.tsx` | 110+ | 题目结构化渲染（✅ 审计新增） |
| `components/batch-operations.tsx` | 110+ | 批量操作工具栏（✅ 审计新增） |
| `components/import-export-buttons.tsx` | 180+ | 导入导出组件（✅ 审计新增） |

---

## 2.5 textbooks（教材模块）— 标杆模块（data-access 层）

**职责**：教材与知识体系管理（教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱）。

**导出函数**：
- Actions（14 个，均为写操作；读操作由 RSC 页面直接调用 data-access）：`createTextbookAction` / `updateTextbookAction` / `deleteTextbookAction` / `createChapterAction` / `updateChapterContentAction` / `deleteChapterAction` / `reorderChaptersAction` / `createKnowledgePointAction` / `updateKnowledgePointAction` / `deleteKnowledgePointAction` / `getKnowledgePointsByChapterAction` / `getKnowledgeGraphDataAction`（✅ Task 7 新增：知识图谱数据查询，支持 structure/student-mastery/class-mastery 三种视图）/ `createPrerequisiteAction`（✅ Task 7 新增：声明前置依赖，含循环检测）/ `deletePrerequisiteAction`（✅ Task 7 新增：删除前置依赖）
- Data-access：`getTextbooks` / `getTextbookById` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` / `createTextbook` / `updateTextbook` / `deleteTextbook` / `createChapter` / `updateChapterContent` / `deleteChapter` / `createKnowledgePoint` / `updateKnowledgePoint` / `deleteKnowledgePoint` / `reorderChapters` / `getTextbooksDashboardStats` / `getKnowledgePointOptions`（跨模块接口，供 questions 调用）/ `getTextbooksWithScope`（P1-1 新增：按数据范围获取教材列表，学生端强制按年级过滤）/ `verifyChapterBelongsToTextbook`（P0-4 新增：资源归属校验）/ `verifyKnowledgePointBelongsToTextbook`（P0-4 新增：资源归属校验）/ `verifyKnowledgePointBelongsToChapter`（P0-4 新增：校验知识点是否属于指定章节）/ `createPrerequisite`（✅ Task 7 新增：创建前置依赖）/ `deletePrerequisite`（✅ Task 7 新增：删除前置依赖）/ `getPrerequisiteEdgesForTextbook`（✅ Task 7 新增：获取教材下所有前置依赖边，用于循环检测）/ `getSubjectLabelKey` / `getGradeLabelKey`（i18n 标签键）
- Data-access-graph（✅ Task 5 新增，图谱只读查询）：`getKnowledgePointsWithRelations`（知识点+依赖+题目数聚合查询）/ `getStudentKpMastery`（学生掌握度）/ `getClassKpMastery`（班级平均掌握度）/ `getPrerequisitesForKp` / `getSuccessorsForKp`
- Constants（✅ 新增）：`SUBJECTS` / `GRADES` / `SUBJECT_COLORS` / `getSubjectColor` / `getSubjectLabelKey` / `getGradeLabelKey`
  - ✅ v1 测试修复：`SUBJECTS` 新增 `Chinese` 学科、`GRADES` 新增 `Grade 1`/`Grade 2`，与 seed 数据对齐
  - ✅ v1 测试修复：`SUBJECT_COLORS` 新增 `Chinese` 主题色（rose）
- Utils（✅ 新增，纯函数 + 单测）：`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`（Task 4 新增：循环依赖检测，DFS 算法）/ `findChapterById`（V3 续审计新增：递归查找章节树中指定 ID 的章节，替代各模块本地重复实现）
- Graph-layout（✅ 新增，纯函数 + 单测）：`computeGraphLayout`
- Components（✅ Task 13 新增）：`GraphNodeDetailPanel` — 知识图谱节点详情侧边面板，展示知识点名称/描述/掌握度/关联题目/前置后置知识点，支持跳转与前置关系增删
- Analytics（✅ 新增）：`TextbookAnalytics` / `TextbookAnalyticsProvider` / `useTextbookAnalytics`

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`@xyflow/react`（知识图谱可视化）、`@dagrejs/dagre`（图谱分层布局算法）
- 被依赖：`questions`（✅ P1-1 已修复：通过 textbooks data-access）、`exams`（通过类型）、`dashboard`（通过 data-access，P0-4 已修复）
- ✅ UI 层跨模块依赖已解耦：`textbooks/components/knowledge-point-dialogs.tsx` 不再直接 import questions 模块，改为通过 render prop 注入创建题目入口

**已知问题**：
- ✅ 无跨模块 DB 访问（data-access 层）
- ✅ actions 层编排模式标杆（权限校验 → 调用 data-access → revalidatePath）
- ✅ data-access 层职责单一
- ✅ P2 已修复：`data-access.ts` 中 `byId.get(pid)!.children.push` 非空断言清理为安全守卫；`or(...)!` 非空断言清理为条件 push
- ✅ P0 跨模块 UI 依赖已修复：`knowledge-point-dialogs.tsx` 通过 render prop 解耦，不再直接 import questions 模块
- ✅ P0 前端权限硬编码已修复：改用 `usePermission().hasPermission()`
- ✅ P0 i18n 已基本接入：`chapter-sidebar-list.tsx` / `actions.ts` / `section-error-boundary.tsx`（默认值改英文）均已接入 next-intl
- ✅ P1 Server Action 资源归属校验已修复：新增 `verifyChapterBelongsToTextbook` / `verifyKnowledgePointBelongsToTextbook`
- ✅ P1 data-access 数据范围过滤已修复：新增 `getTextbooksWithScope`，学生端强制按年级过滤
- ✅ P1 Error Boundary 已补齐：新增 `section-error-boundary.tsx`
- ✅ P1 知识点列表/弹窗重复实现已清理：移除无调用方的 `knowledge-point-panel.tsx`
- ✅ P1 学科/年级选项统一：抽取到 `constants.ts`（`SUBJECTS` / `GRADES` / `SUBJECT_COLORS`）
- ✅ v1 测试修复：`textbook-reader.tsx` 移除 `SheetTrigger`（错误置于 `Sheet` 外部导致 `DialogTrigger must be used within Dialog`），改用受控 `Button` + `onClick` 打开移动端抽屉
- ✅ v1 测试修复：新增 `teacher-textbook-reader.tsx` 客户端包装组件，解决 Server Component 向 Client Component 传递函数 prop（`renderQuestionCreator`）违反 Next.js 序列化约束的问题
- ✅ v1 测试修复：`scripts/seed.ts` 教材 subject/grade 改为英文 value（与 `constants.ts` 对齐），消除 i18n `MISSING_MESSAGE` 错误
- ✅ P1 纯逻辑已导出并补单测：新增 `utils.ts` / `graph-layout.ts` 及对应 `.test.ts`
- ✅ v3 审计修复（2026-06-24）：学生端两个页面添加 `requirePermission(Permissions.TEXTBOOK_READ)` 显式权限校验
- ✅ v3 审计修复（2026-06-24）：`getKnowledgeGraphDataAction` 添加学生年级 scope 过滤，防止跨年级越权查看图谱数据
- ✅ v3 审计修复（2026-06-24）：`getKnowledgeGraphDataAction` 的 class-mastery 模式限制为教师/管理员使用
- ✅ v3 审计修复（2026-06-24）：`deleteChapter` 级联删除 `knowledgePointPrerequisites` 孤儿记录，保证数据完整性
- ✅ v3 审计修复（2026-06-24）：`knowledge-graph.tsx` 前置依赖操作添加 try/catch/finally，防止 loading 状态卡死
- ✅ v3 审计修复（2026-06-24）：`textbook-reader.tsx` `onCreateKnowledgePoint` 添加 try/finally
- ✅ v3 审计修复（2026-06-24）：`section-error-boundary.tsx` 添加 `componentDidCatch` 错误日志记录 + `role="alert"` a11y
- ✅ v3 审计修复（2026-06-24）：4 个 error.tsx 抽取为共享 `RouteError` 组件，消除重复代码
- ✅ v3 审计修复（2026-06-24）：所有 loading.tsx 添加 `role="status"` / `aria-busy` a11y 标记
- ✅ v3 审计修复（2026-06-24）：`chapter-sidebar-list.tsx` 章节项添加 `role="button"` / `tabIndex` / `onKeyDown` 键盘支持
- ✅ v3 审计修复（2026-06-24）：`graph-node-detail-panel.tsx` 图标按钮添加 `aria-label`
- ✅ v3 审计修复（2026-06-24）：`data-access.ts` `as [string, string]` 断言改为返回类型标注
- ✅ v3 审计修复（2026-06-24）：`knowledge-point-dialogs.tsx` `as` 断言改为 void 包装
- ✅ v3 审计修复（2026-06-24）：`graph-kp-node.tsx` 双重 `as unknown as` 断言改为类型守卫函数 `extractNodeData`
- ✅ v3 审计修复（2026-06-24）：`graph-layout.ts` `GraphLayoutNodeData` 新增 `graphData` 字段类型定义
- ✅ v3 审计修复（2026-06-24）：删除死代码 `verifyKnowledgePointBelongsToChapter`、`ReorderChaptersSchema`、deprecated `canEdit` prop
- ✅ v3 审计修复（2026-06-24）：`textbook-settings-dialog.tsx` 新增 `redirectAfterDelete` prop 消除硬编码 URL
- ✅ v3 审计修复（2026-06-24）：`teacher/textbooks/[id]/loading.tsx` 骨架屏布局改为匹配阅读器布局
- ✅ v3 P2 修复（2026-06-24）：`data-access.ts` 3 处硬编码英文异常文案改为 `NotFoundError`（教材/章节），消息可安全返回客户端
- ✅ v3 P2 修复（2026-06-24）：`graph-layout.ts` O(n²) `some()` 查找改为 `Set` 查找，性能降为 O(n)
- ✅ v3 P2 修复（2026-06-24）：抽取 `textbook-form-fields.tsx` 共享组件，消除 `textbook-form-dialog` 与 `textbook-settings-dialog` 表单字段重复
- ✅ v3 P2 修复（2026-06-24）：`textbook-content-panel.tsx` 17 个 props 按 `chapter`/`editing`/`selection`/`highlight` 四组重构，提升可读性
- ✅ v3 P2 修复（2026-06-24）：`textbook-reader.tsx` 移除命令式 `classList` 操作，视觉高亮改为 `isHighlighted` 状态驱动，仅保留 `scrollIntoView`
- ✅ v3 P2 修复（2026-06-24）：`use-kp-crud.ts`（122 行）拆分为 `use-kp-create`/`use-kp-delete`/`use-kp-update` 三个子 Hook + 门面，均在 80 行限制内
- ⚠️ i18n 覆盖率约 99%（v3 新增 `notFound`/`noClassMasteryPermission`/`close`/`prerequisiteAddFailed`/`prerequisiteRemoveFailed` 等键，data-access 异常文案已错误码化）
- ⚠️ P2 图谱方向键导航未实现
- ⚠️ P2 data-access 函数未导出非缓存版本，单测难以 mock

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 502 | 14 个 Server Action（写操作，含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图） |
| `data-access.ts` | 593 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD（v3 P2：硬编码异常改为 NotFoundError） |
| `data-access-graph.ts` | 188 | 知识图谱只读查询（✅ Task 5 新增：知识点关联聚合、学生/班级掌握度、前置后置知识点，标记 `server-only`；v2 修复：前置依赖双向 IN 过滤防跨教材污染） |
| `types.ts` | 94 | 类型定义（含知识图谱类型：GraphViewMode/GraphLayoutMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel） |
| `schema.ts` | 56 | Zod 校验（含 CreatePrerequisiteSchema/DeletePrerequisiteSchema；v2 修复：refine 消息改为英文；v3 修复：移除死代码 ReorderChaptersSchema） |
| `constants.ts` | 91 | 学科/年级常量与颜色映射（✅ 新增；v1 测试修复：新增 Chinese/Grade 1/Grade 2） |
| `utils.ts` | 226 | 章节树构建/排序/查找等纯函数 + 循环检测（✅ 新增，含单测） |
| `graph-layout.ts` | 109 | 知识图谱布局计算纯函数（✅ Task 8 重写为 dagre 集成，输出 React Flow 格式，含单测；v3 P2：O(n²) 改为 Set 查找） |
| `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook（✅ 新增） |
| `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hook（v2 拆分：组合 useKpDialogState + useKpCrud，对外 API 不变） |
| `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hook（v2 新增：从 use-knowledge-point-actions 拆分） |
| `hooks/use-kp-crud.ts` | 52 | 知识点 CRUD 门面 Hook（v3 P2 拆分：组合 useKpCreate/useKpDelete/useKpUpdate，对外 API 不变） |
| `hooks/use-kp-create.ts` | 43 | 知识点创建 Hook（v3 P2 新增：从 use-kp-crud 拆分） |
| `hooks/use-kp-delete.ts` | 49 | 知识点删除 Hook（v3 P2 新增：从 use-kp-crud 拆分） |
| `hooks/use-kp-update.ts` | 36 | 知识点更新 Hook（v3 P2 新增：从 use-kp-crud 拆分） |
| `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook |
| `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook（✅ Task 11 新增：派生值模式避免 effect 中 setState；v2 修复：区分 isLoading/isRefreshing 避免 UI 闪烁） |
| `components/knowledge-graph.tsx` | 408 | 知识图谱主组件（✅ Task 10/15 重写：全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色；v2 新增：添加/删除前置依赖 Dialog；v4 新增：hierarchical/force 布局模式切换，根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图，状态/详情面板/依赖增删对话框共享） |
| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点（✅ Task 9 新增：知识点名称+题目数徽章+掌握度进度条；v2 修复：节点宽度常量化 NODE_WIDTH；v3 修复：类型守卫替代 as 断言） |
| `components/graph-prerequisite-edge.tsx` | 43 | React Flow 自定义边（✅ Task 9 新增：虚线+箭头表示前置依赖；v2 修复：GraphEdgeData 类型安全转换） |
| `components/graph-toolbar.tsx` | 121 | 图谱工具栏（✅ Task 9 新增：视图模式切换 + 搜索 + 重置视图；v2 新增：isRefreshing 轻量指示器；v4 新增：hierarchical/force 布局模式切换 Select） |
| `components/force-graph.tsx` | 451 | Obsidian 风格力导向图谱组件（v4 新增：基于 react-force-graph-2d + d3-force；v5 视觉对齐 Obsidian Graph View：小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环；通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错） |
| `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板（✅ Task 13 新增：描述/掌握度/关联题目/前置后置列表；v2 修复：移除未使用 textbookId prop；v3 修复：aria-label） |
| `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表（v2 修复：canEdit 默认 false + orderUpdateFailed i18n key；v3 修复：键盘导航） |
| `components/section-error-boundary.tsx` | 53 | 章节内容错误边界（v2 修复：默认值改为空字符串 + 条件渲染；v3 修复：componentDidCatch + a11y） |
| `components/textbook-form-fields.tsx` | 103 | 教材表单字段共享组件（v3 P2 新增：消除 form-dialog 与 settings-dialog 表单重复） |
| `components/textbook-content-panel.tsx` | 199 | 章节内容面板（v3 P2：17 props 按 chapter/editing/selection/highlight 四组重构） |
| `components/textbook-form-dialog.tsx` | 68 | 教材创建对话框（v3 P2：复用 TextbookFormFields） |
| `components/textbook-settings-dialog.tsx` | 136 | 教材设置对话框（v3 P2：复用 TextbookFormFields） |
| `components/textbook-reader.tsx` | 458 | 教材阅读器主组件（v3 P2：移除命令式 classList，高亮改为状态驱动；v5 桌面端改用 ResizablePanel 可拖拽分栏，图谱 tab 时图谱初始占 60%，切换 tab 用 key 重置比例） |
| `components/*` | 17 文件 | 教材编辑/知识图谱组件（新增 `textbook-form-fields.tsx`、5 个 graph-* 组件、3 个 use-kp-* 子 Hook；`teacher-textbook-reader.tsx` 已移至 app 层） |

---

## 2.6 grades（成绩模块）— 标杆模块（拆分范例）

**职责**：成绩分析（录入/查询/统计/导出/趋势对比分析）。

**导出函数**：
- Actions：`getGradeRecordsAction` / `createGradeRecordAction`（v4-P1-6 增强：成绩录入后通知学生和家长，调用 `notifyGradeEntered`）/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`（v4-P1-12 增强：新增可选 `studentId` 参数，支持按学生导出，家长视角调用 `exportStudentGradeRecordsToExcel`，校验 studentId 属于家长子女）/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`（✅ v4-P2-7 新增：年级仪表盘维度1，按 gradeId 查询年级整体 + 按班级拆分的成绩分布，GRADE_RECORD_READ 权限）/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`（v4-P1-6 增强：批量成绩录入后通知学生和家长）/ `batchCreateGradeRecordsByExamAction`（✅ 2026-06-24 新增，✅ 2026-06-25 增强错题采集：按试卷批量录入每题得分，流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入（返回 {gradeRecordId, studentId, submissionId}[]）→ Promise.allSettled 并行执行 updateMasteryFromExamScore + collectFromExamSubmission（考试错题采集，失败不阻断）→ notifyGradeEntered → revalidatePath，返回 gradeRecordId 列表供撤销）/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`（✅ v3-P2 新增：成绩录入草稿 Server Actions，分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限）。注：`assertClassInScope` 原位于 actions.ts（✅ P3 新增导出：班级 scope 校验工具，供 actions-analytics 复用），✅ v4-P2-6 修复：因 "use server" 文件要求所有 export 为 async，而 `assertClassInScope` 是同步函数，已迁移至独立文件 `lib/scope-check.ts`，actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- Data-access：`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`（✅ v4-P2-7 新增：年级仪表盘维度1，通过 getClassesByGradeId 获取年级下所有班级，inArray 查询成绩记录，复用 computeGradeDistribution/computeGradeStats 纯函数，返回整体分布 + 按班级拆分）/ `getRankingTrend` / `PaginatedGradeRecords`（✅ P3 新增：分页结果接口 `{ records, total }`）/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`（✅ v3-P2 新增：成绩录入草稿 CRUD，upsert + 24 小时过期）/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`（✅ v3-P2 新增：考试选项查询 + 全校各年级成绩汇总，管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数，加权平均计算全校汇总）/ `batchCreateGradeRecordsByExam`（✅ 2026-06-24 新增，✅ 2026-06-25 更新返回类型：按试卷批量录入成绩，单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null)，使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩，返回 `BatchGradeRecordResult[]`（{ gradeRecordId, studentId, submissionId }）供调用方触发错题采集等后置钩子）
- Types（✅ v3-P2 新增，✅ v4-P2-7 新增年级分布类型，✅ 2026-06-24 新增按试卷录入类型）：`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`（草稿数据接口）/ `GradeDistributionByGradeResult`（✅ v4-P2-7 新增：年级维度成绩分布结果，含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组）/ `GradeDistributionByGradeClassItem`（✅ v4-P2-7 新增：按班级拆分的分布项：classId/className/distribution/stats）/ `GradeRecordAnswer`（✅ 2026-06-24 新增：成绩记录-题目得分明细，对应 grade_record_answers 表）/ `BatchGradeEntryByExamQuestion`（✅ 2026-06-24 新增：按试卷录入时单个题目得分 { questionId, score }）/ `BatchGradeEntryByExamItem`（✅ 2026-06-24 新增：按试卷录入时单个学生所有题目得分 { studentId, answers }）
- Lib（✅ P1-2 新增，✅ P3 更新签名，✅ P3-26 拆分，✅ v4-P2-6 新增 scope-check）：`toNumber` / `normalize`（位于 `lib/grade-utils.ts`）；`buildScopeClassFilter(scope, currentUserId?)`（P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`，P3 修复：`class_members` scope 内置 studentId 过滤，需传入 currentUserId 参数）；`assertClassInScope(scope: DataScope, classId: string): string | null`（✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`：校验 classId 是否在 scope 允许范围内，供 actions.ts 与 actions-analytics.ts 复用。迁移原因：actions.ts 是 "use server" 文件要求所有 export 为 async，而 assertClassInScope 是同步函数）
- Stats-service（✅ P1-1 新增）：`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`（从 3 个 data-access 文件抽取的纯函数，使数据层专注 DB I/O，统计逻辑可独立测试）
- Export（✅ v4-P1-12 新增）：`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`（v4-P1-12 新增：导出单个学生成绩单家长视角，仅含成绩明细 + 个人统计，不含班级数据，scope 为 children 自动按 studentId 过滤）/ `formatDateForFile`（已迁移至 shared/lib/utils）
- Components（✅ P1-5 新增）：`WidgetBoundary`（Error Boundary + Suspense + Skeleton 组合，含 a11y 属性）/ `SchoolWideSummaryCard`（✅ v3-P2 新增：管理员全校成绩汇总卡片，4 个统计卡片 + 各年级对比表格）/ `ScoreCell`（✅ v4-P1-7 新增：成绩单元格组件，根据得分率着色——红<60%/黄60-84%/绿≥85%，使用语义化 Tailwind 类名避免动态拼接，fullScore<=0 时不着色）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`classes`（✅ P1-1 已修复：通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds）、`school`（✅ P1-1 已修复：通过 school data-access.getSubjectOptions/getGradeOptions）、`users`（✅ P1-1 已修复：通过 users data-access.getUserNamesByIds）、`exams`（✅ 2026-06-24 新增：通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用）、`error-book`（✅ 2026-06-25 新增：通过 error-book data-access.collectFromExamSubmission 在成绩录入后自动采集考试错题）、`diagnostic`（通过 diagnostic data-access.updateMasteryFromExamScore 更新掌握度）
- 被依赖：`parent`（通过 data-access，合理）、`dashboard`

**已知问题**：
- ✅ P1-1 已修复：~~多处直查 `classes`/`classEnrollments`/`subjects`/`users` 表~~ 改为调用对应模块 data-access 函数（classes/school/users）
- ✅ P1-1 已修复：~~统计计算业务逻辑混入 data-access（`getClassGradeStats` / `getGradeDistribution`）~~ 抽取为纯函数到 `stats-service.ts`，数据层专注 DB I/O
- ✅ P1-2 已修复：~~`toNumber`/`normalize`/`buildScopeClassFilter` 在 3 个 data-access 文件中重复定义~~ 抽取到 `lib/grade-utils.ts` 统一维护
- ✅ P1-3 已修复：~~12 个查询/分析 Action 缺少 Zod 校验~~ 新增 12 个查询 schema（DeleteGradeRecordSchema/GetGradeRecordByIdSchema/GradeQuerySchema/ClassGradeStatsQuerySchema/StudentGradeSummaryQuerySchema/ClassRankingQuerySchema/ExportGradesSchema/GradeTrendQuerySchema/ClassComparisonQuerySchema/SubjectComparisonQuerySchema/GradeDistributionQuerySchema/RankingTrendQuerySchema），所有 Action 使用 safeParse 校验
- ✅ P1-4 已修复：~~`batch-grade-entry.tsx`/`grade-record-form.tsx`/`grade-distribution-chart.tsx` 中存在 `as` 断言~~ 改用类型守卫函数（isGradeType/isSemester/isDistributionTooltipPayload）
- ✅ P1-5 已修复：~~teacher/grades 与 teacher/diagnostic 路由缺少 loading.tsx/error.tsx~~ 已为 7 个路由补齐 loading.tsx + error.tsx，并新增 `WidgetBoundary` 通用组件
- ✅ P2-1 已修复：~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 4 个成绩图表添加 `role="img"` + `aria-label`，2 个表格添加 `<caption>`，3 个图标按钮添加 `aria-label`
- ✅ P2-2 已修复：~~diagnostic 组件中存在 Tailwind 任意值~~ 改用标准 Tailwind 类
- ✅ P2-4 已修复：~~`buildScopeClassFilter` 中 `grade_managed` scope 返回 `sql\`1=0\``（空数据）~~ 改为子查询 `classId IN (SELECT id FROM classes WHERE grade_id IN (...))` 过滤所管年级的班级
- ✅ P2-6 已修复：~~student/grades 和 management/grade/insights 各自重复定义 `SearchParams` 类型与 `getParam` 函数~~ 改为统一从 `@/shared/lib/search-params` 导入
- ✅ actions 层无直接 DB 访问（标杆）
- ✅ data-access 按职责拆分为 3 个文件（标杆）
- ✅ P2 已修复：`export.ts` 中 `scoreMap.get(r.studentId)!` 非空断言清理为安全守卫（`if (!subjMap) continue`）
- ✅ P3 修复（2026-06-22）：~~`schema.ts` 缺少输入边界约束~~ score 字段添加 `.max(1000)`，records 数组添加 `.max(500)`，查询 schema 补全 studentId/semester/examId 字段
- ✅ P3 修复（2026-06-22）：~~`buildScopeClassFilter` 的 `class_members` scope 未应用 studentId 过滤~~ 新增 `currentUserId` 参数，`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤
- ✅ P3 修复（2026-06-22）：~~`getGradeRecords` 内存分页（全量查询后切片）~~ 改为 DB 层分页（limit/offset），并行查询总数与当前页数据，返回 `PaginatedGradeRecords { records, total }`
- ✅ P3 修复（2026-06-22）：~~`batchCreateGradeRecords` 非原子操作~~ 包裹 `db.transaction` 确保原子性
- ✅ P3 修复（2026-06-22）：~~`updateGradeRecord`/`deleteGradeRecord` 未检查记录是否存在~~ 新增存在性检查，不存在时抛 `NotFoundError("成绩记录")`
- ✅ P3 修复（2026-06-22）：~~`getClassGradeStats`/`getStudentGradeSummary`/`getClassRanking`/`getClassStudentsForEntry`/`getClassGradeStatsWithMeta` 缺少 scope 过滤~~ 所有函数新增 `scope?`/`currentUserId?` 参数，应用 `buildScopeClassFilter`；`getStudentGradeSummary`/`getClassStudentsForEntry` 对 `class_taught` scope 校验学生/班级归属
- ✅ P3 修复（2026-06-22）：~~`getClassRanking` 未处理并列排名~~ 相同平均分获得相同名次，下一名次跳过占位
- ✅ P3 修复（2026-06-22）：~~`getClassComparison`/`getRankingTrend` 缺少 scope 过滤~~ 应用 `buildScopeClassFilter`；`getRankingTrend` 对 `class_taught` scope 校验
- ✅ P3 修复（2026-06-22）：~~actions.ts/actions-analytics.ts catch 块直接返回 `e.message`~~ 改用 `handleActionError(e)` 统一错误处理；`batchCreateGradeRecordsAction` 使用 `safeJsonParse` 解析 JSON
- ✅ P3 修复（2026-06-22）：~~actions-analytics.ts 缺少 class scope 校验~~ `getGradeTrendAction`/`getSubjectComparisonAction`/`getGradeDistributionAction` 新增 `assertClassInScope` 校验
- ✅ P3 修复（2026-06-22）：~~组件直接 try/catch 调用 Server Action~~ 改用 `safeActionCall` 包装器（onError/onFinally 回调）；`batch-grade-entry.tsx` localStorage 访问包裹 `typeof window !== "undefined"` 检查；区分"未录入"与"录入 0"
- ✅ P3 修复（2026-06-22）：~~`grade-trend-card.tsx` 排序未处理 NaN 日期、除法未检查 fullScore > 0~~ 新增日期有效性检查与 `fullScore > 0` 守卫
- ✅ P3 修复（2026-06-22）：~~页面文件缺少 scope 传递~~ teacher/grades/page.tsx 使用 DB 层分页；analytics/page.tsx 添加 EmptyState；entry/page.tsx 与 stats/page.tsx 过滤 class_taught scope 班级；student/grades/page.tsx 与 parent/grades/page.tsx 传递 `ctx.dataScope` 到 data-access
- ✅ v3-P2 改进（2026-06-23）：新增 `grade_drafts` 表（成绩录入草稿，userId+classId+subjectId+type 唯一键，content JSON 存储 scores，24 小时过期，schema.ts 第 1444-1469 行）；新增 `saveGradeDraft`/`getGradeDraft`/`deleteGradeDraft` data-access 函数 + 对应 3 个 Server Actions，支持成绩批量录入草稿服务端持久化
- ✅ v3-P2 改进（2026-06-23）：新增 `getExamOptionsForGrades` data-access 函数，获取指定班级/科目下有成绩记录的考试列表；`AnalyticsFilters` 组件新增 `exams`/`currentExamId`/`currentSemester` props，添加学期和考试筛选 ChipNav；teacher/grades/analytics/page.tsx 新增 semester/examId 搜索参数解析
- ✅ v3-P2 改进（2026-06-23）：`getGradeTrend`/`getGradeDistribution`/`getSubjectComparison`/`getClassComparison` 均新增 `semester` 和 `examId` 可选参数，支持按学期和考试筛选分析
- ✅ v3-P2 改进（2026-06-23）：新增 `getSchoolWideGradeSummary` data-access 函数 + `SchoolWideSummaryCard` 组件 + `SchoolWideGradeSummary`/`SchoolWideGradeSummaryItem` 类型，管理员全校成绩汇总视图（按年级聚合平均分/及格率/优秀率/学生数/班级数，加权平均计算全校汇总）；admin/school/grades/insights/page.tsx 顶部新增 SchoolWideSummaryCard
- ✅ v3-P2 改进（2026-06-23）：parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`，渲染 GradeTrendCard
- ✅ v3-P3-4 改进（2026-06-23）：GradeTrendCard 新增日期范围选择器（全部/近7天/近30天/近90天），通过 nuqs `trendRange` URL 参数持久化，useEffect 中计算截止时间戳避免渲染阶段调用 Date.now()
- ✅ v4-P3-4 改进（2026-06-23）：GradeDistributionChart 色盲友好双重编码——每个分数段使用不同 SVG pattern（条纹/点状/交叉线/反向条纹/网格）+ 颜色，`SimpleBarChart` 新增 `defs` prop 支持自定义 SVG 图案
- ✅ P3 修复（2026-06-23）：~~`lib/grade-utils.ts` 72 行超 40 行工具函数上限~~ P3-26 将 `buildScopeClassFilter` 迁移至 `lib/scope-filter.ts`，grade-utils.ts 仅保留 toNumber/normalize（20 行）
- ✅ P3 修复（2026-06-23）：~~`stats-service.ts` createDefaultBuckets 不必要导出~~ P3-10 移除 export 关键字，改为内部函数
- ✅ P3 修复（2026-06-23）：~~`stats-service.ts` buildGradeTrendPoints 使用 as 断言~~ P3-24 新增 isGradeTrendType 类型守卫函数替代 as 断言
- ✅ P3 修复（2026-06-23）：~~`export.ts` 局部 avg 函数与 stats-service.computeAverageScore 重复~~ P3-6 删除局部 avg，复用 computeAverageScore
- ✅ P3 修复（2026-06-23）：~~`export.ts` TYPE_LABELS 硬编码中文~~ P3-7 改用 next-intl getTranslations，新增 export.sheets/columns/metrics i18n 键（zh-CN/en 同步）
- ✅ v4-P1-6 改进（2026-06-23）：`createGradeRecordAction` / `batchCreateGradeRecordsAction` 成绩录入后通知学生和家长（调用 `notifyGradeEntered`，内部使用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID），通知失败不阻断成绩录入
- ✅ v4-P1-7 改进（2026-06-23）：新增 `ScoreCell` 组件（components/score-cell.tsx），根据得分率着色（红<60%/黄60-84%/绿≥85%），使用语义化 Tailwind 类名避免动态拼接
- ✅ v4-P1-10 改进（2026-06-23）：`grade-record-list.tsx` 使用 `ScoreCell` 替代纯文本分数展示 + 表格 `overflow-x-auto` 水平滚动
- ✅ v4-P1-12 改进（2026-06-23）：`exportGradesAction` 新增可选 `studentId` 参数，支持按学生导出（家长视角）；新增 `exportStudentGradeRecordsToExcel` 导出函数（仅含成绩明细 + 个人统计，不含班级数据）；parent/grades/page.tsx 传 studentId 到 ParentExportButton；ParentExportButton 接入 exportGradesAction
- ✅ v3-P3-1 改进（2026-06-23）：`batch-grade-entry.tsx` 新增"下载模板"按钮，客户端生成 CSV 模板（含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8），教师可下载填好后粘贴到录入表格
- ✅ v3-P3-2 改进（2026-06-23）：`grade-record-list.tsx` 新增多选复选框（全选/单选）+ 批量删除工具栏 + 批量删除确认对话框；新增 `bulkDeleteGradeRecords` data-access 函数（使用 inArray 一次性删除避免 N+1）+ `bulkDeleteGradeRecordsAction` Server Action（限制单次最多 500 条）
- ✅ v4-P3-2 改进（2026-06-23）：`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框（4 步使用说明），使用 localStorage 记住用户关闭状态避免重复显示
- ✅ v4-P2-6 修复（2026-06-23）：~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async，同步 export 会导致构建错误。修复：将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`（含 `import "server-only"`），actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- ✅ 2026-06-24 重新设计：批量录入从"只录总分"改为"按试卷录入每题得分"。新增 `grade_record_answers` 表存储每题得分明细（迁移 0010_grade_record_answers.sql）。`batchCreateGradeRecordsByExam` data-access 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null)，使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩（学生只看到对错，不知道学生答案）。`BatchGradeEntryByExam` 组件完全重写为 Excel 式表格（行=学生，列=题目，末列=总分自动计算），支持多行多列粘贴、Enter 跳下一行、Tab 跳下一格、分数校验、撤销机制（sessionStorage 5 分钟有效）。新增对 `exams` 模块的依赖（getExamsForGradeEntry/getExamForGradeEntry）。i18n 新增 batchByExam 章节（zh-CN/en ~35 键）
- ✅ 审计修复（2026-06-22）：~~`school-wide-summary-card.tsx` 整个组件无 i18n（13 处硬编码中文）~~ 改为 async 组件使用 `getTranslations("grades")`，新增 `schoolWide` 翻译键节（zh-CN/en 同步 14 键）
- ✅ 审计修复（2026-06-22）：~~5 个 teacher/grades 页面文件大量硬编码中文/英文~~ `page.tsx`/`analytics/page.tsx`/`entry/page.tsx`/`stats/page.tsx`/`error.tsx` 全部使用 `getTranslations("grades")` 或 `useTranslations("grades")`，新增 `page` 翻译键节（zh-CN/en 同步 ~30 键）
- ✅ 审计修复（2026-06-22）：~~`actions.ts` 15+ 处硬编码中文错误消息/通知内容~~ `notifyGradeEntered` 使用 `getTranslations("grades")` 获取通知标题和内容；`batchCreateGradeRecordsByExamAction` 错误消息改用 `t("action.*")` 翻译键；`exportGradesAction` 文件名和错误消息改用翻译键，新增 `action` 翻译键节（zh-CN/en 同步 ~20 键）
- ✅ 审计修复（2026-06-22）：~~`data-access.ts` 2 处硬编码中文 throw Error~~ 改为英文错误消息（data-access 层为防御性检查，action 层已先行校验）
- ✅ 审计修复（2026-06-22）：~~`data-access.ts:782` `as` 断言无类型守卫~~ 新增 `isGradeDraftData` 类型守卫函数（lib/type-guards.ts），从 unknown 安全验证 DB JSON 结构
- ✅ 审计修复（2026-06-22）：~~`data-access-analytics.ts:360-362` 非空断言 `!`~~ 改用类型谓词 `filter((r): r is typeof r & { stats: NonNullable<typeof r.stats> } => r.stats !== null)` 消除非空断言
- ✅ 审计修复（2026-06-22）：~~`export.ts` 内存过滤而非 DB 层过滤~~ `exportStudentGradeRecordsToExcel` 传入 `studentId` 到 `getGradeRecords` DB 层过滤
- ✅ 审计修复（2026-06-22）：~~`teacher/grades/page.tsx` 内联类型守卫 + `as` 断言~~ 改用 `lib/type-guards.ts` 的 `isGradeType`/`isSemester` 函数
- ✅ 审计修复（2026-06-22）：~~11 个组件函数缺少返回值类型标注~~ 全部添加 `: JSX.Element` 或 `: Promise<JSX.Element>` 返回值类型
- ✅ 审计修复（2026-06-24）：~~17 处 Tailwind 任意值（`w-[140px]`/`h-[280px]`/`min-w-[80px]` 等）~~ 图表组件移除匹配默认值的 props 或改用标准类（`h-60`/`w-52`/`w-60`），过滤器改用 `w-36`/`w-32`，表格改用 `min-w-32`/`min-w-20`
- ✅ 审计修复（2026-06-24）：~~`batch-grade-entry.tsx` 2 处 `window.confirm()`~~ 改用声明式 AlertDialog + `pendingSwitch` 状态管理，新增 3 个 i18n 键（confirmTitle/confirmAction/cancelAction）
- ✅ 审计修复（2026-06-24）：~~`class-grade-report.tsx` 缺少返回值类型标注~~ 补充 `: Promise<JSX.Element>` 返回值类型
- ✅ 审计修复（2026-06-25 P0）：~~3 个 error.tsx 硬编码中文（parent/grades、admin/school/grades、admin/school/grades/insights）~~ 改为 `useTranslations("grades")` + 新增 `error.*` 翻译键（zh-CN/en 同步）；~~7 个 loading/error.tsx 函数缺返回值类型~~ 补全 `: JSX.Element` 返回值类型标注
- ✅ 审计修复（2026-06-25 P1-1）：~~`actions.ts` 超 800 行（856 行）~~ 拆分草稿相关 3 个 Action 到 `actions-draft.ts`（106 行）；通知逻辑 `notifyGradeEntered` 抽取至 `lib/notify.ts`（60 行）；actions.ts 降至 646 行
- ✅ 审计修复（2026-06-25 P1-2）：~~`data-access.ts` 超 800 行（816 行）~~ 拆分草稿 CRUD 到 `data-access-drafts.ts`（107 行）；拆分按试卷录入到 `data-access-exam-entry.ts`（167 行）；data-access.ts 降至 536 行
- ✅ 审计修复（2026-06-25 P1-3）：~~`actions.ts` 多处英文错误消息未 i18n~~ 新增 `action.error.*` 翻译键，所有错误消息改用 `t("action.error.*")`；scope 校验错误改用 `t("action.scopeError.{code}")`
- ✅ 审计修复（2026-06-25 P1-4）：~~`lib/scope-check.ts` 返回硬编码英文消息~~ 改为返回 `ScopeCheckErrorCode` 枚举（`"class_taught" | "class_members" | "denied"`），由 Action 层通过 i18n 键 `grades.action.scopeError.{code}` 翻译
- ✅ 审计修复（2026-06-25 P1-5）：~~`actions-analytics.ts` scope 校验消息未 i18n~~ 所有 6 个 Action 使用 `getTranslations("grades")` 获取翻译；新增 `ownRankingTrendOnly`/`childrenRankingTrendOnly` 翻译键
- ✅ 审计修复（2026-06-25 P1-6/P1-7）：~~`batch-grade-entry.tsx` 超 500 行（604 行）+ as 断言~~ 拆分为 `BatchGradeEntryTable` + `BatchGradeEntryStats` + `BatchGradeEntryDialog` 子组件 + `useBatchGradeEntryUndo` Hook；主组件降至 422 行；撤销数据使用 `isUndoData` 类型守卫（in 运算符）替代 `as { ids: string[]; timestamp: number }` 断言
- ✅ 审计修复（2026-06-25 P1-8）：~~`parent/grades/page.tsx` 第 63 行 as 断言~~ 改用 `for` 循环 + 类型守卫 `if (summary === null) continue` 替代 `.filter().map()` + `as NonNullable<...>` 断言
- ✅ 审计修复（2026-06-25 P1-9）：~~`teacher/grades/page.tsx` 列表与统计未 WidgetBoundary 包裹~~ 新增 `<WidgetBoundary title={t("title.list")}>` 包裹 `<GradeRecordList>`，避免单数据区块失败导致整页崩溃
- ✅ 审计修复（2026-06-25 P2-2）：~~`grade-filters.tsx`/`grade-query-filters.tsx`/`export-button.tsx` 函数缺返回值类型~~ 补全 `: JSX.Element` / `: Promise<void>` 返回值类型标注
- ✅ 审计修复（2026-06-25 P2-3）：~~`updateGradeRecord` 使用 `Record<string, unknown>` 累积更新字段~~ 改用 `Partial<typeof gradeRecords.$inferInsert>` 类型安全更新
- ✅ 审计修复（2026-06-25 P2-4）：~~`getStudentGradeSummary` 内部调用 `getClassRanking` 形成 data-access 层内部耦合~~ 新增 `getStudentRankInClass` 独立函数（两步法：查学生平均分 → 查所有学生平均分 → 计算严格高于的不同平均分数），避免获取整个班级排名（含姓名查询）只为查单个学生排名
- ✅ 审计修复（2026-06-25 P2-9）：~~`lib/type-guards.ts` 第 8/12 行 as 断言~~ `isGradeType`/`isSemester` 改用 `GRADE_TYPES.some((t) => t === v)` 替代 `(GRADE_TYPES as readonly string[]).includes(v)`；`isGradeDraftData` 改用 `in` 运算符替代 `as Record<string, unknown>` 断言

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 646 | 17 个 Server Action（含 Zod 校验，含 v2-P1-5 安全修复：assertClassInScope + 行级 scope 校验；P3 修复：handleActionError + safeJsonParse + scope 传递 + DB 层分页；v4-P1-12：exportGradesAction 新增 studentId 参数；v3-P3-2 新增：bulkDeleteGradeRecordsAction 批量删除；v4-P2-6：assertClassInScope 迁移至 lib/scope-check.ts；2026-06-24 新增：batchCreateGradeRecordsByExamAction 按试卷录入每题得分；**P1-1 重构（2026-06-25）**：草稿相关 3 个 Action（saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction）迁移至 actions-draft.ts；通知逻辑 notifyGradeEntered 抽取至 lib/notify.ts；错误消息全部 i18n（t(\"action.*\")）；scope 校验错误改用 ScopeCheckErrorCode + t(\"action.scopeError.{code}\") 翻译） |
| `actions-draft.ts` | 106 | **P1-1 新增（2026-06-25）**：成绩录入草稿 3 个 Server Action（saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction），从 actions.ts 拆分以使 actions.ts 降至 800 行以内。权限分别为 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE，调用 data-access-drafts，错误消息使用 i18n 翻译键 |
| `actions-lock.ts` | 197 | **P3-6 新增（2026-06-26）**：协同录入锁 4 个 Server Action（getDraftLockStatusAction/acquireDraftLockAction/renewDraftLockAction/releaseDraftLockAction）。权限 GRADE_RECORD_READ（查询）/GRADE_RECORD_MANAGE（获取/续期/释放），调用 data-access-drafts 锁函数 + assertClassInScope 校验 + getUserNamesByIds 解析锁持有者姓名（避免跨模块 DB join）。releaseDraftLockAction 失败不阻断主流程 |
| `actions-import.ts` | 297 | **P3-10 新增（2026-06-26）**：Excel 批量导入成绩 3 个 Server Action（downloadGradeImportTemplateAction/importGradesFromExcelAction/previewGradeImportAction）。权限 GRADE_RECORD_MANAGE + assertClassInScope，调用 import-export.ts 解析 Excel + 匹配学生姓名 + 批量入库。支持模板下载（含班级学生姓名）、预校验、正式导入 |
| `actions-appeal.ts` | 197 | **P3-7 新增（2026-06-26）**：成绩申诉 5 个 Server Action（createGradeAppealAction/withdrawGradeAppealAction/getStudentAppealsAction/reviewGradeAppealAction/getPendingAppealsForReviewAction）。学生权限 GRADE_RECORD_READ（提交/撤回/查看自己申诉），教师权限 GRADE_RECORD_MANAGE（复核）。审核通过且提供 adjustedScore 时同步更新 grade_records.score |
| `data-access-appeals.ts` | 261 | **P3-7 新增（2026-06-26）**：成绩申诉数据访问层。7 个函数（hasPendingAppeal/createGradeAppeal/getStudentAppeals/getPendingAppealsForReview/getAppealById/reviewGradeAppeal/withdrawGradeAppeal）+ GradeAppeal/GradeAppealWithRecord 接口。申诉状态机 pending→approved/rejected/withdrawn，originalScore 快照留痕，reviewGradeAppeal 同步更新成绩记录 |
| `import-export.ts` | 278 | **P3-10 新增（2026-06-26）**：Excel 导入成绩逻辑。generateGradeImportTemplate 生成含班级学生姓名的模板，parseGradeExcelBuffer 解析 Excel（支持中英双语表头），parseGradeImportData 匹配学生姓名到 ID（处理重名），buildBatchInputFromImport 转换为批量录入输入。依赖 @/shared/lib/excel |
| `actions-analytics.ts` | 219 | 6 个分析 Action（含 Zod 校验，P3 修复：handleActionError + assertClassInScope 校验；v4-P2-6：assertClassInScope 改从 ./lib/scope-check 导入；**P1-5 重构（2026-06-25）**：所有 Action 使用 getTranslations(\"grades\") 获取翻译，错误消息改用 t(\"action.invalidQuery\")/t(\"action.scopeError.{code}\")；学生/家长排名趋势访问校验使用 t(\"action.ownRankingTrendOnly\")/t(\"action.childrenRankingTrendOnly\")） |
| `data-access.ts` | 536 | 成绩 CRUD + 统计（含 v2-P2-9 修复：recorderName 批量查询；P3 修复：PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名；v3-P3-2 新增：bulkDeleteGradeRecords 使用 inArray 批量删除；**P1-2 重构（2026-06-25）**：草稿 CRUD（saveGradeDraft/getGradeDraft/deleteGradeDraft）迁移至 data-access-drafts.ts；按试卷录入（batchCreateGradeRecordsByExam）迁移至 data-access-exam-entry.ts；**P2-3**：updateGradeRecord 类型强化为 Partial<typeof gradeRecords.$inferInsert>；**P2-4**：新增 getStudentRankInClass 解耦 getStudentGradeSummary 与 getClassRanking） |
| `data-access-drafts.ts` | 405 | **P1-2 新增（2026-06-25）**：成绩录入草稿 CRUD（saveGradeDraft/getGradeDraft/deleteGradeDraft）+ GradeDraftData 接口，从 data-access.ts 拆分。使用 cache() memoize getGradeDraft，isGradeDraftData 类型守卫安全解析 DB JSON 列，24 小时过期。**P3-6 扩展（2026-06-26）**：新增协同录入锁机制 5 个函数（getDraftLockStatus/acquireDraftLock/renewDraftLock/releaseDraftLock/cleanupExpiredDraftLocks）+ DraftLockStatus/AcquireLockResult 接口，LOCK_TTL_MS=5 分钟，悲观锁语义+乐观 TTL+令牌续期，锁以 grade_drafts 行为载体（lockedBy/lockedAt/lockToken 列） |
| `data-access-exam-entry.ts` | 167 | **P1-2 新增（2026-06-25）**：按试卷批量录入成绩（batchCreateGradeRecordsByExam）+ BatchGradeRecordResult 接口，从 data-access.ts 拆分。单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null)，返回 BatchGradeRecordResult[]（{gradeRecordId, studentId, submissionId}）供调用方触发错题采集等后置钩子 |
| `data-access-analytics.ts` | 417 | 趋势/对比分析（P3 修复：getClassComparison 应用 buildScopeClassFilter；v3-P2 新增：getExamOptionsForGrades/getSchoolWideGradeSummary；getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数） |
| `data-access-ranking.ts` | 166 | 排名查询（P3 修复：getRankingTrend 接受 scope 参数 + class_taught 校验） |
| `stats-service.ts` | 285 | 统计计算纯函数（P1-1 新增：8 个纯函数 + 2 个常量 + 2 个接口；P3-10：createDefaultBuckets 改为内部函数；P3-24：buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言） |
| `export.ts` | 290+ | Excel 导出（v2-P1-5 修复：传递 currentUserId 到 data-access；P3 修复：适配 PaginatedGradeRecords 结构 + 传递 scope；P3-6：复用 stats-service.computeAverageScore 替代局部 avg；P3-7：硬编码中文改用 next-intl getTranslations；v4-P1-12 新增：exportStudentGradeRecordsToExcel 家长视角单学生导出） |
| `schema.ts` | 237 | Zod 校验（含 12 个查询 schema；P3 修复：score .max(1000) + records .max(500) + 补全查询字段；v3-P2 新增：grade_drafts 表定义；2026-06-24 新增：BatchGradeEntryByExamSchema；**P3-10 新增（2026-06-26）**：ExcelImportRowSchema/ExcelImportGradesSchema + 导出 ExcelImportRowInput/ExcelImportGradesInput 类型；**P3-7 新增（2026-06-26）**：GradeAppealStatusEnum/CreateGradeAppealSchema/ReviewGradeAppealSchema + 导出 CreateGradeAppealInput/ReviewGradeAppealInput 类型） |
| `lib/grade-utils.ts` | 20 | 公共工具函数（toNumber/normalize；P3-26：buildScopeClassFilter 迁移至 scope-filter.ts） |
| `lib/scope-filter.ts` | 56 | DB 行级权限过滤（buildScopeClassFilter；P3-26 从 grade-utils.ts 迁移；v2-P2-2 修复：改用 classes data-access 子查询；P3 修复：新增 currentUserId 参数） |
| `lib/scope-check.ts` | 34 | v4-P2-6 新增：班级 scope 校验工具（assertClassInScope 同步函数，从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制；含 `import "server-only"`；**P1-4 重构（2026-06-25）**：返回类型从 string \| null 改为 ScopeCheckErrorCode \| null（\"class_taught\" \| \"class_members\" \| \"denied\"），消除硬编码英文字符串，调用方通过 i18n 键 grades.action.scopeError.{code} 翻译） |
| `lib/notify.ts` | 60 | **P1-1 新增（2026-06-25）**：成绩录入通知工具（notifyGradeEntered），从 actions.ts 抽取。使用 next-intl getTranslations 获取通知标题和内容（i18n）；**P2-7 改进**：使用 Promise.allSettled 并行化通知写入，避免批量录入 30+ 学生时串行等待；依赖 notifications/data-access.createNotification + parent/data-access.getParentIdsByStudentIds + school/data-access.getSubjectNameById |
| `lib/report-card.ts` | 380 | **P3-1 新增（2026-06-26）**：成绩报告卡数据访问层。getReportCardData（cached）聚合学生成绩+班级信息+班主任+学年+科目排名+总体统计，computeSubjectRank 计算单科排名。三层 scope 校验：owned（学生自己）/children（家长子女）/class_taught（教师班级学生）。静态导入 data-access（getStudentRankInClass/getClassNamesByIds）+ stats-service（computeAverageScore） |
| `lib/type-guards.ts` | 25 | 类型守卫纯函数（isGradeType/isSemester/isGradeDraftData；**P2-9 修复（2026-06-25）**：isGradeType/isSemester 使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v)，消除 as 断言；isGradeDraftData 使用 in 运算符替代 as Record<string, unknown> 断言） |
| `types.ts` | 200+ | 类型定义（v3-P2 新增：SchoolWideGradeSummaryItem/SchoolWideGradeSummary；2026-06-24 新增：GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem） |
| `components/widget-boundary.tsx` | 136 | Widget 边界组件（P1-5 新增，v2-P1-1 已在 3 个页面应用；**P1-9（2026-06-25）**：teacher/grades/page.tsx 列表与统计卡片新增 WidgetBoundary 包裹，避免单数据区块失败导致整页崩溃） |
| `components/school-wide-summary-card.tsx` | - | v3-P2 新增：管理员全校成绩汇总卡片（4 个统计卡片 + 各年级对比表格） |
| `components/score-cell.tsx` | 41 | v4-P1-7 新增：成绩单元格组件，根据得分率着色（红<60%/黄60-84%/绿≥85%），使用语义化 Tailwind 类名 |
| `components/grade-trend-card.tsx` | 69 | 趋势卡片（v2-P2-9 修复：a11y；v2-P1-4：i18n；P3 修复：NaN 日期检查 + fullScore > 0 守卫） |
| `components/grade-record-list.tsx` | 200+ | 成绩记录列表（v2-P1-4：i18n；P3 修复：safeActionCall 包装删除操作；v4-P1-7：使用 ScoreCell；v4-P1-10：overflow-x-auto；v3-P3-2 新增：多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框，接入 bulkDeleteGradeRecordsAction） |
| `components/grade-distribution-chart.tsx` | 100 | 分数分布图（v2-P1-4：i18n） |
| `components/subject-comparison-chart.tsx` | 62 | 科目对比图（v2-P1-4：i18n） |
| `components/class-comparison-chart.tsx` | 194 | 班级对比图（v2-P1-4：i18n；v3-P3-5 新增：显著性分析区域，基于极差和样本量的经验规则判断班级间差异，含可折叠详细分析） |
| `components/grade-trend-chart.tsx` | 59 | 趋势图（v2-P1-4：i18n） |
| `components/grade-record-form.tsx` | 177 | 录入表单（v2-P2-7 修复：Label htmlFor；v2-P1-4：i18n；P3 修复：safeActionCall 包装提交） |
| `components/batch-grade-entry.tsx` | 422 | 2026-06-24 完全重写：导出名改为 BatchGradeEntryByExam，按试卷录入每题得分。Excel 式表格（行=学生，列=题目，末列=总分自动计算）。交互：试卷选择器（按 scope 过滤）→ 班级选择器（按试卷 gradeId 过滤）；多行多列 Excel 粘贴（Tab 分隔）；Enter 跳下一行同一列；Tab 跳下一格；实时统计（已录入/总数/均分/最高/最低）；分数校验（超过题目满分标红）；撤销机制（sessionStorage 5 分钟有效）。**P1-6/P1-7 重构（2026-06-25）**：拆分为 3 个子组件 + 1 个 Hook（BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo），主组件从 604 行降至 422 行；撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言；window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?（原 v2-P2-7 Label htmlFor；v2-P1-4 i18n；P3 safeActionCall + localStorage 安全检查；v3-P2 服务端草稿；v3-P3-1 下载 CSV 模板；v4-P3-2 新手引导均已替换） |
| `components/batch-grade-entry-table.tsx` | 115 | **P1-6 新增（2026-06-25）**：批量录入 Excel 式表格子组件（行=学生，列=题目，末列=总分自动计算）。支持多行多列 Excel 粘贴、Enter 跳下一行、Tab 跳下一格、分数校验（超过题目满分标红）。接受 inputRefs 供父组件管理输入焦点 |
| `components/batch-grade-entry-stats.tsx` | 68 | **P1-6 新增（2026-06-25）**：批量录入统计栏子组件，显示已录入/总数/均分/最高/最低/无效分数徽章，含学生搜索框过滤。导出 BatchGradeEntryStatsData 接口 |
| `components/batch-grade-entry-dialog.tsx` | 58 | **P1-6 新增（2026-06-25）**：批量录入切换确认对话框子组件（替代 window.confirm()）。导出 PendingSwitch 联合类型 { kind: "exam"; examId; message } \| { kind: "class"; classId; message } \| null |
| `hooks/use-batch-grade-entry-undo.ts` | 86 | **P1-6/P1-7 新增（2026-06-25）**：批量录入撤销 Hook。封装撤销令牌的序列化、sessionStorage 读写、5 分钟 TTL 过期检查、撤销 Action 调用。包含 isUndoData 类型守卫函数（替代 as 断言），使用 in 运算符做安全属性访问 |
| `components/grade-filters.tsx` | 76 | 过滤器（v2-P1-4：i18n；**P2-2（2026-06-25）**：补全 : JSX.Element 返回值类型） |
| `components/student-grade-summary.tsx` | 107 | 学生成绩摘要（v2-P1-4：i18n） |
| `components/export-button.tsx` | 79 | 导出按钮（v2-P1-4：i18n；P3 修复：safeActionCall 包装导出操作；**P2-2（2026-06-25）**：handleExport 补全 : Promise<void> 返回值类型） |
| `components/analytics-filters.tsx` | 86+ | 分析过滤器（v2-P1-4：i18n；v3-P2 新增：exams/currentExamId/currentSemester props，添加学期和考试筛选 ChipNav） |
| `components/stats-class-selector.tsx` | 40 | 统计班级选择器（v2-P1-4：i18n） |
| `components/grade-stats-card.tsx` | 74 | 统计卡片（v2-P1-4：i18n） |
| `components/class-grade-report.tsx` | 90 | 班级成绩报告（v2-P1-4：i18n） |
| `components/grade-query-filters.tsx` | 96 | 查询过滤器（v2-P2-7 修复：Label htmlFor；v2-P1-4：i18n；**P2-2（2026-06-25）**：补全 : JSX.Element 返回值类型） |
| `components/report-card-view.tsx` | 323 | **P3-1 新增（2026-06-26）**：A4 打印格式成绩报告卡服务端组件。含表头（学校名+标题+学期）、学生信息网格、科目成绩表格（rowSpan 合并科目名/排名列）、汇总统计（均分/班排名/及格率/优秀率）、教师评语+签字区。使用 globals.css `@media print` 规则控制打印布局 |
| `components/report-card-print-button.tsx` | 106 | **P3-1 新增（2026-06-26）**：跳转至报告卡页面的客户端按钮，支持学年/学期筛选 |
| `components/report-card-print-action.tsx` | 48 | **P3-1 新增（2026-06-26）**：调用 window.print() 的客户端按钮组件，含 loading 状态和错误 toast |
| `components/excel-import-dialog.tsx` | 518 | **P3-10 新增（2026-06-26）**：Excel 批量导入成绩对话框（3 步流程：下载模板→填写参数→上传文件）。展示导入结果（成功/失败计数、未匹配学生、无效行表格前 20 条）。使用 safeActionCall 包装 Action 调用 |
| `components/draft-lock-banner.tsx` | 75 | **P3-6 新增（2026-06-26）**：协同录入锁状态提示条。三种状态：acquiring（loading）/conflict（锁被他人持有，警告+重试按钮）/acquired（不渲染）。使用 Alert 组件 |
| `components/knowledge-point-mastery-chart.tsx` | 232 | **P3-3 新增（2026-06-26）**：知识点掌握度柱状图组件，集成 diagnostic 模块的 getClassMasterySummary 数据。使用 SimpleBarChart + masteryBarColor 函数着色 + isMasteryTooltipPayload 类型守卫 |
| `hooks/use-draft-lock.ts` | 184 | **P3-6 新增（2026-06-26）**：协同录入锁管理 Hook。职责：进入页面自动获取锁、3 分钟心跳续期（TTL 5 分钟）、组件卸载/提交成功释放锁、锁冲突状态暴露。使用 ref 保存最新 token 避免闭包捕获旧值，防重复 acquire/release |

---

## 2.7 classes（班级模块）— 耦合最严重

**职责**：班级 CRUD + 学生/教师管理 + 邀请码注册。

**导出函数**：
- Actions：`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction`
- Data-access：`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`（获取多个班级的所有教师 ID：班主任 + 任课教师，跨模块接口，供 messaging 模块调用）/ `getClassIdByScheduleId(scheduleId: string): Promise<string | null>`（✅ 2026-06-26 审计修复新增：根据课表项 ID 获取所属班级 ID，供 actions-schedule 的 create/update/delete 三个 Action 做越权校验，防止教师通过 scheduleId 修改他人班级课表）（✅ P0-5 已修复：classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts，classes 模块仅保留 classSchedule 读函数；✅ P2 已修复：`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询）
- Schema：`CreateTeacherClassSchema` / `UpdateTeacherClassSchema` / `DeleteTeacherClassSchema` / `CreateAdminClassSchema` / `UpdateAdminClassSchema` / `DeleteAdminClassSchema` / `CreateGradeClassSchema` / `UpdateGradeClassSchema` / `DeleteGradeClassSchema` / `CreateClassScheduleItemSchema` / `UpdateClassScheduleItemSchema` / `DeleteClassScheduleItemSchema` / `EnrollStudentByEmailSchema`
- Components：`AdminClassesClient` / `GradeClassesClient`（P1-D 重构：降至 ≤160 行，组合使用共享组件）/ `ClassListTable`（admin/grade 共享班级列表表格，P1-D 抽取）/ `ClassFormDialog`（admin/grade 共享创建/编辑表单弹窗，`schools` 可选控制学校选择显隐，P1-D 抽取）/ `ClassDeleteDialog`（admin/grade 共享删除确认弹窗，基于 ConfirmDeleteDialog，P1-D 抽取）/ `ClassListToolbar`（admin/grade 共享工具栏：新建按钮+计数徽章，P1-D 抽取）/ `class-form-utils`（`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型，P1-D 抽取）/ `ClassErrorBoundary`（P1-C 新增：class component Error Boundary + i18n + router.refresh 重试）/ `ClassListSkeleton` / `ClassCardSkeleton` / `ClassGridSkeleton` / `StudentListSkeleton` / `ScheduleGridSkeleton`（P1-C 新增：5 个骨架屏组件）/ `MyClassesGrid`（P0-A 修复：已接入 i18n）/ `ScheduleView`（✅ P2 修复：已拆分出 `ScheduleCreateDialog`/`ScheduleEditDialog`/`ScheduleDeleteDialog` 3 个对话框子组件，主组件从 464 行降至 175 行；不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态，下沉到子对话框；WEEKDAYS 常量改用从 `./schedule-utils` 导入的 `SCHEDULE_WEEKDAYS`）/ `ScheduleCreateDialog`（P2 修复新增：课表新增对话框，从 schedule-view 拆分）/ `ScheduleEditDialog`（P2 修复新增：课表编辑对话框，从 schedule-view 拆分）/ `ScheduleDeleteDialog`（P2 修复新增：课表删除确认对话框，从 schedule-view 拆分）/ `ScheduleFilters` / `StudentsTable` / `StudentsFilters`（P0-A 修复：已接入 i18n）/ `ClassHeader` / `ClassOverviewStats` / `ClassQuickActions` / `ClassScheduleWidget` / `ClassAssignmentsWidget` / `ClassStudentsWidget` / `ClassTrendsWidget` / `EditClassDialog`（P0-A 修复：class-detail 下 7 个子组件 + 编辑对话框已接入 i18n）
- 工具函数（components/schedule-utils.ts，2026-06-26 审计修复新增）：`getSubjectColor(subject: string): string`（根据科目名返回对应颜色类名，中英文双语关键词匹配；修复原实现仅匹配英文关键词导致中文 `DEFAULT_CLASS_SUBJECTS` 全部落到 default 分支的颜色分组失效 bug）/ `getPositionStyle(startTime: string, endTime: string): { top: string; height: string }`（根据课程开始/结束时间计算课表块在 8:00-18:00 时间轴上的定位百分比）/ `timeToMinutes(time: string): number`（将 "HH:MM" 时间字符串转换为当天从 0 点起的分钟数）/ `SCHEDULE_WEEKDAYS`（✅ P2 修复新增：课表周常量数组，供 schedule-view 及拆分出的 3 个对话框子组件复用，替换原 schedule-view.tsx 内联的 WEEKDAYS 常量）
- Hooks：`useClassData`（admin/grade 共享对话框+表单状态管理+重置逻辑，P1-E 抽取）/ `useClassFilters`（按学校筛选年级，admin 模式使用，P1-E 抽取）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`school`（✅ P1-1 已修复：通过 school data-access.isGradeHead/isGradeManager/findGradeIdByHeadAndName）、`homework`（✅ P0-7 已修复：通过 `homework/data-access-classes` 暴露的函数获取作业数据，不再直查 homework/exams 表）
- 被依赖：`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`（通过 data-access，P0-4 已修复）/`parent`/`course-plans`/`users`（✅ P1-1 已修复：8+ 处直查 classes 表改为通过 classes data-access）/`messaging`（通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId，支持学生/家长给班级教师发消息）

**已知问题**：
- ✅ P0-1 已修复：`data-access.ts` 已拆分为 5 个文件（data-access/data-access-stats/data-access-schedule/data-access-students/data-access-admin），所有文件均 ≤800 行
- ✅ P0-5 已修复：classSchedule 写函数（createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem）已迁移至 scheduling/data-access-class-schedule.ts，classes 模块仅保留 classSchedule 读函数（getStudentSchedule/getClassSchedule）；新增 verifyTeacherOwnsClass 供 scheduling 模块跨模块校验教师班级归属
- ✅ P0-7 已修复：`data-access-stats.ts` 和 `data-access-students.ts` 不再直查 `homeworkAssignmentQuestions`/`homeworkAssignmentTargets`/`homeworkAssignments`/`homeworkSubmissions`/`exams` 表，改为调用 `homework/data-access-classes.ts` 暴露的函数（`getAssignmentIdsForStudents`/`getHomeworkAssignmentsWithSubject`/`getHomeworkAssignmentsByIds`/`getAssignmentMaxScoreById`/`getAssignmentTargetCounts`/`getHomeworkSubmissionsForStudents`/`getPublishedHomeworkAssignmentsWithSubject`/`getHomeworkSubmissionsForAssignments`）
- ✅ P1-1 已修复：~~`actions.ts` 直查 `grades` 表做权限校验~~ 改为调用 `school/data-access` 函数
- ✅ P1-1 已修复：~~`getSessionTeacherId` 在 data-access 调用 `auth()`~~ 改为通过 `shared/lib/auth-guard.getAuthContext()` 获取
- ✅ P2 已修复：`data-access.ts` 中 `idByName.get(name)!` 非空断言清理为 `flatMap` 安全过滤；`data-access-admin.ts` 中同类非空断言清理
- ✅ P0-3 修复（2026-06-22）：~~`actions.ts` 974 行接近 1000 行硬上限~~ 拆分为 6 个文件（actions-teacher/actions-admin/actions-grade/actions-invitations/actions-schedule/actions-shared），原 `actions.ts` 改为 50 行 barrel re-export
- ✅ P1-1 修复（2026-06-22）：~~`ctx.roles.includes("admin"/"teacher"/"student")` 角色硬编码~~ 改为 `hasAdminScope(ctx)`/`hasTeacherScope(ctx)`/`hasStudentScope(ctx)` 基于 `dataScope.type` 判断
- ✅ P1-4 修复（2026-06-22）：`types.ts` 中 ClassHomeworkInsights 等跨领域类型保留在 classes 模块（因为是 classes 对 homework 数据的视图），添加注释说明归属决策
- ✅ P1-D 修复（2026-06-24）：~~`admin-classes-view.tsx` 533 行与 `grade-classes-view.tsx` 439 行 ~80% 代码重复~~ 拆分为组合模式：抽取 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `ClassListToolbar` 4 个共享组件 + `class-form-utils`（`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型）；admin-classes-view 降至 159 行，grade-classes-view 降至 153 行；admin 删除弹窗统一改用 `ConfirmDeleteDialog`（与 grade 一致）
- ✅ P1-E 修复（2026-06-24）：抽取 `useClassData` hook（对话框开关+表单字段+提交中标记+重置逻辑，admin/grade 共享）+ `useClassFilters` hook（按学校筛选年级，admin 模式使用）
- ✅ P0-A 修复（2026-06-24）：~~classes 模块 13/14 个组件未接入 i18n~~ 全部 13 个组件（admin-classes-view/grade-classes-view/my-classes-grid/schedule-view/schedule-filters/students-filters/students-table/class-detail 下 7 个子组件/edit-class-dialog）已接入 `useTranslations("classes")`；my-classes-grid 中 5 处硬编码英文（"Failed to regenerate"/"Failed to copy"/"No Room"/"vs last week"）已替换为翻译键
- ✅ P0-B 修复（2026-06-24）：~~classes.json 仅 55 行~~ 扩充至 ~370 行，覆盖 invitation/class/list/form/delete/filters/myClasses/students/schedule/detail（header/overview/trends/assignments/widgets/edit/empty）/grade/bulk/errors 全部场景；en 镜像同步
- ✅ P1-C 修复（2026-06-24）：新增 `class-error-boundary.tsx`（class component Error Boundary + i18n + router.refresh 重试）和 `class-skeleton.tsx`（ClassListSkeleton/ClassCardSkeleton/ClassGridSkeleton/StudentListSkeleton/ScheduleGridSkeleton）；6 个页面均已包裹 Error Boundary：admin/school/classes + teacher/classes/my + teacher/classes/my/[id] + teacher/classes/schedule + teacher/classes/students（ClassErrorBoundary）、management/grade/dashboard（SchoolErrorBoundary）；schedule/students 页面 EmptyState 硬编码英文替换为 i18n
- ✅ 2026-06-26 审计修复（i18n 全覆盖 + 越权校验 + 角色硬编码消除 + 错误处理统一 + 路由错误边界完善）：
  - **i18n 翻译键扩充**：classes.json 新增 `actions` 命名空间（42 个键，覆盖全部 Server Action 错误/成功消息）、`error` 命名空间（3 个键：title/description/retry，供 ErrorState 组件使用）、`filters` 命名空间新增 10 个键、`detail` 命名空间新增 12 个键
  - **越权校验加固**：`actions-schedule.ts` 新增 `assertClassOwnership` 辅助函数（调用 `getClassIdByScheduleId` + `verifyTeacherOwnsClass`），create/update/delete 三个 Action 增加班级归属校验，防止教师通过 scheduleId 修改他人班级课表；`actions-teacher.ts` 的 `updateTeacherClassAction` / `deleteTeacherClassAction` 增加越权校验
  - **角色硬编码消除**：`data-access.ts` / `data-access-teacher.ts` / `data-access-admin.ts` 中所有 `roles.name, "teacher"` / `roles.name, "student"` 改为 `ROLE_NAMES.TEACHER` / `ROLE_NAMES.STUDENT`（共 7 处），统一引用 `@/shared/types/permissions` 常量
  - **错误处理统一**：`actions-admin.ts` / `actions-grade.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-teacher.ts` 所有 catch 块改为使用 `handleActionError`（来自 `@/shared/lib/action-utils`），所有 message 改为使用 `await getTranslations("classes")` + t()，移除 `PermissionDeniedError` 直接 import；`actions-invitations.ts` 的 `assertClassOwnership` 签名变更：新增 `t` 参数；`actions-shared.ts` 的 `toWeekday` / `parseSubjectTeachers` 改为 async，使用 `ValidationError` 代替 `throw new Error`
  - **DEFAULT_CLASS_SUBJECTS 回退逻辑**：`data-access.ts` 的 `getClassSubjects()` 新增回退逻辑——subjects 表为空时返回 `DEFAULT_CLASS_SUBJECTS`；`data-access-teacher.ts` / `data-access-admin.ts` 中 `inArray(subjects.name, DEFAULT_CLASS_SUBJECTS)` 改为 `inArray(subjects.name, await getClassSubjects())`，确保筛选条件与回退数据源一致
  - **DB 错误处理修正**：`data-access-teacher.ts` 的 `getTeacherClasses()` DB 错误时 throw 而非返回空数组（避免静默吞错）
  - **路由错误边界完善**（10 个 error.tsx + 1 个 loading.tsx 新增）：teacher/classes/my/error.tsx、teacher/classes/my/[id]/error.tsx + loading.tsx、teacher/classes/students/error.tsx、teacher/classes/schedule/error.tsx、student/learning/error.tsx + loading.tsx、student/learning/courses/error.tsx、student/learning/courses/[classId]/error.tsx、student/schedule/error.tsx；统一使用 shared 层 `ErrorState` 组件
  - **新增工具函数文件**：`components/schedule-utils.ts` 抽取 `getSubjectColor` / `getPositionStyle` / `timeToMinutes` 3 个纯函数（修复中文科目颜色分组失效 bug + 提升可测试性）
  - **新增权限点使用**：`student/learning/page.tsx` / `student/learning/courses/page.tsx` / `student/learning/courses/[classId]/page.tsx` / `student/schedule/page.tsx` 接入 `requirePermission(CLASS_READ)`
  - **generateMetadata 接入**（✅ P2 修复新增）：8 个 page.tsx 新增 `generateMetadata` 导出（通过 next-intl `getTranslations` 生成页面标题）：`teacher/classes/my/page.tsx`、`teacher/classes/my/[id]/page.tsx`、`teacher/classes/schedule/page.tsx`、`teacher/classes/students/page.tsx`、`student/learning/page.tsx`、`student/learning/courses/page.tsx`、`student/learning/courses/[classId]/page.tsx`、`student/schedule/page.tsx`

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 548 | 核心班级 CRUD + 邀请码 + 教师班级管理（含 re-export 向后兼容） |
| `data-access-stats.ts` | 513 | 作业统计查询（班级/年级作业洞察，通过 homework/data-access-classes 获取数据） |
| `data-access-schedule.ts` | 93 | 课表查询（学生/班级课表只读，P0-5 已修复：写函数已迁移至 scheduling 模块） |
| `data-access-students.ts` | 253 | 学生相关查询（科目成绩、学生名单、学生班级，通过 homework/data-access-classes 获取数据） |
| `data-access-admin.ts` | 406 | 管理员班级管理（管理员班级 CRUD、年级管理班级查询） |
| `actions.ts` | 50 | Barrel re-export（P0-3 修复：从 974 行拆分为 6 个文件） |
| `actions-teacher.ts` | 100 | 教师班级 CRUD（3 个 Action） |
| `actions-admin.ts` | 120 | 管理员班级 CRUD（3 个 Action） |
| `actions-grade.ts` | 110 | 年级组长班级 CRUD（3 个 Action） |
| `actions-invitations.ts` | 502 | 邀请码与注册 + 批量操作（10 个 Action，P2-4 新增批量导入学生/批量分配教师） |
| `actions-schedule.ts` | 90 | 班级课表 CRUD（3 个 Action） |
| `actions-shared.ts` | 60 | 共享工具（hasAdminScope/hasTeacherScope/hasStudentScope/parseSubjectTeachers/toWeekday） |
| `schema.ts` | 152 | Zod 校验（13 个 schema：教师/管理员/年级班级 CRUD + 课表 CRUD + 邮箱注册） |
| `types.ts` | 201 | 类型定义（含跨领域类型说明注释，P1-4 修复） |
| components/admin-classes-view.tsx | 159 | 管理员班级客户端（P1-D 重构：组合共享组件） |
| components/grade-classes-view.tsx | 153 | 年级班级客户端（P1-D 重构：组合共享组件） |
| components/class-list-table.tsx | 115 | 班级列表表格（admin/grade 共享，P1-D 抽取） |
| components/class-form-dialog.tsx | 200 | 班级创建/编辑表单弹窗（admin/grade 共享，P1-D 抽取） |
| components/class-delete-dialog.tsx | 35 | 班级删除确认弹窗（admin/grade 共享，P1-D 抽取） |
| components/class-list-toolbar.tsx | 30 | 班级列表工具栏（admin/grade 共享，P1-D 抽取） |
| components/class-form-utils.ts | 35 | formatSubjectTeachers + 表单类型（P1-D 抽取） |
| components/schedule-utils.ts | 89 | 课表纯函数工具集（2026-06-26 审计修复新增：getSubjectColor/getPositionStyle/timeToMinutes；✅ P2 修复新增 SCHEDULE_WEEKDAYS 周常量；修复中文科目颜色分组失效 bug + 提升可测试性） |
| components/class-error-boundary.tsx | 45 | 班级模块 Error Boundary（P1-C 新增） |
| components/class-skeleton.tsx | 80 | 班级模块骨架屏组件（5 个：List/Card/Grid/StudentList/ScheduleGrid，P1-C 新增） |
| components/my-classes-grid.tsx | 418 | 教师班级卡片网格（P0-A 修复：已接入 i18n） |
| components/schedule-view.tsx | 175 | 课表视图（P0-A 修复：已接入 i18n；✅ P2 修复：拆分出 ScheduleCreateDialog/ScheduleEditDialog/ScheduleDeleteDialog 3 个对话框子组件，主组件从 464 行降至 175 行；不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态，下沉到子对话框；WEEKDAYS 常量改用从 ./schedule-utils 导入的 SCHEDULE_WEEKDAYS） |
| components/schedule-create-dialog.tsx | - | 课表新增对话框（✅ P2 修复新增：从 schedule-view.tsx 拆分，承接原 isWorking/handleCreate 状态） |
| components/schedule-edit-dialog.tsx | - | 课表编辑对话框（✅ P2 修复新增：从 schedule-view.tsx 拆分，承接原 isWorking/handleUpdate 状态） |
| components/schedule-delete-dialog.tsx | - | 课表删除确认对话框（✅ P2 修复新增：从 schedule-view.tsx 拆分，承接原 isWorking/handleDelete 状态） |
| components/schedule-filters.tsx | 60 | 课表筛选器（P0-A 修复：已接入 i18n） |
| components/students-filters.tsx | 60 | 学生筛选器（P0-A 修复：已接入 i18n） |
| components/students-table.tsx | 120 | 学生表格（P0-A 修复：已接入 i18n） |
| components/class-detail/class-header.tsx | 100 | 班级详情头部（P0-A 修复：已接入 i18n） |
| components/class-detail/class-overview-stats.tsx | 80 | 班级概览统计（P0-A 修复：已接入 i18n） |
| components/class-detail/class-quick-actions.tsx | 60 | 快捷操作（P0-A 修复：已接入 i18n） |
| components/class-detail/class-schedule-widget.tsx | 120 | 课表 Widget（P0-A 修复：已接入 i18n） |
| components/class-detail/class-assignments-widget.tsx | 100 | 作业 Widget（P0-A 修复：已接入 i18n） |
| components/class-detail/class-students-widget.tsx | 120 | 学生 Widget（P0-A 修复：已接入 i18n） |
| components/class-detail/class-trends-widget.tsx | 150 | 趋势 Widget（P0-A 修复：已接入 i18n） |
| components/class-detail/edit-class-dialog.tsx | 145 | 编辑班级对话框（P0-A 修复：已接入 i18n） |
| hooks/use-class-data.ts | 95 | 对话框+表单状态管理 hook（P1-E 抽取） |
| hooks/use-class-filters.ts | 30 | 按学校筛选年级 hook（P1-E 抽取） |

**路由文件清单**（`app/(dashboard)/`，2026-06-26 审计修复新增错误边界 + 加载骨架）：
| 文件 | 职责 |
|------|------|
| `teacher/classes/my/error.tsx` | 教师我的班级错误边界（使用 shared `ErrorState` + namespace="classes"） |
| `teacher/classes/my/[id]/error.tsx` | 班级详情错误边界（使用 shared `ErrorState`） |
| `teacher/classes/my/[id]/loading.tsx` | 班级详情加载骨架（ClassGridSkeleton） |
| `teacher/classes/students/error.tsx` | 学生管理错误边界（使用 shared `ErrorState`） |
| `teacher/classes/schedule/error.tsx` | 课表错误边界（使用 shared `ErrorState`） |
| `student/learning/error.tsx` | 学生学习中心错误边界（使用 shared `ErrorState`） |
| `student/learning/loading.tsx` | 学生学习中心加载骨架 |
| `student/learning/courses/error.tsx` | 学生课程列表错误边界（使用 shared `ErrorState`） |
| `student/learning/courses/[classId]/error.tsx` | 学生课程详情错误边界（使用 shared `ErrorState`） |
| `student/schedule/error.tsx` | 学生课表错误边界（使用 shared `ErrorState`） |

---

## 2.8 school（学校模块）

**职责**：学校/学年/部门/年级的 CRUD。

**导出函数**：
- Actions：`createSchoolAction` / `updateSchoolAction` / `deleteSchoolAction` / `createAcademicYearAction` / `updateAcademicYearAction` / `deleteAcademicYearAction` / `createDepartmentAction` / `updateDepartmentAction` / `deleteDepartmentAction` / `createGradeAction` / `updateGradeAction` / `deleteGradeAction` / `promoteGradesAction`（编排层：权限校验 + Zod 校验 + 调用 data-access + revalidatePath + after(logAudit)；`promoteGradesAction` 年级升级，审计日志 `grade.promote`）
- Data-access：只读查询（`getSchools` / `getGrades` / `getDepartments` / `getAcademicYears` / `getStaffOptions` / `getGradesForStaff` / `getOrgTree` / `getSubjectOptions` / `getGradeOptions` / `getSubjectNameMapByIds`（P1-1 新增：批量科目名称映射，供 homework/data-access-classes 调用）/ `getGradeOverviewStats`（✅ v4-P2-6 新增：年级概览统计，返回 `GradeOverviewStats[]`，每个年级的 classCount/studentCount/teacherCount，供年级管理卡片视图使用，动态导入 classes/data-access 避免循环依赖））+ 写操作（`create/update/delete` × `Department/School/Grade/AcademicYear`）+ `promoteGrades(schoolId)` 年级升级（order +1 + 名称升级，辅助函数 `promoteGradeName`）
- Types（✅ v4-P2-6 新增）：`GradeOverviewStats`（年级概览统计接口：{ gradeId, classCount, studentCount, teacherCount }，位于 data-access.ts）
- Components：`SchoolsClient` / `SchoolFormDialog` / `SchoolDeleteDialog` / `SchoolListToolbar` / `SchoolErrorBoundary` / `SchoolListSkeleton` / `SchoolCardSkeleton` / `OrgTreeNav` / `GradesClient`（✅ v4-P2-6 更新：新增 `gradeStats: GradeOverviewStats[]` prop，渲染年级概览卡片视图——每个年级卡片展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口）/ `GradeInsightsFilters`（✅ v4-P2-6 新增：年级洞察筛选器，使用 ChipNav 替代原生 form get，点击 chip 即时通过 URL 参数切换，无整页刷新）/ `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel`（✅ v4-P2-7 新增：年级仪表盘 4 个维度面板组件，位于 `components/grade-dashboard/` 子目录，服务端组件直接渲染数据，无客户端交互）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`users`（⚠️ `getStaffOptions` 直查 users/roles，可接受）
- 被依赖：`exams`（✅ P1-1 已修复：通过 school data-access）、`homework`（✅ P1-1 已修复：通过 school data-access）、`grades`（✅ P1-1 已修复：通过 school data-access）、`questions`（✅ P1-1 已修复：通过 textbooks data-access）、`classes`（✅ P1-1 已修复：通过 school data-access）、`course-plans`（合理）

**已知问题**：
- ✅ P0-8 已修复：`actions.ts` 不再直接导入 `db` 和 schema，所有 DB 写操作下沉到 `data-access.ts`，符合三层架构
- ✅ P2 已修复：`logAudit()` 通过 Next.js `after()` 异步非阻塞执行
- ✅ P2 已修复：`data-access.ts` 中 8 处 catch 块添加 `console.error` 输出错误上下文（getDepartments/getAcademicYears/getSchools/getGrades/getStaffOptions/getGradesForStaff/getSubjectOptions/getGradeOptions）
- ⚠️ P2：审计日志不一致（仅 school 实体记录，department/academicYear/grade 未记录）
- ⚠️ P2：`getStaffOptions`/`getGrades` 直查 users/roles（展示用，可接受）
- ✅ P0-2 修复（2026-06-22）：~~年级 CRUD 逻辑与 `grade-management` 模块重复定义~~ `grade-management` 死模块已删除，年级 CRUD 统一由 school 模块负责
- ✅ P0-5 修复（2026-06-22）：~~`school/components/*` 4 个组件缺少 i18n~~ 全部 4 个组件（schools-view/grades-view/departments-view/academic-year-view）已接入 `useTranslations("school")`；`school.json` i18n 文件已创建并扩充
- ✅ P1-3 修复（2026-06-22）：新增 school-error-boundary.tsx（class component Error Boundary + i18n + router.refresh 重试）和 school-skeleton.tsx（SchoolListSkeleton 表格骨架 + SchoolCardSkeleton 卡片骨架）；4 个页面（schools/grades/departments/academic-year）均已包裹 SchoolErrorBoundary；school.json 补充 errors.boundary.* 翻译键
- ✅ P1-5 修复（2026-06-22）：~~`schools-view.tsx` 硬编码 Table+Dialog+AlertDialog~~ 拆分为组合模式：SchoolListToolbar + SchoolFormDialog + SchoolDeleteDialog + useSchoolData hook
- ✅ P1-6 修复（2026-06-22）：新增 `getSchoolsForUser(userId)` / `getGradesForUser(userId)` 权限感知查询函数，根据用户角色返回可见数据范围
- ✅ P2-1 修复（2026-06-22）：抽取 `use-school-data` hook，将对话框状态管理逻辑与 UI 分离
- ✅ P2-2 修复（2026-06-23）：新增 `getOrgTree()` data-access 函数（学校→年级→班级三级树，通过 `import("@/modules/classes/data-access")` 动态导入获取班级数据避免循环依赖）+ `OrgTreeNode` 类型 + `OrgTreeNav` 组件（三级树形导航，支持搜索过滤/选中高亮/展开折叠/不同节点类型图标）；school.json 补充 `orgTree.*` 翻译键
- ✅ P2-3 修复（2026-06-23）：新增 `promoteGradesAction` + `promoteGrades(schoolId)` + `promoteGradeName(name)` 辅助函数（中文数字 一→二…十二、阿拉伯数字 1→2…12 识别），按 order 降序逐条 +1 避免唯一约束冲突；含 `after(() => logAudit({ action: "grade.promote" }))` 审计日志
- ✅ P2-4 修复（2026-06-23）：新增 `bulkEnrollStudentsAction`（CSV 批量导入学生，复用 enrollStudentByEmail）+ `bulkAssignSubjectTeachersAction`（CSV 批量分配教师，简化实现含 TODO 待完善查找逻辑）；classes/actions.ts barrel 导出
- ✅ P2-5 修复（2026-06-23）：为 department/academicYear/grade 的 9 个 CRUD Action 补充 `after(() => logAudit(...))` 审计日志（action: department.create/update/delete、academicYear.create/update/delete、grade.create/update/delete），与 school 实体审计日志策略一致
- ✅ v4-P2-6 改进（2026-06-23）：年级管理体验增强——新增 `getGradeOverviewStats()` data-access 函数（返回 `GradeOverviewStats[]`：每个年级的 classCount/studentCount/teacherCount，动态导入 classes/data-access 避免循环依赖）；`GradesClient` 新增 `gradeStats` prop，渲染年级概览卡片视图（班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口）；admin/school/grades/page.tsx 新增 `getGradeOverviewStats()` 查询并传入 GradesClient
- ✅ v4-P2-6 改进（2026-06-23）：新增 `GradeInsightsFilters` 组件（使用 ChipNav 替代原生 form get，点击 chip 即时通过 URL 参数切换，无整页刷新）；admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写，使用 `GradeInsightsFilters` 替代原生 form get，添加 i18n，表格添加 `overflow-x-auto` 水平滚动
- ✅ P1-A 修复（2026-06-24）：~~`grades-view.tsx` 860 行超出组件行数限制~~ 拆分为组合模式：`GradeFormDialog`（创建/编辑表单对话框，含客户端校验+去重+isDirty 检测）+ `GradeDeleteDialog`（删除确认）+ `GradeListToolbar`（搜索/筛选/排序工具栏）+ `GradeOverviewCards`（年级概览卡片视图）+ `useGradeData` hook（对话框状态管理）；主容器 `grades-view.tsx` 降至 290 行，参照 `schools-view.tsx` 组合模式

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 457 | 13 个 Server Action（编排层，无 DB 直访；含 `promoteGradesAction` 年级升级） |
| `data-access.ts` | 782 | 只读查询 + 12 个写操作 + `promoteGrades` 年级升级 + `getOrgTree` 组织架构树 + 跨模块查询接口 + 权限感知函数（getSchoolsForUser/getGradesForUser） |
| `schema.ts` | 56 | Zod 校验（含 `PromoteGradesSchema`） |
| `types.ts` | 103 | 类型定义（含 Insert/Update 入参类型 + `OrgTreeNode`） |
| components/schools-view.tsx | 132 | 学校列表容器（组合模式，P1-5 修复） |
| components/school-form-dialog.tsx | 80 | 学校创建/编辑对话框（P1-5 修复） |
| components/school-delete-dialog.tsx | 50 | 学校删除确认对话框（P1-5 修复） |
| components/school-list-toolbar.tsx | 30 | 学校列表工具栏（P1-5 修复） |
| components/school-error-boundary.tsx | 72 | 共享 Error Boundary（P1-3 修复） |
| components/school-skeleton.tsx | 69 | 共享骨架屏（P1-3 修复） |
| components/org-tree-nav.tsx | 134 | 学校→年级→班级三级树形导航（P2-2 修复：搜索过滤 + 选中高亮 + 展开折叠 + 节点类型图标） |
| components/grades-view.tsx | 290 | 年级管理客户端（P1-A 修复：拆分为组合模式，仅负责列表渲染+筛选+组合子组件） |
| components/grade-form-dialog.tsx | 280 | 年级创建/编辑表单对话框（P1-A 新增：含客户端校验+去重+isDirty 检测，useActionMutation 管理 mutation） |
| components/grade-delete-dialog.tsx | 65 | 年级删除确认对话框（P1-A 新增） |
| components/grade-list-toolbar.tsx | 115 | 年级列表工具栏（P1-A 新增：搜索+学校筛选+主任筛选+排序+新建按钮） |
| components/grade-overview-cards.tsx | 165 | 年级概览卡片视图（P1-A 新增：班级/学生/教师数+年级主任/教学主任+快捷操作） |
| components/grade-insights-filters.tsx | 48 | v4-P2-6 新增：年级洞察筛选器（ChipNav 替代原生 form get，点击 chip 即时通过 URL 参数切换，无整页刷新） |
| hooks/use-school-data.ts | 40 | 学校数据管理 hook（P2-1 修复） |
| hooks/use-grade-data.ts | 42 | 年级数据管理 hook（P1-A 新增：对话框状态管理，参照 use-school-data 模式） |

---

## 2.8b grade-management（年级管理模块）— ✅ 已删除

> **2026-06-22 审计发现**：该模块拥有完整的理想架构（Service 接口 + Context DI + 角色配置 + Error Boundary + Skeleton + i18n + hooks 分离），但 **13 个相关页面中无任何一个导入此模块**。`management/grade/*` 页面实际依赖 `classes` 和 `school` 模块的 data-access。
>
> **2026-06-22 处置决策（P0-1/P0-2 修复）**：该死模块已**完整删除**。年级 CRUD 统一由 `school` 模块负责（`school/actions.ts` + `school/data-access.ts`），避免两套重复实现。详见 `docs/architecture/audit/school-grade-class-audit-report.md`。

---

## 2.9 scheduling（排课模块）

**职责**：自动排课算法 + 课表调整 + 排课规则管理。

**导出函数**：
- Actions：`autoScheduleAction` / `applyAutoScheduleAction` / `getSchedulingRulesAction` / `updateSchedulingRulesAction` / `getScheduleChangesAction` / `createScheduleChangeAction` / `updateScheduleChangeAction` / `deleteScheduleChangeAction`
- Data-access：`getSchedulingRules` / `getScheduleChanges` / `getAdminClassesForScheduling` / `getTeachersForScheduling` / `getClassroomsForScheduling` / `getClassSubjectsForScheduling`
- Data-access-class-schedule（✅ P0-5 新增）：`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`（从 classes 模块迁移，含教师班级归属校验，通过 `classes/data-access.verifyTeacherOwnsClass` 跨模块校验）
- Data-access（低级写入）：`insertClassScheduleItem` / `updateClassScheduleItemById` / `deleteClassScheduleItemById` / `replaceClassSchedule`（统一 classSchedule DB 写入口）
- 算法：`findOptimalSlot` / `validateSchedule` / `autoSchedule` / `buildDefaultTimeSlots`（纯函数，标杆）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`classes`（✅ P0-5 已修复：通过 `classes/data-access.verifyTeacherOwnsClass` / `getTeacherIdForMutations` 校验教师班级归属，不再直写 classSchedule 表的写入口分散在 classes 模块）、`school`（⚠️ 排课辅助查询，可接受）、`users`（✅ P1-1 已修复：通过 users data-access.getUserNamesByIds）
- 被依赖：`classes/actions.ts`（✅ P0-5 已修复：通过 `scheduling/data-access-class-schedule` 调用写函数）

**已知问题**：
- ✅ P0-5 已修复：~~`applyAutoScheduleAction` 直接 transaction 写 `classSchedule` 表（第三个写入口）~~ 改为调用 `replaceClassSchedule` 统一写入口；classSchedule 所有写函数统一在 scheduling 模块
- ✅ P1-1 已修复：~~`autoScheduleAction` 直查 `users` 表~~ 改为调用 `users/data-access.getUserNamesByIds`
- ⚠️ P2：`actions.ts` 末尾 re-export data-access 函数（反模式）
- ✅ P2 已修复：`data-access.ts` 中 3 处非空断言清理（`userIds[0]!`、`rows[i]!`、`rows[j]!`）；`auto-scheduler.ts` 中 2 处非空断言清理（`schedule[i]!`、`schedule[j]!`）
- ✅ `auto-scheduler.ts` 是算法独立化的最佳实践（纯函数、无 DB、可测试）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `auto-scheduler.ts` | 310 | 排课算法（纯函数，标杆） |
| `actions.ts` | 302 | 8 个 Server Action |
| `data-access.ts` | 398 | 排课辅助查询 + 规则/变更 CRUD + classSchedule 低级写入（insert/update/delete/replace） |
| `data-access-class-schedule.ts` | 165 | classSchedule 业务写入（P0-5 新增，从 classes 模块迁移，含教师归属校验） |
| `schema.ts` | - | Zod 校验 |
| `types.ts` | - | 类型定义（含 P0-5 迁移的 CreateClassScheduleItemInput / UpdateClassScheduleItemInput） |

---

## 2.10 attendance（考勤模块）— 结构典范

**职责**：考勤记录管理 + 统计分析 + 规则配置。✅ L-6 节次考勤：支持按节次（早读/上午/下午/晚自习/全日）记录与过滤。✅ L-9 关联分析: 考勤与成绩跨模块关联分析(Pearson 相关系数 + 风险分级)。

**导出函数**：
- Actions（5 个写 Action）：`recordAttendanceAction`（✅ L-6：formData 读取 period）/ `batchRecordAttendanceAction`（✅ L-2：保存成功后调用 `notifyParentsOfAbsence` 异步通知家长，失败不阻断主流程；✅ L-6：每条记录携带 period）/ `updateAttendanceAction`（✅ L-6：formData 读取 period）/ `deleteAttendanceAction` / `saveAttendanceRulesAction`（✅ 审计修复：均调用 `requirePermission(ATTENDANCE_MANAGE)` + 资源归属校验 + `trackEvent` 埋点）
- Data-access：`getAttendanceRecords`（✅ 审计修复：移除跨模块 JOIN，委托 `getUserNamesByIds`/`getClassNamesByIds`；✅ L-6：支持按 period 过滤）/ `getClassAttendanceForDate`（✅ L-6：新增可选 period 参数）/ `buildPeriodFilter`（✅ L-6 新增：纯函数，full_day 匹配 NULL OR 'full_day' 向后兼容）/ `createAttendanceRecord`（✅ L-6：写入 period 默认 full_day）/ `batchCreateAttendanceRecords`（✅ L-6：批量写入 period）/ `updateAttendanceRecord`（✅ L-6：支持更新 period）/ `deleteAttendanceRecord` / `getClassStudentsForAttendance`（✅ 审计修复：委托 `classes/data-access.getClassActiveStudentsWithInfo`）/ `getAttendanceStats`（✅ 审计修复：SQL 聚合 + 返回统一 `AttendanceStats` 类型）/ `upsertAttendanceRules` / `getAttendanceRules` / `getAttendanceRecordClassId` / `syncExcusedFromLeaveRequest`（✅ L-5 新增：请假审批通过后遍历日期区间插入 excused 记录，幂等不覆盖已有记录；✅ L-6：插入时 period: full_day；被 `leave-requests/actions.reviewLeaveRequestAction` 调用）
- Data-access-stats：`getStudentAttendanceSummary`（✅ 审计修复：SQL 聚合 + 委托 data-access；✅ L-6：recentRecords 映射 period）/ `getClassAttendanceStats`（✅ 审计修复：SQL 聚合 + 委托 data-access；✅ L-6：studentRecords 映射 period）/ `computeStats`（✅ 纯函数导出，可单测）
- Notifications（✅ L-2 新增）：`notifyParentsOfAbsence`（批量考勤录入后多渠道通知家长：in_app/sms/email/wechat，一次性批量查询 parent-student 映射避免 N+1）
- Import-export：`exportAttendanceRecordsToExcel`（✅ 审计修复：类型守卫 + i18n 列头）
- Services（✅ 审计新增 P1-12/P2-6）：`AttendanceDataService` 接口 / `AttendanceReadService` / `AttendanceWriteService` / `AttendanceRepository` / `AttendanceProvider` / `useAttendanceService` / `createAttendanceDataService` / `createAttendanceReadService`
- Components：`AttendanceSheet`（✅ 审计修复：AlertDialog + 常量统一 + 容器内 keydown + useMemo；✅ L-6：新增节次选择器，3 列布局，提交时携带 period）/ `AttendanceRecordList`（✅ 审计修复：`usePermission` 控制删除按钮 + `resolveActionError`；✅ L-6：新增节次列，null 视为 full_day 展示）/ `AttendanceFilters` / `AttendanceStatsCard` / `AttendanceStatsCards`（✅ 审计修复：统一 `AttendanceStats` 类型）/ `AttendanceStatsClassSelector` / `AttendanceRulesForm`（✅ 审计修复：`resolveActionError`）/ `StudentAttendanceView` / `AttendancePageLayout`
- Data-access-correlation(✅ L-9 新增): `getAttendanceGradeCorrelation`(跨模块委托 classes/users/grades data-access 聚合数据) / `canAccessCorrelationAnalysis`(权限校验)
- Correlation-compute(✅ L-9 新增纯函数, 与 IO 解耦便于单测): `computePearsonCorrelation` / `classifyRiskLevel` / `interpretCorrelation` / `computeCorrelationSummary`
- Components L-9(✅ L-9 新增): `AttendanceGradeCorrelationCard`(散点图 + 风险表格, recharts ScatterChart 风险等级着色)

**依赖关系**：
- 依赖：`shared/*`（含 `shared/constants/attendance-status` ✅ 审计新增：状态类型与视觉常量下沉至 shared）、`@/auth`、`classes`（✅ 审计修复：通过 `classes/data-access` 获取班级学生/名称）、`users`（✅ 审计修复：通过 `users/data-access.getUserNamesByIds` 获取姓名）、`parent`（✅ L-2 新增：通过 `parent/data-access.getParentStudentMapByStudentIds` 获取家长-学生映射）、`notifications`（✅ L-2 新增：通过 `sendBatchNotifications` 多渠道分发）、`grades`(✅ L-9 新增：通过 `grades/data-access.getGradeRecords` 获取成绩数据用于关联分析）、`next-intl`
- 被依赖：`parent`（✅ 审计修复：通过 `createAttendanceReadService` 接口消费，不再直接 import attendance 组件；常量改从 `shared/constants/attendance-status` 导入）、`app/api/export`（通过 `exportAttendanceRecordsToExcel`）、`leave-requests`（✅ L-5 新增：审批通过后通过 `syncExcusedFromLeaveRequest` 同步 excused 考勤记录）

**L-6 节次考勤数据模型**：
- DB：`attendanceRecords.period` enum('morning_reading','morning','afternoon','evening','full_day') nullable（向后兼容 NULL 视为 full_day）
- 索引：`classDatePeriodIdx`（class_id + date + period 复合索引）
- 迁移：`drizzle/0021_attendance_period.sql`
- 类型：`AttendancePeriod`（types.ts），`AttendanceRecord`/`AttendanceListItem`/`AttendanceQueryParams` 新增 period 字段
- Schema：`AttendancePeriodEnum`（Zod），`RecordAttendanceSchema` 默认 full_day，`UpdateAttendanceSchema` 可选 period
- i18n：`attendance.period.{label,morning_reading,morning,afternoon,evening,full_day,allPeriods}` 中英文翻译
- 过滤策略：`buildPeriodFilter` 对 full_day 使用 `or(isNull(period), eq(period, "full_day"))` 匹配旧数据
- 对外统一：mapListItem 中 `r.period ?? "full_day"`，前端展示 null 统一为 full_day

**L-9 考勤与成绩关联分析**：
- 类型： `AttendanceGradeRiskLevel`(high/medium/low) / `AttendanceGradeCorrelationItem` / `AttendanceGradeCorrelationSummary`(types.ts)
- 纯函数(correlation-compute.ts)： `computePearsonCorrelation`(Pearson r) / `classifyRiskLevel`(high=低出勤+低分, medium=中低出勤+中低分, low=其余) / `interpretCorrelation` / `computeCorrelationSummary`
- 数据访问(data-access-correlation.ts)： `getAttendanceGradeCorrelation` 跨模块委托 classes/users/grades data-access; `canAccessCorrelationAnalysis` scope 校验
- UI(attendance-grade-correlation-card.tsx)： recharts ScatterChart 散点图, 风险等级着色(red/amber/emerald)
- 集成： admin/attendance/page.tsx 指定班级时显示
- i18n： attendance.correlation.* 中英文翻译

**已知问题**（详见 `docs/architecture/audit/attendance-audit-report.md`）：
- ✅ P0-1 已修复：teacher/sheet、teacher/stats 页面增加 `requirePermission(ATTENDANCE_READ)`
- ✅ P0-2 已修复：teacher 分页使用后端返回的 `totalPages`
- ✅ P0-3 已修复：`safeParseDate` fieldName 改为中性 key，前端通过 `resolveActionError` 按 errorCode 本地化
- ✅ P0-4 已修复：error.tsx 重试按钮改用 `t("actions.retry")`
- ✅ P0-5 已修复：data-access 移除跨模块 JOIN，改为委托 data-access
- ✅ P1-1 已修复：teacher/student 主页面增加 `requirePermission(ATTENDANCE_READ)`
- ✅ P1-2 已修复：parent 通过 `AttendanceReadService` 接口消费
- ✅ P1-3 已修复：parent-attendance-calendar 改从 `shared/constants/attendance-status` 导入
- ✅ P1-6 已修复：export.ts 类型守卫替代 `as` 断言
- ✅ P1-7 已修复：calendar 使用 `useLocale()` 替代硬编码 locale
- ✅ P1-8 已修复：teacher 子路由新增 error.tsx
- ✅ P1-9 已修复：常量统一从 `constants.ts`（re-export shared）导入
- ✅ P1-10 已修复：4 个 error.tsx 抽取为 shared `RouteErrorBoundary`
- ✅ P1-11 已修复：`getClassAttendanceStats` 改用 SQL 聚合
- ✅ P1-12 已修复：新增 `AttendanceDataService` 接口 + Context 注入
- ✅ P2-1 已修复：删除按钮 `usePermission` 控制显隐
- ✅ P2-5 已修复：stats 数据结构统一为 `AttendanceStats`
- ✅ P2-6 已修复：导出 `AttendanceRepository` 接口类型
- ✅ P2-7 已修复：keydown 限容器内 + 排除 Select/contenteditable
- ✅ P2-8 已修复：parent-attendance-warning/rate-card 改为 RSC
- ✅ P2-9 已修复：statusCounts 使用 `useMemo`
- ✅ stats 独立拆分为 `data-access-stats.ts`（拆分范例）
- ✅ DataScope 完整接入 6 种 scope 类型
- ✅ actions 层无直接 DB 访问
- ✅ Server Action 均有 `trackEvent` 埋点

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 258 | 5 个写 Server Action（权限校验 + 归属校验 + Zod + 埋点；✅ L-6 读取 period） |
| `data-access.ts` | ~410 | 考勤 CRUD + 规则 upsert + 总览统计（SQL 聚合 + 委托 data-access；✅ L-6 period 过滤/写入/更新） |
| `data-access-stats.ts` | ~230 | 学生/班级考勤汇总（SQL 聚合 + 委托 data-access；✅ L-6 period 映射） |
| `export.ts` | 93 | Excel 导出（类型守卫 + i18n 列头） |
| `schema.ts` | ~60 | Zod 校验（6 个 schema + ✅ L-6 AttendancePeriodEnum） |
| `types.ts` | ~95 | 类型定义（状态常量已迁移至 shared；✅ L-6 AttendancePeriod + period 字段） |
| `constants.ts` | 19 | 重导出 `shared/constants/attendance-status` |
| `services/types.ts` | 166 | 服务接口抽象（AttendanceDataService/Repository） |
| `services/attendance-context.tsx` | 46 | React Context 注入（AttendanceProvider + hook） |
| `services/attendance-data-service.ts` | 162 | 服务实现 + 工厂函数 |
| `components/attendance-sheet.tsx` | ~480 | 批量点名表单（AlertDialog + 容器内 keydown + useMemo；✅ L-6 节次选择器） |
| `components/attendance-record-list.tsx` | ~165 | 记录列表 + 删除对话框（权限控制 + resolveActionError；✅ L-6 节次列） |
| `components/attendance-filters.tsx` | 94 | URL 同步筛选器 |
| `components/attendance-stats-card.tsx` | 82 | 单卡片统计（8 指标） |
| `components/attendance-stats-cards.tsx` | 83 | 管理员总览 6 卡片网格（统一 AttendanceStats） |
| `components/attendance-stats-class-selector.tsx` | 27 | 班级筛选 ChipNav |
| `components/attendance-rules-form.tsx` | 155 | 规则配置表单（resolveActionError） |
| `components/student-attendance-view.tsx` | 111 | 学生只读视图 |
| `components/attendance-page-layout.tsx` | 38 | 页面布局骨架（插槽模式） |
| `correlation-compute.ts` | ~90 | L-9 纯函数: Pearson 相关系数 + 风险分级 + 汇总 (与 IO 解耦) |
| `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access |
| `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) |

---

## 2.11 users（用户模块）

**职责**：用户资料管理 + 批量导入导出 + 管理员用户列表管理。

**导出函数**：
- Actions：`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` / `updateUserRoleAction` / `deleteUserAction`
- Data-access：`getUserProfile` / `getCurrentStudentUser`（✅ P2-20 已修复：从 homework 模块迁移而来，6 个 student 页面通过此函数获取学生身份，不再依赖 homework 模块）/ `getAdminUsers`（管理员用户列表分页查询，支持搜索+角色聚合）/ `getAdminUserRoles`（角色名列表，用于筛选下拉框）
- Import-export：`generateUserImportTemplate` / `parseUserImportData` / `exportUsersToExcel`（+ re-export `batchImportUsers` / `UserImportResult` 保持向后兼容）
- User-service：`batchImportUsers`（用户创建 + 密码哈希 + 角色分配）
- Class-registration：`registerStudentByInvitationCode`（委托 classes/data-access 完成班级注册）
- Components：`UserImportDialog`（批量导入对话框）/ `AdminUsersView`（管理员用户列表客户端组件，搜索+筛选+分页+删除）

**依赖关系**：
- 依赖：`shared/*`（含 `shared/lib/role-utils`，✅ P2 已修复：删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`，统一复用 `shared/lib/role-utils.resolvePrimaryRole`）、`@/auth`、`classes`（✅ P1-4 已修复：通过 `class-registration.ts` 调用 `classes/data-access.enrollStudentByInvitationCode`，不再直写 classEnrollments）
- 被依赖：`dashboard`（通过 data-access，P0-4 已修复）、`grades`（✅ P1-1 已修复：通过 users data-access）、`homework`（✅ P1-1 已修复：通过 users data-access）、`scheduling`（✅ P1-1 已修复：通过 users data-access）、`diagnostic`（✅ P1-1 已修复：通过 users data-access）、`elective`（✅ P1-1 已修复：通过 users data-access）、`proctoring`（✅ P1-1 已修复：通过 users data-access）、`parent`（✅ P1-1 已修复：通过 users data-access）

**已知问题**：
- ✅ P1 已解决：`import-export.ts` 四重职责已拆分为 `import-export.ts`（解析/生成）+ `user-service.ts`（用户创建）+ `class-registration.ts`（班级注册）
- ✅ P1 已解决：`batchImportUsers` 不再跨模块直写 `classEnrollments`，改为调用 `classes/data-access.enrollStudentByInvitationCode`
- ✅ P2 已解决：删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`，统一复用 `shared/lib/role-utils.resolvePrimaryRole`，消除重复代码
- ✅ P1-1 已修复：~~`updateUserProfile` 绕过 data-access 直接 DB 写~~ 已下沉到 data-access
- ✅ P2-20 已修复：新增 `getCurrentStudentUser` 函数（从 homework 模块迁移），6 个 student 页面通过此函数获取学生身份，不再依赖 homework 模块
- ✅ P2 已解决：`data-access.ts` 已扩充写操作（updateUserProfile 已下沉）
- ⚠️ 已知限制：`AdminUsersView` 客户端组件的删除操作通过 `fetch("/api/admin/users/:id")` 调用，对应 API 路由尚未实现（`deleteUserAction` Server Action 已就绪，可作为后续 API 路由的实现基础）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `import-export.ts` | 157 | 文件解析/生成（模板生成 + 解析校验 + Excel 导出）+ re-export 向后兼容 |
| `user-service.ts` | 82 | 用户创建（批量导入 + 密码哈希 + 角色分配） |
| `class-registration.ts` | 21 | 班级注册（委托 classes/data-access） |
| `actions.ts` | 218 | 7 个 Server Action（profile 更新 + 模板下载/导入/导出 + 角色更新 + 删除） |
| `data-access.ts` | 394 | getUserProfile + 用户查询 + 管理员用户列表分页查询（getAdminUsers/getAdminUserRoles） |
| `components/admin-users-view.tsx` | 290 | 管理员用户列表客户端组件（搜索+筛选+表格+分页+删除对话框） |

---

## 2.12 dashboard（仪表盘模块）

**职责**：管理员/教师/学生/家长仪表盘数据聚合 + 权限校验 + i18n + 纯逻辑工具函数 + 数据服务接口抽象 + 配置驱动 Widget 渲染 + 监控埋点接口 + 时间范围筛选 + 数据对比 + 自定义仪表盘 + 通知集成 + 实时更新 + 数据导出 + 移动端优化。

**导出函数**：
- Actions：`getAdminDashboardAction` / `getTeacherDashboardAction` / `getStudentDashboardAction` / `getParentDashboardAction`（均调用 `requirePermission()` 校验对应 `DASHBOARD_*_READ` 权限）
- Data-access：`getAdminDashboardData`（并行调用 6 个模块 stats 函数，V4 添加趋势数据 TODO 注释）
- Streams：`getAdminDashboardStreams`（V4 新增：返回各分区独立 Promise 供 React `use()` 流式消费）
- Lib 纯函数：`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule`（V4 修复：移除 `as T[]` 断言，改为非泛型函数）/ `computeTeacherMetrics` / `getGreetingKey` / `timeToMinutes`（V4 新增）/ `getScheduleStatus`（V4 新增）/ `getDueUrgency`（V4 新增）/ `getActionLabelKey`（V4 新增）/ `getActionVariant`（V4 新增）/ `computeComparison`（L3 新增：数据对比计算）/ `toCSV`（L7 新增：CSV 导出）/ `downloadFile`（L7 新增：浏览器下载）/ `exportDashboardCSV`（L7 新增：仪表盘 CSV 导出便捷方法）
- Services（V4 新增）：`DashboardService` 接口（抽象数据依赖，含 4 个角色方法签名）/ `DashboardAnalytics` 接口（监控埋点：Widget 点击 / 空状态 / 错误重试 / 页面停留）/ `DashboardServiceProvider`（React Context 依赖注入）/ `useDashboardService` / `useDashboardAnalytics`
- Config（V4 新增）：`ADMIN_WIDGET_CONFIG` / `TEACHER_WIDGET_CONFIG` / `STUDENT_WIDGET_CONFIG` / `PARENT_WIDGET_CONFIG` / `getWidgetConfig`（配置驱动 Widget 布局，新增角色只需修改配置）
- Hooks（L4/L6 新增）：`useDashboardPreferences`（L4 自定义仪表盘偏好，localStorage 持久化）/ `useDashboardRealtime`（L6 SSE 实时更新，指数退避重连）
- Components：`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `ParentDashboard`（V4 P1-4 从 parent 模块迁移，使用 slots 组合注入）/ `UserGrowthChart` / `DashboardGreetingHeader`（共享问候头部，V2 抽象，V4 支持空用户名 fallback）/ `DashboardSection`（分区 Error Boundary + Suspense + 骨架屏，V4 P3-1 新增 ariaLabel prop 支持键盘导航）/ `DashboardTimeRangeFilter`（L2 时间范围筛选）/ `ComparisonBadge`（L3 数据对比徽章）/ `DashboardNotificationWidget`（L5 通知中心 Widget）/ `DashboardResponsiveLayout`（L8 移动端响应式布局）/ `MobileSwipeContainer`（L8 移动端水平滑动）/ `DesktopGrid`（L8 桌面端网格）
- Types（V4 新增）：`DashboardRole` / `WidgetSkeletonVariant` / `DashboardWidgetConfig` / `DashboardLayoutConfig` / `ComparisonTrend` / `ComparisonResult`（L3）/ `TimeRange`（L2）/ `ConnectionStatus`（L6）/ `DashboardNotificationItem`（L5）/ `ExportRow` / `ExportColumn`（L7）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`classes`（通过 data-access）、`homework`（通过 data-access）、`users`（通过 data-access）、`parent`（通过 data-access.getParentDashboardData + 组件 slots 注入 ChildCard/ParentAttentionBanner）、`textbooks`/`questions`/`exams`（通过各模块 dashboard stats 函数）、`recharts`、`next-intl`
- 被依赖：无

**权限点**：
- `DASHBOARD_ADMIN_READ`（admin）
- `DASHBOARD_TEACHER_READ`（teacher）
- `DASHBOARD_STUDENT_READ`（student）
- `DASHBOARD_PARENT_READ`（parent）

**已知问题**：
- ✅ P0-4 已修复：`getAdminDashboardData` 改为并行调用各模块 dashboard stats 函数，不再直查跨模块表
- ✅ P0 已修复（2026-06-22）：所有仪表盘页面通过 `actions.ts` 调用 `requirePermission()` 进行权限校验，不再裸调 data-access
- ✅ P0 已修复（2026-06-22）：根重定向页 `/dashboard` 改用 `resolvePermissions()` + 权限点判断，不再 `role === "xxx"` 硬编码
- ✅ P0 已修复（2026-06-22）：所有仪表盘组件接入 next-intl（`useTranslations` / `getTranslations`），翻译文件 `messages/{zh-CN,en}/dashboard.json`
- ✅ P1 已修复（2026-06-22）：业务逻辑（weekday 转换、作业统计、教师指标计算、问候语时段）抽取至 `lib/dashboard-utils.ts` 纯函数，与 UI 分离
- ✅ P2 已修复（2026-06-22）：新增 `components/dashboard-section.tsx`，每个独立数据区块用 Error Boundary + Suspense + 骨架屏包裹，单区块崩溃/加载不波及整页（5 种骨架变体：stats/card/chart/table/list）
- ℹ️ V1 新增：`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段，`data-access.ts` 当前返回空数组占位（V4 P2-4 添加 TODO 注释），待后续接入真实统计
- ✅ V4 P1-4 已修复：`ParentDashboard` 组件从 `modules/parent/components/` 迁移至 `modules/dashboard/components/parent-dashboard/`，使用 slots 组合注入 ChildCard 和 ParentAttentionBanner，parent 模块仅保留数据访问
- ✅ P0 已修复（2026-06-22 V2）：10 个子组件 i18n 遗漏全部补齐，新增 ~50 个翻译键
- ✅ P1 已修复（2026-06-22 V2）：抽象共享组件 `DashboardGreetingHeader`，消除 teacher/student 头部 90% 重复代码
- ✅ P2 已修复（2026-06-22 V2）：为 6 个纯函数添加 31 个单元测试
- ✅ P2 已修复（2026-06-22 V2）：a11y 增强 — admin 表格 `<caption>`、teacher/student 视图语义化标签
- ✅ P0 已修复（2026-06-22 V4）：`filterTodaySchedule` 移除 `as T[]` 类型断言，改为非泛型函数
- ✅ P1 已修复（2026-06-22 V4）：5 个组件内嵌纯函数抽取至 `lib/dashboard-utils.ts`
- ✅ P1 已修复（2026-06-22 V4）：`teacherName` 硬编码 "Teacher" fallback 改为空字符串
- ✅ P1 已修复（2026-06-22 V4）：`teacher-schedule.tsx` 移除本地类型定义，从 `types.ts` 导入
- ✅ P2 已修复（2026-06-22 V4）：新增 `services/dashboard-service.tsx`，定义 `DashboardService` 接口 + `DashboardAnalytics` 监控埋点接口 + React Context 依赖注入
- ✅ P2 已修复（2026-06-22 V4）：新增 `config/widget-configs.ts`，4 个角色的 Widget 布局配置，支持配置驱动渲染
- ✅ V4 P2-4 已修复：admin 趋势数据占位添加 TODO 注释，标记后续实现
- ✅ V4 P2-5 已修复：4 个角色 StatCard 使用模式统一（均传 color + valueClassName="tabular-nums"）
- ✅ V4 P3-1 已修复：`DashboardSection` 新增 `ariaLabel` prop，渲染 `<section role="region" tabIndex={0}>`，admin/teacher/student 视图全部接入
- ✅ V4 P3-2 已修复：`AdminTrendCharts` 添加 TODO 注释标记趋势数据待接入
- ✅ V4 P3-3 已修复：`TeacherTodoCard` 排序逻辑改用 `VARIANT_PRIORITY` 数值映射，提升可读性
- ✅ L1 已实施：Widget 下钻导航 — admin StatCard 添加 href 跳转，ContentRow 支持可选 href 下钻
- ✅ L2 已实施：时间范围筛选 — 新增 `DashboardTimeRangeFilter` 客户端组件，URL search param 持久化，集成到 admin dashboard
- ✅ L3 已实施：数据对比 — 新增 `ComparisonBadge` 组件 + `computeComparison` 纯函数
- ✅ L4 已实施：自定义仪表盘 — 新增 `useDashboardPreferences` Hook，localStorage 持久化 Widget 显示/隐藏偏好
- ✅ L5 已实施：通知中心集成 — 新增 `DashboardNotificationWidget` 组件
- ✅ L6 已实施：实时更新 — 新增 `useDashboardRealtime` Hook（SSE + 指数退避重连）
- ✅ L7 已实施：数据导出 — 新增 `lib/export-utils.ts`（CSV 导出 + 浏览器下载）
- ✅ L8 已实施：移动端优化 — 新增 `DashboardResponsiveLayout` / `MobileSwipeContainer` / `DesktopGrid` 组件

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 167 | 4 个 Server Action（编排层，含 `requirePermission()` 权限校验） |
| `data-access.ts` | 51 | admin 仪表盘数据聚合（并行调用各模块 stats 函数，V4 添加趋势数据 TODO） |
| `streams.ts` | 34 | admin 流式数据源（返回未解析 Promise 供 React `use()` 消费） |
| `lib/dashboard-utils.ts` | 279 | 纯逻辑工具函数（weekday / 统计 / 排序 / 指标计算 / 问候语 / 课表状态 / 作业紧急度 / 操作按钮 V4 新增 5 个函数） |
| `lib/export-utils.ts` | 73 | L7 新增：CSV 导出工具（toCSV / downloadFile / exportDashboardCSV） |
| `types.ts` | 107 | Admin / Teacher / Student 类型定义 + V4 新增 Widget 配置类型 |
| `services/dashboard-service.tsx` | 110 | V4 新增：DashboardService 接口 + DashboardAnalytics 接口 + React Context 依赖注入 |
| `config/widget-configs.ts` | 62 | V4 新增：4 个角色的 Widget 布局配置（配置驱动渲染） |
| `hooks/use-dashboard-preferences.ts` | 82 | L4 新增：自定义仪表盘偏好 Hook（localStorage 持久化） |
| `hooks/use-dashboard-realtime.ts` | 73 | L6 新增：SSE 实时更新 Hook（指数退避重连） |
| `components/dashboard-section.tsx` | 186 | 分区 Error Boundary + Suspense + 骨架屏（V4 P3-1 新增 ariaLabel prop） |
| `components/dashboard-greeting-header.tsx` | 36 | 共享问候头部组件（V2 抽象，V4 支持空用户名 fallback） |
| `components/dashboard-time-range-filter.tsx` | 57 | L2 新增：时间范围筛选器（URL search param 持久化） |
| `components/comparison-badge.tsx` | 75 | L3 新增：数据对比徽章 + computeComparison 纯函数 |
| `components/dashboard-notification-widget.tsx` | 85 | L5 新增：通知中心 Widget |
| `components/dashboard-responsive-layout.tsx` | 72 | L8 新增：移动端响应式布局组件 |
| `components/parent-dashboard/parent-dashboard.tsx` | 115 | V4 P1-4 迁移：家长仪表盘视图（slots 组合注入） |
| `components/admin-dashboard/admin-dashboard.tsx` | 180 | 管理员仪表盘视图（V4 接入时间范围筛选 + ariaLabel） |
| `components/admin-dashboard/admin-sections.tsx` | 245 | 管理员 6 个分区组件（V4 StatCard 下钻 + ContentRow href） |
| `components/admin-dashboard/user-growth-chart.tsx` | 65 | recharts 折线图（i18n） |
| `components/teacher-dashboard/*.tsx` | 9 文件 | 教师仪表盘组件（V4 修复 teacher-schedule 类型导入 + TeacherTodoCard 排序优化 + ariaLabel） |
| `components/student-dashboard/*.tsx` | 6 文件 | 学生仪表盘组件（V4 抽取纯函数 + ariaLabel） |
| `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数的 31 个单元测试（V2 新增） |

---

## 2.13 messaging（私信模块）

**职责**：站内私信（messages 表 CRUD）+ 消息草稿（message_drafts 表 CRUD）。

**导出函数**：
- Actions：`sendMessageAction`（✅ 审计 V1-P0-1：新增 `isReceiverAllowed` 二次校验收件人是否在 sender DataScope 内 + ✅ 审计 V1-P0-6：校验 `parentMessageId` 属于当前用户会话 + ✅ 审计 V1-P1-9：失败埋点 `message.send_failed`）/ `markMessageAsReadAction` / `deleteMessageAction` / `getMessagesAction`（✅ 审计 V1：参数新增 `starredOnly?: boolean`）/ `getRecipientsAction` / `getUnreadMessageCountAction`（✅ P1-4 已修复：通知 CRUD Action 已迁移至 notifications 模块；✅ V3-P1-3 已修复：通知偏好 Action 已迁移至 notifications 模块，messaging 仅保留私信 Action；✅ V2-P2-13c 新增：`toggleMessageStarAction` 星标切换 / `getMessageDraftsAction` 草稿列表 / `saveMessageDraftAction` 草稿保存（✅ V3-P0-1：使用 `MessageDraftSchema` Zod 校验替代 `as` 断言）/ `deleteMessageDraftAction` 草稿删除；✅ 审计 V1-P0-4 已删除：`getMessageDetailAction`（V2-P1-3 重构遗留死代码，详情数据由 RSC 通过 `getMessageDetailPageData` 直接获取）；✅ 审计 V1-P1-3 新增：`getMessageThreadAction` 获取消息线程（替代组件直接调用 data-access，统一权限边界））
- Data-access：`getMessages`（✅ V2-P2-13c 新增 starredOnly 星标筛选）/ `getMessageById` / `getMessageThread`（✅ 审计 V1-P0-2：签名变更为 `(messageId, userId)`，内部校验当前用户对根消息的访问权）/ `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar`（✅ V2-P2-13c 新增：接收方星标切换）/ `getUnreadMessageCount` / `getRecipients`（✅ 审计 V1-P1-6：返回 `role` 收紧为 `RecipientRole` 类型，admin 分支补 role）/ `isReceiverAllowed`（✅ 审计 V1-P0-1 新增：校验收件人是否在 sender DataScope 内）/ `getMessageDetailPageData`（✅ V2-P1-3 新增：消息详情页编排函数，获取详情并自动标记已读；✅ 审计 V1-P0-3：`getMessagesPageData` 已删除，编排迁回 messages/page.tsx 页面层 `Promise.all` 调用 messaging + notifications data-access，保持模块独立性）/ `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`（✅ V2-P2-13c 新增：消息草稿 CRUD，操作 message_drafts 表）
- Hooks：`useMessageSearch`（✅ P1-7 新增：消息搜索 hook，含防抖和请求竞态取消）
- Lib：`buildReplyHref`（✅ 审计 V1-P1-7 新增：构建回复消息 URL 纯函数，使用 URLSearchParams 安全编码，替代组件内字符串拼接）
- Notification-preferences：~~re-export shim（实际逻辑在 `notifications/preferences.ts`）~~ ✅ P0-b 已修复：`notification-preferences.ts` 文件已删除（通知模块去重），消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`notifications`（✅ P0-4 / P1-5 已修复：通过 `sendNotification` dispatcher 发送通知，通知 CRUD 和偏好已迁移至 notifications 模块；✅ P1-4 已修复：通知 UI 组件已迁移至 notifications 模块；✅ 审计 V1-P0-3 已修复：data-access 不再动态 import notifications，编排迁回页面层）、`classes`（通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID，支持学生 class_members 和家长 children 数据范围）、`users`（通过 data-access.getUserNamesByIds 获取用户显示名称）
- 被依赖：`notifications`（✅ 已消除反向依赖）、`settings`（通知偏好表单）、`layout`（✅ P1-4 已修复：通知下拉组件改为从 notifications 模块导入）

**已知问题**：
- ✅ P0-4 已修复：~~`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`~~ 改为调用 `notifications.sendNotification`，通知 CRUD 已迁移至 notifications 模块
- ✅ P0 已修复：~~与 notifications 双向依赖 + 职责重叠~~ 通知相关表（messageNotifications / notificationPreferences）所有权已移交 notifications 模块，messaging 仅保留 messages 表
- ✅ P1-5 已修复：~~同时管理 3 类数据（messages + messageNotifications + notificationPreferences）~~ 仅管理 messages 表，通知相关数据由 notifications 模块管理
- ✅ P1 已修复：~~通知相关 Action 使用 `requireAuth()` 而非 `requirePermission()`~~ 通知 Action 已迁移至 notifications 模块并改为 `requirePermission(Permissions.MESSAGE_READ)`
- ✅ P1 已修复：~~`markMessageAsReadAction` / `deleteMessageAction` / `getMessageDetailAction` 缺少 Zod 校验~~ 已添加 `MessageIdSchema` 校验 messageId 参数
- ✅ P1 已修复：~~`updateNotificationPreferencesAction` 缺少 Zod 校验~~ 已添加 `UpdateNotificationPreferencesSchema` 校验 8 个布尔字段
- ✅ P2 已修复：`data-access.ts` 中 3 处 `or(...)!` 非空断言清理为安全守卫（条件 push）
- ✅ P0-b 已修复：~~`notification-preferences.ts` re-export shim 文件~~ 已删除（通知模块去重），8 个消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`，消除 messaging 模块对通知偏好的冗余 re-export 层
- ✅ P1 已修复：~~全模块零 i18n，中英文案硬编码~~ 所有组件接入 next-intl（`useTranslations("messages")`），新增 `src/shared/i18n/messages/{zh-CN,en}/messages.json` 翻译字典（title/description/tabs/actions/form/status/meta/notificationType/search/empty/messages/error 共 13 个命名空间；✅ 审计 V1-P1-4 新增 `pagination` / `drafts` / `thread` 三个命名空间）；所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复：~~缺 Error Boundary~~ 新增 3 个 `error.tsx` 错误边界（`/messages`、`/messages/[id]`、`/messages/compose`），统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复：a11y 改进，`message-list.tsx` 添加 `aria-label` / `aria-hidden`；✅ 审计 V1-P1-4：分页 `aria-label` i18n 化（替代硬编码 "Previous page" / "Next page"），星标按钮加 `aria-pressed`
- ✅ P1-4 已修复：~~通知组件（notification-list.tsx / notification-dropdown.tsx）放在 messaging/components 下，直接 import notifications 模块类型和 messaging/actions~~ 两个组件已迁移至 `notifications/components/`，通知 CRUD Action 已迁移至 `notifications/actions.ts`，messaging 模块仅保留私信组件
- ✅ 审计 V1-P0-3 已修复：~~`getMessagesPageData` 在 data-access 层动态 `await import("@/modules/notifications/data-access")` 跨模块编排~~ 编排迁回 `messages/page.tsx` 页面层 `Promise.all([getMessages(...), getNotifications(...)])`，保持 messaging data-access 模块独立性
- ✅ 审计 V1-P0-4 已修复：~~`getMessageDetailAction` 死代码（V2-P1-3 重构遗留，全代码库无引用）~~ 已彻底删除
- ✅ 审计 V1-P0-1 已修复：~~`sendMessageAction` 未校验 `receiverId` 是否在 sender DataScope 内~~ 新增 `isReceiverAllowed(userId, receiverId, scope)` data-access 函数，Server Action 二次校验，防止越权发送
- ✅ 审计 V1-P0-2 已修复：~~`getMessageThread(messageId)` 缺少 userId 参数和权限校验~~ 签名变更为 `(messageId, userId)`，where 子句加 `(senderId=userId OR receiverId=userId)`，纵深防御
- ✅ 审计 V1-P0-5 已修复：~~`MessageCompose` 自动保存 effect 在用户快速提交后仍会触发新草稿创建~~ 新增 `submittedRef` 标志位，提交后跳过自动保存，失败时重置
- ✅ 审计 V1-P0-6 已修复：~~`sendMessageAction` 未校验 `parentMessageId` 属于当前用户会话~~ 新增 `getMessageById(parentMessageId, ctx.userId)` 校验，防止将回复注入他人会话线程
- ✅ 审计 V1-P1-1 已修复：~~compose 页面无草稿列表 UI~~ 新增 `MessageDraftList` 组件，compose page 通过 `getMessageDraftsAction` 获取草稿并渲染，支持"继续编辑"和"删除草稿"
- ✅ 审计 V1-P1-2 已修复：~~MessageList 缺少"星标" Tab，data-access `starredOnly` 筛选不可达~~ Tab 类型新增 `starred`，切换时调用 `getMessagesAction({type:"all", starredOnly:true})`
- ✅ 审计 V1-P1-3 已修复：~~MessageDetail 不渲染消息线程，`getMessageThread` 已实现但 UI 缺失~~ 新增 `getMessageThreadAction` + 线程渲染区块，时间正序展示回复
- ✅ 审计 V1-P1-6 已修复：~~`RecipientOption.role` 为宽松的 `string?`~~ 新增 `RecipientRole = "student" | "teacher" | "admin" | "parent"` 类型，admin 分支补 role
- ✅ 审计 V1-P1-7 已修复：~~`replyHref` 构建逻辑散落在 MessageDetail 组件内，URL 参数未安全编码~~ 抽取为 `lib/build-reply-href.ts` 纯函数，使用 `URLSearchParams` 编码
- ✅ 审计 V1-P1-9 已修复：~~`sendMessageAction` 失败路径无埋点~~ catch 块调用 `trackEvent({event:"message.send_failed"})`，权限拒绝除外避免噪音
- ✅ P1-7 已修复：~~消息列表客户端 `useEffect` + `setTimeout` 防抖搜索未取消已发出的请求~~ 搜索逻辑抽离为 `useMessageSearch` hook（含防抖 + 请求竞态取消）；~~无分页 UI~~ 新增客户端分页 UI（PAGE_SIZE=20，ChevronLeft/ChevronRight 按钮）
- ✅ P1-9 已修复：~~`deleteMessage` 两个独立 UPDATE 无事务~~ 改为 `db.transaction` 包裹 senderDeletedAt 和 receiverDeletedAt 更新，保证原子性
- ✅ P2-11 已修复：~~发送/删除消息无埋点~~ `sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` 新增 `trackEvent` 埋点（message.sent / message.marked_read / message.deleted）
- ✅ V2-P0-2 已修复：~~通知标题硬编码~~ `sendMessageAction` 通过 `getTranslations('messages')` 生成 i18n 通知标题（`notification.messageTitle` / `messageTitleNoSubject`）
- ✅ V2-P1-2 已修复：~~MessageList 客户端过滤冗余~~ 客户端过滤仅在初始数据（type=all）时执行，搜索结果已由服务端按 tab 过滤
- ✅ V2-P1-3 已修复：~~消息详情页分散编排~~ 新增 `getMessageDetailPageData` 编排函数，替代 page.tsx 中 `after()` + `getMessageById` + `markMessageAsRead` 的分散编排
- ✅ V2-P1-4 已修复：~~表单无服务端校验错误展示~~ `message-compose.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示（receiverId/subject/content）
- ✅ V2-P2-1 已修复：~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量（~~60_000ms~~ → 30_000ms，✅ V2-P3 与通知组件保持一致）
- ✅ V2-P2-13c 已实现：消息星标和草稿功能。`messages` 表新增 `isStarred` 字段（接收方可标记重要消息）+ `messages_receiver_starred_idx` 复合索引；新增 `message_drafts` 表（userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt，含 `message_drafts_user_idx` 和 `message_drafts_user_updated_idx` 索引）；data-access 新增 `toggleMessageStar` / `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`；actions 新增 `toggleMessageStarAction` / `getMessageDraftsAction` / `saveMessageDraftAction` / `deleteMessageDraftAction`；`getMessages` 支持 `starredOnly` 筛选；i18n 新增 actions.star/unstar/saveDraft/deleteDraft + messages.draftSaved/draftDeleted/starToggled + empty.noDrafts/noStarred 翻译键；迁移文件 `drizzle/0008_message_star_draft.sql`
- ✅ V3-P0-1 已修复：~~`saveMessageDraftAction` 中 5 处 `formData.get("xxx") as string \| null` 断言~~ 改为使用 `MessageDraftSchema` Zod 校验安全转换 FormData 值
- ✅ V3-P1-3 已修复：~~`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 放在 messaging 模块~~ 已迁移至 `notifications/actions.ts`，messaging 仅保留私信 Action
- ✅ V3-P1-6 已修复：~~`getMessageDraftsAction` 和 `getMessagesPageData` 使用内联 `import("./types").XXX` 类型导入~~ 改为顶部 `import type` 语句
- ✅ V3-P2-7 已修复：~~`message-list.tsx` 中 `setTab(v as Tab)` 断言~~ 改为 `isTab` 类型守卫

**未实施项（P1-5 / P1-8 / P2-* 中长期）**：
- ⏳ P1-5：`UnreadMessageBadge` 接入 SSE 实时推送（需扩展 `/api/notifications/stream` 端点 payload）
- ✅ P1-8 已实施：`MessageList` 依赖注入化（定义 `MessageListService` 接口通过 `MessageListServiceProvider` Context 注入，`default-message-list-service.ts` 绑定现有 Server Actions）
- ✅ P2-1 已实施：批量操作（`bulkMarkMessagesAsRead` / `bulkDeleteMessages` / `bulkToggleMessagesStar`，使用 inArray + 事务）
- ✅ P2-2 已实施：群组消息（fan-out on write 策略，`messages.groupMessageId` 列 + `sendGroupMessage` data-access + `sendGroupMessageAction`，教师→全班家长，DataScope 校验）
- ✅ P2-3 已实施：附件支持（复用 `fileAttachments` 表，`targetType="message"` + `getMessageAttachments` data-access + `getMessageAttachmentsAction`，UI 含 `MessageAttachmentUploader` 上传 + `MessageAttachmentList` 展示下载）
- ✅ P2-4 已实施：消息撤回（`messages.recalledAt` 列 + `recallMessage` data-access，2 分钟窗口校验，UI 显示"已撤回"占位）
- ✅ P2-5 已实施：举报+屏蔽（新增 `message_reports` 表 + `user_blocks` 表，`reportMessage` / `blockUser` / `unblockUser` / `isEitherUserBlocked` data-access，`reportMessageAction` / `blockUserAction` / `unblockUserAction` / `getUserBlocksAction`；`sendMessageAction` 集成 `isEitherUserBlocked` 屏蔽校验）
- ✅ P2-6 已实施：快捷回复模板（`message_templates` 表 + CRUD data-access + `MessageTemplatePicker` UI 组件集成到 compose）
- ✅ P2-7 已实施：配置驱动的角色组合（`RecipientResolver` 注册表模式，`RECIPIENT_RESOLVERS` 按 DataScope.type 索引，新增角色仅新增配置项）
- ✅ P2-8 已实施：Error Boundary 细化（列表区/详情区/草稿区分别 `SectionErrorBoundary` 包裹）
- ✅ P2-9 已实施：React Suspense 流式渲染（RSC 使用 `Suspense` 包裹 MessageList + NotificationList，骨架屏 fallback）
- ✅ P2-10 已实施：跨设备草稿同步（`message_drafts.deviceId` + `version` 列 + `userParentIdx` 索引，`updateMessageDraft` 带乐观锁返回 `"ok" | "not_found" | "conflict"`，`saveMessageDraftAction` 处理冲突与 not_found 重建）
- ✅ P2-11 已实施：组合搜索（`getMessages` 扩展 `senderId` / `dateFrom` / `dateTo` / `isRead` 参数）
- ✅ P1-9 已实施：失败埋点（`message.send_failed` / `message.group_send_failed` / `message.report_failed` 等）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~360 | 11 个私信 Server Action（✅ 审计 V1-P0-4：删除 `getMessageDetailAction` 死代码；✅ 审计 V1-P1-3：新增 `getMessageThreadAction`；✅ 审计 V1-P0-1/P0-6/P1-9：sendMessageAction 增加 receiverId 二次校验 + parentMessageId 校验 + 失败埋点；✅ V3-P1-3：通知偏好 Action 已迁移至 notifications；✅ V3-P0-1：`saveMessageDraftAction` 使用 Zod 校验；✅ V2-P0-2：通知标题 i18n 化） |
| `data-access.ts` | ~430 | 私信 CRUD + ✅ 审计 V1-P0-1 新增 `isReceiverAllowed` + ✅ 审计 V1-P0-2 `getMessageThread` 增加 userId 校验 + ✅ 审计 V1-P0-3 删除 `getMessagesPageData` 编排迁出至页面层 + `getMessageDetailPageData` 编排函数（✅ P1-5 新增；✅ V3-P1-6：内联类型导入改为顶部 import type） |
| `schema.ts` | 40 | 私信发送校验 + messageId 校验 + 消息草稿校验（✅ V3-P0-1 新增 `MessageDraftSchema`；✅ V3-P1-3：`UpdateNotificationPreferencesSchema` 已迁移至 notifications/schema.ts） |
| `types.ts` | 85 | 私信类型 + ✅ 审计 V1-P1-6 新增 `RecipientRole` 类型 + re-export 通知类型（向后兼容） |
| `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增：消息搜索 hook（防抖 + 请求竞态取消） |
| `lib/build-reply-href.ts` | ~30 | ✅ 审计 V1-P1-7 新增：构建回复 URL 纯函数（URLSearchParams 编码） |

**组件清单**：
| 组件 | 职责 |
|------|------|
| `components/message-list.tsx` | 消息列表（✅ P1-7：使用 `useMessageSearch` hook + 客户端分页 UI，PAGE_SIZE=20；✅ V2-P1-2：客户端过滤仅在初始数据时执行；✅ 审计 V1-P1-2：新增 `starred` Tab + 调用 `getMessagesAction({starredOnly:true})`；✅ 审计 V1-P1-4：分页 aria-label i18n 化） |
| `components/message-detail.tsx` | 消息详情（含回复；✅ 审计 V1-P1-3：渲染消息线程；✅ 审计 V1-P1-7：使用 `buildReplyHref` 纯函数；✅ 审计 V1-P1-4：星标按钮加 `aria-pressed`） |
| `components/message-compose.tsx` | 撰写新消息（✅ V2-P1-4：fieldErrors + aria-invalid 字段级错误展示；✅ 审计 V1-P0-5：`submittedRef` 防止提交后自动保存竞态） |
| `components/message-draft-list.tsx` | ✅ 审计 V1-P1-1 新增：草稿列表组件（继续编辑 + 删除草稿 + 乐观更新） |
| `components/unread-message-badge.tsx` | 未读消息计数徽章（侧边栏，每 30 秒轮询 `getUnreadMessageCountAction`；✅ V2-P2-1：POLL_INTERVAL_MS 常量；✅ V2-P3：间隔从 60s 缩短为 30s 与通知一致） |

**客户端行为**：
- `message-list.tsx`：客户端调用 `getMessagesAction` 搜索消息（useMessageSearch hook，400ms 防抖，请求竞态取消）；V2-P1-2 优化：客户端过滤仅在初始数据（type=all）时执行；✅ 审计 V1-P1-2：starred Tab 单独加载 `getMessagesAction({type:"all", starredOnly:true})`
- `unread-message-badge.tsx`：每 `POLL_INTERVAL_MS`（30_000ms）轮询 `getUnreadMessageCountAction` 刷新未读计数
- `message-detail.tsx`：✅ 审计 V1-P1-3 客户端调用 `getMessageThreadAction(messageId)` 加载回复链
- `message-compose.tsx`：✅ 审计 V1-P0-5 `submittedRef` 标志位阻止发送后自动保存 effect 触发新草稿

---

## 2.14 notifications（通知分发模块）

**职责**：多渠道通知分发（SMS/Email/WeChat/InApp）+ 站内通知 CRUD + 通知偏好管理 + 通知 UI 组件。

**导出函数**：
- Actions：`sendNotificationAction` / `sendClassNotificationAction` / `getNotificationsAction` / `getUnreadNotificationCountAction` / `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` / `archiveNotificationAction`（✅ P1-4 新增：后 4 个通知 CRUD Action 从 messaging 模块迁移；✅ V2-P2-13b 新增：archiveNotificationAction 归档 Action；✅ V3-P1-3 新增：`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 从 messaging 模块迁移）
- Dispatcher：`sendNotification(payload)` / `sendBatchNotifications(payloads)`
- Data-access：`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`（✅ P0-4 / P1-5 修复后从 messaging 迁移；✅ V2-P2-13b 新增：archiveNotification / unarchiveNotification 归档函数）
- Preferences：`getNotificationPreferences` / `upsertNotificationPreferences`（✅ P0-4 / P1-5 修复后从 messaging 迁移）
- Channels：`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
- Components：`NotificationList` / `NotificationDropdown`（✅ P1-4 新增：从 messaging/components 迁移；✅ V2-P2-13b：NotificationList 支持优先级 Badge 显示和归档操作；✅ V2-P2-13c：NotificationList 支持按类型筛选）
- Hooks：`useNotificationStream`（✅ V2-P3 新增：SSE 实时推送 + 轮询降级 Hook）、`useDesktopNotifications`（✅ V2-P2-13c 新增：浏览器桌面推送 Hook，支持权限管理和新通知桌面提醒）
- Types：`NotificationPriority`（✅ V2-P2-13b 新增：通知优先级类型 low/normal/high/urgent）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`classes`（✅ P1-1 已修复：通过 classes data-access.getClassExists/getStudentIdsByClassId）
- 被依赖：`messaging`（✅ P0-4 / P1-5 已修复：messaging 通过 `sendNotification` dispatcher 发送通知；✅ P1-4 已修复：通知 UI 组件由 notifications 模块自持，messaging 不再反向依赖）、`layout`（✅ P1-4 已修复：通知下拉组件直接从 notifications 模块导入）、`app/(dashboard)/messages`（✅ P1-4 已修复：通知列表组件直接从 notifications 模块导入）

**已知问题**：
- ✅ P0-4 已修复：~~不拥有任何数据，全部依赖 messaging 模块~~ messageNotifications 和 notificationPreferences 表所有权已从 messaging 迁移至 notifications 模块
- ✅ P0 已修复：~~与 messaging 双向依赖~~ notifications 不再反向依赖 messaging，in-app-channel 改为静态导入本地 createNotification
- ✅ P1-1 已修复：~~`sendClassNotificationAction` 直查 `classes`/`classEnrollments`~~ 改为调用 `classes/data-access.getClassExists` / `getStudentIdsByClassId`
- ✅ P1-4 已修复：~~通知 UI 组件放在 messaging/components 下，直接 import notifications 类型和 messaging/actions~~ `notification-list.tsx` 和 `notification-dropdown.tsx` 已迁移至 `notifications/components/`，通知 CRUD Action 已从 messaging 迁移至 `notifications/actions.ts`，消除 UI 层跨模块耦合
- ✅ P2-11 已修复：~~通知标记已读无埋点~~ `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` 新增 `trackEvent` 埋点（notification.marked_read / notification.marked_all_read）
- ✅ V2-P0-1 已修复：~~通知 i18n 键混在 messages.json 中~~ 新增独立的 `notifications.json` 命名空间（zh-CN/en），通知组件 `useTranslations` 从 `"messages"` 切换到 `"notifications"`；`src/i18n/request.ts` 新增 notifications 命名空间加载
- ✅ V2-P2-1 已修复：~~轮询间隔魔法数字~~ `notification-dropdown.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量（30_000ms）
- ✅ V2-P3 已优化：~~30 秒轮询~~ `notification-dropdown.tsx` 改为 SSE 实时推送（`/api/notifications/stream`），SSE 不可用时自动降级为轮询（调用 Server Actions，间隔 30 秒）
- ✅ V2-P2-13b 新增：通知优先级和归档功能。schema.ts `messageNotifications` 表新增 `priority`（low/normal/high/urgent，默认 normal）和 `isArchived`（默认 false）字段 + 2 个索引；types.ts 新增 `NotificationPriority` 类型；data-access.ts 新增 `archiveNotification` / `unarchiveNotification` 函数，`getNotifications` 支持归档和优先级筛选（默认仅返回未归档），`createNotification` 支持 priority；actions.ts 新增 `archiveNotificationAction`（含 trackEvent 埋点 notification.archived）；NotificationList 组件支持优先级 Badge 显示和归档按钮；i18n 新增 priority/actions.archive/messages.archiveFailed 翻译键
- ✅ V2-P2-13c 新增：通知分类筛选 + 浏览器桌面推送。NotificationList 组件新增按类型筛选按钮组（all/message/announcement/homework/grade），空状态区分"无通知"和"无筛选结果"；新增 `useDesktopNotifications` Hook（浏览器 Notification API，支持权限管理、自动请求权限、新通知桌面推送、点击跳转）；NotificationDropdown 集成桌面推送（监听新通知并触发桌面提醒，首次加载不批量推送）；i18n 新增 filter.all / empty.noFilterResults / empty.noFilterResultsDesc 翻译键
- ⚠️ P1：发送日志仅 console，无 `notification_logs` 表
- ✅ 渠道抽象优秀（接口 + 工厂 + Mock 实现）
- ✅ V3-P1-3 已修复：~~通知偏好 Action 放在 messaging 模块~~ `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 已迁移至 `notifications/actions.ts`，新增 `notifications/schema.ts`（`UpdateNotificationPreferencesSchema` 从 messaging 迁移）
- ✅ V3-P1-4 已修复：~~`sendBatchNotifications` 重复日志~~ 移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用（日志已在 `sendNotification` 内部记录）
- ✅ V3-P1-5 已修复：~~腾讯云短信 `SmsSdkAppId` 使用 `templateCode`~~ 新增 `SMS_SDK_APP_ID` 环境变量和 `smsSdkAppId` 配置字段，`SmsSdkAppId` 使用独立配置值
- ✅ V3-P2-7 已修复：~~`notification-list.tsx` 中 `Object.keys(TYPE_ICON) as NotificationType[]` 断言~~ 改为 `TYPE_KEYS` 常量数组
- ✅ V3-P2-8 已标记：`logNotificationSend` 中 `console.info` / `console.error` 添加 TODO 注释，预留统一日志服务接入点

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `dispatcher.ts` | ~145 | 渠道选择 + 并行分发（✅ V3-P1-4：移除 `sendBatchNotifications` 重复日志） |
| `data-access.ts` | ~250 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化（notification_logs 表；P0-4 / P1-5 修复后新增通知 CRUD；✅ V2-P2-13b：新增归档函数 + 优先级/归档筛选；✅ V3-P2-8：console 日志添加 TODO 统一日志服务标记） |
| `preferences.ts` | 166 | 通知偏好 CRUD（P0-4 / P1-5 修复后从 messaging 迁移） |
| `actions.ts` | ~380 | 9 个 Server Action（✅ P1-4：新增 4 个通知 CRUD Action；✅ V2-P2-13b：新增 archiveNotificationAction；✅ V3-P1-3：新增 getNotificationPreferencesAction / updateNotificationPreferencesAction 从 messaging 迁移） |
| `schema.ts` | 22 | ✅ V3-P1-3 新增：通知偏好校验 Schema（从 messaging/schema.ts 迁移） |
| `types.ts` | ~130 | 通知负载 + 渠道配置 + 通知记录 + 偏好类型（P0-4 / P1-5 修复后扩充；✅ V2-P2-13b：新增 NotificationPriority 类型 + priority/isArchived 字段） |
| `index.ts` | ~85 | 对外导出入口（✅ P1-4：新增组件和 CRUD Action 导出；✅ V2-P2-13b：新增归档函数/Action/类型导出；✅ V2-P2-13c：新增 useNotificationStream / useDesktopNotifications Hook 导出；✅ V3-P1-3：新增通知偏好 Action 和 Schema 导出） |
| `channels/*` | 5 文件 | 4 个渠道实现 |
| `components/notification-list.tsx` | ~190 | ✅ P1-4 新增（从 messaging 迁移）：通知列表组件；✅ V2-P2-13b：支持优先级 Badge 显示和归档操作；✅ V2-P2-13c：支持按类型筛选 + 空状态区分 |
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增（从 messaging 迁移）：通知下拉菜单组件；✅ V2-P3：改用 SSE 实时推送 + 轮询降级；✅ V2-P2-13c：集成 useDesktopNotifications 桌面推送 |
| `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增：SSE 实时推送 Hook（EventSource + 轮询降级） |
| `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增：浏览器桌面推送 Hook（Notification API + 权限管理 + 点击跳转） |

**组件清单**：
| 组件 | 职责 |
|------|------|
| `components/notification-list.tsx` | 通知列表（消息页底部，展示所有通知，支持标记已读；✅ V2-P0-1：useTranslations 命名空间从 "messages" 切换到 "notifications"；✅ V2-P2-13b：支持优先级 Badge 显示和归档操作；✅ V2-P2-13c：支持按类型筛选 + 空状态区分"无通知"/"无筛选结果"） |
| `components/notification-dropdown.tsx` | 通知下拉菜单（站点头部，✅ V2-P3：改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级；✅ V2-P0-1：useTranslations 命名空间切换；✅ V2-P2-1：POLL_INTERVAL_MS 常量；✅ V2-P2-13c：集成 useDesktopNotifications 桌面推送，监听新通知并触发桌面提醒） |

**客户端行为**：
- `notification-dropdown.tsx`：✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送（`/api/notifications/stream`），SSE 不可用时自动降级为轮询（调用 `getNotificationsAction` + `getUnreadNotificationCountAction`，间隔 30 秒）；✅ V2-P2-13c 集成 `useDesktopNotifications` Hook，通过 `prevNotificationIds` 对比检测新通知并触发桌面推送（首次加载不批量推送）
- `hooks/use-notification-stream.ts`：✅ V2-P3 新增，管理 SSE 连接生命周期（EventSource onopen/onmessage/onerror），降级时通过 `pollFnRef` 调用 Server Actions 轮询
- `hooks/use-desktop-notifications.ts`：✅ V2-P2-13c 新增，封装浏览器 Notification API（权限管理、自动请求权限、桌面推送、点击跳转回调），SSR 安全（`typeof window !== "undefined"` 检查）

---

## 2.15 audit（审计模块）

**职责**：操作日志 + 登录日志 + 数据变更日志查询与导出 + 审计概览仪表盘 + 数据保留策略（含 i18n + Error Boundary + 骨架屏 + 埋点 + Service 接口抽象）。

**导出函数**：
- Actions：`getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` / `getAuditRetentionConfigAction` / `saveAuditRetentionConfigAction` / `purgeAuditLogsAction`（✅ 审计重构：导出逻辑下沉到 `export.ts`，actions 仅编排；新增 `trackEvent` 埋点 `audit.exported`；✅ P2-5 新增保留策略 3 个 Action；✅ v2 修正：保留策略操作 event 改为 `audit.retention`；✅ v2 新增 `revalidatePath` 精确刷新缓存）
- Data-access：`getAuditLogs` / `getLoginLogs` / `getDataChangeLogs` / `getAuditModuleOptions` / `getDataChangeStats` / `getDataChangeTableOptions` / `getAuditLogsForExport` / `getLoginLogsForExport` / `getDataChangeLogsForExport` / `getAuditOverviewStats` / `getAuditTrend` / `getDataChangeActionStats`（✅ P2-4 新增概览统计 3 个函数）
- Export（✅ 审计重构新增）：`buildAuditLogExport` / `buildLoginLogExport` / `buildDataChangeLogExport`（Excel 列定义 + 行映射纯函数，从 actions 抽取）
- Retention（✅ P2-5 新增）：`getAuditRetentionConfig` / `saveAuditRetentionConfig` / `purgeExpiredAuditLogs` / `calculateRetentionThreshold` / `isValidRetentionDays`（保留策略配置读写 + 清理 + 纯函数）
- Hooks（✅ 审计重构新增）：`useLogPagination`（分页 URL 状态管理，三处重复逻辑抽取）
- Services（✅ P2-8 新增，✅ v2 接线完成）：`AuditService` 接口 + `AuditServiceProvider` Context + `useAuditService` / `useAuditAnalytics` hook + `adminAuditService` 默认实现（v2 改造为 Client-safe：查询方法抛错强制走 RSC props，写操作委托 Server Action）+ `mockAuditService` 测试用实现（v2 修正：消除 9 处 `as` 断言，为所有方法添加显式返回类型标注）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`next-intl`（i18n）、`shared/lib/track-event`（埋点）、`settings`（保留策略配置存储于 `system_settings` 表 `audit_retention` 分类）
- 被依赖：无

**已知问题**：
- ✅ 审计重构 P0 已修复：~~Excel 导出逻辑内联在 actions 层~~ 已抽取到 `export.ts`（144 行），actions.ts 从 212 行精简到 139 行
- ✅ 审计重构 P0 已修复：~~三个导出 Action 结构高度重复~~ 列定义 + 行映射统一到 export.ts 的 `buildXxxExport` 函数
- ✅ 审计重构 P0 已修复：~~全模块零 i18n~~ 所有组件接入 `useTranslations("audit")` + `useLocale()`，formatDate 从硬编码 `zh-CN` 改为 locale 变量；新增 `src/shared/i18n/messages/{zh-CN,en}/audit.json`（含 table/filter/empty/export/error/detail/overview/retention 命名空间）
- ✅ 审计重构 P0 已修复：~~缺 Error Boundary + 骨架屏~~ 新增 `AuditErrorBoundary`（class 组件，捕获错误展示 EmptyState + 重试按钮）；4 个页面新增 `loading.tsx`（骨架屏）+ `error.tsx`（错误边界）
- ✅ 审计重构 P1 已修复：~~DataChangeLogTable 281 行过大~~ 拆分为 `data-change-log-table.tsx`（146 行，仅表格）+ `data-change-log-filters.tsx`（138 行，独立筛选器）+ `data-change-log-view.tsx`（53 行，视图组合）
- ✅ 审计重构 P1 已修复：~~三处分页逻辑重复~~ 抽取 `useLogPagination` hook（27 行）
- ✅ 审计重构 P1 已修复：~~导出无埋点~~ `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` 新增 `trackEvent` 埋点（`audit.exported`）
- ✅ 审计重构 P1 已修复：a11y 改进，筛选器添加 `aria-label`、`aria-expanded` 属性
- ✅ 审计重构 P1 已修复：~~api/export/route.ts 中 `as Record<string, string>` 类型断言~~ 改为 for 循环 + 类型守卫
- ✅ P2 已修复：`data-access.ts` 中 6 处 catch 块添加 `console.error` 输出错误上下文；`toIso`/`clampPageSize`/`clampPage` 工具函数补齐显式返回类型
- ✅ P2-1 已修复：~~data-access 错误吞没~~ 所有 catch 块改为 `throw error`（不再返回空数组），UI 通过 ErrorBoundary 捕获
- ✅ P2-2 已修复：~~无日志详情视图~~ 新增 `AuditLogDetailDialog`（discriminated union 模式，支持 audit/login/dataChange 三种类型）
- ✅ P2-3 已修复：~~无用户维度筛选~~ 三个筛选器均新增 `userId` Input + URL 状态管理
- ✅ P2-4 已修复：~~无审计概览仪表盘~~ 新增 `/admin/audit-logs/overview` 页面（统计卡片 + 趋势图 + 分布图 + 快捷入口 + 保留策略配置）
- ✅ P2-5 已修复：~~无数据保留策略~~ 新增 `retention.ts`（配置读写 + 清理纯函数）+ 3 个 Server Action + cron API 路由 + `AuditRetentionSettings` 组件
- ✅ audit-P2-5（auth 审计）已修复：~~`login_logs` 与 `audit_logs`/`data_change_logs` 共用同一保留期（180 天），无法为登录日志配置更长的安全取证保留期~~ 新增独立 `loginLogRetentionDays` 配置项（`DEFAULT_LOGIN_LOG_RETENTION_DAYS = 365`），`AuditRetentionConfig` 类型新增 `loginLogRetentionDays` 字段；`getAuditRetentionConfig` / `saveAuditRetentionConfig` 读写新配置项；`purgeExpiredAuditLogs(retentionDays, loginLogRetentionDays?)` 支持为 login_logs 传入独立保留期（向后兼容，第二参数可选）；`purgeAuditLogsAction` / `AuditService.purgeExpiredAuditLogs` 接口同步更新；`AuditRetentionSettings` 组件新增登录日志保留天数 Input；新增 `vercel.json` 配置 cron schedule `0 3 * * *`（每日凌晨 3 点触发 `/api/cron/audit-cleanup`）；`env.mjs` 新增 `CRON_SECRET` 可选环境变量校验
- ✅ P2-6 已修复：~~无单测~~ 新增 `retention.test.ts`（9 个测试）+ `export.test.ts`（13 个测试），全部通过
- ✅ P2-8 已修复：~~无 Service 接口抽象~~ 新增 `services/audit-service.tsx`（接口 + Context + hook）+ `admin-audit-service.ts`（默认实现）+ `mock-audit-service.ts`（测试用）
- ✅ v2 P0-1 已修复：~~Service 抽象层定义但从未接线（死代码）~~ `admin-audit-service.ts` 改造为 Client-safe（移除 `server-only` + data-access 依赖；查询方法抛错强制走 RSC props；写操作委托 Server Action）；`overview/page.tsx` 注入 `AuditServiceProvider`；`AuditRetentionSettings` 与 `AuditLogExportButton` 改用 `useAuditService()` + `useAuditAnalytics()` hook
- ✅ v2 P0-2 已修复：~~`mock-audit-service.ts` 使用 9 处 `as` 类型断言违规~~ 为所有方法对象字面量添加显式返回类型标注 `Promise<T>`，消除全部 `as` 断言
- ✅ v2 P0-3 已修复：~~保留策略 Server Action 无 `revalidatePath`~~ `saveAuditRetentionConfigAction` 与 `purgeAuditLogsAction` 末尾新增 `revalidatePath("/admin/audit-logs/overview")` 精确刷新缓存
- ✅ v2 P1-1 已修复：~~`trackEvent` event 名称与业务不匹配（保留策略操作用了 `audit.exported`）~~ 改为 `audit.retention`（同步更新 `actions.ts` + `api/cron/audit-cleanup/route.ts` + `shared/lib/track-event.ts` EventName 联合类型新增 `audit.retention`）
- ✅ v2 P1-2 已修复：~~`AuditErrorBoundary` 使用 `namespace="common"` 不符合模块约定~~ 改为 `namespace="audit"` + 新增 `error.boundaryTitle` / `error.boundaryDescription` 翻译键
- ✅ v2 P1-3 已修复：~~`AuditLogExportButton` 与 `AuditRetentionSettings` 未接入埋点 hook~~ `AuditLogExportButton` 调用 `analytics.trackExport(exportType, 0)`；`AuditRetentionSettings` 调用 `analytics.trackRetentionConfigChange(config)` + `analytics.trackPurge(result)`
- ✅ v2 P1-4 已修复：~~`getAuditModuleOptions` / `getDataChangeTableOptions` catch 块吞错误返回空数组~~ 改为 `throw error`，UI 通过 ErrorBoundary 捕获
- ✅ v2 P1-5 已修复：~~日期筛选器两个 Input 共用 `aria-label={t("table.time")}` 无法区分起止~~ 改为 `aria-label={t("filter.startDate")}` 和 `aria-label={t("filter.endDate")}`（同步更新三个 filters 组件）
- ✅ v2 P1-6 已修复：~~`AuditLogDetailDialog` 的 `DialogDescription` 与 DialogTitle 重复~~ 改为 `t("detail.description")` 并新增对应翻译键
- ✅ v2 P1-7 已修复：~~骨架屏缺少 `aria-hidden` 屏幕阅读器会朗读骨架~~ `audit-log-table-skeleton.tsx` 根 div 与 4 个 `loading.tsx` 添加 `aria-hidden="true" role="presentation"`
- ✅ data-access 职责清晰，无跨模块问题

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 210 | 7 个 Server Action（编排层：3 导出 + 3 保留策略 + 1 查询；✅ v2 新增 `revalidatePath` + `audit.retention` 埋点；✅ audit-P2-5 `purgeAuditLogsAction` 新增 `loginLogRetentionDays` 参数 + trackEvent 埋点） |
| `data-access.ts` | 387 | 日志查询（12 个函数：3 查询 + 3 导出 + 3 选项/统计 + 3 概览统计；✅ v2 P1-4 错误处理统一为 throw） |
| `export.ts` | 144 | Excel 导出纯函数（列定义 + 行映射 + buildXxxExport） |
| `retention.ts` | 181 | 保留策略（配置读写 + 清理 + 纯函数，✅ P2-5 新增；✅ audit-P2-5 新增 `DEFAULT_LOGIN_LOG_RETENTION_DAYS=365` + `loginLogRetentionDays` 配置 + `purgeExpiredAuditLogs` 第二可选参数） |
| `types.ts` | 167 | 类型定义（含概览统计 + 保留策略类型，✅ P2-4/P2-5 扩展；✅ audit-P2-5 `AuditRetentionConfig` 新增 `loginLogRetentionDays` 字段） |
| `hooks/use-log-pagination.ts` | 27 | 分页 URL 状态管理 hook |
| `services/audit-service.tsx` | 124 | Service 接口 + Context Provider + hook（✅ P2-8 新增） |
| `services/admin-audit-service.ts` | 82 | 管理员默认实现（✅ v2 P0-1 改造为 Client-safe：查询抛错 + 写操作委托 Server Action） |
| `services/mock-audit-service.ts` | 70 | 测试用 Mock 实现（✅ v2 P0-2 消除 9 处 `as` 断言） |
| `retention.test.ts` | 85 | 保留策略纯函数单测（9 个测试，✅ P2-6 新增） |
| `export.test.ts` | 175 | 导出映射纯函数单测（13 个测试，✅ P2-6 新增） |

**组件清单**：
| 组件 | 行数 | 职责 |
|------|------|------|
| `components/audit-error-boundary.tsx` | 19 | 审计模块错误边界（class 组件，✅ v2 P1-2 namespace 改为 audit） |
| `components/audit-log-table-skeleton.tsx` | 42 | 骨架屏（✅ v2 P1-7 添加 `aria-hidden`） |
| `components/audit-log-table.tsx` | 115 | 操作日志表格（i18n + a11y + 详情对话框） |
| `components/audit-log-filters.tsx` | 97 | 操作日志筛选器（i18n + a11y + userId 筛选；✅ v2 P1-5 日期 aria-label 区分起止） |
| `components/audit-log-view.tsx` | 51 | 操作日志视图（筛选+表格+分页） |
| `components/audit-log-export-button.tsx` | 76 | 导出按钮（i18n；✅ v2 P1-3 接入 `useAuditAnalytics().trackExport`） |
| `components/audit-log-detail-dialog.tsx` | 132 | 日志详情对话框（discriminated union，✅ P2-2 新增；✅ v2 P1-6 修复 DialogDescription 重复标题） |
| `components/login-log-table.tsx` | 109 | 登录日志表格（i18n + a11y + 详情对话框） |
| `components/login-log-filters.tsx` | 83 | 登录日志筛选器（i18n + a11y + userId 筛选；✅ v2 P1-5 日期 aria-label 区分起止） |
| `components/login-log-view.tsx` | 49 | 登录日志视图 |
| `components/data-change-log-table.tsx` | 150 | 数据变更日志表格（i18n + a11y + 详情对话框） |
| `components/data-change-log-filters.tsx` | 139 | 数据变更日志筛选器（i18n + a11y + userId 筛选；✅ v2 P1-5 日期 aria-label 区分起止） |
| `components/data-change-log-view.tsx` | 53 | 数据变更日志视图 |
| `components/audit-overview-stats-bar.tsx` | 52 | 概览统计卡片栏（✅ P2-4 新增） |
| `components/audit-activity-trend-chart.tsx` | 67 | 活动趋势折线图（✅ P2-4 新增） |
| `components/data-change-distribution-chart.tsx` | 72 | 数据变更分布柱状图（✅ P2-4 新增） |
| `components/audit-overview-view.tsx` | 113 | 概览页视图（统计+图表+快捷入口+保留配置，✅ P2-4 新增） |
| `components/audit-retention-settings.tsx` | 179 | 保留策略配置组件（✅ P2-5 新增；✅ v2 P0-1 + P1-3 改用 `useAuditService` + `useAuditAnalytics`） |

**页面路由**：
- `/admin/audit-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/login-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/data-changes` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/overview` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹（✅ P2-4 新增）
- `/api/cron/audit-cleanup` → CRON_SECRET 鉴权（✅ P2-5 新增）
- ✅ 4 个页面均新增 `loading.tsx`（骨架屏）+ `error.tsx`（错误边界）

---

## 2.16 announcements（公告模块）

**职责**：公告 CRUD + 发布/归档 + 发布通知。

**导出函数**：
- Actions：`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`（✅ P1 已修复：actions 层不再直接访问 DB，全部下沉到 data-access；✅ 发布公告时触发通知模块 `sendBatchNotifications`）/ `toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`（✅ V2-P2-13d 新增：置顶切换、已读标记、批量已读状态查询；✅ Audit-P0-3：`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 新增资源所有权/可见性二次校验；✅ Audit-P1-1：所有 Action 返回 i18n 化 message；✅ Audit-P1-7：`handleActionError` 集成 console.error + trackEvent("announcement.action_error") + 区分 PermissionDeniedError / FORBIDDEN_ANNOUNCEMENT）
- Data-access：`getAnnouncements`（✅ Audit-P0-2：`audience` 字段从单值升级为数组 `{ gradeIds, classIds }`，过滤逻辑改用 `inArray`）/ `getAnnouncementById` / `getAnnouncementByIdForUser`（✅ Audit-P0-1 新增：用户端详情页专用，结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回，防止越权）/ `isAnnouncementVisibleToAudience`（✅ Audit-P0-1 新增：纯函数，判断公告对受众是否可见，可独立单测）/ `resolveUserAudience`（✅ Audit-P0-2：从原 `resolveAudience` 重命名，遍历所有 childrenIds/classIds/gradeIds 而非仅首个）/ `resolveAnnouncementTargetUserIds`（✅ Audit-P1-5：从 actions.ts 下沉到 data-access）/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`（后 5 个为 P1-2 新增）/ `toggleAnnouncementPin`（✅ Audit-P1-6：原子化 UPDATE ... SET isPinned = NOT isPinned）/ `markAnnouncementAsRead`（✅ Audit-P1-6：onDuplicateKeyUpdate 实现幂等 upsert）/ `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`（✅ V2-P2-13d 新增：置顶切换 + 已读回执 CRUD）/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`（✅ P1-5 新增：管理端列表页和编辑页编排函数）/ `getUserAnnouncementsPageData`（✅ V3-P0-2 新增：用户端列表页编排函数）/ `getAdminAnnouncementDetailPageData`（✅ Audit-P1-2 新增：管理端详情页编排函数，并行 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`school`（获取年级列表）、`classes`（获取班级列表 + 解析受众）、`users`（获取目标用户 ID 列表）、`notifications`（发布公告时发送通知）、`shared/i18n`（✅ Audit-P1-1：Actions 通过 `getTranslations('announcements')` 返回 i18n 化 message）
- 被依赖：无

**已知问题**：
- ✅ P1-2 已修复：~~所有写操作直接在 actions 层 `db.insert/update/delete`，未下沉到 data-access~~ 写操作已下沉到 data-access（5 个新函数）
- ✅ P2 已修复：~~`getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(ANNOUNCEMENT_READ)`~~ 改为 `requirePermission(Permissions.ANNOUNCEMENT_READ)`
- ✅ 已修复：用户端列表页传入 `audience` 受众过滤（school/grade/class），管理端返回所有公告
- ✅ 已修复：用户端新增公告详情页 `/announcements/[id]`（只读模式）
- ✅ 已修复：管理端列表页传递 `classes` 数据给 `AdminAnnouncementsView`
- ✅ 已修复：发布公告时（`publishAnnouncementAction` / `createAnnouncementAction` 直接发布 / `updateAnnouncementAction` 状态变为 published）触发通知模块 `sendBatchNotifications`
- ✅ 已修复：新增 `loading.tsx` 骨架屏（用户端 + 管理端）
- ✅ P1 已修复：~~全模块零 i18n，中英文案硬编码~~ 所有组件接入 next-intl（`useTranslations("announcements")`），新增 `src/shared/i18n/messages/{zh-CN,en}/announcements.json` 翻译字典（title/description/filter/status/type/form/actions/messages/meta/empty/error 共 11 个命名空间）；所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复：~~缺 Error Boundary~~ 新增 4 个 `error.tsx` 错误边界（`/announcements`、`/announcements/[id]`、`/admin/announcements`、`/admin/announcements/[id]`），统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复：a11y 改进，`announcement-card.tsx` / `announcement-detail.tsx` 添加 `aria-label`
- ✅ P1-5 已修复：~~页面层 `Promise.all` 编排 announcements/school/classes 三个模块的 data-access~~ 新增 `getAdminAnnouncementsPageData` 和 `getEditAnnouncementPageData` 编排函数，页面层仅调用单一函数
- ✅ P1-6 已修复：~~`targetGradeId` / `targetClassId` 为 optional，未根据 `type` 做条件必填校验~~ `CreateAnnouncementSchema` 和 `UpdateAnnouncementSchema` 添加 `superRefine(refineAudience)`，年级公告强制 `targetGradeId`，班级公告强制 `targetClassId`
- ✅ P2-11 已修复：~~发布/归档/删除公告无埋点~~ `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction` 新增 `trackEvent` 埋点（announcement.created / announcement.updated / announcement.published / announcement.deleted / announcement.archived）
- ✅ V2-P0-2 已修复：~~通知标题硬编码~~ `createAnnouncementAction` / `updateAnnouncementAction` / `publishAnnouncementAction` 通过 `getTranslations('announcements')` 生成 i18n 通知标题（`notification.publishedTitle` / `publishedContent`）
- ✅ V2-P1-1 已修复：~~AnnouncementList 客户端 useState/useMemo 过滤~~ 改为纯服务端过滤模式，Select 切换仅更新 URL `?status=` 触发 RSC 重新渲染
- ✅ V2-P1-4 已修复：~~表单无服务端校验错误展示~~ `announcement-form.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示（title/content/targetGradeId/targetClassId）
- ✅ V2-P2-13d 已实现：公告置顶 + 已读回执。`announcements` 表新增 `isPinned` 字段（置顶优先排序，`getAnnouncements` orderBy 改为 `desc(isPinned), desc(createdAt)`）；新增 `announcement_reads` 表（唯一索引 announcementId+userId 保证幂等）；新增 3 个 Server Action（`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`）+ 5 个 data-access 函数（`toggleAnnouncementPin` / `markAnnouncementAsRead` / `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`）；`Announcement` 类型新增 `isPinned` / `isReadByCurrentUser?` / `readCount?` 字段；新增 `AnnouncementRead` 类型；埋点新增 `announcement.pin_toggled` / `announcement.marked_read`
- ✅ V3-P0-2 已修复：~~用户端公告列表页 `resolveAudience` 50 行业务逻辑在路由层~~ 新增 `getUserAnnouncementsPageData` 编排函数（含 `resolveAudience` 内部函数），将受众解析逻辑从 `page.tsx` 下沉到 `data-access.ts`，页面层仅调用单一函数
- ✅ Audit-P0-1 已修复：~~用户端详情页越权访问草稿/他人班级公告~~ 新增 `getAnnouncementByIdForUser`，结合 `status="published"` + 受众过滤（`isAnnouncementVisibleToAudience` 纯函数）+ 已读状态 + readCount 一次返回，`markAnnouncementAsReadAction` 调用此函数做资源可见性二次校验，不可见时抛 `FORBIDDEN_ANNOUNCEMENT`
- ✅ Audit-P0-2 已修复：~~`resolveAudience` 仅取首个 child/class/grade 导致家长多孩子/教师多班级时只看到首个受众公告~~ audience 类型升级为 `{ gradeIds: string[]; classIds: string[] }`，`resolveAudience` 重命名为 `resolveUserAudience` 遍历全部；`getAnnouncements` 过滤逻辑改用 `inArray` 支持数组
- ✅ Audit-P0-3 已修复：~~`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 缺资源所有权二次校验~~ 前者新增资源存在性校验（不存在返回 404）；后者调用 `getAnnouncementByIdForUser` 做可见性二次校验
- ✅ Audit-P1-1 已修复：~~Actions 返回英文硬编码 message~~ 全部改用 `t("messages.created")` / `t("messages.updated")` / `t("messages.deleted")` / `t("messages.published")` / `t("messages.archived")` / `t("messages.pinned")` / `t("messages.unpinned")` / `t("messages.markedRead")` / `t("messages.invalidFormData")` / `t("messages.unexpectedError")` / `t("messages.permissionDenied")` / `t("messages.forbidden")`
- ✅ Audit-P1-2 已修复：~~管理端 `/admin/announcements/[id]` 原为编辑表单页，缺独立详情页~~ 拆分为详情页（`AnnouncementDetail canManage=true`，调用 `getAdminAnnouncementDetailPageData` 含 readCount 统计）+ `/edit` 子路由（编辑表单）；用户端 `AnnouncementList` 通过 `service.getReadStatus` 批量获取已读状态传入卡片做已读/未读视觉区分
- ✅ Audit-P1-3 已修复：~~组件直接 import actions 耦合~~ 引入 `AnnouncementsServiceContext` 依赖注入（`AnnouncementsServiceProvider` + `useAnnouncementsService()` + `DefaultAnnouncementsService` 默认实现代理到真实 Server Actions），所有交互型组件（Card/Detail/Form/List）通过 Hook 消费，便于测试 mock 和未来角色差异化实现
- ✅ Audit-P1-4 已修复：~~`AnnouncementForm` 硬编码 `router.push("/admin/announcements")` 路由~~ 新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底，便于在 Dialog 中复用
- ✅ Audit-P1-5 已修复：~~`resolveTargetUserIds` 业务逻辑位于 actions 层~~ 下沉到 data-access `resolveAnnouncementTargetUserIds`
- ✅ Audit-P1-6 已修复：~~`toggleAnnouncementPin` read-then-write 竞态~~ 原子化 `UPDATE ... SET isPinned = NOT isPinned`；~~`markAnnouncementAsRead` select-then-insert 双查询~~ 改用 `onDuplicateKeyUpdate` 幂等 upsert
- ✅ Audit-P1-7 已修复：~~`handleActionError` 缺日志/埋点/i18n~~ 重写为接收 `t` 函数，集成 `console.error` + `trackEvent("announcement.action_error")` + 区分 `PermissionDeniedError` / `FORBIDDEN_ANNOUNCEMENT` / 其他错误
- ✅ Audit-P1-8 已修复：~~表单与 Dialog 行为耦合（保存后路由跳转关闭不了 Dialog）~~ `AdminAnnouncementsView` Dialog 内表单通过 `onSuccess={handleFormSuccess}` 关闭弹窗
- ✅ Audit-P2-1 已修复：~~a11y：置顶按钮嵌套在 Link 内导致键盘导航异常~~ 改为绝对定位 + `onKeyDown` 处理 Enter/Space，脱离外层 Link 语义
- ✅ Audit-P2-2 已修复：~~`mapRow` 类型不安全~~ 改用 `typeof announcements.$inferSelect` 推导
- ✅ Audit-P2-3 已修复：~~i18n 死键与缺键~~ 清理 `description.detail` / `description.create` 等死键，补 `messages.invalidFormData` / `messages.unexpectedError` / `messages.permissionDenied` / `messages.forbidden`
- ✅ Audit-P2-4 已修复：~~`AnnouncementList.detailHrefBuilder` 死 prop~~ 删除（已由 V3 的 `detailHrefPrefix` 替代，本次彻底清理）
- ✅ Audit-P2-5 已修复：~~两处 loading.tsx 重复骨架屏 JSX~~ 抽取 `AnnouncementListSkeleton` 共享组件，支持 `showCreateButton` prop
- ✅ Audit-P2-6 已修复：~~用户端公告列表无分页 UI~~ 新增 `AnnouncementPagination` 组件（URL `?page=N` 持久化、a11y、窗口策略、边界处理）；新增 `countAnnouncements` data-access 函数；`getUserAnnouncementsPageData` 升级为接收 `page`/`pageSize` 并返回 `PaginatedAnnouncements` 类型 `{ items, total, page, pageSize }`，内部并行执行列表查询与 count 查询；`/announcements/page.tsx` 读取 `?page=` 查询参数并传入分页数据；新增 i18n 键 `pagination.label` / `prev` / `next` / `page`
- ✅ Audit-P2-7 已修复：~~模块零测试覆盖~~ 新增 3 个测试文件：`schema.test.ts`（18 个测试，覆盖 `refineAudience` 条件校验矩阵：school/grade/class 类型与 target 必填组合 + transform 归一化）；`is-announcement-visible.test.ts`（15 个测试，覆盖 `isAnnouncementVisibleToAudience` 纯函数，含 P0-2 多孩子/多班级场景）；`components/announcement-card.test.tsx`（16 个测试，覆盖基础渲染、isRead 视觉区分、isPinned 交互、a11y 键盘导航 Enter/Space、toggling 禁用、错误回滚）。同时修复项目级 `@testing-library/dom` peer dependency 缺失（已影响 `dashboard-section.test.tsx` 等现有测试）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~400 | 9 个 Server Action + 通知触发逻辑 + trackEvent 埋点（P1-2 已修复，无直接 DB 操作；V2-P2-13d 新增置顶/已读/批量已读状态 3 个 Action；Audit-P0-3 新增资源所有权二次校验；Audit-P1-1 i18n 化 message；Audit-P1-7 `handleActionError` 集成日志/埋点/i18n） |
| `data-access.ts` | ~580 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` / `getUserAnnouncementsPageData` 编排函数 + `getAdminAnnouncementDetailPageData`（Audit-P1-2 新增）+ `countAnnouncements`（Audit-P2-6 新增）；V2-P2-13d 置顶切换 + 已读回执 5 个函数；Audit-P0-1 新增 `getAnnouncementByIdForUser` / `isAnnouncementVisibleToAudience`；Audit-P0-2 `resolveAudience` → `resolveUserAudience` 遍历全部受众；Audit-P1-5 `resolveAnnouncementTargetUserIds` 下沉；Audit-P1-6 `toggleAnnouncementPin` / `markAnnouncementAsRead` 原子化；Audit-P2-2 `mapRow` 改用 `$inferSelect` 推导 |
| `schema.ts` | ~95 | Zod 校验 + `refineAudience` 条件校验（✅ P1-6 新增 superRefine） |
| `types.ts` | ~130 | 类型定义（`GetAnnouncementsParams.audience` 升级为数组 Audit-P0-2；新增 `UserAudience` Audit-P0-2；新增 `AnnouncementsService` 接口 Audit-P1-3；新增 `PaginatedAnnouncements` Audit-P2-6） |
| `schema.test.ts` | ~200 | ✅ Audit-P2-7 新增：18 个测试，覆盖 `refineAudience` 条件校验矩阵 + transform 归一化 |
| `is-announcement-visible.test.ts` | ~180 | ✅ Audit-P2-7 新增：15 个测试，覆盖 `isAnnouncementVisibleToAudience` 纯函数（含 P0-2 多孩子/多班级场景） |
| `components/announcements-service-context.tsx` | ~40 | ✅ Audit-P1-3 新增：`AnnouncementsServiceProvider` + `useAnnouncementsService()` |
| `components/default-announcements-service.ts` | ~60 | ✅ Audit-P1-3 新增：`AnnouncementsService` 默认实现，代理到真实 Server Actions |
| `components/announcement-list-skeleton.tsx` | ~40 | ✅ Audit-P2-5 新增：共享骨架屏，支持 `showCreateButton` prop |
| `components/announcement-pagination.tsx` | ~100 | ✅ Audit-P2-6 新增：分页组件，URL `?page=N` 持久化，a11y + 窗口策略 + 边界处理 |
| `components/announcement-card.test.tsx` | ~300 | ✅ Audit-P2-7 新增：16 个测试，覆盖基础渲染、isRead 视觉、isPinned 交互、a11y 键盘导航、toggling 禁用、错误回滚 |

**组件清单**：
| 组件 | 职责 |
|------|------|
| `components/announcement-list.tsx` | 公告列表（用户端，支持状态筛选；✅ V2-P1-1：纯服务端过滤，Select 切换更新 URL ?status= 触发 RSC 重新渲染；✅ V3：新增 `detailHrefPrefix` prop 替代 `detailHrefBuilder` 函数 prop；✅ Audit-P1-2：用户端通过 `service.getReadStatus` 批量获取已读状态传入卡片；✅ Audit-P2-4：删除死 prop `detailHrefBuilder`；✅ Audit-P2-6：新增 `pagination` prop，传入时在列表底部渲染 `AnnouncementPagination`） |
| `components/announcement-card.tsx` | 公告卡片（列表项；✅ Audit-P1-3：通过 `useAnnouncementsService()` 消费 togglePin；✅ Audit-P2-1：置顶按钮绝对定位 + onKeyDown 处理；✅ Audit-P1-2：新增 `isRead` prop 已读/未读视觉区分） |
| `components/announcement-detail.tsx` | 公告详情（只读；✅ Audit-P1-3：所有 Action 调用改用 `useAnnouncementsService()`） |
| `components/announcement-form.tsx` | 公告表单（创建/编辑，✅ P1-6：条件校验由 schema superRefine 保证；✅ V2-P1-4：fieldErrors + aria-invalid；✅ Audit-P1-3：通过 `useAnnouncementsService()` 消费 create/update；✅ Audit-P1-4/P1-8：新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底） |
| `components/admin-announcements-view.tsx` | 管理端公告视图（列表 + 筛选；✅ Audit-P1-3：包裹 `AnnouncementsServiceProvider`；✅ Audit-P1-8：Dialog 内表单通过 `onSuccess` 关闭弹窗） |
| `components/announcement-pagination.tsx` | ✅ Audit-P2-6 新增：分页导航（URL 持久化、a11y、窗口策略、边界处理） |

---

## 2.17 files（文件模块）

**职责**：文件附件 CRUD + 批量删除 + 统计 + 上传/预览 UI。

**导出函数**：
- Actions（✅ 2026-06-25 新增 `actions.ts` 编排层，P0 修复）：`uploadFileAction`（FILE_UPLOAD 权限 + Zod 校验 + storageProvider.save + trackEvent + logAudit）/ `getFileAction`（FILE_READ 权限 + ownership 检查：非 FILE_DELETE 持有者只能读自己上传的）/ `deleteFileAction`（FILE_DELETE 权限 + storageProvider.delete）/ `batchDeleteFilesAction`（FILE_DELETE 权限 + Zod 校验 max 100 ids）/ `getFileListAction`（FILE_READ 权限）/ `getFileStatsAction`（FILE_READ 权限）
- Data-access：`createFileAttachment` / `getFileAttachment`（cache）/ `getFileAttachmentsByTarget`（cache）/ `getFileAttachmentsByUploader`（cache）/ `getAllFileAttachments`（cache）/ `deleteFileAttachment` / `deleteFileAttachments`（批量）/ `getFileAttachmentsWithFilters`（cache）/ `getFileStats`（cache）/ `getFileByUrl`（cache，✅ 2026-06-25 新增：按 url 字段查询）/ `getFileAttachmentsByIds`（cache）（✅ P2 已修复：7 个读函数使用 `React.cache()` 包装实现请求级 memoization）
- Types（✅ 2026-06-25 更新）：`FileTargetType` 新增 `"user_avatar"` 枚举值，现为 `"exam" | "textbook" | "question" | "announcement" | "homework" | "user_avatar"`，用于用户头像上传场景（targetId = users.id）；`FileUploadResult` / `FileAttachment` / `CreateFileAttachmentInput` / `FileAttachmentQueryParams` / `FileStats` / `BatchDeleteResult`
- Schema（✅ 2026-06-25 新增 `schema.ts`）：`UploadMetadataSchema` / `BatchDeleteSchema`（max 100 ids）/ `FileListQuerySchema` / `FileTargetTypeSchema` / `validateFileSize`
- Components：`FileUpload` / `FilePreview` / `FilePreviewDialog`（✅ 2026-06-25 补录）/ `FileIcon` / `AdminFilesView`
- Hooks（✅ 2026-06-25 新增 `hooks/` 目录）：`useFileUpload`（XHR 上传、进度、状态机、校验）/ `useFileBatchOperations`（客户端筛选、选择、批量删除）/ `useFilePreview` + `useImageZoom`（图片缩放）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`
- 被依赖：`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete`（✅ 2026-06-25 重构：3 个 API 路由改为委托 `actions.ts`，仅保留速率限制和请求解析）/ `homework`（✅ 2026-06-24：`homework/actions.deleteScanAction` 调用 `deleteFileAttachment`；`homework/components/scan-uploader` 调用 `/api/upload`，targetType="homework"）/ `settings`（✅ 2026-06-25 新增：`settings/actions-avatar.ts` 通过 `storageProvider` 抽象删除旧头像文件，targetType="user_avatar"；`settings/components/avatar-upload.tsx` 上传头像走 `/api/upload`）

**已知问题**：
- ✅ P0 已修复：~~无 `actions.ts`，data-access 被路由直接调用~~ 新建 `actions.ts` 编排层，所有 API 路由（upload/files/[id]/files/batch-delete）改为委托 actions，路由层仅保留速率限制
- ✅ P0 已修复：~~`app/api/upload/route.ts` 仅 `requireAuth()` 无权限校验~~ 改为 `requirePermission(FILE_UPLOAD)`
- ✅ P0 已修复：~~`app/api/files/[id]/route.ts` 横向越权（任意用户可读/删他人文件）~~ `getFileAction` 加入 ownership 检查（非 FILE_DELETE 持有者只能读自己上传的）
- ✅ P1 已修复：~~i18n 缺失，组件硬编码英文~~ 补全 `en/files.json` + `zh-CN/files.json`（88 行，含 title/upload/list/preview/admin 等命名空间），所有组件接入 `useTranslations`
- ✅ P1 已修复：~~缺少 `loading.tsx` 骨架屏和 `error.tsx` 错误边界~~ 新增 `app/(dashboard)/admin/files/loading.tsx` + `error.tsx`
- ✅ P1 已修复：~~组件逻辑与 UI 耦合，不可单测~~ 抽取 3 个自定义 hooks：`useFileUpload` / `useFileBatchOperations` / `useFilePreview`
- ✅ P1 已修复：~~`settings/actions-avatar.ts` 直接 import `fs/promises` 绕过 storageProvider 抽象~~ 改用 `storageProvider.delete`
- ✅ P1 已修复：~~`FileTargetType` 缺 `"user_avatar"` 枚举值导致 settings 头像上传 targetType 未注册~~ 已加入枚举
- ✅ P2 已修复：~~`file-list.tsx` 125 行死代码~~ 已删除
- ✅ P2 已修复：~~所有函数 try-catch 吞错误返回空数组/null~~ 所有 catch 块已添加 `console.error` 输出错误上下文
- ✅ P2 已修复：`getFileAttachmentsWithFilters` 中 `or(...)!` 非空断言清理为安全守卫
- ✅ P2 已修复：~~`getFileAttachmentsWithFilters` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
- ✅ P2 已修复：~~`admin-files-view.tsx` 中 `t(`filter.${key}` as never)` 类型断言~~ 改为 `renderTypeLabel` 辅助函数 + 字面量联合类型 + switch
- ✅ P2 已修复：~~`file-preview-dialog.tsx` 中 `label` 变量赋值后未使用~~ 已移除
- ✅ P2 新增：监控埋点 — `actions.ts` 接入 `trackEvent`（file.uploaded/upload_failed/deleted/batch_deleted/viewed/action_error）+ `logAudit`
- ✅ P2 新增：a11y — 所有图标加 `aria-hidden`，按钮加 `aria-label`，`AdminFilesView` 用三层 `WidgetBoundary` 包裹（StatsSection / UploadSection / ListSection）
- ✅ 职责单一，不跨模块查询

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 305 | 文件 CRUD + 批量删除 + 统计（11 个函数 + mapRow） |
| `actions.ts` | 330 | 6 个 Server Action（编排层，权限+Zod+storageProvider+trackEvent+logAudit） |
| `schema.ts` | - | Zod 校验（UploadMetadata/BatchDelete/FileListQuery/FileTargetType） |
| `types.ts` | 70 | 类型定义（FileTargetType 含 user_avatar） |
| `components/*` | 5 文件 | 上传/预览/预览弹窗/图标/管理视图 |
| `hooks/*` | 3 文件 | 上传/批量操作/预览 hooks |

---

## 2.18 course-plans（课程计划模块）

**职责**：课程计划 CRUD + 周计划项 CRUD + 排序 + 批量操作 + 复制 + 年级进度统计 + 日历视图 + CSV 导出 + 模板库 + 拖拽排序 + 数据联动 + 组件级 Error Boundary。

**导出函数**：
- Actions：`getCoursePlansAction` / `getCoursePlanAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` / `getGradeCoursePlanProgressAction`（年级仪表盘维度4）/ `reorderCoursePlanItemsAction`（✅ P1-7 新增：拖拽排序周计划条目）/ `bulkToggleItemsAction`（✅ P2-4 新增：批量标记完成状态）/ `copyCoursePlanAction`（✅ P2-4 新增：复制计划到其他班级，P2-5 复用为"从模板创建"）/ `getTemplateCandidatesAction`（✅ P2-5 新增：获取模板候选列表，复用现有计划作为模板）/ `trackCoursePlanEvent`（✅ P2-8 新增：监控埋点接口预留，已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用）
- Data-access：与 actions 对应 + `getGradeCoursePlanProgress` + `reorderCoursePlanItems`（✅ P1-7 新增）+ `bulkUpdateItemCompleted`（✅ P2-4 新增）+ `copyCoursePlanToClasses`（✅ P2-4 新增）+ `enrichPlanRows`（✅ P1-1 新增：批量解析班级/科目/教师名称，通过动态 import 对方 data-access，**不再直接 JOIN 其他模块表**）+ `buildScopeCondition`（✅ P0-3 新增：DataScope 权限过滤）
- Lib（✅ P2 新增）：`lib/export-utils.ts`（纯函数 `planToExportRows`/`buildExportColumns`/`exportCoursePlanReport`，CSV 导出不依赖 i18n，标签由调用方传入）/ `lib/calendar-utils.ts`（纯函数 `planToCalendarEvents`/`buildMonthGrid`/`eventsOnDay`/`filterEventsInRange` + 日期工具 `addDays`/`addMonths`/`startOfWeek`/`startOfMonth`/`endOfMonth`/`isSameDay`/`isWithinRange`/`formatDateISO`/`parseISODate`）
- Types（✅ v4-P2-7 新增）：`GradeCoursePlanProgressItem` / `GradeCoursePlanProgressResult` / `CoursePlanQueryScope`（✅ P0-3 新增：数据范围上下文，含 userId/isAdmin/classIds/teacherId/studentId）/ `ReorderCoursePlanItemInput`（✅ P1-7 新增）/ 类型守卫 `isCoursePlanStatus` / `isCoursePlanSemester`（✅ P1-4 新增：替代 `as` 断言）/ 配置驱动 `ROLE_WIDGET_CONFIG`（✅ P2 新增：admin/teacher/parent/student 四角色 Widget 配置）/ `CalendarEvent`（✅ P2-7 新增：日历事件类型）/ `CoursePlanExportColumnKey` + `CoursePlanColumnLabels`（✅ P2-6 新增：导出列类型与标签接口）
- Components：`CoursePlanList`（✅ P1 修复：使用 `<Link>` + 类型守卫 + i18n）/ `CoursePlanDetail`（✅ P1 修复：批量选择 + `successHref` prop + 埋点；✅ P2-1 增强：`SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图；✅ P2-3 增强：`textbooksHref`/`homeworkHref` props 支持数据联动跳转；✅ P2-6 增强：CSV 导出按钮；✅ P2-7 增强：嵌入 `CoursePlanCalendar`）/ `CoursePlanForm`（✅ P1 修复：类型守卫 + `successHref` prop；✅ P2-5 增强：嵌入 `TemplatePickerDialog`）/ `CoursePlanItemEditor`（✅ P1 修复：i18n）/ `CoursePlanProgress`（✅ P1 修复：a11y aria 属性 + i18n）/ `SortableWeekRow`（✅ P1-7/P2-3 新增：@dnd-kit/sortable 拖拽行 + 数据联动跳转链接 + 复选框）/ `CoursePlanCalendar`（✅ P2-7 新增：月历视图 6×7 网格 + 上一月/下一月/今天导航 + 事件列表）/ `TemplatePickerDialog`（✅ P2-5 新增：模板选择器 + 搜索过滤 + 克隆到目标班级）

**依赖关系**：
- 依赖：`shared/*`（含 ✅ P2-6 新增 `shared/lib/export-utils.ts` 通用 CSV 导出工具）、`@/auth`、`classes`（✅ P1-1 已修复：通过动态 import `getClassNamesByIds` 批量解析，不再直接 JOIN classes 表）、`school`（✅ P1-1 已修复：通过动态 import `getSubjectNameMapByIds` 批量解析；`getSubjectOptions` 已移至 school 模块）、`users`（✅ P1-1 已修复：通过动态 import `getUserNamesByIds` 批量解析）、`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`（✅ P1-7 新增：拖拽排序依赖）
- 被依赖：无（parent/student 路由通过 page.tsx 调用 data-access，✅ P2-2 已新增）

**已知问题**：
- ✅ P0-1 已修复：教师详情页信息泄露 — `getCoursePlanById` 增加 `scope` 参数，非 admin 仅能查看自己负责的计划
- ✅ P0-2 已修复：admin 页面无 requirePermission — 所有 admin 页面补充 `requirePermission(COURSE_PLAN_READ/MANAGE)`
- ✅ P0-3 已修复：data-access 无 DataScope 过滤 — `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数
- ✅ P0-4 已修复：i18n 严重缺失 — 翻译键从 5 个扩展至 ~230 个，覆盖状态/筛选/列表/详情/表单/条目/进度/Toast/教师/家长/学生/错误/日历/导出/批量/模板/加载 + Error Boundary 文案
- ✅ P1-1 已修复：跨模块直接 JOIN — 移除 `buildPlanSelect` 的 `leftJoin(classes/subjects/users)`，改用 `enrichPlanRows` 动态 import 对方 data-access 批量查询
- ✅ P1-2 已修复：缺 loading.tsx/error.tsx — 12 个路由（含 P2-2 新增 parent/student）均补充骨架屏 + 错误边界（使用 `EmptyState` + i18n）
- ✅ P1-3 已修复：原生 `<a>` 标签 — 替换为 Next.js `<Link>` 组件
- ✅ P1-4 已修复：`as` 断言 — 替换为类型守卫函数 `isCoursePlanStatus`/`isCoursePlanSemester`
- ✅ P1-5 已修复：URL 路径推断角色 — 改为 `successHref` prop 显式传入
- ✅ P1-6 已修复：Action 入参未验证 — `getCoursePlansAction`/`getGradeCoursePlanProgressAction`/`reorderCoursePlanItemsAction`/`bulkToggleItemsAction`/`copyCoursePlanAction` 增加 Zod schema 验证
- ✅ P1-7 已修复：死代码 `reorderCoursePlanItems` — 已补充对应 Action + UI 拖拽排序（`SortableWeekRow` + `DndContext` + `SortableContext`，PointerSensor + KeyboardSensor 支持 a11y）
- ✅ P2-1 已修复：无 Error Boundary — `SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图三个数据区块
- ✅ P2-2 已修复：parent/student 无路由 — 新增 parent/student 课程计划只读路由（按子女班级/学生班级 scope 过滤）
- ✅ P2-3 已修复：无数据联动 — 周计划项章节文本渲染为教材跳转链接，行尾新增作业跳转按钮（通过 `textbooksHref`/`homeworkHref` props 按角色注入）
- ✅ P2-4 已修复：无批量操作 — 批量标记完成 + 批量复制计划到其他班级
- ✅ P2-5 已修复：无模板复用 — `TemplatePickerDialog` 复用现有计划作为模板，搜索 + 选择 + 克隆到目标班级
- ✅ P2-6 已修复：无导出 — CSV 导出（UTF-8 BOM 兼容 Excel），通用工具迁移至 `shared/lib/export-utils.ts`
- ✅ P2-7 已修复：无日历视图 — `CoursePlanCalendar` 月历视图，周计划项按 `plan.startDate` 映射到日期范围
- ✅ P2-8 已修复：无监控埋点 — `trackCoursePlanEvent` 预留接口，已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用
- ✅ actions 层使用 `handleActionError`/`revalidatePlanPaths` 辅助函数（良好范例）
- ✅ `resolveScope` 函数正确处理 DataScope union 类型（admin/teacher/parent/student 四角色范围解析）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 507 | 课程计划 + 周计划项 CRUD + 批量 + 复制 + 名称解析解耦 |
| `actions.ts` | 520 | 14 个 Server Action + 埋点接口 + scope 解析 + 模板候选 |
| `schema.ts` | 213 | Zod 校验（含 Action 入参验证 schema） |
| `types.ts` | 161 | 类型定义 + 类型守卫 + 角色配置驱动 + CalendarEvent + 导出列类型 |
| `lib/export-utils.ts` | 91 | CSV 导出纯函数（planToExportRows + buildExportColumns + exportCoursePlanReport） |
| `lib/calendar-utils.ts` | 175 | 日历视图纯函数（事件映射 + 月历网格 + 日期工具） |
| `components/course-plan-list.tsx` | 162 | 列表组件（i18n + Link + 类型守卫） |
| `components/course-plan-detail.tsx` | 384 | 详情组件（批量选择 + 埋点 + successHref + Error Boundary + 拖拽 + 导出 + 日历 + 数据联动） |
| `components/course-plan-form.tsx` | 326 | 表单组件（类型守卫 + successHref + 模板选择器） |
| `components/course-plan-item-editor.tsx` | 250 | 周计划编辑器（i18n） |
| `components/course-plan-progress.tsx` | 42 | 进度条（a11y + i18n） |
| `components/sortable-week-row.tsx` | 176 | 拖拽行（@dnd-kit + 数据联动跳转 + 复选框 + a11y） |
| `components/course-plan-calendar.tsx` | 200 | 月历视图（6×7 网格 + 导航 + 事件列表 + a11y） |
| `components/template-picker-dialog.tsx` | 195 | 模板选择器（搜索 + 列表 + 克隆 + a11y） |
| `i18n/zh-CN/course-plans.json` | 233 | 中文翻译（~230 键） |
| `i18n/en/course-plans.json` | 233 | 英文翻译（~230 键） |

**页面路由**（✅ P1-2 全部补充 loading.tsx + error.tsx，✅ P2-2 新增 parent/student 路由）：
| 路由 | 角色 | 权限 | 说明 |
|------|------|------|------|
| `admin/course-plans` | admin | COURSE_PLAN_READ | 列表（全局视图） |
| `admin/course-plans/[id]` | admin | COURSE_PLAN_READ | 详情（含日历/导出/拖拽） |
| `admin/course-plans/[id]/edit` | admin | COURSE_PLAN_MANAGE | 编辑 |
| `admin/course-plans/create` | admin | COURSE_PLAN_MANAGE | 创建（含从模板创建） |
| `teacher/course-plans` | teacher | COURSE_PLAN_READ | 列表（按 teacherId 过滤） |
| `teacher/course-plans/[id]` | teacher | COURSE_PLAN_READ | 详情（scope 过滤防越权 + 数据联动） |
| `parent/course-plans` | parent | COURSE_PLAN_READ | 列表（按子女 classIds 过滤） |
| `parent/course-plans/[id]` | parent | COURSE_PLAN_READ | 详情（按子女 classIds scope 过滤） |
| `student/course-plans` | student | COURSE_PLAN_READ | 列表（按 class_members scope 过滤） |
| `student/course-plans/[id]` | student | COURSE_PLAN_READ | 详情（按 class_members scope 过滤 + 教材联动） |

---

## 2.19 parent（家长模块）

**职责**：家长视角的子女数据聚合与展示。

**导出函数**：
- Data-access：`getChildren` / `getChildBasicInfo` / `getChildDashboardData`（V3-11 增强：并行调用 `homework/data-access.getStudentExamResults` 获取考试结果）/ `getParentDashboardData` / `verifyParentChildRelation` / `getChildNameList`（✅ v4 新增：用于详情页头部多子女切换器，一次批量查询避免 N+1）
- Types（V3-11 新增）：`ChildExamResultItem`（单条考试结果：submissionId/examId/examTitle/assignmentId/assignmentTitle/score/maxScore/submittedAt/status）；`ChildDashboardData` 扩展 `examResults: ChildExamResultItem[]` 字段
- Components：`ParentDashboard` / `ChildCard` / `ChildDetailHeader` / `ChildDetailPanel`（V3-11 增强：新增 exams Tab）/ `SiblingSwitcher` / `ChildHomeworkSummary` / `ChildHomeworkDetail`（v4 新增）/ `ChildGradeSummary` / `ChildGradeDetail`（v4 新增）/ `ChildScheduleCard` / `ChildExamDetail`（V3-11 新增：子女考试详情视图，汇总卡片+考试列表）/ `ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttentionBanner`（v4 新增）/ `ParentAttendanceWarning`（v4 新增）/ `ParentAttendanceRateCard`（v4 新增）/ `ParentAttendanceCalendar`（v4 新增）/ `ParentExportButton`（v4 新增）

**v4 修复（产品/UX 维度）**：
- ✅ FEAT-G01：新增 `/parent/leave` 请假申请占位页（含 loading.tsx）
- ✅ FEAT-G02：详情页 Schedule Tab 支持完整周课表（新增 `weeklySchedule` 字段 + `ChildWeeklyScheduleItem` 类型 + `buildWeeklySchedule` 函数）
- ✅ FEAT-G03：详情页 Grades Tab 新增 `ChildGradeDetail` 按科目分组展示（平均分、趋势、最近成绩）
- ✅ FEAT-G04：详情页 Homework Tab 新增 `ChildHomeworkDetail` 展示完整作业信息（状态、截止、提交时间、尝试次数）
- ✅ FEAT-G05：考勤页新增 `ParentAttendanceWarning` 异常预警横幅（聚合缺勤/迟到/低出勤率）
- ✅ FEAT-G06：详情页底部新增"Contact Teacher"快捷入口
- ✅ FEAT-G07：详情页头部新增 `SiblingSwitcher` 多子女切换器
- ✅ LAYOUT-P01：仪表盘新增 `ParentAttentionBanner` 待办事项横幅
- ✅ LAYOUT-P02：`ChildCard` 突出 Overdue 异常（红色边框 + AlertTriangle 图标）
- ✅ LAYOUT-P03：仪表盘快捷入口改为 4 宫格大图标卡片
- ✅ LAYOUT-P04：详情页改为 Tab 布局（Overview/Homework/Grades/Schedule/Attendance/Diagnostic）
- ✅ LAYOUT-P05：详情页新增面包屑导航
- ✅ LAYOUT-P07：成绩趋势图 X 轴改用序号，避免日期重叠
- ✅ LAYOUT-P08：成绩页新增 `ParentExportButton` 导出按钮（占位）
- ✅ LAYOUT-P09：考勤页新增 `ParentAttendanceCalendar` 月历视图（按状态着色，支持按月切换）
- ✅ LAYOUT-P10：考勤异常高亮（与 FEAT-G05 同步实现）
- ✅ NAV-P02：Grades/Attendance 页面描述明确职责（多子女对比 vs 单子女详情）
- ✅ NAV-P03：详情页实现 `?tab=` 参数支持
- ✅ NAV-P04：所有 parent 路由新增 `loading.tsx` 骨架屏 + `error.tsx` 错误边界
- ✅ DATA-P02：成绩卡片新增 TrendIcon 进步/退步/持平标识
- ✅ DATA-P03：排名展示新增"Top X%"百分比
- ✅ DATA-P04：作业列表新增科目标识 Badge
- ✅ DATA-P05：作业分数显示新增"pts"单位
- ✅ DATA-P06：考勤页新增 `ParentAttendanceRateCard` 出勤率汇总卡片
- ✅ HABIT-P01：仪表盘"一眼定位异常"能力（AttentionBanner 聚合）
- ✅ HABIT-P02：待办横幅作业项直接跳转详情页 homework tab（1 次点击到达）
- ✅ HABIT-P03：多子女切换无需返回仪表盘
- ✅ HABIT-P06：仪表盘展示未读/待办数量
- ✅ A11Y-P02：Overdue 状态增加 AlertTriangle 图标辅助
- ✅ A11Y-P04：成绩图表容器新增 aria-label
- ✅ PERF-P01/P02：骨架屏 + 错误边界
- ✅ PERF-P03：空状态新增"Contact support"引导按钮
- ✅ PERF-P04：`ChildCard` Link 添加 `prefetch`
- ✅ MOBILE-P03：移动端子女卡片改为水平滑动 Carousel（snap-x）
- ✅ MOBILE-P04：作业/成绩列表项 `min-h-[44px]` 触摸区域

**V3-11 修复（家长端考试详情）**：
- ✅ V3-11：`parent/types.ts` 新增 `ChildExamResultItem` 类型；`ChildDashboardData` 扩展 `examResults` 字段
- ✅ V3-11：`parent/data-access.ts` `getChildDashboardData` 并行调用 `homework/data-access.getStudentExamResults` 获取子女考试结果（加入现有 `Promise.all`）
- ✅ V3-11：新增组件 `ChildExamDetail`（考试成绩汇总卡片：已参加考试数/平均分/最高分 + 考试成绩列表：考试标题/分数/得分率 Progress/提交时间）
- ✅ V3-11：`child-detail-panel.tsx` 新增 exams Tab（对标智学网家长端考试详情）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`classes`（合理）、`homework`（合理；V3-11 新增 `getStudentExamResults` 调用）、`grades`（合理）、`users`（合理）、`school`（合理）、`attendance`（v4 新增：考勤页复用 `StudentAttendanceView`；⚠️ 跨模块 UI 类型依赖：3 个组件直接 import `@/modules/attendance/types`）
- 被依赖：无

**已知问题**：
- ⚠️ P1：3 个 parent 组件（`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`）直接 import `@/modules/attendance/types`，违反模块解耦原则（应通过 data-access 接口或 shared 类型抽象）
- ⚠️ P1：parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量，与 attendance 模块重复
- ⚠️ P1：3 个 parent 组件的纯函数（`buildWarnings`/`aggregate`/`rateTone`/`formatDateKey`/`parseDateKey`/`buildCalendarDays`/`isSameDay`）未导出，无法单测
- ✅ P1 已修复：~~`app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB（违反三层架构）~~ 改为调用 `verifyParentChildRelation` data-access 函数
- ✅ P1 已修复：~~权限校验未加 parentId 条件，存在信息泄露风险~~ `verifyParentChildRelation` 同时按 parentId + studentId 过滤
- ✅ P2 已修复：~~`getChildBasicInfo` 多次串行查询~~ 改为 `Promise.all` 并行化，并使用 `getStudentActiveClass` 一次 JOIN
- ✅ P2 已修复：~~`getGradeOptions` 全量查询效率低~~ 改为 `getGradeNameById` 按 ID 查询
- ✅ P2 已修复：~~`buildHomeworkSummary` 中 `[...assignments].sort()` 不必要拷贝~~ 改为 `toSorted()`
- ✅ P2 已修复：~~`in7Days` 死代码~~ 已删除
- ⚠️ v4 保留：`/parent/leave` 为占位页，待后端实现请假审批流后接入
- ✅ v4-P1-5 改进（2026-06-23）：新增 `getParentIdsByStudentIds` data-access 函数，批量查询多个学生的家长 userId（去重），供 diagnostic/grades 模块通知场景调用
- ✅ v4-P1-9 改进（2026-06-23）：parent/diagnostic/page.tsx 新增错误卡片展示子女诊断查询失败原因
- ✅ v4-P1-12 改进（2026-06-23）：`ParentExportButton` 接入 `exportGradesAction`，支持按 studentId 导出单个子女成绩；parent/grades/page.tsx 传 studentId 到 ParentExportButton
- ⚠️ v4 保留：详情页 Attendance/Diagnostic Tab 为占位提示，待对应功能实现后填充
- ✅ v3-P2 改进（2026-06-23）：parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`，渲染 GradeTrendCard；parent/diagnostic/page.tsx 传入 `practiceHrefBase={null}` 隐藏练习按钮
- ✅ 职责单一，正确复用其他模块 data-access

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 243+ | 子女关系 + 仪表盘数据聚合 + 关系校验 + 子女姓名列表（v4 新增 `getChildNameList` + `buildWeeklySchedule`；V3-11 `getChildDashboardData` 并行调用 `getStudentExamResults`） |
| `types.ts` | 79+ | 类型定义（含 JSDoc，v4 新增 `ChildWeeklyScheduleItem`；V3-11 新增 `ChildExamResultItem` + `ChildDashboardData.examResults`） |
| `components/parent-dashboard.tsx` | 110 | 仪表盘（v4 重构：待办横幅 + 宫格快捷入口 + 移动端水平滑动） |
| `components/parent-attention-banner.tsx` | 128 | v4 新增：待办事项/异常聚合横幅（作业项直接跳转详情页 homework tab） |
| `components/parent-attendance-warning.tsx` | 89 | v4 新增：考勤异常预警 |
| `components/parent-attendance-rate-card.tsx` | 105 | v4 新增：考勤出勤率汇总卡片 |
| `components/parent-attendance-calendar.tsx` | 175 | v4 新增：考勤月历视图（use client） |
| `components/parent-export-button.tsx` | 50 | v4 新增：成绩导出按钮（v4-P1-12 已接入 exportGradesAction，支持按 studentId 导出单个子女成绩） |
| `components/child-card.tsx` | 148 | 子女卡片（v4 增强：异常突出 + 趋势图标） |
| `components/child-detail-header.tsx` | 78 | 详情页头部（v4 增强：面包屑） |
| `components/child-detail-panel.tsx` | 200+ | 详情页 Tab 面板 + SiblingSwitcher（v4 重写，集成 Homework/Grade Detail；V3-11 新增 exams Tab；✅ i18n：硬编码英文提取为 `parent.childDetail.*` 翻译键，新增 parent.json；修复 P2-4：`as` 断言改用 `isTab` 类型守卫） |
| `components/child-homework-summary.tsx` | 147 | 作业摘要（v4 增强：科目标识 + 触摸区域 + pts 单位） |
| `components/child-homework-detail.tsx` | 145 | v4 新增：作业详情视图（完整作业信息） |
| `components/child-grade-summary.tsx` | 159 | 成绩趋势（v4 增强：趋势图标 + aria-label） |
| `components/child-grade-detail.tsx` | 165 | v4 新增：成绩详情视图（按科目分组分析） |
| `components/child-exam-detail.tsx` | 165 | V3-11 新增 / ✅ P3-4 改进：子女考试详情视图（汇总卡片+考试列表，对标智学网家长端；P3-4：所有硬编码英文文案改为 i18n 翻译键 `examHomework.homework.parentExam.*`，新增 ChevronRight 导航图标，ul/li 语义化标签 + ARIA 属性，触控目标 ≥ 44px） |
| `components/child-schedule-card.tsx` | 119 | 课表卡片（v4 增强：周课表视图） |
| `components/parent-children-data-page.tsx` | 92 | 共享数据页（v4 增强：headerExtra） |

**路由清单**：
| 路由 | 文件 | 说明 |
|------|------|------|
| `/parent/dashboard` | `dashboard/page.tsx` + `loading.tsx` | 家长仪表盘 |
| `/parent/grades` | `grades/page.tsx` + `loading.tsx` | 多子女成绩聚合（v3-P2：并行查询 getClassAverageTrend + GradeTrendCard） |
| `/parent/diagnostic` | `diagnostic/page.tsx` + `loading.tsx` + `error.tsx` | P2-5 新增：多子女学情诊断聚合（v3-P2：practiceHrefBase={null} 隐藏练习按钮） |
| `/parent/attendance` | `attendance/page.tsx` + `loading.tsx` | 多子女考勤聚合（v4 新增预警横幅） |
| `/parent/leave` | `leave/page.tsx` + `loading.tsx` | v4 新增：请假申请（占位） |
| `/parent/children/[studentId]` | `children/[studentId]/page.tsx` + `loading.tsx` | 子女详情页（v4 重构：Tab 布局 + 多子女切换） |
| `error.tsx` | `error.tsx` | v4 新增：错误边界 |

---

## 2.20 elective（选课模块）

**职责**：选修课程管理 + 学生选课 + 抽签。

**导出函数**：
- Actions（11 个）：`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction` / `openSelectionAction` / `closeSelectionAction`（✅ P2-2：8 个写 Action 错误消息改用 `getTranslations("elective")` 国际化）
- Data-access：`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `openSelection` / `closeSelection` / `buildCourseSelect` / `mapCourseRow` / `resolveCourseDisplayNames`（✅ P2-7：使用 React `cache()` 包装 subject/grade options 请求级去重；✅ P2-8：通过 `resolvers.ts` 接口抽象跨模块依赖）/ `CourseCoreRow`
- Data-access-operations：`selectCourse`（✅ P2-9：新增 `checkScheduleConflict` 时间冲突检测；✅ P2-10：新增 `checkCreditLimit` 学分上限校验；✅ P2-4：学分上限改用 `getElectiveCreditLimit` 配置化，按年级覆盖；✅ P2-4：FCFS 录取后触发 `notifyCapacityThresholdIfNeeded` 容量阈值通知）/ `dropCourse`（✅ P2-4：新增 `dropReason` 参数 + `dropDeadline` 校验）/ `runLottery` / `buildLotteryRankCase`（⚠️ 未导出，无法单测）/ `checkScheduleConflict`（P2-9 新增）/ `checkCreditLimit`（P2-10 新增，P2-4 重构为配置化）
- Data-access-selections：`getCourseSelections` / `getStudentSelections` / `getStudentGradeId`（✅ P2-8：通过 `resolvers.ts` StudentGradeResolver 接口抽象；✅ P2-4：被 `selectCourse` 调用以获取年级用于学分上限）/ `getAvailableCoursesForStudent` / `resolveStudentDisplayNames`（✅ P2-8：通过 `resolvers.ts` CourseDisplayResolver 接口抽象）
- Data-access-settings（✅ P2-4 新增）：`getElectiveCreditLimit(gradeId?)`（按年级覆盖的学期学分上限，fallback 到全局，默认 10）/ `getCapacityNotifyThreshold()`（容量阈值通知比例，默认 0.9）/ `ELECTIVE_DEFAULTS`
- Resolvers（✅ P2-8 新增）：`CourseDisplayResolver` / `StudentGradeResolver`（接口）/ `getCourseDisplayResolver` / `getStudentGradeResolver` / `setCourseDisplayResolver` / `setStudentGradeResolver` / `resetResolvers`（可注入测试 mock）
- Lib 纯函数（✅ P2-9 新增）：`parseSchedule`（解析时间段字符串）/ `isScheduleConflict`（判断两个时间段是否冲突）
- Import-export（✅ P2-11 新增）：`exportElectiveCoursesToExcel`（课程列表导出）/ `exportCourseSelectionsToExcel`（选课名单导出）
- Components：`ElectiveCourseList`（课程卡片网格 + 管理操作）/ `ElectiveCourseForm`（课程创建/编辑表单）/ `ElectiveFilters`（nuqs 筛选栏）/ `StudentMySelectionsSection` + `StudentAvailableCoursesSection`（✅ P2-3：由原 `StudentSelectionView` 拆分为两个独立客户端组件，支持 Suspense 流式渲染）/ `ElectivePageLayout`（✅ P2-4 新增：页面布局骨架，admin/teacher 选课页复用）/ `ElectiveStatsCards`（✅ P1-13 新增）/ `ElectiveCourseDetail`（✅ P2-2 新增：课程详情复用组件）/ `ParentSelectionView`（✅ P0-2 新增：家长只读视图）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`school`（✅ P3 已修复：通过 school data-access.getSubjectOptions/getGradeOptions 获取科目/年级名称；✅ P2-8：通过 resolvers.ts 接口抽象）、`users`（✅ P3 已修复：通过 users data-access.getUserNamesByIds；✅ P2-8：通过 resolvers.ts 接口抽象）、`classes`（通过 classes data-access.getStudentActiveGradeId；✅ P2-8：通过 resolvers.ts 接口抽象）、`next-intl`（✅ P2-2：Server Action 错误消息 i18n；✅ P2-4：容量阈值通知文案 i18n）、`notifications`（✅ P2-4 新增：通过 `sendNotification` 发送容量阈值通知给教师/管理员）、`systemSettings` 表（✅ P2-4：读取 elective 学分上限与容量阈值配置）
- 被依赖：`app/api/export`（✅ P2-11：通过 `exportElectiveCoursesToExcel` / `exportCourseSelectionsToExcel`）

**已知问题**（详见 `docs/architecture/audit/elective-audit-report.md` 与 `attendance-elective-audit-report.md`）：
- ✅ P2-2 已修复：8 个写 Action 错误消息改用 `getTranslations("elective")` 国际化
- ✅ P2-4 已修复：新增 `ElectivePageLayout` 组件，admin/teacher 选课页复用布局
- ✅ P2-7 已修复：`resolveCourseDisplayNames` 使用 React `cache()` 包装 subject/grade options 请求级去重
- ✅ P2-8 已修复：新增 `resolvers.ts`，跨模块依赖（users/school/classes）通过接口抽象，支持测试注入 mock
- ✅ P2-9 已修复：`selectCourse` 新增时间冲突检测（`parseSchedule` + `isScheduleConflict` 纯函数 + `checkScheduleConflict` 异步查询）
- ✅ P2-10 已修复：`selectCourse` 新增学分上限校验（`checkCreditLimit`，MAX_CREDIT_PER_TERM=10）
- ✅ P2-11 已修复：新增 `export.ts`，课程列表/选课名单可导出 Excel
- ✅ P0-1 已修复（2026-06-25）：教师页面跳转 admin 路由修复，新增 `teacher/elective/create` 与 `teacher/elective/[id]/edit` 路由（含权限校验、教师越权防护）
- ✅ P0-2 已修复（2026-06-25）：新增 `parent/elective/page.tsx`（含 loading.tsx + error.tsx），使用 `ParentChildrenDataPage` 模式 + `parentId+studentId` 双重校验
- ✅ P0-3 已修复（2026-06-25）：`admin/elective/page.tsx` 与 `student/elective/page.tsx` 入口添加 `requirePermission(Permissions.ELECTIVE_READ)` 纵深防御
- ✅ P0-4 已修复（2026-06-25）：`data-access-operations.ts` 引入 `ElectiveBusinessError`（带 i18n code + params），由 `actions.ts` 通过 `translateBusinessError` 翻译后再返回
- ✅ P0-5 已修复（2026-06-25）：`export.ts` 移除 `as` 类型断言，改用 `ElectiveCourseStatusEnum.safeParse()` 类型守卫
- ✅ P1-6 已修复（2026-06-25）：`elective-course-form.tsx` 全量 i18n 化，所有硬编码英文替换为 `t("form.*")` 键
- ✅ P1-7 已修复（2026-06-25）：`elective-course-list.tsx` 中 `<a>` 替换为 Next.js `<Link>`
- ✅ P1-8 已修复（2026-06-25）：3 个 `error.tsx` 文案修正为 `t("errors.title")/t("errors.description")/t("actions.retry")`
- ✅ P1-9 已修复（2026-06-25）：`export.ts` 表头全量 i18n 化（statusHeader/priorityHeader/selectedAtHeader/enrolledAtHeader/indexHeader）
- ✅ P1-10 已修复（2026-06-25）：`getElectiveCourses` / `getElectiveCourseById` 移除 try-catch 静默吞错，让异常冒泡到 RSC 触发 error.tsx
- ✅ P1-11 已修复（2026-06-25）：`parseSchedule` 重写，支持完整英文星期（Monday/Tuesday 等）+ 中文（周一/星期一）+ 多时段（逗号/分号分隔）
- ✅ P1-12 已修复（2026-06-25）：`runLottery` 不再自动将课程置为 `closed`，保留 `status="open"` 支持重抽
- ✅ P1-13 已修复（2026-06-25）：新增 `data-access-stats.ts` 与 `ElectiveStatsCards` 组件，集成到 admin 列表页顶部展示 4 项关键指标
- ✅ P2-1 已修复（2026-06-25）：新增 `tests/integration/elective/elective-pure-functions.test.ts`（35 个单测，覆盖 `parseSchedule` / `isScheduleConflict` / `normalizeDay` / `buildLotteryRankCase` / `mapCourseRow`）
- ✅ P2-2 已修复（2026-06-25）：新增 `ElectiveCourseDetail` 复用组件 + admin/student 课程详情页（含 loading.tsx）
- ✅ P2-3 已修复（2026-06-25）：student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界；admin 列表与统计卡片分离 Suspense；`StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection`；新增 4 个异步 RSC loader（`MySelectionsLoader` / `AvailableCoursesLoader` / `StatsCardsLoader` / `CourseListLoader`）+ 分段骨架屏
- ✅ P2-4 已修复（2026-06-25）：配置化学分上限（`data-access-settings.ts` 读取 systemSettings，按年级覆盖，默认 10）；退课截止时间 `dropDeadline` 字段 + 校验；退课理由 `dropReason` 字段（可选填写，最长 255 字符）；容量阈值通知（FCFS 录取后跨过 `capacityNotifyThreshold` 阈值时通知教师，fire-and-forget）；新增 `ElectiveErrorCode.dropDeadlinePassed`；通知文案 i18n 化
- ✅ P1 已修复：~~`buildCourseSelect` 跨模块 join users/subjects/grades 表~~ 改为只查 electiveCourses 表，通过 `resolveCourseDisplayNames` 调用 school/users data-access 获取显示名称
- ✅ P1 已修复：~~`getSubjectOptions` 本地直查 subjects 表且与 school 模块重复~~ 删除本地实现，改用 `school/data-access.getSubjectOptions`
- ✅ P1 已修复：~~`selectCourse`/`dropCourse` 缺事务包裹~~ 改为 `db.transaction` 包裹，FCFS 模式下使用 `FOR UPDATE` 行锁防止并发超卖
- ✅ P2 已修复：~~`mapCourseRow` 在 data-access.ts 与 data-access-selections.ts 重复定义~~ 抽取到 data-access.ts 统一导出，data-access-selections.ts 复用
- ✅ P2 已修复：~~`runLottery` 使用 `sort(() => Math.random() - 0.5)` 有偏 shuffle~~ 改为 Fisher-Yates 无偏洗牌算法
- ✅ P2 已修复：~~`selectCourse` FCFS 并发超卖风险~~ 使用 `db.transaction` + `.for("update")` 行锁
- ✅ 权限校验完整（ELECTIVE_MANAGE/SELECT/READ，所有页面入口均调用 `requirePermission`）
- ⚠️ 无 P2 待办：所有审计报告 P0/P1/P2 项均已实施完毕

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 348 | 11 个 Server Action（P0-4：translateBusinessError 翻译 ElectiveBusinessError） |
| `data-access.ts` | 258 | 课程 CRUD + scope 过滤 + 共享映射函数（P1-10：移除静默吞错） |
| `data-access-operations.ts` | 374 | 选课操作（P0-4：ElectiveBusinessError + i18n code；P1-11：parseSchedule 支持完整星期+多时段；P1-12：抽签可重跑） |
| `data-access-selections.ts` | 147 | 选课记录查询 + 学生可选课程 |
| `data-access-stats.ts` | 88 | 管理员概览统计（P1-13 新增：getElectiveOverviewStats） |
| `resolvers.ts` | 83 | 跨模块依赖接口抽象 |
| `export.ts` | 102 | Excel 导出（P0-5：移除 as 断言；P1-9：表头全量 i18n） |
| `schema.ts` | 179 | Zod 校验 |
| `types.ts` | 73 | 类型定义 |
| `constants.ts` | 55 | i18n key 映射 + Badge variant + 类型守卫 |
| `components/elective-course-list.tsx` | 236 | 课程卡片网格 + 管理操作（P1-7：a 转 Link） |
| `components/elective-course-form.tsx` | 301 | 课程创建/编辑表单（P1-6：全量 i18n 化） |
| `components/elective-filters.tsx` | 51 | nuqs 筛选栏（搜索 + 模式） |
| `components/student-selection-view.tsx` | 278 | 学生选课视图（✅ P2-3：拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件，支持 Suspense 流式渲染） |
| `components/elective-page-layout.tsx` | 30 | 页面布局骨架 |
| `components/elective-stats-cards.tsx` | 73 | 管理员统计卡片网格（P1-13 新增） |
| `components/parent-selection-view.tsx` | 80 | 家长查看子女选课只读视图（P0-2 新增） |
| `components/elective-course-detail.tsx` | 130 | 课程详情复用组件（P2-2 新增：admin/student 共用） |
| `app/(dashboard)/student/elective/_components/my-selections-loader.tsx` | 17 | 异步 RSC loader：我的选课（P2-3 新增） |
| `app/(dashboard)/student/elective/_components/available-courses-loader.tsx` | 45 | 异步 RSC loader：可选课程 + 过滤逻辑（P2-3 新增） |
| `app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx` | 12 | 异步 RSC loader：统计卡片（P2-3 新增） |
| `app/(dashboard)/admin/elective/_components/course-list-loader.tsx` | 30 | 异步 RSC loader：课程列表 + 状态过滤（P2-3 新增） |

---

## 2.21 proctoring（监考模块）

**职责**：考试监考事件记录 + 防作弊监控 + 监考面板。

**导出函数**：
- Actions：`recordProctoringEventAction` / `getProctoringDashboardAction`
- Data-access：`recordProctoringEvent` / `getExamSubmissionForProctoring` / `getExamForProctoring` / `getExamProctoringSummary` / `getStudentProctoringStatuses` / `getRecentProctoringEvents`（✅ P2 已修复：`getExamProctoringSummary` 使用 `Promise.all` 并行化考试信息与提交记录查询、事件类型统计与学生事件统计查询；合并两次 filter 为单次循环统计 started/submitted）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`exams`（✅ P1-1 已修复：通过 exams data-access.getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionsForExam/getExamTitleById）、`users`（✅ P1-1 已修复：通过 users data-access.getUserNamesByIds）
- 被依赖：无

**已知问题**：
- ✅ P1-2 已修复：`exam-mode-config.tsx` 已集成到考试表单（`exam-form.tsx` 调用 `<ExamModeConfig />`），移除全部 10 处 `as` 类型断言，改用 `useFormContext` 替代 `Control` prop 避免 `Control<T>` 不变型问题
- ✅ P1-3 已修复：`proctoring-dashboard.tsx` 用类型守卫函数 `isProctoringEventType` + `toProctoringEventTypes` 替代 `Object.keys(...) as ProctoringEventType[]` 断言
- ✅ P0-6 已修复：~~事件上报存在 Server Action 与 REST API 双通道重复~~ 删除 `/api/proctoring/event` REST 路由（移至 deletes/），Server Action `recordProctoringEventAction` 为唯一规范路径
- ✅ P1-1 已修复：~~跨模块直查 `exams`/`examSubmissions`/`users`~~ 改为通过 exams/users data-access 函数获取数据
- ✅ P2 已修复：`actions.ts` 不再直接 import `db` 和 `examSubmissions`，submission 归属校验已下沉到 data-access；`recordProctoringEventAction` 改用 `requirePermission(EXAM_SUBMIT)` 并增加 `revalidatePath`
- ✅ P2 已修复：~~`getStudentProctoringStatuses` 串行查询（getUserNamesByIds 后再查事件）~~ 改为 `Promise.all` 并行拉取学生姓名与事件记录

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 409 | 事件记录 + 查询 + 摘要统计 + submission 归属校验 |
| `actions.ts` | 139 | 2 个 Server Action |
| `types.ts` | 136 | 类型定义 + 标签常量 + 阈值常量 |
| `components/anti-cheat-monitor.tsx` | - | 学生端防作弊监控 |
| `components/exam-mode-config.tsx` | - | 考试模式配置（✅ P1-2 已修复：已集成到 exam-form，useFormContext 替代 Control prop，无 as 断言） |
| `components/proctoring-dashboard.tsx` | - | 教师监考面板（✅ P1-3 已修复：类型守卫函数替代 as 断言） |

---

## 2.22 diagnostic（学情诊断模块）

**职责**：知识点掌握度查询 + 诊断报告生成。

**导出函数**：
- Actions：`generateStudentReportAction` / `generateClassReportAction` / `generateGradeReportAction`（v4-P2-3 新增：生成年级诊断报告）/ `publishReportAction`（v4-P1-4 + v4-P1-5 增强：班级报告发布时通知全班学生 + 全班学生家长，个人报告通知学生本人 + 其家长，调用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID）/ `deleteReportAction`（v2-P2-3 修复：删除死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction`，页面直接调用 data-access 并自行权限校验）/ `exportDiagnosticReportAction`（v3-P2-4 新增：导出 Excel）/ `getClassStudentsByKnowledgePointAction`（v3-P2-5 新增：按知识点筛选学生）
- Data-access：`updateMasteryFromSubmission`（v2-P1-8 修复：累积模式；v2-P2-5 修复：db.transaction 包裹）/ `getStudentMastery`（P3-19 修复：移除 export，改为模块内部函数）/ `getStudentMasterySummary`（P3-18 修复：getUserNamesByIds 与 getStudentMastery 并行查询）/ `getClassMasterySummary`（v2-P2-4 修复：totalStudents 语义 + 班级平均掌握度按学生平均）/ `getGradeMasterySummary`（v4-P2-3 新增：年级掌握度摘要）/ `getKnowledgePointStats`（v2-P1-7 修复：页面先查班级再传参）/ `getClassStudentsByKnowledgePoint`（v3-P2-5 新增）
- Data-access-reports：`generateDiagnosticReport` / `generateClassDiagnosticReport`（v2-P2-6 修复：校验掌握度数据；P3-27 修复：使用 DiagnosticReportError 结构化错误码）/ `generateGradeDiagnosticReport`（v4-P2-3 新增：生成年级诊断报告）/ `getDiagnosticReports`（P3-15 修复：支持分页 limit/offset，返回 { reports, total } 结构；v4-P1-1 增强：新增可选 `scope?: DataScope` 参数，支持 children/class_taught 行级权限过滤，class_taught 通过 `getStudentIdsByClassIds` 解析所教班级学生 ID）/ `getDiagnosticReportById` / `publishDiagnosticReport` / `deleteDiagnosticReport`（✅ P2 已修复：使用 `React.cache()` 包装实现请求级 memoization；P3-1 修复：toNumber 从 grades 模块导入）/ `DiagnosticReportError`（P3-27 新增：结构化错误码类，v4-P2-3 新增 GRADE_NOT_FOUND/GRADE_NO_MASTERY_DATA 错误码）
- Stats-service（✅ v2-P1-6 新增）：`serializeMasteryWithKp` / `computeAverageMastery` / `classifyStrengthsWeaknesses`（P3-16 修复：弱项阈值从 <60 改为 <80，消除 60-79 盲区）/ `buildStudentMasterySummary` / `aggregateClassMastery` / `computeKpStats` / `computeClassAverageMastery` / `buildStudentsNeedingAttention` / `buildClassMasterySummary` / `buildGradeMasterySummary`（v4-P2-3 新增）/ `buildStudentReportContent` / `buildClassReportContent` / `buildGradeReportContent`（v4-P2-3 新增）/ `computeMasteryLevel` / `serializeMastery`（从 data-access / data-access-reports 抽取的纯函数）
- Export（v3-P2-4 新增）：`exportDiagnosticReportToExcel`（v4-P2-1 增强：班级报告导出新增知识点统计+需关注学生 Sheet；i18n 全覆盖）/ `buildDiagnosticReportFilename`（v4-P2 改为 async，使用 i18n 翻译文件名）
- Role-config（v4-P2-2 新增）：`DIAGNOSTIC_ROLE_CONFIG` / `getDiagnosticRoleConfig` / `DiagnosticRole` 类型（配置驱动角色差异，新增角色只需添加配置项）
- Schema：`GenerateStudentReportSchema` / `GenerateClassReportSchema` / `GenerateGradeReportSchema`（v4-P2-3 新增）/ `PublishReportSchema` / `DeleteReportSchema`（v2-P2-3 修复：删除死代码 `GetDiagnosticReportsSchema` / `GetDiagnosticReportByIdSchema`）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`exams`（✅ P1-1 已修复：通过 exams data-access.getExamSubmissionWithAnswers）、`questions`（✅ P1-1 已修复：通过 questions data-access.getKnowledgePointsForQuestions）、`classes`（✅ P1-1 已修复：通过 classes data-access.getClassExists/getClassNameById/getActiveStudentIdsByClassId；v2-P1-7 新增 getStudentActiveClassId）、`users`（✅ P1-1 已修复：通过 users data-access.getUserNamesByIds/getUserIdsByGradeId）、`grades`（P3-1 修复：通过 grades/lib/grade-utils.toNumber 复用工具函数）、`school`（v4-P2-3 新增：通过 school data-access.getGradeNameById 获取年级名称）
- 被依赖：无

**已知问题**：
- ✅ P1-1 已修复：~~`updateMasteryFromSubmission` 跨模块直查 4 张表~~ 改为调用 `exams/data-access.getExamSubmissionWithAnswers` 和 `questions/data-access.getKnowledgePointsForQuestions`
- ✅ P2 已修复：~~`data-access-reports.ts` 有未使用代码（`round2` + `void round2`）~~ 已删除死代码
- ✅ P2 已修复：~~`updateMasteryFromSubmission` 循环内串行 await upsert~~ 改为 `Promise.all` 并行执行所有 upsert
- ✅ P2 已修复：~~`getClassMasterySummary` 串行查询~~ 改为两组 `Promise.all` 并行
- ✅ P2 已修复：~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
- ✅ P0-2 已修复：~~`data-access-reports.ts` 直查 `users` 表获取姓名~~ 改为通过 `users/data-access.getUserNamesByIds`
- ✅ P2-2 已修复：~~Tailwind 任意值~~ 改用标准 Tailwind 类
- ✅ P2-1 已修复：~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 5 个图表添加 `role="img"` + `aria-label`，2 个表格添加 `<caption>`，3 个列表添加 `role="list"`，3 个图标按钮添加 `aria-label`
- ✅ P2-3 已修复：~~班级报告将生成者 ID 存入 `studentId` 字段~~ schema 改为可空，班级报告 `studentId` 置空
- ✅ v2-P1-6 已修复：~~统计逻辑混在 data-access 层~~ 抽取 `stats-service.ts`（352 行，12 个纯函数 + 2 个常量 + 4 个接口）
- ✅ v2-P1-7 已修复：~~`getKnowledgePointStats` 无参调用~~ 页面先查 `getStudentActiveClassId` 再传参
- ✅ v2-P1-8 已修复：~~`updateMasteryFromSubmission` 覆盖模式~~ 改为累积计算（读取已有记录后累加）
- ✅ v2-P2-3 已修复：~~死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction` 全局零调用~~ 已删除，页面直接调用 data-access
- ✅ v2-P2-4 已修复：~~`totalStudents` 语义错误 + 班级平均掌握度计算偏差~~ 改为实际有掌握度记录的学生数；先算学生个人平均再取平均
- ✅ v2-P2-5 已修复：~~多 upsert 无事务包裹~~ 使用 `db.transaction()` 保证原子性
- ✅ v2-P2-6 已修复：~~生成报告未校验掌握度数据~~ 添加 `totalKnowledgePoints === 0` 和 `studentCount === 0` 校验
- ✅ v2-P1-4 已修复：~~4 个组件 i18n 完全未接入~~ 全部接入 `useTranslations("diagnostic")`
- ✅ v2-P2-7 已修复：~~`report-list.tsx` 过滤器 Label 缺少 `htmlFor`~~ 添加 `htmlFor` 和 `id`
- ✅ 与 grades 模块无职责重叠
- ✅ v3-P2 改进（2026-06-23）：`StudentDiagnosticView` 新增 `practiceHrefBase` prop（string | null），null 时隐藏练习按钮；teacher/diagnostic/student/[studentId] 传入 `practiceHrefBase="/teacher/questions"`，parent/diagnostic 传入 `practiceHrefBase={null}` 隐藏练习按钮
- ✅ P3-1 已修复（2026-06-23）：~~`data-access-reports.ts` 本地定义 `toNumber` 与 `grades/lib/grade-utils.ts` 重复~~ 改为从 grades 模块导入
- ✅ P3-15 已修复（2026-06-23）：~~`getDiagnosticReports` 无分页，可能返回大量数据~~ 添加 limit/offset 分页支持，返回 `{ reports, total }` 结构，Promise.all 并行查询总数和数据
- ✅ P3-16 已修复（2026-06-23）：~~强弱项分类存在 60-79 盲区~~ 弱项阈值从 <60 改为 <80，确保所有知识点都被分类
- ✅ P3-17 已修复：~~班级报告 strengths 无数量上限~~ `buildClassReportContent` 中 strengths 已限制为前 5 个（按掌握度降序）
- ✅ P3-18 已修复（2026-06-23）：~~`getStudentMasterySummary` 串行查询用户名和掌握度~~ 改为 Promise.all 并行查询
- ✅ P3-19 已修复（2026-06-23）：~~`getStudentMastery` 使用 export 但仅内部使用~~ 移除 export，改为模块内部函数
- ✅ P3-27 已修复（2026-06-23）：~~`generateDiagnosticReport` / `generateClassDiagnosticReport` 中 `throw new Error("...")` 直接暴露给用户~~ 改为使用 `DiagnosticReportError` 结构化错误码（继承 BusinessError）
- ✅ P3-21 已修复（2026-06-23）：`ClassDiagnosticView` View 按钮已有描述性 aria-label（`classDiagnostic.viewAriaLabel`，含学生名）
- ✅ P3-22 已修复（2026-06-23）：`StudentDiagnosticView` Practice 按钮添加 aria-label（`studentDiagnostic.practiceAriaLabel`，含知识点名）
- ✅ v4-P3-6 已修复（2026-06-23）：`MasteryRadarChart` 雷达图添加 `tabIndex={0}` 键盘可达性 + `focus-visible` 焦点样式（原有 `role="img"` + `aria-label` 保留）
- ✅ v4-P1-1 改进（2026-06-23）：`getDiagnosticReports` 新增可选 `scope?: DataScope` 参数，支持 children/class_taught 行级权限过滤；teacher/diagnostic/page.tsx 传入 dataScope；student/diagnostic/page.tsx 传 status:"published" + dataScope；parent/diagnostic/page.tsx 传 status:"published" + dataScope + 错误卡片
- ✅ v4-P1-3 改进（2026-06-23）：`student-diagnostic-view.tsx` 移除草稿回退逻辑（不再展示 draft 状态报告）
- ✅ v4-P1-4 + v4-P1-5 改进（2026-06-23）：`publishReportAction` 班级报告发布时通知全班学生 + 全班学生家长（通过 `getStudentIdsByClassId` 获取全班学生，通过 `parent/data-access.getParentIdsByStudentIds` 获取家长 ID）；个人报告通知学生本人 + 其家长
- ✅ v4-P1-8 + v4-P1-11 改进（2026-06-23）：`class-diagnostic-view.tsx` 热力图添加图例 + 表格 `overflow-x-auto` 水平滚动
- ✅ v4-P1-9 改进（2026-06-23）：parent/diagnostic/page.tsx 新增错误卡片（Error Card）展示子女诊断查询失败原因
- ✅ v4-P1 数据库变更（2026-06-23）：`learningDiagnosticReports` 表新增 `classId` 字段（varchar(128), references classes.id, onDelete: set null）+ `diagnostic_class_idx` 索引；`DiagnosticReport` 接口新增 `classId: string | null` 字段（班级报告关联班级 ID，个人报告为 null）
- ✅ v4-P1 师生关系校验（2026-06-23）：teacher/diagnostic/student/[studentId]/page.tsx 新增 class_taught 师生关系校验
- ✅ v4-P2 审计修复（2026-06-24）：P0-1 跨模块 import WidgetBoundary → 提升到 `shared/components/widget-boundary.tsx`，diagnostic 和 grades 模块统一从 shared 引用
- ✅ v4-P2 审计修复（2026-06-24）：P0-2/P0-3 教师页面标题 + error.tsx 硬编码 → 接入 `getTranslations("diagnostic")` / `useTranslations("diagnostic")`
- ✅ v4-P2 审计修复（2026-06-24）：P1-1 parent 错误卡片中英文混用 → 提取完整 i18n 键 `error.childLoadFailed` / `error.childLoadFailedDesc`
- ✅ v4-P2 审计修复（2026-06-24）：P1-2 `as` 类型断言 → 改用类型守卫函数 `isReportType` / `isReportStatus` 返回值收窄
- ✅ v4-P2 审计修复（2026-06-24）：P1-3 分享链接指向不存在路由 → 移除分享功能（无报告详情页需求）
- ✅ v4-P2 审计修复（2026-06-24）：P1-4 报告内容硬编码中文 → stats-service 接收 `ReportContentTranslations` 接口参数，actions 层从 next-intl 构建翻译注入
- ✅ v4-P2 审计修复（2026-06-24）：P1-5 Excel 导出表头硬编码 → export.ts 使用 `getTranslations("diagnostic.exportContent")`，`buildDiagnosticReportFilename` 改为 async
- ✅ v4-P2 审计修复（2026-06-24）：P2-1 班级报告导出缺少明细 → 利用 `classId` 字段查询 `getClassMasterySummary`，导出知识点统计 Sheet + 需关注学生 Sheet
- ✅ v4-P2 审计修复（2026-06-24）：P2-2 角色差异通过 props 硬编码 → 新增 `role-config.ts`，`DIAGNOSTIC_ROLE_CONFIG` 配置对象驱动 `practiceHrefBase`，组件接受 `role` prop
- ✅ v4-P2 审计修复（2026-06-24）：P2-3 无年级诊断报告入口 → 新增 `gradeId` 字段（schema + 迁移 0012）、`GradeMasterySummary` 类型、`getGradeMasterySummary` / `buildGradeMasterySummary` / `buildGradeReportContent` / `generateGradeDiagnosticReport` / `generateGradeReportAction` / `GenerateGradeReportSchema`，i18n 新增 `gradeSummary` / `gradeRecommendation` / `gradeNoWeakness`
- ✅ v4-P2 审计修复（2026-06-24）：P2-4 热力图色块缺少键盘导航 → 添加 `tabIndex={0}` + `focus-visible:ring-2` 样式，外层容器 `role` 从 `img` 改为 `group`
- ✅ v2-P0-1 审计修复（2026-06-25）：3 个子路由 error.tsx 硬编码中文 → teacher/diagnostic/class/[classId]/error.tsx、teacher/diagnostic/student/[studentId]/error.tsx、parent/diagnostic/error.tsx 全部接入 `useTranslations("diagnostic")`，新增 `error.classLoadFailed/Desc`、`error.studentLoadFailed/Desc`、`error.parentLoadFailed/Desc` i18n 键
- ✅ v2-P1-1 审计修复（2026-06-25）：~~`getDiagnosticReports` 未过滤 `grade_managed` DataScope~~ 在 data-access-reports.ts 中新增 `grade_managed` 分支，调用 `users/data-access.getUserIdsByGradeId` 解析年级学生 ID 后用 `inArray` 过滤，修复年级主任越权查看全校报告的安全漏洞
- ✅ v2-P1-2 审计修复（2026-06-25）：i18n 弱项标签"弱项（<60%）"与代码逻辑（<80%）不一致 → 修正为"弱项（<80%）"
- ✅ v2-P1-3 审计修复（2026-06-25）：热力图 aria-label 硬编码中文标点 → 使用 i18n 模板键 `classDiagnostic.heatmapCellAriaLabel: "{name}：{level}%，{label}，{mastered}/{total}"`
- ✅ v2-P1-4 审计修复（2026-06-25）：组件直接 import actions 无接口抽象 → 新增 `services/diagnostic-service.ts`（DiagnosticService 接口）、`services/diagnostic-service-context.tsx`（DiagnosticServiceProvider + useDiagnosticService）、`services/default-diagnostic-service.ts`（默认实现绑定 Server Actions），report-list.tsx 和 class-diagnostic-view.tsx 改用 Context 注入
- ✅ v2-P1-5 审计修复（2026-06-25）：置信度计算过于简化（仅 null/非 null 两级）→ 改为基于知识点数量的多级判断（0 insufficient / 1-3 low / 4-8 medium / >8 high）
- ✅ v2-P1-6 审计修复（2026-06-25）：无 Error Boundary 包裹独立数据区块 → student-diagnostic-view（雷达图/强弱项/最新报告/历史报告）和 class-diagnostic-view（概览/热力图/筛选/排名/关注列表/生成报告）各数据区块均用 WidgetBoundary 包裹
- ✅ v2-P2-1 审计修复（2026-06-25）：死 i18n 键未清理 → 移除 9 个 share 相关 i18n 键（share/shareAriaLabel/shareTitle 等）
- ✅ v2-P2-2 审计修复（2026-06-25）：教师报告列表页客户端过滤 class_members → 移除客户端 filter，DataScope 过滤已在 data-access 层完成
- ✅ v2-P2-3 审计修复（2026-06-25）：export.ts 错误消息硬编码英文 → 新增 `DiagnosticExportError` 结构化错误码（继承 BusinessError），`throw new Error("Report not found")` 改为 `throw new DiagnosticExportError("REPORT_NOT_FOUND", ...)`
- ✅ v2-P2-5 审计修复（2026-06-25）：雷达图名称截断无 tooltip → 改用完整 `knowledgePoint` 作为 angleKey 使 Tooltip 显示完整名称，通过 `angleTickFormatter` 截断轴上显示文本（8 字符+省略号）
- ✅ v2-P2-6 审计修复（2026-06-25）：通知类型复用 "grade" → NotificationType 新增 "diagnostic" 类型，更新 notification-list/notification-dropdown TYPE_ICON 和 TYPE_KEYS，diagnostic/actions.ts 两处 createNotification 改用 `type: "diagnostic"`
- ✅ v2-P2-7 审计修复（2026-06-25）：无监控埋点接口 → 新增 `services/diagnostic-monitor.ts`（DiagnosticMonitor 接口 + noopDiagnosticMonitor 实现）、`services/diagnostic-monitor-context.tsx`（DiagnosticMonitorProvider + useDiagnosticMonitor）、`services/monitored-diagnostic-service.ts`（createMonitoredDiagnosticService 工厂函数，组合模式包装服务添加埋点），两个 teacher 页面接入 DiagnosticMonitorProvider
- ✅ v2-P2-9 审计修复（2026-06-25）：parent/diagnostic noRecordsTitle 与 noRecordsDescription 重复 → 新增 `parent.noReportsTitle` / `parent.noReportsDescription` 独立 i18n 键

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 477 | 知识点掌握度查询 + 更新（v2-P1-8 累积模式；v2-P2-5 事务；v2-P2-4 语义修正；v2-P1-6 改用 stats-service 纯函数；v4-P2-3 新增 getGradeMasterySummary） |
| `data-access-reports.ts` | 256+ | 诊断报告 CRUD（v2-P2-6 校验；v2-P1-6 改用 stats-service 纯函数；v4-P1-1：getDiagnosticReports 新增 scope 参数支持行级权限过滤；v4-P2-3 新增 generateGradeDiagnosticReport） |
| `stats-service.ts` | 388+ | 统计计算纯函数（v2-P1-6 新增：12 个纯函数 + 2 个常量 + 4 个接口；v4-P2-3 新增 buildGradeMasterySummary/buildGradeReportContent） |
| `actions.ts` | 274+ | 5 个 Server Action（v2-P2-3 删除 2 个死代码读 Action；v4-P1-4/v4-P1-5：publishReportAction 新增全班学生 + 家长通知；v4-P2-3 新增 generateGradeReportAction） |
| `schema.ts` | 31+ | Zod 校验（4 个 schema，v2-P2-3 删除 2 个死代码 schema；v4-P2-3 新增 GenerateGradeReportSchema） |
| `types.ts` | 109+ | 类型定义（v4-P2-3 新增 GradeMasterySummary 接口） |
| `export.ts` | 165+ | Excel 导出（v3-P2-4 新增；v4-P2-1 班级报告导出新增知识点统计+需关注学生 Sheet；i18n 全覆盖） |
| `role-config.ts` | 40+ | 角色配置驱动（v4-P2-2 新增：DIAGNOSTIC_ROLE_CONFIG 配置对象，通过 role prop 驱动 practiceHrefBase 等角色差异） |
| `components/class-diagnostic-view.tsx` | 448+ | 班级诊断视图（v2-P1-6 热力图 a11y；v2-P1-4 i18n；v4-P1-8 热力图图例；v4-P1-11 表格 overflow-x-auto；v4-P2-4 热力图色块 tabIndex+focus-visible 键盘导航） |
| `components/student-diagnostic-view.tsx` | 293+ | 学生诊断视图（v2-P1-4 i18n；v3-P2 新增：practiceHrefBase prop；v4-P1-3 移除草稿回退逻辑；v4-P2-2 改为 role prop 配置驱动） |
| `components/mastery-radar-chart.tsx` | 85 | 雷达图（v2-P1-4 i18n） |
| `components/report-list.tsx` | 445 | 报告列表（v2-P2-7 Label htmlFor；v2-P1-4 i18n；v4-P1-3 移除分享功能避免 404） |
| `components/confidence-utils.ts` | 31 | 置信度计算（v4-P3-7 新增；v2-P1-5 改为基于知识点数量的多级判断） |
| `services/diagnostic-service.ts` | 58 | DiagnosticService 接口（v2-P1-4 新增：抽象客户端可调用的诊断操作） |
| `services/diagnostic-service-context.tsx` | 46 | DiagnosticServiceProvider + useDiagnosticService（v2-P1-4 新增：Context 注入） |
| `services/default-diagnostic-service.ts` | 86 | 默认服务实现绑定 Server Actions（v2-P1-4 新增） |
| `services/diagnostic-monitor.ts` | 95 | DiagnosticMonitor 接口 + noopDiagnosticMonitor（v2-P2-7 新增：监控埋点扩展点） |
| `services/diagnostic-monitor-context.tsx` | 56 | DiagnosticMonitorProvider + useDiagnosticMonitor（v2-P2-7 新增） |
| `services/monitored-diagnostic-service.ts` | 159 | createMonitoredDiagnosticService 工厂（v2-P2-7 新增：组合模式包装服务添加埋点） |

---

## 2.23 settings（设置模块）— V3 AI 配置统一入口

**职责**：系统设置（学校信息/安全策略/文件上传/通知配置）+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页（学生/教师概览）+ 学校品牌配置（audit-P2-6 新增：登录/注册页 logo/标语/作者）。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面，移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。V3.1 新增 public/private 可见性模型：管理员发布 public Provider 全员可用，普通用户可创建 private Provider 仅本人可见。audit-P2-6 新增品牌配置：`brand-config.ts` 纯类型 + 默认值（无 `server-only`，可被 Client Component 导入）+ `data-access-brand.ts` 数据访问（`server-only`，从 `system_settings` 表 `brand` 分类读取 4 个键）+ `actions-brand.ts` Server Action + `BrandConfigCard` 管理员配置组件。

**导出函数**：
- Actions：`getAiProviderSummaries`（V3.1 增强：按用户身份过滤 public + own private）/ `upsertAiProviderAction`（V3.1 增强：visibility 字段，非管理员强制 private）/ `testAiProviderAction` / `deleteAiProviderAction`（V3 新增删除能力）/ `canConfigurePublicAiProvider`（V3.1 新增：检查当前用户是否拥有 AI_CONFIGURE 权限）
- Actions-password：`changePasswordAction`（✅ P1 已修复：使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access）
- Actions-avatar：`updateUserAvatarAction` / `removeUserAvatarAction`（✅ P2-8 新增：头像上传/删除，复用 `/api/upload` 路由）
- Actions-notifications：`sendTestNotificationAction`（✅ P2-10 新增：发送测试通知，占位实现待接入真实通知服务）
- Actions-system-settings：`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`（✅ P0-3 新增：管理员系统设置 CRUD，4 分类 Zod 校验）
- Actions-brand：`getBrandConfigAction` / `saveBrandConfigAction`（✅ audit-P2-6 新增：品牌配置 CRUD，`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath` 刷新 `/(auth)/login` / `/(auth)/register` / `/admin/settings`）
- Actions-security：`getSecurityCenterAction` / `toggleTwoFactorAction`（✅ P2-9 新增：2FA 状态查询/切换 + 最近登录历史）
- Data-access：`getAiProviderSummaries`（管理员视图，返回全部）/ `getAiProviderSummariesForUser`（V3.1 新增：用户视图，返回 public + own private）/ `countDefaultAiProviders` / `getAiProviderForUpdate`（V3.1 增强：可选 userId 做所有权校验）/ `updateAiProvider`（V3.1 增强：visibility 字段）/ `createAiProvider`（V3.1 增强：visibility 字段）/ `deleteAiProvider`（V3 新增：事务删除 + 自动转移默认；V3.1 增强：可选 userId 做所有权校验）/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`（P1 新增，从 actions 下沉）
- Data-access-system-settings：`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`（✅ P0-3 新增：system_settings 表 CRUD，键值对存储模式；✅ audit-P2-6 `SystemSettingCategory` 联合类型新增 `"brand"`）
- Data-access-brand：`getBrandConfig` / `saveBrandConfig`（✅ audit-P2-6 新增：`server-only` 数据访问，从 `system_settings` 表 `brand` 分类并行读取 4 个键 schoolName/logoUrl/testimonialQuote/testimonialAuthor，未配置项使用 `DEFAULT_BRAND_CONFIG` 默认值）
- Brand-config（纯类型，无 `server-only`）：`BrandConfig`（接口：schoolName / logoUrl: string|null / testimonialQuote / testimonialAuthor）/ `DEFAULT_BRAND_CONFIG`（默认值：schoolName="Next_Edu" / logoUrl=null / 默认英文标语）
- Components：`SettingsView`（统一设置页布局，V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security；角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入；Tab URL 持久化；每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏）、`SettingsServiceProvider` / `useSettingsService`（Context 注入 `SettingsService` 接口，解耦组件对 users/messaging actions 的直接依赖）、`SettingsSectionErrorBoundary`（分区 Error Boundary，局部失败不影响整页）、`QuickLinksCard`（快捷链接卡片，i18n 键驱动）、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`（学生概览异步 Server Component + 骨架屏）、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`（教师概览异步 Server Component + 骨架屏）、`AdminSettingsView`（✅ P0-3 已修复：从 mock 改为真实数据层；✅ v3 已拆分：编排 4 个子 Card）、`SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard`（✅ v3 新增：从 AdminSettingsView 拆分的 4 个独立 Card 组件，接收 values + onChange props）、`AvatarUpload`（✅ P2-8 新增：头像上传/预览/删除客户端组件，文件验证 + i18n）、`SecurityCenterCard`（✅ P2-9 新增；✅ v3 已拆分：改为编排器，加载数据 + 渲染两个子区块）、`SecurityTwoFactorSection`（✅ v3 新增：2FA 启用/关闭/重新生成备份码，从 SecurityCenterCard 拆分）、`SecurityRecentLoginsSection`（✅ v3 新增：最近登录历史 + 远程登出，从 SecurityCenterCard 拆分）、`ThemePreferencesCard`（✅ P2-11 已增强：集成 `LocaleSwitcher` 语言切换）、`AiProviderSettingsCard`（V3 增强：新增删除按钮 + AlertDialog 确认；V3.1 增强：visibility 选择器 + 可见性/归属 Badge；✅ v3 已拆分：提取 Selector + DeleteDialog 子组件）、`AiProviderSelector`（✅ v3 新增：服务商选择器 + 密钥状态展示，从 AiProviderSettingsCard 拆分）、`AiProviderDeleteDialog`（✅ v3 新增：删除确认对话框，从 AiProviderSettingsCard 拆分）、`BrandConfigCard`（✅ audit-P2-6 新增：管理员品牌配置卡片，独立表单 + 独立保存，通过 `getBrandConfigAction`/`saveBrandConfigAction` 调用，挂载在 `AdminSettingsView` 主表单之后以避免嵌套 `<form>`）
- Config：`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`（配置驱动角色 → 设置视图映射，新增角色只需添加条目）、`ProfileOverviewType` / `resolveProfileOverviewType`（✅ v3 新增：配置驱动角色 → 概览类型映射，消除 roles.includes 硬编码，优先级 student > teacher > none）
- Lib：`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`（纯数据计算函数，与 UI 分离，便于单元测试）、`toSettingItem` / `SettingItem`（✅ v3 新增：从 actions-system-settings.ts 提取的纯函数，将表单值序列化为数据库存储格式，便于单元测试）
- Types：`AiProviderSummary`（V3.1 增强：新增 visibility/createdBy 字段）/ `AiProviderName`（V3.2 增强：新增 'ollama' 本地部署支持）/ `AiProviderVisibility`（V3.1 新增：'public' | 'private'）/ `AiProviderExisting`（V3.1 增强：新增 visibility/createdBy 字段）/ `SettingsService` / `ProfileService` / `NotificationPreferenceService`（服务接口定义，用于依赖注入解耦）

**依赖关系**：
- 依赖：`shared/*`（含 `shared/lib/bcrypt-utils`）、`@/auth`、`messaging`（页面层通过 `SettingsService` 接口注入，组件层不直接 import）、`users`（页面层通过 `SettingsService` 接口注入）、`classes` / `homework` / `dashboard`（ProfileStudentOverview 异步组件获取学生概览数据）、`notifications`（页面层获取通知偏好）
- 被依赖：无

**已知问题**：
- ⚠️ P2：混合 5 类职责（AI Provider + 密码 + 资料 + 主题 + 通知偏好）
- ✅ P1 已修复：~~无 `data-access.ts`，`actions.ts` 直接使用 `db`~~ 新建 `data-access.ts`，所有 DB 操作已下沉
- ✅ P1 已修复：~~`changePasswordAction` 使用 `requireAuth()` 无 Zod 校验~~ 改为 `requirePermission(USER_PROFILE_UPDATE)` + `ChangePasswordSchema` Zod 校验 + 并行查询优化
- ✅ audit-P2-4 已修复：~~`changePasswordAction` 未检测新密码是否在已知数据泄露中，用户可将弱密码（如 "Pass1234"）改回使用~~ 在 `validatePassword` 校验通过后、DB 查询前调用 `checkBreachedPassword(newPassword)`，命中则拒绝改密并提示用户更换密码；fail-open 策略：HIBP API 不可用时跳过检查不阻断改密流程
- ✅ audit-P2-6 已修复：~~AuthLayout 学校品牌信息（学校名/logo/标语/作者）硬编码在组件内，无法由管理员动态配置~~ 新增品牌配置完整链路：`brand-config.ts`（纯类型 + `DEFAULT_BRAND_CONFIG` 默认值，无 `server-only` 可被 Client Component 导入）+ `data-access-brand.ts`（`server-only` 数据访问，从 `system_settings` 表 `brand` 分类读取 4 个键）+ `actions-brand.ts`（`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath`）+ `BrandConfigCard`（管理员配置卡片，独立表单 + 独立保存，挂载在 `AdminSettingsView` 主表单之后以避免嵌套 `<form>`）。`SystemSettingCategory` 联合类型新增 `"brand"`。i18n 键 `settings.brand.*`（11 个键）已添加到 zh-CN + en
- ✅ P2 已修复：`actions-password.ts` 删除本地 `normalizeBcryptHash`，统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`，消除重复代码
- ✅ P2-a 已修复：~~admin/teacher/student/parent 四个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局 + `resolveRoleSettingsConfig` 配置驱动角色路由，删除 `parent-settings-view.tsx` / `student-settings-view.tsx` / `teacher-settings-view.tsx`
- ✅ parent 角色路由已修复：通过 `ROLE_SETTINGS_CONFIG` 配置驱动，parent 用户正确渲染对应配置
- ✅ Tab URL 持久化已修复：`SettingsView` 改为受控模式，通过 `useSearchParams` 读取 `tab` 参数，`router.push` 更新 URL
- ✅ 登出二次确认已修复：`SettingsView` 的 Log out 按钮使用 `AlertDialog` 包裹，点击时弹出确认对话框
- ✅ AiProviderSettingsCard 已集成：`SettingsView` 新增 AI 标签页，条件渲染需 `AI_CONFIGURE` 权限
- ✅ password-change-form 任意值 Tailwind 类已修复：~~`[&>div]:bg-red-500` 等任意值类~~ Progress 组件新增 `indicatorClassName` prop，使用标准颜色类
- ✅ P0 已修复：~~`notification-preferences-form.tsx` 跨模块直接 import messaging/actions~~ 改为通过 `useSettingsService().notifications.updatePreferences` 调用，页面层注入实现
- ✅ P0 已修复：~~`profile-settings-form.tsx` 跨模块直接 import users/actions~~ 改为通过 `useSettingsService().profile.updateProfile` 调用，页面层注入实现
- ✅ P0 已修复：~~i18n 完全缺失~~ 新增 `settings.json` 翻译文件（zh-CN + en），所有组件改用 `useTranslations` / `getTranslations`
- ✅ P1 已修复：~~缺少 Error Boundary~~ 新增 `SettingsSectionErrorBoundary`，每个 TabsContent + profile 页面角色概览区块均包裹
- ✅ P1 已修复：~~缺少 Suspense 骨架屏~~ 每个 TabsContent 包裹 `Suspense` + `SettingsSectionSkeleton`；profile 页面包裹 `ProfileStudentOverviewSkeleton` / `ProfileTeacherOverviewSkeleton`
- ✅ P1 已修复：~~profile/page.tsx 业务逻辑与 UI 混合~~ 抽取 `buildStudentOverviewData` 等纯函数到 `lib/student-overview-data.ts`；拆分 `ProfileStudentOverview` / `ProfileTeacherOverview` 异步组件
- ✅ 密码修改有速率限制
- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验
- ✅ P0-3 已修复：~~AdminSettingsView 为 mock 实现，无数据持久化~~ 新增 `system_settings` 表（键值对存储）+ `data-access-system-settings.ts` + `actions-system-settings.ts`，AdminSettingsView 改为真实数据层
- ✅ P2-8 已修复：~~无头像上传~~ 新增 `updateUserAvatar` data-access + `actions-avatar.ts` + `AvatarUpload` 组件，复用 `/api/upload` 路由
- ✅ P2-9 已修复：~~无 2FA / 会话管理~~ 新增 `actions-security.ts` + `SecurityCenterCard` 组件（2FA 开关占位 + 最近登录历史来自 login_logs 表）
- ✅ P2-10 已修复：~~通知偏好表单无测试通知按钮~~ 新增 `sendTestNotificationAction`，每个已启用渠道旁显示测试按钮
- ✅ P2-11 已修复：~~语言切换未集成到设置页~~ `ThemePreferencesCard` 集成 `LocaleSwitcher` 到 Appearance 标签页
- ✅ v1.1 已修复：~~`/settings` 页面 ErrorBoundary 触发（Functions cannot be passed directly to Client Components）~~ 新增 `actions-service.ts`（"use server" 文件），导出 `updateProfileAction` + `updateNotificationPreferencesAction` 两个 Server Action wrapper；`page.tsx` 直接传递 Server Action 引用；`SettingsService` 接口的 `getProfile`/`getPreferences` 改为可选
- ✅ v1.1 已修复：~~i18n 键双重 `settings.` 前缀（MISSING_MESSAGE）~~ `role-settings-config.tsx` 中 `descriptionKey` 去掉 `settings.` 前缀
- ✅ v1.1 已修复：~~AI 标签页 FormLabel 在 Form 上下文外使用（getFieldState null）~~ `ai-provider-settings-card.tsx` 中两处 `<FormLabel>` 改为 `<Label>`
- ✅ v1.1 已修复：~~非管理员访问 `/settings?tab=ai` 显示空白~~ `settings-view.tsx` 新增 `resolveTab()` 函数，对无权限的 tab 回退到 `general`
- ✅ v1.1 已修复：~~数据库 `notification_preferences` 表缺少 `quiet_hours_*` 字段~~ 通过 ALTER TABLE 添加缺失字段
- ✅ v1.1 已修复：~~数据库 `system_settings` 表不存在~~ 通过 CREATE TABLE 创建
- ✅ v3 已修复：~~ProfileStudentOverview / ProfileTeacherOverview 直接 import classes/homework data-access~~ 新增 `data-access-profile-overview.ts` 包装层，组件通过本模块 data-access 调用，消除跨模块直接依赖
- ✅ v3 已修复：~~profile/page.tsx 使用 `roles.includes("student")` 角色硬编码~~ 新增 `config/profile-overview-config.ts` 配置驱动映射，消除硬编码
- ✅ v3 已修复：~~i18n 命名空间不一致（profilePage vs profile.page）~~ 统一合并到 `profile.page`，删除顶层 `profilePage` 节点（zh-CN + en）
- ✅ v3 已修复：~~SecurityCenterCard 645 行超过 500 行限制~~ 拆分为 `SecurityTwoFactorSection`（2FA 流程）+ `SecurityRecentLoginsSection`（登录历史），主文件改为 ~115 行编排器
- ✅ v3 已修复：~~AiProviderSettingsCard 529 行超过 500 行限制~~ 拆分出 `AiProviderSelector`（选择器）+ `AiProviderDeleteDialog`（删除对话框），主文件降至 ~443 行
- ✅ v3 已修复：~~AdminSettingsView 444 行内联 4 个 Card~~ 拆分为 `SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard` 4 个独立组件，主文件降至 ~191 行
- ✅ v3 已修复：~~settings/page.tsx metadata 为静态 "Settings" 字符串~~ 改为 `generateMetadata` + `getTranslations("settings")`，支持 i18n
- ✅ v3 已修复：~~toSettingItem 为 actions 文件内私有函数，无法单元测试~~ 提取到 `lib/system-settings-utils.ts` 并导出，新增 `system-settings-utils.test.ts` 单元测试覆盖 string/number/boolean/json 4 种类型 + 边界情况

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 160 | AI Provider CRUD + 测试（P1 已修复，无直接 DB 操作） |
| `actions-password.ts` | 101 | 修改密码（P1 已修复：requirePermission + Zod + data-access；✅ audit-P2-4 新增 `checkBreachedPassword()` HIBP 检测） |
| `actions-avatar.ts` | 56 | 头像上传/删除（P2-8 新增：requirePermission + revalidatePath） |
| `actions-notifications.ts` | 46 | 发送测试通知（P2-10 新增：占位实现待接入真实通知服务） |
| `actions-system-settings.ts` | 186 | 管理员系统设置 CRUD（P0-3 新增：4 分类 Zod 校验 + upsert） |
| `actions-security.ts` | 165 | 2FA 状态查询/切换 + 最近登录历史（P2-9 新增） |
| `actions-service.ts` | 60 | 设置页 Server Action wrapper（v1.1 新增：updateProfileAction + updateNotificationPreferencesAction，作为 Server Action 引用传递给 Client Component，避免 Server→Client 函数传递违规） |
| `data-access.ts` | 158 | AI Provider CRUD + 密码修改 DB 操作（P1 新增） |
| `data-access-system-settings.ts` | 112 | system_settings 表 CRUD（P0-3 新增：键值对存储模式；✅ audit-P2-6 `SystemSettingCategory` 联合类型新增 `"brand"`） |
| `data-access-brand.ts` | 72 | 品牌配置数据访问（audit-P2-6 新增：`server-only`，从 `system_settings` 表 `brand` 分类并行读取 4 个键，未配置项使用 `DEFAULT_BRAND_CONFIG` 默认值；re-exports `BrandConfig` + `DEFAULT_BRAND_CONFIG` 类型） |
| `brand-config.ts` | 25 | 品牌配置类型 + 默认值（audit-P2-6 新增：纯类型无 `server-only`，可被 Server / Client Component 安全导入；`BrandConfig` 接口 + `DEFAULT_BRAND_CONFIG` 默认值） |
| `actions-brand.ts` | 70 | 品牌配置 Server Action（audit-P2-6 新增：`getBrandConfigAction` / `saveBrandConfigAction`，`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath` 刷新 `/(auth)/login` / `/(auth)/register` / `/admin/settings`） |
| `types.ts` | 60 | 类型定义（AiProviderSummary + SettingsService/ProfileService/NotificationPreferenceService 接口） |
| `config/role-settings-config.tsx` | 85 | 角色设置页配置驱动映射（ROLE_SETTINGS_CONFIG + resolveRoleSettingsConfig） |
| `lib/student-overview-data.ts` | 150 | 学生概览纯数据计算（buildStudentOverviewData + computeStudentStats 等，便于单测） |
| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局（5 标签页 + Error Boundary + Suspense + i18n + SecurityCenterCard 集成） |
| `components/settings-service-context.tsx` | 39 | SettingsServiceProvider + useSettingsService（Context 注入服务接口） |
| `components/settings-section-error-boundary.tsx` | 64 | 分区 Error Boundary（局部失败不影响整页） |
| `components/quick-links-card.tsx` | 42 | 快捷链接卡片（i18n 键驱动） |
| `components/profile-student-overview.tsx` | 91 | 学生概览异步 Server Component + 骨架屏 |
| `components/profile-teacher-overview.tsx` | 115 | 教师概览异步 Server Component + 骨架屏 |
| `components/admin-settings-view.tsx` | 176 | AdminSettingsView 系统设置视图（P0-3 已修复：真实数据层 + 4 个 Card + i18n；✅ audit-P2-6 在主表单后挂载 `BrandConfigCard`，独立表单避免嵌套 `<form>`） |
| `components/brand-config-card.tsx` | 148 | 品牌配置卡片（audit-P2-6 新增：管理员配置学校品牌信息 schoolName/logoUrl/testimonialQuote/testimonialAuthor；独立表单 + 独立保存；通过 `getBrandConfigAction`/`saveBrandConfigAction` 调用；i18n `settings.brand.*`） |
| `components/avatar-upload.tsx` | ~150 | 头像上传/预览/删除客户端组件（P2-8 新增：文件验证 + i18n） |
| `components/security-center-card.tsx` | ~240 | 安全中心卡片（P2-9 新增：2FA 开关 + 最近登录历史） |
| `components/profile-settings-form.tsx` | 158 | 个人资料表单（通过 SettingsService 注入，i18n） |
| `components/notification-preferences-form.tsx` | ~160 | 通知偏好表单（通过 SettingsService 注入，i18n + P2-10 测试按钮） |
| `components/password-change-form.tsx` | ~130 | 密码修改表单（i18n + a11y） |
| `components/theme-preferences-card.tsx` | ~80 | 主题偏好卡片（i18n + P2-11 LocaleSwitcher 集成） |
| `components/ai-provider-settings-card.tsx` | ~200 | AI 服务商配置卡片（i18n） |

---

## 2.24 auth（认证模块）

**职责**：认证页面 UI（登录/注册/布局）+ 注册 Server Action + 注册 data-access + 登录 2FA 服务编排。

> **架构变更（2026-06-25，audit-P0 重构）**：模块从"纯 UI"升级为完整三层结构。原 `app/(auth)/register/page.tsx` 内联的 167 行 `registerAction` 下沉至 `modules/auth/actions.ts`；原 `auth.ts` 跨模块依赖 `modules/settings/actions-security` 的 2FA 校验函数，改为同模块 `modules/auth/services/two-factor-service.ts` 编排（仍通过 `modules/settings/data-access-two-factor` 与 `modules/settings/lib/totp` 获取数据，属合理跨模块 data-access 调用）。

**导出函数**：
- UI 组件：`LoginForm` / `RegisterForm` / `AuthLayout`（✅ audit-P2-6 增强：接受 `brand?: BrandConfig` prop，由 `app/(auth)/layout.tsx` Server Component 调用 `getBrandConfig()` 注入；未配置时使用 `DEFAULT_BRAND_CONFIG` 默认值；渲染 schoolName / logoUrl（img 或 GraduationCap 图标）/ testimonialQuote / testimonialAuthor）
- Server Actions（`actions.ts`）：`registerAction` / `checkEmailAvailabilityAction`
- Data-access（`data-access.ts`）：`createUser` / `isEmailAvailable` / `buildRegisterInput`
- Services（`services/two-factor-service.ts`）：`verifyTwoFactorForLogin` / `preflightTwoFactorByEmail`
- Schema（`schema.ts`）：`RegisterSchema`（Zod，含 `superRefine` 未成年人 guardian 字段条件校验、`calcAge`、`isMinorByBirthDate` 纯函数）
- Types（`types.ts`）：`RegisterResult` / `RegisterInput` / `EmailAvailabilityResult` / `RegisterErrorCode`

**依赖关系**：
- 依赖：`shared/*`（含 `shared/lib/rate-limit`、`shared/lib/login-logger`、`shared/lib/bcrypt-utils`、`shared/lib/http-utils`）、`modules/settings/data-access-two-factor`、`modules/settings/lib/totp`（✅ 合理跨模块 data-access 调用）
- 被依赖：`app/(auth)/*`、`src/auth.ts`（动态 import `services/two-factor-service`）

**已知问题**：
- ✅ audit-P0-1 已修复：~~`app/(auth)/register/page.tsx` 内联 167 行 `registerAction`~~ 下沉至 `modules/auth/actions.ts`，页面降至 13 行
- ✅ audit-P0-2 已修复：~~`auth.ts` 跨模块依赖 `modules/settings/actions-security`~~ 改为同模块 `services/two-factor-service.ts` 编排
- ✅ audit-P0-5 已修复：~~`auth.ts` 3 处 `as` 断言~~ 通过 `User` 接口扩展（`next-auth.d.ts`）与 `"token" in message` 类型收窄消除
- ✅ audit-P1-1 已修复：~~`actions.ts` 的 `ERROR_MESSAGES` 硬编码中文字符串~~ 替换为 next-intl `getTranslations("auth.register")` 翻译键（`messageForError()` 按 `RegisterErrorCode` 查表 `errors.${code}`）；`register-form.tsx` 与 `login-form.tsx` 同步迁移至 `useTranslations`，含 `t.rich()` 嵌入隐私政策/用户协议链接；新增 `preflightTwoFactorAction` Server Action 消除 login-form 对 `modules/settings/actions-security` 的跨模块依赖
- ✅ audit-P1-3 已修复：~~`auth.ts` authorize 回调混合 7 类职责（107 行）~~ 抽取至 `modules/auth/services/login-service.ts` 的 `authenticateUser()` 函数（输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载 7 步），auth.ts authorize 降至 3 行薄包装（动态 import + 委托调用），可独立测试
- ✅ audit-P1-6 已修复：~~`shared/lib/rate-limit.ts` 单实例内存实现~~ 重构为 `shared/lib/rate-limit/` 目录：抽象 `RateLimiter` 接口（`limit`/`reset` 均 Promise 返回）；默认 `MemoryRateLimiter` 进程级实现；可选 `RedisRateLimiter` 基于 `@upstash/ratelimit` + `@upstash/redis`（懒加载，按 `${limit}:${windowMs}` 缓存 Ratelimit 实例）；通过 `env.RATE_LIMIT_DRIVER=memory|redis` 切换；`rateLimit()`/`resetRateLimit()` 改为 Promise 返回以支持异步后端，调用方全部更新为 `await`；`rateLimitKey`/`RATE_LIMIT_RULES`/`rateLimitHeaders` 保持同步纯函数
- ✅ audit-P1-7 已修复：~~JWT 中存储完整 `permissions[]` 数组（67 项约 1.1KB）导致 cookie 体积过大~~ 新增 `shared/lib/permission-bitmap.ts`，将 67 个权限点编码为 BigInt → base36 字符串（约 13 字符，体积减少 99%）。`auth.ts` JWT callback 写入 `token.permissionsBitmap`（不再写 `token.permissions` 数组）；`session` callback 通过 `decodePermissionsBitmap()` 还原为 `Permission[]` 填充 `session.user.permissions`，业务代码（`getAuthContext()` / `usePermission()`）无需感知位图存在；`proxy.ts` 直接调用 `hasPermissionInBitmap()` 检查单个权限，无需完整解码（性能优化：边缘运行时每请求都经过）。`PERMISSION_BITMAP_ORDER` 数组顺序固定不可变，新增权限只能追加末尾。`next-auth.d.ts` JWT 接口新增 `permissionsBitmap: string`，`permissions` 标记为 `@deprecated`
- ✅ audit-P1-9 已修复：~~auth 模块未定义任何 trackEvent 事件，登录成功/失败/登出/注册/2FA 启用/禁用/账户锁定/限流触发均无业务埋点~~ `shared/lib/track-event.ts` EventName 新增 8 个 auth 事件（`auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`）+ `trackAuthEvent` 便捷函数；`auth.ts` events.signIn/signOut 调用 `trackAuthEvent`；`login-service.ts` 在限流/账户锁定/密码错误/2FA 失败 4 个失败分支调用 `trackAuthEvent`；`modules/auth/actions.ts` `registerAction` 成功后调用 `trackAuthEvent("auth.signup")`；`modules/settings/actions-security.ts` `verifyTwoFactorAction`/`disableTwoFactorAction` 调用 `trackAuthEvent("auth.2fa_enabled/disabled")`
- ✅ audit-P1-10 已修复：~~proxy.ts `SPECIFIC_ROUTE_PERMISSIONS` 仅覆盖 3 个 `/admin/*` 子路径（ai-settings/roles/permissions），其他 `/admin/announcements`/`/admin/audit-logs`/`/admin/users` 等子路径全部走 `/admin` 前缀匹配 SCHOOL_MANAGE 权限，导致拥有 ANNOUNCEMENT_MANAGE/AUDIT_LOG_READ/USER_MANAGE 等细粒度权限的非管理员角色（如教师、年级主任）无法访问这些页面~~ 扩展 `SPECIFIC_ROUTE_PERMISSIONS` 至 15 个条目，新增 `/admin/announcements`(ANNOUNCEMENT_MANAGE)、`/admin/audit-logs` + 3 子路径(AUDIT_LOG_READ)、`/admin/elective`(ELECTIVE_MANAGE)、`/admin/questions`(QUESTION_READ)、`/admin/users`(USER_MANAGE)、`/admin/error-book`(ERROR_BOOK_ANALYTICS_READ)、`/admin/lesson-plans`(LESSON_PLAN_READ)、`/admin/course-plans`(COURSE_PLAN_READ)。精确路由优先级高于前缀匹配，使非管理员角色可直接访问对应权限的 `/admin/*` 页面
- ✅ audit-P1-12 已修复：~~proxy.ts 内联 4 个权限映射常量（SPECIFIC_ROUTE_PERMISSIONS / ROUTE_PERMISSIONS / DASHBOARD_ROUTE_PERMISSIONS / API_PERMISSIONS），新增路由或权限点时必须修改 proxy.ts 逻辑代码，违反配置驱动设计~~ 抽取 `shared/lib/route-permissions.ts` 配置文件，导出 4 个 `Record<string, Permission>` 类型常量（重命名 `ROUTE_PERMISSIONS`→`ROUTE_PREFIX_PERMISSIONS`、`API_PERMISSIONS`→`API_ROUTE_PERMISSIONS` 以更准确表达语义）。proxy.ts 从此文件 import，新增路由权限只需修改配置文件无需改 proxy.ts 逻辑；所有映射 value 类型从 `string` 收紧为 `Permission`，消除 `as Permission` 断言
- ✅ audit-P2-3 已实施：~~系统开放注册缺少预分配角色/班级的机制，管理员无法批量引导特定用户（如新教师/家长）注册~~ 新增 `invitation_codes` 表 + `modules/invitation-codes` 模块（types/schema/lib/code-generator/data-access/actions），8 字符安全字母表（剔除 I/O/0/1）+ Set 去重 + 批量插入；admin 可批量生成邀请码（1-100 张/批）并指定角色/限定邮箱/绑定班级/设置过期/备注，生成结果仅此一次返回明文供复制；注册流程集成 `invitationCode` 字段（可选），`registerAction` 调用 `validateInvitationCode()` → `consumeInvitationCode()`（乐观锁 `UPDATE ... WHERE usedBy IS NULL`），邀请码角色覆盖默认 student 角色；admin 管理页 `/admin/invitation-codes`（USER_MANAGE 权限）含统计卡片/表格/生成对话框/删除确认/复制；i18n `invitationCodes` 命名空间已注册（zh-CN + en）
- ✅ audit-P2-4 已修复：~~注册流程未检测密码是否在已知数据泄露中出现，弱密码（如 "Pass1234" 等热门泄露密码）可通过 Zod 最小长度校验~~ 新增 `shared/lib/breached-password.ts` 接入 Have I Been Pwned API（k-anonymity 范围查询：仅发送 SHA-1 前 5 字符，本地比对完整哈希后缀，不泄露用户密码）；`registerAction` 在邮箱可用性检查后、`createUser` 前调用 `checkBreachedPassword(data.password)`，命中则返回新错误码 `BREACHED_PASSWORD` + 写审计日志；i18n 键 `auth.register.errors.BREACHED_PASSWORD` 已添加（zh-CN + en）。fail-open 策略：HIBP API 不可用时（超时/网络错误）返回 `checkSkipped=true` 跳过检查，不阻断注册流程
- ✅ audit-P2-6 已修复：~~`AuthLayout` 学校品牌信息（学校名/logo/标语/作者）硬编码在组件内，无法由管理员动态配置~~ `AuthLayout` 接受 `brand?: BrandConfig` prop；`app/(auth)/layout.tsx` 改为 async Server Component，调用 `modules/settings/data-access-brand.getBrandConfig()` 读取品牌配置并注入；数据库不可用时 try/catch fail-open 使用 `DEFAULT_BRAND_CONFIG` 默认值，不阻断认证页面渲染。品牌配置由 `modules/settings` 提供完整 CRUD 链路（详见 settings 模块）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 161 | 注册 Server Action（`registerAction` + `checkEmailAvailabilityAction` + `preflightTwoFactorAction`，Zod 校验 + 独立 rate limit key `register:{ip}` + 审计日志 + 结构化错误码 `REGISTER_ERROR_CODES` + `messageForError()` i18n 翻译；✅ audit-P1-1 已迁移 next-intl；✅ audit-P1-9 注册成功埋点 `trackAuthEvent("auth.signup")`；✅ audit-P2-4 邮箱可用性检查后调用 `checkBreachedPassword()` HIBP k-anonymity 检测） |
| `data-access.ts` | 90 | 注册数据访问（`createUser` bcrypt cost=12 不自动创建角色；`isEmailAvailable`；`buildRegisterInput` 从 FormData 构造 RegisterInput） |
| `schema.ts` | 95 | Zod `RegisterSchema`（含 `superRefine` 未成年人 guardian 字段条件校验、`calcAge`、`isMinorByBirthDate` 纯函数） |
| `types.ts` | 35 | 类型定义（`RegisterResult` / `RegisterInput` / `EmailAvailabilityResult` / `RegisterErrorCode`，纯类型无 `server-only`） |
| `services/two-factor-service.ts` | 75 | 登录 2FA 编排（`verifyTwoFactorForLogin` + `preflightTwoFactorByEmail`，依赖 `modules/settings/data-access-two-factor` + `modules/settings/lib/totp`，✅ audit-P0-2 新增） |
| `services/login-service.ts` | 177 | audit-P1-3 新增：登录认证服务（`authenticateUser` 函数，从 auth.ts authorize 回调提取 7 类职责：输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载；导出 `AuthenticateUserInput` / `AuthenticatedUser` 类型；audit-P1-6 后 `rateLimit()`/`resetRateLimit()` 调用改为 `await`） |
| `components/auth-layout.tsx` | 46 | 认证页面布局（audit-P2-6 增强：接受 `brand?: BrandConfig` prop，渲染 schoolName / logoUrl（img 或 GraduationCap 图标 fallback）/ testimonialQuote / testimonialAuthor；未配置时使用 `DEFAULT_BRAND_CONFIG`） |
| `components/login-form.tsx` | - | 登录表单 |
| `components/register-form.tsx` | - | 注册表单（`registerAction` 通过 prop 注入，含 `agreedTerms`/`agreedGuardian` hidden input 确保 Zod 收到 boolean） |

---

## 2.24b onboarding（首次登录引导模块）

**职责**：首次登录引导流程（独立路由 `/onboarding` + Server Action + proxy 重定向，v3 对标 PowerSchool/Veracross/Auth0）。

> **架构变更（2026-06-25，audit-P1-2 拆分）**：原 `onboarding-stepper.tsx` 单文件 451 行（接近 500 上限）拆分为三层结构：
> - 状态与提交逻辑 → `hooks/use-onboarding-form.ts`（208 行，含所有 React state / 导航 / `completeOnboardingAction` 调用）
> - 家长子女子表单 → `components/parent-children-form.tsx`（122 行，纯受控组件，props: `childRows`/`isSubmitting`/`onUpdate`/`onAdd`/`onRemove`）
> - 主文件 `onboarding-stepper.tsx` 降至 259 行，成为纯展示编排器
> - `ChildRow` / `EMPTY_CHILD_ROW` 类型与常量从组件内提取至 `types.ts`，便于跨文件复用

**导出函数**：
- UI 组件：`OnboardingStepper` / `ParentChildrenForm`
- Hook：`useOnboardingForm` / `UseOnboardingFormReturn`（类型）
- Server Actions（`actions.ts`）：`getOnboardingStatusAction` / `completeOnboardingAction`
- Data-access（`data-access.ts`）：`getOnboardingStatus` / `updateUserProfile` / `bindParentToChild`
- Schema（`schema.ts`）：`OnboardingSchema`（Zod，v3 重构为 children 数组）
- Types（`types.ts`）：`OnboardingRoleInfo` / `OnboardingStatus` / `OnboardingCompleteData` / `OnboardingFailureItem` / `BindParentToChildParams` / `ChildRow` / `EMPTY_CHILD_ROW`

**依赖关系**：
- 依赖：`shared/*`（含 `shared/lib/auth-guard`）、`modules/classes`（班级码绑定 data-access）、`modules/users`（用户表读写）
- 被依赖：`app/onboarding/page.tsx`、`proxy.ts`（读 `token.onboarded`）

**已知问题**：
- ✅ audit-P1-2 已修复：~~`onboarding-stepper.tsx` 451 行接近 500 上限~~ 拆分为 Hook + 子组件 + 编排器三层，主文件降至 259 行
- ✅ audit-P1-8 已修复：~~`bindParentToChild` 未做独立速率限制，单次 Action 内可循环调用 10 次，三因子（生日 × 手机后4）枚举攻击窗口~~ 在 `completeOnboardingAction` 中针对家长角色且 `children.length > 0` 时添加独立速率限制 key `onboarding:bind:{parentId}`，限制每小时 5 次（5 × 10 = 50 次尝试/小时，远低于 3.65M 组合枚举所需量级）；触发限流时写审计日志 `onboarding.bind_rate_limited` 并返回中文提示（i18n 待后续迁移）
- ✅ audit-P1-13 已修复：~~步骤列表与角色过滤逻辑硬编码在 `use-onboarding-form.ts`（`STEPS_KEYS` 常量 + `isAdmin ? filter : identity` 三元表达式），新增/调整步骤必须修改 Hook 逻辑~~ 抽取配置驱动设计：在 `types.ts` 新增 `OnboardingStepId` / `OnboardingStepConfig` 类型 + `ONBOARDING_STEPS` 配置数组 + `getOnboardingStepsForRole(role)` 纯函数过滤步骤；`use-onboarding-form.ts` 消费配置派生 `stepConfigs`/`stepKeys`/`currentStepId`/`canSkip`，不再内联步骤定义；`onboarding-stepper.tsx` 按 `currentStepId` 渲染内容（替代数字下标 `step === N`），跳过按钮可见性由配置 `canSkip` 驱动。**附带修复**：原 `grade_head`/`teaching_head` 在 roleInfo 步骤无内容却无法跳过（`canSkipStep2 = isAdmin || isStudent || isTeacher` 未包含他们），现配置 `applicableRoles: ["teacher","student","parent"]` 使他们直接跳过该步骤
- ✅ P2-4 已修复：~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已删除，迁移至 `modules/onboarding/`

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | - | Server Actions（`getOnboardingStatusAction` + `completeOnboardingAction`，含幂等检查/局部错误收集/教师多科目循环绑定/审计日志/audit-P1-8 家长绑定子女独立速率限制 `onboarding:bind:{userId}` 5次/小时） |
| `data-access.ts` | - | 数据访问（`getOnboardingStatus` + `updateUserProfile` + `bindParentToChild` v3 三因子验证） |
| `schema.ts` | - | Zod `OnboardingSchema`（含 children 数组，支持多子女） |
| `types.ts` | - | 类型定义（含 `ChildRow` / `EMPTY_CHILD_ROW`，audit-P1-2 新增） |
| `hooks/use-onboarding-form.ts` | 208 | audit-P1-2 新增：表单状态与处理逻辑 Hook（step/name/phone/children 状态 + 导航 + URL query 持久化 + completeOnboardingAction 调用） |
| `components/onboarding-stepper.tsx` | 259 | 客户端 stepper 编排器（4 步，audit-P1-2 重构后纯展示） |
| `components/parent-children-form.tsx` | 122 | audit-P1-2 新增：家长子女绑定子表单（纯受控组件） |

---

## 2.25 layout（布局模块）

**职责**：应用骨架（侧边栏 + 顶部导航 + 导航配置 + 多角色切换）。

**导出函数**：`AppSidebar` / `SidebarProvider` / `SiteHeader` + `navigation` 配置

**依赖关系**：
- 依赖：`shared/hooks/use-permission`、`@/auth`（useSession）、`messaging`（通知下拉）、`shared/components/ui/select`（角色切换下拉）
- 被依赖：`app/(dashboard)/layout.tsx`

**已知问题**：
- ✅ P2 已修复：~~用权限反推角色~~ `app-sidebar.tsx` 改用 `hasRole()` 判断角色，并新增多角色切换机制（`SidebarContext.currentRole`/`setCurrentRole`，null 表示自动检测；当用户拥有多个角色时在侧边栏底部显示 `Select` 下拉切换）
- ✅ navigation.ts 无幽灵路由（13 个已修复）

**文件清单**：
| 文件 | 职责 |
|------|------|
| `components/app-sidebar.tsx` | 侧边栏（根据权限渲染 + 多角色切换下拉） |
| `components/sidebar-provider.tsx` | 侧边栏状态 Context（含 `currentRole`/`setCurrentRole`） |
| `components/site-header.tsx` | 顶部导航（含通知下拉） |
| `config/navigation.ts` | 导航配置（4 个角色） |

---

## 2.26 student（学生 UI 模块）

**职责**：学生端 UI 组件（课程视图 + 课表筛选/视图）。

**导出函数**：`StudentCoursesView` / `StudentScheduleFilters` / `StudentScheduleView`

**依赖关系**：
- 依赖：`shared/*`
- 被依赖：`app/(dashboard)/student/*`

**已知问题**：
- ⚠️ P2：与 classes 模块的 `schedule-view.tsx`/`schedule-filters.tsx` 可能功能重叠
- ✅ 纯 UI 模块，数据由页面通过 classes data-access 获取
- ✅ 认证模式已统一：所有 student 页面使用 `getCurrentStudentUser()`（users 模块）或 `getAuthContext()`（shared 模块），不再直接调用 `auth()` 或 `getDemoStudentUser()`

**文件清单**：
| 文件 | 职责 |
|------|------|
| `components/student-courses-view.tsx` | 学生课程视图（含 `ClassCard` memo 组件 + 加入班级表单，使用 `useTransition`） |
| `components/student-schedule-filters.tsx` | 课表筛选器 |
| `components/student-schedule-view.tsx` | 学生课表视图 |

**路由文件清单**（`app/(dashboard)/student/`）：
| 文件 | 职责 |
|------|------|
| `dashboard/page.tsx` + `loading.tsx` | 学生仪表盘 + 骨架屏 |
| `attendance/page.tsx` + `loading.tsx` | 学生考勤 + 骨架屏 |
| `diagnostic/page.tsx` + `loading.tsx` | 学情诊断 + 骨架屏 |
| `elective/page.tsx` + `loading.tsx` | 选课中心 + 骨架屏 |
| `grades/page.tsx` + `loading.tsx` | 我的成绩 + 骨架屏 |
| `learning/page.tsx` | 学习中心入口（课程/作业/教材 3 个卡片；✅ 2026-06-26 审计修复：权限点改为 CLASS_READ） |
| `learning/assignments/page.tsx` + `loading.tsx` | 作业列表（含 `AssignmentCard` 组件）+ 骨架屏 |
| `learning/assignments/[assignmentId]/page.tsx` + `loading.tsx` | 作业作答/复习 + 骨架屏 |
| `learning/courses/page.tsx` + `loading.tsx` | 课程列表 + 骨架屏（✅ 2026-06-26 审计修复：权限点改为 CLASS_READ） |
| `learning/courses/[classId]/page.tsx` | 课程详情（✅ 2026-06-26 审计修复新增路由：权限点 CLASS_READ） |
| `learning/textbooks/page.tsx` + `loading.tsx` | 教材列表 + 骨架屏 |
| `learning/textbooks/[id]/page.tsx` + `loading.tsx` | 教材阅读 + 骨架屏 |
| `schedule/page.tsx` + `loading.tsx` | 课表 + 骨架屏（✅ 2026-06-26 审计修复：权限点改为 CLASS_READ） |
| `error.tsx` | 路由组错误边界（提供"重试"按钮） |
| `learning/error.tsx` + `loading.tsx` | ✅ 2026-06-26 审计修复新增：学习中心错误边界（shared `ErrorState`）+ 加载骨架 |
| `learning/courses/error.tsx` | ✅ 2026-06-26 审计修复新增：课程列表错误边界（shared `ErrorState`） |
| `learning/courses/[classId]/error.tsx` | ✅ 2026-06-26 审计修复新增：课程详情错误边界（shared `ErrorState`） |
| `schedule/error.tsx` | ✅ 2026-06-26 审计修复新增：学生课表错误边界（shared `ErrorState`） |

---

## 2.27 lesson-preparation（备课模块）

**职责**：教师备课，基于教材章节创建课案（**节点图编辑器 React Flow**），支持模板、版本管理、知识点标注、题目创建/拉取、作业发布。

> 架构变更（2026-06-21）：编辑器从列表式（BlockRenderer + @dnd-kit）升级为节点图式（NodeEditor + @xyflow/react）。数据结构从 v1（blocks 数组）升级到 v2（nodes + edges 节点图），旧数据通过 `migrateV1ToV2()` 自动迁移。

> 架构变更（2026-06-23）：数据结构从 v2 升级到 v3（课文锚点画布格式）。正文节点（textbook_content）作为画布中央核心，教学节点围绕正文组织。新增锚点系统（range/point 两种锚定方式）、占位符注入算法、11 种定制节点类型。旧数据通过 `migrateV2ToV3()` 自动迁移。

**数据结构**：
- v1（已废弃，仅向后兼容读取）：`{ version: 1, blocks: Block[] }`
- v2（已废弃，仅向后兼容读取）：`{ version: 2, nodes: LessonPlanNode[]; edges: LessonPlanEdge[] }`
- v3（当前）：`{ version: 3, textbookContentNodeId: string; nodes: AnyLessonPlanNode[]; edges: AnyLessonPlanEdge[]; anchors: NodeAnchor[] }`
  - `TextbookContentNode`：正文节点（`draggable: false`，画布中央，可缩放）
  - `LessonPlanNode`：教学节点（`Block` + `position`，可拖动可连线）
  - `NodeAnchor`：锚点（`{ id, nodeId, type: "range"|"point", start, end?, textPreview?, invalid? }`）
  - `AnchorEdge`：锚点连线（教学节点 → 正文节点，默认 10% 透明度）
  - `FlowEdge`：流程连线（教学节点 → 教学节点）

**导出函数**：
- Data-access（`data-access.ts`）：`getLessonPlans`（V4：查询后按 textbookId+chapterId+creatorId 聚合版本，返回代表项 + versionCount + versions 摘要数组）/ `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`（v3 规范化，兼容 v1/v2 旧数据）/ `buildDefaultSkeleton`（v3 默认 10 节点骨架）/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` / `publishLessonPlan`（V3 新增，设置 status=published）/ `unpublishLessonPlan`（V3 新增，设置 status=draft，仅 published 课案）
- Lib（`lib/document-migration.ts`）：`defaultDataForType` / `migrateV1ToV2` / `migrateV2ToV3` / `normalizeDocument` / `buildInitialContent` / `buildDefaultSkeleton` / `isTextbookContentNode` / `isAnchorEdge` / `getAnchorsForNode` / `getActiveAnchorIds` / `getAnchorEdges`
- Lib（`lib/anchor-injector.ts`）：`markdownToPlainText` / `injectPlaceholders` / `parseAnchoredText` / `toCircledNumber` / `getNextPointIndex` / `relocateAnchors` / `getAnchorColor`
- Lib（`lib/node-summary.ts`）：`getNodeSummary` / `getTextbookContentSummary` / `getNodeColor` / `NODE_COLORS`
- Lib（`lib/rf-mappers.ts`）：`toRfNodes`（支持 textbook_content 节点；V3：ctx 新增 `anchorableNodes` 和 `onCreateNewNode` 字段）/ `toRfEdges`（区分 anchor/flow 边透明度；V3：锚点边颜色使用 `getNodeColor(anchor.nodeId)` 替代硬编码，anchorId 存入 edge.data）/ `fromRfEdges`（V3：从 `e.data.anchorId` 读取 anchorId，回退到 className 判断）
- Data-access-versions（`data-access-versions.ts`）：`getLessonPlanVersions` / `createLessonPlanVersion` / `getVersionContent` / `revertToVersion` / `pruneAutoVersions`
- Data-access-templates（`data-access-templates.ts`）：`getLessonPlanTemplates` / `saveAsTemplate` / `deletePersonalTemplate`
- Data-access-knowledge（`data-access-knowledge.ts`）：`getLessonPlansByKnowledgePoint`（V3：增加 userId 参数，按 creatorId 过滤 + 排除 archived）/ `getLessonPlansByQuestion`（V3：增加 userId 参数，按 creatorId 过滤 + 排除 archived）
- Publish-service（`publish-service.ts`）：`publishLessonPlanHomework`
- AI-suggest（`ai-suggest.ts`）：`suggestKnowledgePoints`
- Actions：`getLessonPlansAction` / `getLessonPlanByIdAction` / `createLessonPlanAction` / `updateLessonPlanAction` / `saveLessonPlanVersionAction` / `getLessonPlanVersionsAction` / `revertLessonPlanVersionAction` / `deleteLessonPlanAction` / `duplicateLessonPlanAction` / `getLessonPlanTemplatesAction` / `saveAsTemplateAction` / `deleteTemplateAction` / `suggestKnowledgePointsAction` / `publishLessonPlanHomeworkAction` / `getKnowledgePointOptionsAction` / `getTextbooksForPickerAction` / `getChaptersForPickerAction` / `publishLessonPlanAction`（V3 新增，requirePermission(LESSON_PLAN_PUBLISH)）/ `unpublishLessonPlanAction`（V3 新增，requirePermission(LESSON_PLAN_PUBLISH)）

**依赖关系**：
- 依赖：`shared/*`、`@/auth`、`shared/lib/ai`、`@xyflow/react`（节点图编辑器）、`textbooks`（只读章节/知识点树）、`questions`（创建/查询题目）、`exams`（创建 exam 草稿）、`homework`（创建作业下发）、`classes`（查询教师班级）、`files`（附件）
- 被依赖：无

**已知问题**：
- ✅ 通过对方 data-access 调用跨模块数据，无直查跨模块表
- ✅ data-access 按职责拆分为 4 个文件（data-access/data-access-versions/data-access-templates/data-access-knowledge）
- ✅ actions 按职责拆分为 4 个文件（actions/actions-publish/actions-ai/actions-kp）
- ✅ 编辑器架构升级：NodeEditor（React Flow 画布）+ NodeEditPanel（侧边内容编辑面板）+ LessonNode（自定义节点组件），支持节点拖拽、连线、画布缩放
- ✅ V4：`block-renderer.tsx`（@deprecated）已删除，无残留引用

> 架构变更（2026-06-22，本次审计修复）：
> - **P0-1 跨模块直查修复**：`publish-service.ts` 不再直接 `db.insert(examQuestions)` 和本地实现 `getStudentIdsByClassIds`，改为调用 `exams/data-access.addExamQuestions` 和 `classes/data-access.getStudentIdsByClassIds`，恢复三层架构约束
> - **P0-2 i18n 接入**：新增 `shared/i18n/messages/zh-CN/lesson-preparation.json` 和 `shared/i18n/messages/en/lesson-preparation.json`，注册 `lessonPreparation` 命名空间到 `src/i18n/request.ts`，17 个组件改造为 `useTranslations`/`getTranslations`
> - **P0-3 DataScope 过滤修复**：`data-access.ts` 的 `buildScopeCondition` 按 scope 类型精确过滤——`class_taught` 增加 `subjectId IN teacher.subjects AND gradeId IN teacher.grades`，`grade_managed` 限制 `gradeId IN managedGrades`，`class_members`/`children` 仅允许查看 `published` 课案
> - **P1-1 类型安全修复**：`as never` 断言全部替换为类型守卫函数（`isQuestionType`/`isV1Document`/`isV2Document`）+ `validTypes` 数组；`block-renderer.tsx` 使用 `as ExerciseBlockData`/`as TextStudyBlockData` 精确断言；`constants.ts` 的 `BLOCK_TYPE_LABELS`/`LESSON_PLAN_STATUS_LABELS` 改为 i18n 键（`BLOCK_TYPE_KEYS`/`LESSON_PLAN_STATUS_KEYS`）
> - **P1 纯函数抽取**：新增 `lib/document-migration.ts`（migrateV1ToV2/normalizeDocument/buildInitialContent，使用类型守卫替代 as 断言）、`lib/node-summary.ts`（getNodeSummary + NODE_COLORS + getNodeColor，接受翻译函数注入）、`lib/rf-mappers.ts`（toRfNodes/toRfEdges/fromRfEdges），data-access.ts 改为从 lib/ 导入并 re-export 保持向后兼容
> - **P1-2/P1-3 错误边界 + 骨架屏**：新增 `components/lesson-plan-error-boundary.tsx`（LessonPlanErrorBoundary 类组件）和 `components/lesson-plan-skeleton.tsx`（VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton）
> - **P1-4 阻塞式 UI 修复**：`alert()` 全部替换为 `sonner` toast；`confirm()` 全部替换为 `AlertDialog`（shadcn）；`window.location.reload` 替换为 `router.refresh()`；涉及 lesson-plan-card/version-history-drawer/inline-question-editor/text-study-block/exercise-block
> - **P1-5/P1-7 多实例 + 角色配置驱动**：新增 `providers/lesson-plan-provider.tsx`（LessonPlanProvider + Context + 4 个角色配置 TEACHER/ADMIN/STUDENT/PARENT + LessonPlanDataService 接口 + LessonPlanTracker 埋点接口 + useLessonPlanContextSafe/useRoleConfig 等 hooks）和 `services/default-data-service.ts`（包装 Server Actions 为 DataService 实现），lesson-plan-list/lesson-plan-card 通过 Context 注入数据服务，useRoleConfig 控制按钮可见性
> - **P1-8 Block 注册表**：新增 `config/block-registry.tsx`（BLOCK_REGISTRY 配置表 + getBlockComponent/isRichTextBlock），`node-edit-panel.tsx` 重构为配置驱动渲染，移除 if/else 链
> - **P1-4 window.location.reload 修复**：`exercise-block.tsx` 改用 `router.refresh()` 精确刷新缓存
> - **P2-1 a11y 修复**：5 个组件（question-bank-picker/publish-homework-dialog/knowledge-point-picker/exercise-block/text-study-block）添加 `role="dialog"`/`aria-modal`/`aria-label`；inline-question-editor 添加 `role="dialog"`/`aria-modal`/`aria-label`
> - **P2-4 监控埋点预留**：`providers/lesson-plan-provider.tsx` 定义 `LessonPlanTracker` 接口 + `noopTracker` 默认空实现，生产环境可替换为真实埋点

> 架构变更（2026-06-23，本次审计修复）：
> - **Zod schema 补全**：`schema.ts` 新增 `publishLessonPlanHomeworkSchema`（planId/blockId 必填，classIds 至少 1 个，availableAt/dueAt 日期格式校验），导出类型 `PublishLessonPlanHomeworkInput`；3 个 action 文件（actions/actions-publish/actions-ai）补全 Zod 校验
> - **data-access-versions.ts 事务修复**：`revertToVersion` 包裹 `db.transaction` 确保原子性（回滚版本时同步更新 lessonPlans.content 与版本记录）
> - **统一错误处理**：所有 Server Action catch 块改用 `handleActionError`；`JSON.parse` 改用 `safeJsonParse`
> - **block-renderer 拖拽 BUG 修复**：修复拖拽时节点位置计算错误

> 架构变更（2026-06-23，V3 多角色课案查看）：
> - **V3-1 课案发布/撤回**：新增 `publishLessonPlan(planId, userId)` / `unpublishLessonPlan(planId, userId)` data-access 函数 + `publishLessonPlanAction(planId)` / `unpublishLessonPlanAction(planId)` Server Actions（均 requirePermission(LESSON_PLAN_PUBLISH)），支持课案 status=published 供学生/家长/教研组长只读查看
> - **V3-2 只读画布组件**：新增 `LessonPlanReadonlyView`，复用 React Flow（nodesDraggable=false, nodesConnectable=false），供学生/家长/管理员/教研组长查看已发布课案
> - **V3-3 多角色视图**：`LessonPlanCard` 新增 viewMode prop（teacher/student/parent/admin/gradeHead），动态跳转链接 + 发布/撤回按钮；`LessonPlanEditor` 新增 initialStatus prop + 发布/撤回按钮（AlertDialog 确认）；`LessonPlanList` 新增 viewMode prop
> - **V3-4 锚点选择器重写**：`TextbookContentNode` 新增 props: `anchorableNodes`, `onCreateNewNode`；AnchorNodeSelector 重写为节点列表+创建新节点选项；`NodeEditPanel` 选中 textbook_content 时显示操作提示 + 锚点列表（含删除功能）
> - **V3-5 模板分区显示**：`TemplatePicker` 加载并显示个人模板（调用 getLessonPlanTemplatesAction），分区显示系统/个人模板
> - **V3-6 rf-mappers 增强**：`toRfNodes` ctx 新增 `anchorableNodes` 和 `onCreateNewNode` 字段；`toRfEdges` 锚点边颜色使用 `getNodeColor(anchor.nodeId)` 替代硬编码，anchorId 存入 edge.data；`fromRfEdges` 从 `e.data.anchorId` 读取 anchorId，回退到 className 判断
> - **V3-7 权限扩展**：student/parent/grade_head/teaching_head 角色新增 `LESSON_PLAN_READ` 权限，可查看已发布课案
> - **V3-8 DataScope 扩展**：`class_members` 和 `children` 新增可选 `gradeIds?: string[]` 字段；auth-guard `resolveDataScope` 中 student 通过 `classEnrollments.innerJoin(classes)` 预解析 gradeIds，parent 通过孩子的 classEnrollments.innerJoin(classes) 预解析 gradeIds
> - **V3-9 新增路由**：`/student/lesson-plans`、`/student/lesson-plans/[planId]/view`、`/parent/lesson-plans`、`/parent/lesson-plans/[planId]/view`、`/admin/lesson-plans`、`/admin/lesson-plans/[planId]/view`
> - **V3-10 导航变更**：admin 导航新增「课案管理」（/admin/lesson-plans）；student 导航新增「我的课案」（/student/lesson-plans）；parent 导航新增「孩子课案」（/parent/lesson-plans）

> 架构变更（2026-06-24，V3 深度审计修复）：
> - **P0-1 admin 直查 DB 修复**：`admin/lesson-plans/page.tsx` 移除 `import { db }` 和 `import { lessonPlans }`，改用 data-access 新增的 `getLessonPlanStats()` 函数（返回 total/published/draft/archived 统计）
> - **P0-2 publish-service 硬编码中文 + as 断言修复**：`publish-service.ts` 移除 `as ExerciseBlockData`/`as typeof validTypes[number]` 断言，改用 `lib/type-guards.ts` 的 `isExerciseBlockData`/`isValidQuestionType` 类型守卫；移除硬编码中文标题/描述，改为接受 `homeworkTitle`/`homeworkDescription` 参数由 actions 层 i18n 翻译后传入
> - **P0-3 duplicateLessonPlan 硬编码修复**：`duplicateLessonPlan` 接受 `duplicateSuffix` 参数（默认 " - Copy"），由 actions 层传入 `t("error.duplicateSuffix")`
> - **P0-4 schema Zod 错误消息 i18n 化**：`schema.ts` 所有错误消息从硬编码中文改为 i18n 键（如 `error.titleRequired`）；新增 `lib/i18n-errors.ts`（`translateFieldErrors`/`safeParseWithI18n`）在 actions 层翻译 Zod 错误
> - **P0-5 loading/error 边界补全**：为 6 个路由新增 12 个 `loading.tsx` 和 `error.tsx` 文件（teacher/lesson-plans、teacher/lesson-plans/new、teacher/lesson-plans/[planId]/edit、admin/lesson-plans、admin/lesson-plans/[planId]/view、student/lesson-plans、student/lesson-plans/[planId]/view、parent/lesson-plans、parent/lesson-plans/[planId]/view）
> - **P1-2 组件完全通过 service 调用**：`lesson-plan-card.tsx`/`lesson-plan-list.tsx`/`lesson-plan-editor.tsx` 移除所有直接 `import { xxxAction } from "../actions"`，改为通过 `useLessonPlanContextSafe()` 获取 service 调用；`LessonPlanDataService` 接口扩展 5 个方法（getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan），`default-data-service.ts` 实现扩展
> - **P1-3 as 断言修复**：新增 `lib/type-guards.ts` 集中类型守卫（11 种 BlockData 类型守卫 + 节点/题目类型守卫）；`block-registry.tsx` 所有 `as XxxBlockData` 替换为类型守卫；`node-edit-panel.tsx` 移除冗余 `as LessonPlanNode`（TypeScript 判别联合自动收窄）；`node-editor.tsx` MiniMap `nodeColor` 使用类型守卫替代 `as { node?: AnyLessonPlanNode }`；`rf-mappers.ts` 移除冗余 `as TextbookContentNode`/`as LessonPlanNode`；`use-lesson-plan-editor.ts` `updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更
> - **P1-4 MiniMap 颜色硬编码修复**：`lesson-plan-readonly-view.tsx` MiniMap 移除硬编码 `#455a64`/`#1976d2`，改用 `getNodeColor(nodeData.type)` 与编辑器保持一致
> - **i18n 错误码扩展**：`zh-CN/lesson-preparation.json` 和 `en/lesson-preparation.json` 新增 `publish.homeworkTitle`/`publish.homeworkDescription`/`error.invalidQuestionType`/`error.duplicateSuffix`/`error.titleRequired`/`error.titleTooLong`/`error.templateRequired`/`error.invalidDate`/`error.classRequired` 等键

> 架构变更（2026-06-24，V3 深度审计修复续）：
> - **P0 硬编码中文修复（6处）**：`actions.ts` 的 `getLessonPlanByIdAction`/`revertLessonPlanVersionAction` 错误消息改为 i18n 键（`t("error.notFound")`/`t("error.versionNotFound")`）；`data-access-versions.ts` 的 `revertToVersion` 接受 `revertLabel` 参数替代硬编码 `` `回退到 v${versionNo}` ``；`lesson-plan-error-boundary.tsx` 重写为包装组件模式（内部类组件接受 `errorText`/`retryText` props，外部函数组件通过 `useTranslations` 注入 i18n 文案）；`lesson-plan-provider.tsx` 的 `useLessonPlanContext` 错误消息改为英文
> - **P1 组件完全通过 service 调用（5个组件迁移）**：`version-history-drawer.tsx`/`template-picker.tsx`/`knowledge-point-picker.tsx`/`publish-homework-dialog.tsx`/`question-bank-picker.tsx` 移除所有直接 `import { xxxAction } from "../actions"`，改用 `useLessonPlanContextSafe()` 获取 service 调用；`LessonPlanDataService` 接口扩展 7 个方法（createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions）+ 6 个导出类型（TextbookPickerOption/ChapterPickerOption/KnowledgePointOption/PublishHomeworkInput/QuestionPickerParams/QuestionPickerItem）；`default-data-service.ts` 实现扩展；新增 `providers/lesson-plan-provider-setup.tsx`（页面层 Provider 设置包装组件，自动注入默认数据服务和角色配置）；3 个页面（teacher/lesson-plans/page、teacher/lesson-plans/new/page、teacher/lesson-plans/[planId]/edit/page）用 `LessonPlanProviderSetup` 包裹；`question-bank-picker.tsx` 新增 `isQuestionType` 类型守卫替代 `as QuestionType` 断言
> - **P2 文件拆分**：`textbook-content-node.tsx` 从 578 行拆分为 3 个文件——主文件（471行）+ `anchor-node-selector.tsx`（AnchorNodeSelector 组件，62行）+ `textbook-segments.tsx`（renderSegments 函数，75行）
> - **P3 as 断言修复（8处）**：`textbook-content-node.tsx` 的 `as unknown as TextbookContentNodeProps` 替换为 `isTextbookContentNodePropsData` 类型守卫；7 个 block 组件（blackboard/import/exercise/objective/key-point/homework/reflection）的 select onChange `as` 断言替换为类型守卫——`lib/type-guards.ts` 新增 7 个字段值类型守卫（isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect）

> 架构变更（2026-06-25，V3 续审计 P1/P2 修复）：
> - **P1-1a node-summary.ts as 断言修复**：移除 `node.data as { html?: string; ... }` 断言，改用 `isRecord`/`getArrayLength`/`getString`/`getNumber` 安全字段提取辅助函数从 `unknown` 收窄
> - **P1-1b ai-suggest.ts as 断言修复**：移除 `as Record<string, unknown>` 断言，改用 `isRecord` 类型守卫；`extractNodeText` 从 `unknown` 节点安全提取 `html`/`sourceText` 字段
> - **P1-1c data-access-templates.ts + data-access.ts as 断言修复**：新增 `normalizeTemplateBlocks` 规范化函数（`lib/type-guards.ts`），替代 `as LessonPlanTemplate["blocks"]` 和 `as unknown as LessonPlanTemplate["blocks"]` 断言从 DB `unknown` 安全转换
> - **P1-1d inline-question-editor.tsx as 断言修复**：移除 `as typeof QUESTION_TYPES[number]` 断言，改用 `isInlineQuestionType` 类型守卫
> - **P1-1e data-access-knowledge.ts as 断言修复**：移除两处 `as unknown as` 断言，改用 `getKnowledgePointIds`/`getQuestionIds` 安全字段提取辅助函数
> - **P1-2/P1-3 actions-ai.ts + actions-kp.ts 隐式 any 修复**：显式类型标注 `const nodes: unknown[]` 和 `let kps: KnowledgePointOption[]` 替代隐式 any；统一使用 `handleActionError(e)` + `translateFieldErrors` 处理错误
> - **P1-6 findChapterById 提取到共享工具**：新增 `textbooks/utils.ts` 的 `findChapterById` 递归函数，消除 4 个页面文件（admin/student/parent/teacher lesson-plans view/edit page）+ `data-access.ts` 共 5 处重复本地递归实现
> - **P1-7 inline-question-editor.tsx FocusTrap**：包裹 `FocusTrap`（`shared/components/a11y/focus-trap.tsx`）实现焦点陷阱
> - **P1-8 a11y + 类型守卫修复**：补全 aria-label（inline-question-editor 选项/删除/知识点按钮）+ 新增 `optionLabel` i18n 键；`lesson-node.tsx` 使用 `isLessonNodeData` 类型守卫替代 `as { node: LessonPlanNode }` 断言 + 空状态回退；`template-picker.tsx` 通过 `ChapterPickerOption.children` 递归类型消除 `as ChapterPickerOption[]` 断言
> - **P2-2 lesson-plan-card.tsx useCallback 性能优化**：4 个异步处理函数（handleArchive/handleDuplicate/handlePublish/handleUnpublish）用 `useCallback` 包裹
> - **P2-4 lesson-plan-list.tsx 筛选加载状态**：增加 `isLoading` 状态，传递给 `LessonPlanFilters`；`lesson-plan-filters.tsx` 加载时禁用输入并设置 `aria-busy`
> - **P2-6 ai-suggest.ts AI prompt 提取为常量**：新增 `AI_SUGGEST_PROMPT_TEMPLATE` 常量，便于维护和未来国际化

> 架构变更（2026-06-25，V4 深度审计修复）：
> - **P0-1 跨模块越权修复**：`parent/lesson-plans/[planId]/view/page.tsx` 和 `student/lesson-plans/[planId]/view/page.tsx` 新增 `assertPlanInScope(planId, userId)` 调用，防止越权访问非本人/子女范围课案
> - **P0-2 权限校验补全**：9 个 `lesson-plans` 路由 `page.tsx`（teacher/admin/student/parent）新增 `requirePermission(LESSON_PLAN_READ)` 调用
> - **P0-3 锚点 a11y 修复**：`textbook-segments.tsx` 锚点 span 补全 `role="button"`/`tabIndex={0}`/`aria-label`/键盘事件处理
> - **P0-4 跨模块解耦**：新增 `default-data-service.ts` 的 `getQuestions` 方法 + `QuestionService` 接口；`question-bank-picker.tsx` 移除直接 `import` `@/modules/questions/actions`，改为通过 `useQuestionService()` 注入；`lesson-plan-provider-setup.tsx` 自动注入默认 QuestionService
> - **P1-1 FocusTrap + ESC**：4 个 dialog/drawer（publish-homework-dialog/knowledge-point-picker/version-history-drawer/question-bank-picker）包裹 `FocusTrap`（`shared/components/a11y/focus-trap.tsx`）+ 添加 ESC 键监听关闭
> - **P1-2 骨架屏替换加载文本**：3 个 picker/drawer 的 `{loading ? <p>{t("loading")}</p>}` 替换为 `<VersionListSkeleton/>`/`<QuestionBankSkeleton/>`/`<KnowledgePointSkeleton/>`
> - **P1-3 ErrorBoundary 独立包裹**：9 处子组件渲染（NodeEditor/VersionHistoryDrawer/QuestionBankPicker/InlineQuestionEditor/PublishHomeworkDialog/KnowledgePointPicker）独立用 `<LessonPlanErrorBoundary>` 包裹，避免单个区块错误导致整个编辑器崩溃
> - **P2-1 并行查询**：3 个 view 页（teacher/admin/student/parent `lesson-plans/[planId]/view`）的 `getTextbookById` + `getChaptersByTextbookId` 改为 `Promise.all` 并行
> - **P2-2 notFound() 统一**：3 个 view 页 `if (!plan) return <div>...</div>` 改为 `notFound()`，与 Next.js 约定一致
> - **P2-3 async IIFE 替代 Promise 链**：3 个组件（version-history-drawer/knowledge-point-picker/question-bank-picker）的 `Promise.resolve().then()`/`queueMicrotask` 改为 `useEffect` + async IIFE + `cancelled` flag，避免竞态
> - **P2-4 废弃文件删除**：`components/block-renderer.tsx`（@deprecated，无源码引用）删除
> - **P2-5 anchor-injector regex 一致性**：`markdownToPlainText` 的 `/^>\s+/gm` 改为 `/^\s*>\s+/gm`；`skipPatterns` 5 个 regex 统一添加 `g` 标志，与 `markdownToPlainText` 保持一致
> - **P2-6 Zustand store 拆 slice**：`hooks/use-lesson-plan-editor.ts` 从 303 行单体 store 拆分为 3 个独立 slice——`hooks/editor-slice.ts`（文档结构 + 文档操作）/`hooks/selection-slice.ts`（选中状态）/`hooks/version-slice.ts`（版本与保存状态）；主文件重写为 25 行薄层，组合 3 个 slice 并导出 `EditorState` 类型
> - **type-guards 导出补全**：`lib/type-guards.ts` 的 `VALID_QUESTION_TYPES` 常量从模块私有改为 `export`（供 `inline-question-editor.tsx` 复用，消除重复定义）

**文件清单**：
| 文件 | 职责 |
|------|------|
| `types.ts` | 类型定义（含 v1/v2/v3 文档类型、TextbookContentNode、LessonPlanNode、NodeAnchor、AnchorEdge、FlowEdge、11 种 BlockData 接口） |
| `constants.ts` | 常量定义 |
| `schema.ts` | Zod 验证（V3：错误消息改为 i18n 键，如 `error.titleRequired`） |
| `lib/type-guards.ts` | **集中类型守卫（V3 新增）**：11 种 BlockData 类型守卫（isRichTextBlockData/isTextStudyBlockData/isExerciseBlockData/isObjectiveBlockData/isKeyPointBlockData/isImportBlockData/isNewTeachingBlockData/isSummaryBlockData/isHomeworkBlockData/isBlackboardBlockData/isReflectionBlockData）+ 节点类型守卫（isTextbookContentNode/isLessonPlanNode）+ 题目类型守卫（isValidQuestionType + **V4 导出 `VALID_QUESTION_TYPES` 常量 + `ValidQuestionType` 类型**）+ 基础类型守卫（isLessonPlanStatus/isTemplateType/isTemplateScope/isBlockType）+ **Block 字段值类型守卫（V3 续新增）**：isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect（用于 select onChange 替代 `as` 断言）+ **normalizeTemplateBlocks 规范化函数（V3 续审计新增）**：从 DB `unknown` 安全转换为 `TemplateBlockSkeleton[]`，替代 `as LessonPlanTemplate["blocks"]` 断言 |
| `lib/i18n-errors.ts` | **Zod 错误 i18n 翻译辅助（V3 新增）**：`translateFieldErrors`（将 Zod fieldErrors 中的 i18n 键翻译为实际消息）/ `safeParseWithI18n`（安全解析 Zod 结果并返回带翻译的 ActionState 错误格式） |
| `lib/document-migration.ts` | **纯函数**：v1→v2（migrateV1ToV2）/ v2→v3（migrateV2ToV3）/ 规范化（normalizeDocument，兼容 v1/v2/v3）/ 初始内容（buildInitialContent）/ 默认骨架（buildDefaultSkeleton，10 节点 + 正文节点）/ defaultDataForType / 工具函数（isTextbookContentNode/isAnchorEdge/getAnchorsForNode/getActiveAnchorIds/getAnchorEdges） |
| `lib/anchor-injector.ts` | **纯函数**：锚点注入算法（markdownToPlainText/injectPlaceholders/parseAnchoredText/toCircledNumber/getNextPointIndex/relocateAnchors/getAnchorColor；**V4：`markdownToPlainText` 引用去除 regex 与 `skipPatterns` 一致化为 `/^\s*>\s+/gm`，5 个 skipPatterns 统一添加 `g` 标志**） |
| `lib/node-summary.ts` | **纯函数**：getNodeSummary（支持 11 种节点类型）+ getTextbookContentSummary + NODE_COLORS + getNodeColor（V3 续审计：移除 `as { html?: string; ... }` 断言，改用 isRecord/getArrayLength/getString/getNumber 安全字段提取从 `unknown` 收窄） |
| `lib/rf-mappers.ts` | **纯函数**：toRfNodes（V3：移除冗余 `as` 断言，TypeScript 判别联合自动收窄）/ toRfEdges / fromRfEdges |
| `config/block-registry.tsx` | **配置驱动**：BLOCK_REGISTRY 注册表 + BlockRenderer（V3：使用 `lib/type-guards.ts` 类型守卫替代所有 `as XxxBlockData` 断言） |
| `providers/lesson-plan-provider.tsx` | **Provider + Context**：LessonPlanProvider 注入数据服务/角色配置/埋点；定义 LessonPlanDataService 接口（V3：扩展 5 个方法 getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan；**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions + 6 个导出类型**）、4 个角色配置、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTracker；hooks：useLessonPlanContextSafe/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe |
| `providers/lesson-plan-provider-setup.tsx` | **页面层 Provider 设置包装组件（V3 续新增）**：LessonPlanProviderSetup 自动注入默认数据服务（createDefaultDataService）和角色配置（TEACHER_ROLE_CONFIG），3 个 teacher 页面用此组件包裹 |
| `services/default-data-service.ts` | **默认数据服务实现**：createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现（V3：实现扩展的 5 个方法；**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions**），测试可替换为 mock |
| `data-access.ts` | 课案 CRUD + 模板查询（V3：新增 `getLessonPlanStats()` 统计函数 + `LessonPlanStats` 接口；`duplicateLessonPlan` 接受 `duplicateSuffix` 参数消除硬编码；V3 续审计：使用 `normalizeTemplateBlocks` 替代 `as unknown as LessonPlanTemplate["blocks"]` 断言；从 `textbooks/data-access` 导入 `findChapterById` 消除本地重复实现） |
| `data-access-versions.ts` | 版本管理（创建/查询/回滚/清理） |
| `data-access-templates.ts` | 个人模板 CRUD（V3 续审计：使用 `normalizeTemplateBlocks` 替代 `as LessonPlanTemplate["blocks"]` 断言） |
| `data-access-knowledge.ts` | 按知识点/题目反查课案（V3 续审计：移除两处 `as unknown as` 断言，改用 `getKnowledgePointIds`/`getQuestionIds` 安全字段提取辅助函数） |
| `actions.ts` | 课案 CRUD/版本/模板 Server Actions（V3：所有 action 使用 `translateFieldErrors` 翻译 Zod 错误；`duplicateLessonPlanAction` 传入 i18n 翻译的副本后缀；`getLessonPlanByIdAction` 返回类型改为 `ActionState<{ plan: LessonPlan }>`） |
| `actions-publish.ts` | 发布作业 Server Action（V3：actions 层注入 i18n 翻译的作业标题/描述/日期标签；新增 `INVALID_QUESTION_TYPE` 错误码映射） |
| `actions-ai.ts` | AI 知识点建议 Server Action（V3 续审计：显式 `unknown[]` 类型标注替代隐式 any；统一使用 `handleActionError` + `translateFieldErrors` 处理错误） |
| `actions-kp.ts` | 知识点选项 Server Action（V3 续审计：显式 `KnowledgePointOption[]` 类型标注替代隐式 any；统一使用 `handleActionError` + `translateFieldErrors` 处理错误） |
| `publish-service.ts` | 发布作业服务（V3：移除 `as` 断言改用类型守卫；移除硬编码中文，接受 `homeworkTitle`/`homeworkDescription` 参数；新增 `INVALID_QUESTION_TYPE` 错误码） |
| `ai-suggest.ts` | AI 知识点建议服务（V3 续审计：移除 `as Record<string, unknown>` 断言改用 `isRecord` 类型守卫；AI prompt 提取为 `AI_SUGGEST_PROMPT_TEMPLATE` 模块常量） |
| `seed-templates.ts` | 模板种子数据 |
| `hooks/use-lesson-plan-editor.ts` | **课案编辑器 Zustand store 主文件（V4：从 303 行单体 store 重写为 25 行薄层，组合 3 个 slice 并导出 `EditorState` 类型）**（V3：`updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更） |
| `hooks/editor-slice.ts` | **编辑器 slice（V4 P2-6 新增）**：Zustand `StateCreator`，包含 planId/title/doc 状态及所有文档操作方法（addNode/updateNode/updateNodePosition/removeNode/updateTextbookContent/getTextbookContentNode/addAnchor/removeAnchor/updateAnchor/connect/disconnect/setEdges/setTitle/setPlanId） |
| `hooks/selection-slice.ts` | **选中状态 slice（V4 P2-6 新增）**：Zustand `StateCreator`，包含 selectedNodeId 状态 + selectNode 方法 |
| `hooks/version-slice.ts` | **版本与保存状态 slice（V4 P2-6 新增）**：Zustand `StateCreator`，包含 isDirty/isSaving/lastSavedAt 状态 + hydrate/markSaved/setSaving/replaceDoc 方法 |
| `components/lesson-plan-list.tsx` | 课案列表（V3：完全通过 service 调用，移除直接 import `getLessonPlansAction`；V3 续审计：新增 `isLoading` 状态，加载时显示加载文本，传递给 `LessonPlanFilters`） |
| `components/lesson-plan-card.tsx` | 课案卡片（V3：完全通过 service 调用，移除直接 import actions；V3 续审计：4 个异步处理函数用 `useCallback` 包裹性能优化） |
| `components/lesson-plan-filters.tsx` | 课案筛选器（V3 续审计：新增 `isLoading` prop，加载时禁用输入并设置 `aria-busy`） |
| `components/lesson-plan-editor.tsx` | 课案编辑器（V3：完全通过 service 调用，移除直接 import 5 个 actions；所有操作改为 `service.updateLessonPlan`/`service.saveLessonPlanVersion`/`service.getLessonPlanById`/`service.publishLessonPlan`/`service.unpublishLessonPlan`） |
| `components/lesson-plan-readonly-view.tsx` | **只读画布组件**（V3：MiniMap 使用 `getNodeColor` 替代硬编码颜色） |
| `components/node-editor.tsx` | **节点图画布**（V3：MiniMap `nodeColor` 使用类型守卫替代 `as { node?: AnyLessonPlanNode }` 断言） |
| `components/node-edit-panel.tsx` | **侧边内容编辑面板**（V3：移除冗余 `as LessonPlanNode` 断言，TypeScript 判别联合自动收窄） |
| `components/nodes/lesson-node.tsx` | **自定义教学节点组件**（V3 续审计：使用 `isLessonNodeData` 类型守卫替代 `as { node: LessonPlanNode }` 断言 + 空状态回退） |
| `components/nodes/textbook-content-node.tsx` | **正文节点组件**（V3 续：拆分为 3 文件，主文件 471 行；`as unknown as` 替换为 `isTextbookContentNodePropsData` 类型守卫） |
| `components/nodes/anchor-node-selector.tsx` | **锚点节点选择器（V3 续新增，从 textbook-content-node.tsx 抽取）**：渲染可锚定教学节点列表 + 关联到选中节点 + 创建新节点选项 |
| `components/nodes/textbook-segments.tsx` | **锚点段落渲染函数（V3 续新增，从 textbook-content-node.tsx 抽取）**：renderSegments 遍历 segments 数组渲染文本/区间锚点/点锚点 |
| `components/lesson-plan-error-boundary.tsx` | **错误边界**（V3 续：重写为包装组件模式——内部类组件接受 `errorText`/`retryText` props，外部函数组件通过 `useTranslations` 注入 i18n 文案） |
| `components/lesson-plan-skeleton.tsx` | **骨架屏**：VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton |
| `components/template-picker.tsx` | 模板选择器（V3 续：完全通过 service 调用，移除直接 import 4 个 actions；V3 续审计：通过 `ChapterPickerOption.children` 递归类型消除 `as ChapterPickerOption[]` 断言） |
| `components/version-history-drawer.tsx` | 版本历史抽屉（V3 续：完全通过 service 调用，移除直接 import 2 个 actions） |
| `components/knowledge-point-picker.tsx` | 知识点选择器（V3 续：完全通过 service 调用，移除直接 import actions-kp） |
| `components/question-bank-picker.tsx` | 题库选择器（V3 续：完全通过 service 调用，移除跨模块直接 import `@/modules/questions/actions`；新增 `isQuestionType` 类型守卫替代 `as QuestionType`） |
| `components/inline-question-editor.tsx` | 内联题目编辑器（V3 续审计：移除 `as typeof QUESTION_TYPES[number]` 断言改用 `isInlineQuestionType` 类型守卫；包裹 `FocusTrap` 实现焦点陷阱；补全 aria-label + 新增 `optionLabel` i18n 键） |
| `components/publish-homework-dialog.tsx` | 发布作业对话框（V3 续：完全通过 service 调用，移除直接 import actions-publish） |
| `components/blocks/rich-text-block.tsx` | 富文本 Block |
| `components/blocks/text-study-block.tsx` | 课文研读 Block |
| `components/blocks/objective-block.tsx` | 教学目标 Block（V3 续：select onChange 使用 `isObjectiveDimension` 类型守卫替代 `as` 断言） |
| `components/blocks/key-point-block.tsx` | 重难点 Block（V3 续：select onChange 使用 `isKeyPointType` 类型守卫替代 `as` 断言） |
| `components/blocks/import-block.tsx` | 导入 Block（V3 续：select onChange 使用 `isImportMethod` 类型守卫替代 `as` 断言） |
| `components/blocks/new-teaching-block.tsx` | 新授 Block |
| `components/blocks/summary-block.tsx` | 总结 Block |
| `components/blocks/homework-block.tsx` | 作业 Block（V3 续：select onChange 使用 `isHomeworkType` 类型守卫替代 `as` 断言） |
| `components/blocks/blackboard-block.tsx` | 板书 Block（V3 续：select onChange 使用 `isBlackboardLayout` 类型守卫替代 `as` 断言） |
| `components/blocks/exercise-block.tsx` | 练习 Block（V3 续：select onChange 使用 `isExercisePurpose` 类型守卫替代 `as` 断言） |
| `components/blocks/reflection-block.tsx` | 反思 Block（V3 续：select onChange 使用 `isReflectionAspect` 类型守卫替代 `as` 断言） |
| `components/calendar-view.tsx` | **M9 日历视图组件（2026-07-01 新增）**：客户端组件，支持周历/月历切换，6 种事件类型颜色映射 |
| `components/curriculum-map-view.tsx` | **M4 课程地图热力图组件（2026-07-01 新增）**：学科×年级矩阵表格，颜色编码覆盖率 |
| `data-access-calendar.ts` | **M9 日历事件数据访问层（2026-07-01 新增）** |
| `data-access-review.ts` | **M3 审核工作流数据访问层（2026-07-01 新增）** |
| `data-access-comments.ts` | **M2 协同备课评论数据访问层（2026-07-01 新增）** |
| `data-access-formative.ts` | **M5 形成性评价数据访问层（2026-07-01 新增）** |
| `data-access-attachments.ts` | **M6 资源附件库数据访问层（2026-07-01 新增）** |
| `data-access-substitutes.ts` | **M12 代课教师数据访问层（2026-07-01 新增）** |
| `data-access-ai-evaluation.ts` | **M8 AI 评估数据访问层（2026-07-01 新增）** |
| `data-access-analytics.ts` | **M10 备课分析仪表盘数据访问层（2026-07-01 新增）** |
| `actions-calendar.ts` | **M9 日历 Server Actions（2026-07-01 新增）** |
| `actions-review.ts` | **M3 审核工作流 Server Actions（2026-07-01 新增）** |
| `actions-comments.ts` | **M2 评论 Server Actions（2026-07-01 新增）** |
| `actions-formative.ts` | **M5 形成性评价 Server Actions（2026-07-01 新增）** |
| `actions-attachments.ts` | **M6 附件库 Server Actions（2026-07-01 新增）** |
| `actions-substitutes.ts` | **M12 代课教师 Server Actions（2026-07-01 新增）** |
| `actions-ai-evaluation.ts` | **M8 AI 评估 Server Actions（2026-07-01 新增）** |
| `actions-analytics.ts` | **M10 分析仪表盘 Server Actions（2026-07-01 新增）** |

> 架构变更（2026-07-01，M1-M12 中长期计划完整实施）：
> - **M1 课标对标体系**：新增 modules/standards/ 模块；新增 3 个 STANDARD_* 权限点；新增路由 /admin/curriculum-map
> - **M2 协同备课评论**：新增 data-access-comments.ts + actions-comments.ts，支持 Block 级评论、resolved 状态
> - **M3 审核工作流**：新增 data-access-review.ts + actions-review.ts，状态机 draft → submitted → approved/rejected → published
> - **M4 课程地图**：新增 admin/curriculum-map 路由 + curriculum-map-view.tsx 热力图组件
> - **M5 形成性评价闭环**：新增 data-access-formative.ts + actions-formative.ts，4 种互动类型
> - **M6 资源附件库**：新增 data-access-attachments.ts + actions-attachments.ts
> - **M7 移动端 PWA**：见 1.1.1 章节
> - **M8 AI 差异化生成 + 评估**：新增 data-access-ai-evaluation.ts + actions-ai-evaluation.ts
> - **M9 日历视图**：新增 data-access-calendar.ts + actions-calendar.ts + calendar-view.tsx + teacher/lesson-plans/calendar 路由
> - **M10 备课分析仪表盘**：新增 data-access-analytics.ts + actions-analytics.ts，3 个分析维度
> - **M11 版本 diff 预览**：data-access-versions.ts 的 getVersionContent 支持读取历史版本内容
> - **M12 代课教师**：新增 data-access-substitutes.ts + actions-substitutes.ts
> - **权限扩展**：新增 STANDARD_READ/MANAGE/LINK + LESSON_PLAN_SUBMIT/REVIEW/ANALYTICS_READ/SUBSTITUTE_MANAGE
> - **i18n 扩展**：zh-CN 新增 9 个命名空间；status 扩展为 6 状态
> - **数据库表**：新增 6 张表（lessonPlanComments/lessonPlanReviewRecords/lessonPlanFormativeItems/lessonPlanAttachments/lessonPlanAiEvaluations/lessonPlanSubstituteAssignments）
>
> 架构变更（2026-07-03，V5 场景缺口修复第一阶段）：
> - **V5-1 自动保存失败 UI 兜底**：version-slice.ts 新增 saveError/isOnline 状态；lesson-plan-editor.tsx 监听 online/offline 事件 + toast 提示 + 重试按钮
> - **V5-2 撤销/重做**：新增 hooks/history-slice.ts（past/future 栈，50 步上限）；editor-slice.ts 所有 mutation 包装 pushHistory；编辑器顶部 Undo/Redo 按钮 + Ctrl/Cmd+Z/Shift+Z 快捷键
> - **V5-3 发布前作业单预览**：publish-homework-dialog.tsx 改为 3 步流程（选班级 → 预览题目+总分+学生数 → 确认发布）；exercise-block.tsx 传入 items
> - **V5-4 导出/打印**：新增 lib/export.ts（文档扁平化，详细版/简洁版）+ components/print-view.tsx（打印视图）；编辑器顶部新增"导出"按钮
> - **V5-5 多媒体嵌入 + 素材库**：types.ts RichTextBlockData 新增 attachments 字段；rich-text-block.tsx 集成 @tiptap/extension-image；新增 components/attachment-picker.tsx（复用 use-file-upload + M6 附件表 data-access）；LessonPlanDataService 接口新增 getLessonPlanAttachments/createLessonPlanAttachment/deleteLessonPlanAttachment 三个方法；新增依赖 @tiptap/extension-image；FileTargetType 新增 "lesson_plan"
> - **i18n 扩展**：zh-CN/en 同步新增 action.undo/redo、status.retrySave/saveFailed/recovered/offline、publish.step1-3/preview*/confirm*、questionBank.type 多题型、export.*、attachment.*、editor.textbookContent
> - **审计报告**：新增 docs/architecture/audit/lesson-preparation-audit-report-v5.md（场景缺口维度，对照 v4 架构合规维度）
>
> 架构变更（2026-07-03，V5 场景缺口修复第二阶段 V5-6~V5-21）：
> - **V5-6 W1 题库组卷预览**：exercise-block.tsx 增加组卷预览模式（题目列表 + 总分统计）
> - **V5-7 W3/W4 课案绑定课时**：新增 data-access-schedules.ts + actions-schedules.ts + components/schedule-dialog.tsx；编辑器新增「安排课时」按钮；数据库新增 lesson_plan_schedules 表
> - **V5-8 U2 画布自动布局**：新增 lib/auto-layout.ts（@dagrejs/dagre DAG 自动布局，项目已安装依赖无需新增）；editor-slice.ts 新增 autoLayout action
> - **V5-9 F5 教材模糊搜索 + 最近使用**：template-picker.tsx 增加客户端模糊搜索（标题/学科/年级/出版社）+ localStorage 最近使用 5 个教材
> - **V5-10 P1 画布性能优化**：node-editor.tsx ReactFlow 新增 onlyRenderVisibleElements 虚拟化 + minZoom={0.2}/maxZoom={2.5}/elevateNodesOnSelect={false}
> - **V5-12 R4 校内课案库**：新增 teacher/lesson-plans/library 路由 + loading.tsx + error.tsx；actions.ts 新增 duplicateLessonPlanFormAction（FormData wrapper 适配 form action）
> - **V5-13 P3 移动端只读视图**：新增 components/lesson-plan-mobile-view.tsx（按教学阶段分组线性卡片视图）；lesson-plan-readonly-view.tsx 集成 useMediaQuery("(max-width: 768px)") 小屏切换
> - **V5-14 F4 板书可视化工具**：blackboard-block.tsx 增加轻量级可视化预览（不引入新库）——编辑/预览模式切换、structure 层级树、mindmap 中心+分支、text 等宽展示；新增 BlackboardPreview 子组件 + parseContent 纯函数（按缩进识别层级）
> - **V5-15 T1 教学阶段分组**：types.ts 新增 TeachingStage 类型 + TEACHING_STAGE_KEYS 常量；Block 接口新增 stage? 字段；node-edit-panel.tsx 添加教学阶段选择器 UI
> - **V5-16 T2 反思闭环（版本对比）**：新增 lib/version-diff.ts（纯函数 diffDocuments 对比两个 LessonPlanDocument 节点级差异，added/removed/modified/unchanged + changedFields）；新增 components/version-diff-view.tsx；version-history-drawer.tsx 集成版本对比（currentDoc prop + 对比按钮）
> - **V5-17 A1/A2 AI 反馈闭环**：新增 lib/ai-feedback.ts（generateLessonPlanFeedback 纯服务端函数，4 维度 strengths/improvements/alignment/differentiation，含解释性 reason 字段）；actions-ai.ts 新增 generateLessonPlanFeedbackAction；新增 components/ai-feedback-dialog.tsx；编辑器工具栏新增「AI 反馈」按钮
> - **V5-18 W6 差异化教学标记**：types.ts 新增 DifferentiationLevel 类型 + DIFFERENTIATION_LEVEL_KEYS 常量；Block 接口新增 differentiation? 字段；node-edit-panel.tsx 添加差异化标记选择器 UI
> - **V5-19 T3 目标-评价一致性校验**：新增 lib/consistency-check.ts（纯函数 checkConsistency 输出 ConsistencyResult，校验规则：缺少 objective/exercise 警告、exercise 无知识点警告、objective 未被评价覆盖警告）；新增 components/consistency-check-dialog.tsx；编辑器工具栏新增「一致性校验」按钮
> - **V5-20 T4 课标覆盖热力图**：新增 lib/curriculum-coverage.ts（纯函数 computeCurriculumCoverage 按 chapterId 分组统计覆盖率，识别 isBlindSpot 教学盲点；getHeatLevel 0-4 热力等级）；新增 components/curriculum-heatmap.tsx；新增 teacher/lesson-plans/heatmap 路由 + loading.tsx + error.tsx
> - **V5-21 A3/A4/A5 AI 差异化模块**：新增 lib/ai-differentiation.ts（3 个纯服务端函数：generateDifferentiationSuggestions/checkCurriculumAlignment/generateExplainableAssessment，复用 ai-feedback.ts 的 createAiChatCompletion + Zod + JSON 提取模式）；actions-ai.ts 新增 4 个 server actions（generateDifferentiationSuggestionsAction/checkCurriculumAlignmentAction/generateExplainableAssessmentAction/getKnowledgePointsForAlignmentAction）；新增 components/ai-differentiation-dialog.tsx（3 Tab 视图：差异化建议/课标核对/可解释评估，A4 按教材 ID 按需加载知识点）；编辑器工具栏新增「AI 差异化」按钮
> - **i18n 扩展**：zh-CN/en 同步新增 editor.stage*/editor.differentiation*/editor.consistency*/consistency.*/diff.*/library.*/feedback.*/heatmap.*/aiDifferentiation.*/blackboard.editMode/previewMode/editHint/previewEmpty/untitled
> - **V5-11 R3 协同编辑**：需引入新依赖（Yjs/Liveblocks）或采用轻量协作方案（评论+@提醒+版本对比），暂标记为待规划，待用户决策

---

## 2.27b standards（课标模块，2026-07-01 新增）

**职责**：K12 课标管理，支持课标树、按学科+年级筛选、课案与课标多对多关联。

**导出函数**：
- Data-access：getStandards / getStandardsTree / getStandardById / createStandard / updateStandard / deactivateStandard / searchStandards / getStandardsByPlanId / linkPlanToStandard / unlinkPlanFromStandard
- Actions：10 个 Server Actions，全部使用 requirePermission() + ActionState<T>
- Schema：createStandardSchema / updateStandardSchema / getStandardsParamsSchema / linkPlanToStandardSchema
- Types：Standard / StandardTreeNode / LessonPlanStandardLink

**权限点**：
- STANDARD_READ（standard:read）
- STANDARD_MANAGE（standard:manage）
- STANDARD_LINK（standard:link）

**依赖关系**：
- 依赖：shared/*、modules/lesson-preparation/lib/i18n-errors
- 被依赖：modules/lesson-preparation、app/(dashboard)/admin/curriculum-map

**文件清单**：
| 文件 | 职责 |
|------|------|
| types.ts | 类型定义 |
| schema.ts | Zod 验证 |
| data-access.ts | 课标 CRUD + 树形查询 + 课案关联 |
| actions.ts | 10 个 Server Actions |

---

## 2.28 error-book（错题本模块）

**职责**：自动采集考试/作业错题，基于 SM-2 间隔重复算法科学复习，提供知识点薄弱度分析与多角色视图（学生/教师/家长/管理员）。

**导出函数**：
- Actions：`getErrorBookItemsAction` / `getErrorBookItemDetailAction` / `getErrorBookStatsAction` / `createErrorBookItemAction` / `updateErrorBookNoteAction` / `reviewErrorBookItemAction` / `deleteErrorBookItemAction` / `archiveErrorBookItemAction` / `collectFromSubmissionAction`
- Data-access（`data-access.ts`，学生端 CRUD）：`getErrorBookItems` / `getErrorBookItemById` / `getErrorBookStats`（SQL 聚合优化）/ `createErrorBookItem` / `updateErrorBookNote` / `recordReview` / `deleteErrorBookItem` / `archiveErrorBookItem` / `buildStudentErrorWhereClause`（共享）/ `toStatus`（共享）/ `toStringArray`（共享）
- Data-access（`data-access-analytics.ts`，教师/管理员分析查询）：`getStudentErrorBookSummaries`（SQL 聚合）/ `getTopWrongQuestionsByStudentIds`（SQL 聚合）/ `getKnowledgePointWeakness` / `getSubjectErrorDistribution`（SQL 聚合）/ `getChapterWeakness` / `getClassErrorOverviews`（SQL 聚合）/ `getSubjectErrorOverviews`（SQL 聚合）/ `getStudentNameMap` / `getStudentIdsByClassIdList` / `getAllStudentIds`
- Data-access（`data-access-collection.ts`，自动采集）：`collectFromExamSubmission` / `collectFromHomeworkSubmission`（通过 `data-access.ts` re-export）
- Schema：`CreateErrorBookItemSchema` / `UpdateErrorBookNoteSchema` / `ReviewErrorBookItemSchema` / `CollectFromSubmissionSchema`
- Types：`ErrorBookItem` / `ErrorBookItemDetail` / `ErrorBookReviewRecord` / `ErrorBookStats` / `StudentErrorBookSummary` / `KnowledgePointWeakness` / `SubjectErrorDistribution`
- Components：`ErrorBookStatsCards` / `ErrorBookFilters` / `ErrorBookItemCard` / `ReviewButtons` / `ErrorBookDetailDialog` / `ErrorBookList` / `AddErrorBookDialog` / `TopWrongQuestions`

**权限点**：
- `ERROR_BOOK_READ`（`error_book:read`）：查看错题本（student 自己、parent 子女）
- `ERROR_BOOK_MANAGE`（`error_book:manage`）：管理错题（添加/复习/删除/归档，student）
- `ERROR_BOOK_ANALYTICS_READ`（`error_book:analytics_read`）：查看错题分析（teacher/admin/grade_head/teaching_head）

**依赖关系**：
- 依赖：`shared/*`（db、auth-guard、types、utils）、`modules/classes`（getStudentIdsByClassIds）
- 被依赖：`app/(dashboard)/student/error-book`、`app/(dashboard)/teacher/error-book`、`app/(dashboard)/parent/error-book`、`app/(dashboard)/admin/error-book`、`modules/homework`（collectFromHomeworkSubmission，作业批改后自动采集）、`modules/grades`（collectFromExamSubmission，✅ 2026-06-25 新增：教师录入考试成绩后自动采集考试错题）
- ✅ 2026-06-24 i18n + 解耦改造：`error-book-detail-dialog` 移除直接 import `AiErrorBookAnalysis`（modules/ai）和 `createPracticeSessionAction`（modules/adaptive-practice），改为通过 `aiAnalysisSlot` 和 `onStartVariantPractice` props 注入；`add-error-book-dialog` 移除直接 import `getQuestionsAction`（modules/questions），改为通过 `questionOptions` 和 `onLoadQuestions` props 注入。跨模块依赖上移至 `app/(dashboard)/student/error-book/page.tsx`

**DataScope 行级权限**：
| 角色 | DataScope | 说明 |
|------|-----------|------|
| student | `owned` | 仅查看/管理自己的错题 |
| parent | `children` | 查看子女的错题统计 |
| teacher | `class_taught` | 查看所教班级学生的错题分析 |
| admin | `all` | 查看全校错题分析 |
| grade_head | `grade_managed` | 查看所管年级学生的错题分析 |
| teaching_head | `grade_managed` | 查看所管年级学生的错题分析 |

**核心算法：SM-2 间隔重复（简化版）**：
- 独立纯函数模块：[sm2-algorithm.ts](file:///e:/Desktop/CICD/src/modules/error-book/sm2-algorithm.ts)（可替换为其他算法如 FSRS）
- 4 级评级：`again`（重来）/ `hard`（困难）/ `good`（良好）/ `easy`（简单）
- 初始间隔：1/2/4/7 天
- 间隔增长：`again` 重置为 1 天；`hard` ×1.2；`good` ×1.5；`easy` ×2
- 掌握度：0-5 级，`again` -1，`hard` ±0，`good` +1，`easy` +2
- 已掌握判定：掌握度 ≥5 或连续答对 ≥3 次
- 复习时间：次日早上 9 点
- 单元测试：[sm2-algorithm.test.ts](file:///e:/Desktop/CICD/src/modules/error-book/sm2-algorithm.test.ts)（39 个测试用例，全部通过）
- 导出函数：`calculateNewInterval` / `calculateNewMastery` / `deriveStatus` / `calculateNextReviewAt` / `calculateNewCorrectStreak` / `calculateSm2Result`

**自动采集机制**：
- `collectFromExamSubmission`：从考试提交记录中筛选得分 < 满分的题目，去重后批量插入
- `collectFromHomeworkSubmission`：从作业提交记录中筛选错题，去重后批量插入
- 自动关联知识点（通过 `questionsToKnowledgePoints` 表）
- subjectId 来源：`questions` 表无 subjectId 字段，考试错题从 `exams.subjectId` 获取；作业错题从 `homeworkAssignments.sourceExamId` 关联到源试卷的 `exams.subjectId` 获取（独立作业无学科归属则为 null）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~180 | 9 个 Server Actions，全部使用 `requirePermission()` + `ActionState<T>` |
| `data-access.ts` | ~480 | 学生端 CRUD（10 个函数）+ 共享工具函数（buildStudentErrorWhereClause/toStatus/toStringArray）+ re-export 采集函数。✅ 2026-06-24 拆分：分析函数迁移至 data-access-analytics.ts，修复 as 断言/or()! 非空断言/JSON 列类型安全，getErrorBookStats 优化为 SQL GROUP BY 聚合 |
| `data-access-analytics.ts` | ~430 | ✅ 2026-06-24 新增：教师/管理员分析查询（9 个函数），SQL 聚合优化（getStudentErrorBookSummaries/getTopWrongQuestionsByStudentIds/getSubjectErrorDistribution/getClassErrorOverviews/getSubjectErrorOverviews 使用 GROUP BY + 条件聚合） |
| `data-access-collection.ts` | ~170 | 自动采集逻辑（从考试/作业提交采集错题，通过跨模块接口避免直查多表） |
| `sm2-algorithm.ts` | ~180 | SM-2 间隔重复算法（独立纯函数模块，可替换，支持时间注入测试） |
| `sm2-algorithm.test.ts` | ~280 | SM-2 算法单元测试（39 个测试用例，覆盖所有函数和边界条件） |
| `schema.ts` | ~60 | 4 个 Zod 验证 schema |
| `types.ts` | ~245 | 11 个类型定义 + 状态映射常量 + 错误标签常量（新增 ChapterWeakness/ClassErrorOverview/SubjectErrorOverview） |
| `components/error-book-stats-cards.tsx` | ~80 | 5 个统计卡片（总数/待学习/学习中/已掌握/待复习） |
| `components/analytics-stats-cards.tsx` | ~110 | 教师/管理员分析视图 5 个统计卡片（覆盖学生/错题总数/平均掌握率/待复习/知识点数） |
| `components/error-book-filters.tsx` | ~100 | 筛选栏（搜索/状态/来源/待复习），使用 nuqs |
| `components/error-book-item-card.tsx` | ~150 | 错题卡片（预览/标签/笔记/掌握度/操作） |
| `components/review-buttons.tsx` | ~80 | 4 按钮复习面板（again/hard/good/easy） |
| `components/error-book-detail-dialog.tsx` | ~340 | 详情对话框（题目/答案/复习/笔记/历史）。✅ 2026-06-24 i18n + 解耦：移除直接 import `AiErrorBookAnalysis`/`createPracticeSessionAction`，改为 `aiAnalysisSlot`/`onStartVariantPractice` props 注入；移除 `as` 断言 |
| `components/error-book-list.tsx` | ~60 | 网格列表。✅ 2026-06-24 i18n + 解耦：新增 `aiAnalysisSlot?: (item: ErrorBookItem) => React.ReactNode` 和 `onStartVariantPractice?: (item: ErrorBookItem) => void` props，透传至 `ErrorBookItemCard`/`ErrorBookDetailDialog` |
| `components/add-error-book-dialog.tsx` | ~170 | 手动添加对话框（题库选择 + 标签）。✅ 2026-06-24 i18n + 解耦：移除直接 import `getQuestionsAction`，改为 `questionOptions`/`onLoadQuestions` props 注入；移除 `as` 断言 |
| `components/subject-tabs.tsx` | ~95 | 学科切换 Tab（显示每个学科错题数概览，URL 参数持久化） |
| `components/class-filter.tsx` | ~85 | 班级筛选器（显示每个班级错题数和待复习数，URL 参数持久化） |
| `components/class-error-bar-chart.tsx` | ~115 | 班级错题数对比柱状图（recharts，tooltip 显示学生数/人均/掌握率） |
| `components/subject-distribution-chart.tsx` | ~110 | 学科错题分布柱状图（管理员视图，recharts） |
| `components/knowledge-point-weakness-chart.tsx` | ~150 | 知识点薄弱度横向柱状图（按掌握率红/黄/绿着色，显示章节归属） |
| `components/chapter-weakness-chart.tsx` | ~165 | 章节错题分布横向柱状图（哪些课在错，含 Top 3 薄弱知识点） |
| `components/grouped-student-error-table.tsx` | ~180 | 按班级分组的学生错题表格（可展开/折叠，显示每个学生的错题详情） |
| `components/top-wrong-questions.tsx` | ~80 | 高频错题列表（Top 10） |

**路由清单**：
| 路由 | 文件 | 说明 |
|------|------|------|
| `/student/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` + `student-error-book-list-client.tsx` + `add-error-book-dialog-with-questions.tsx` | 学生错题本（统计/筛选/列表/手动添加/详情复习）。✅ 2026-06-24 解耦：app 层新增两个包装组件承接跨模块依赖：`StudentErrorBookListClient` 注入 `AiErrorBookAnalysis`（modules/ai）+ `createPracticeSessionAction`（modules/adaptive-practice）到 `ErrorBookList` 的 `aiAnalysisSlot`/`onStartVariantPractice` props；`AddErrorBookDialogWithQuestions` 注入 `getQuestionsAction`（modules/questions）+ `extractQuestionPreview`（shared/lib）到 `AddErrorBookDialog` 的 `onLoadQuestions` prop |
| `/teacher/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 教师错题分析（学科Tab/班级筛选/统计卡片/班级对比图/章节错题图/知识点薄弱度图/按班级分组学生表/高频错题） |
| `/parent/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 家长错题本（子女错题统计/薄弱知识点/高频错题） |
| `/admin/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 管理员错题分析（学科Tab/统计卡片/学科分布图/章节错题图/知识点薄弱度图/按班级分组学生Top50/高频错题） |

**数据库表**：
| 表 | 说明 |
|------|------|
| `errorBookItems` | 错题条目主表（18 列/4 索引/2 外键）：studentId、questionId、sourceType、sourceId、studentAnswer、correctAnswer、subjectId、knowledgePointIds、status、masteryLevel、nextReviewAt、reviewInterval、reviewCount、correctStreak、note、errorTags、createdAt、updatedAt |
| `errorBookReviews` | 复习记录表（8 列/2 索引/2 外键）：itemId、studentId、result、reviewedAt、newInterval、newMasteryLevel |

**i18n**：
- `src/shared/i18n/messages/zh-CN/error-book.json`
- `src/shared/i18n/messages/en/error-book.json`

**导航配置**：
- 6 个角色（admin/teacher/student/parent/grade_head/teaching_head）均添加「错题分析」导航项
- 图标：`BookX`（lucide-react）

---

## 2.29 ai（AI 模块）— ✅ 新增 / V2 增强 / V3 安全加固+竞品对标

**职责**：统一 AI 能力封装，为备课、错题集、试卷、改题等业务模块提供 AI 服务。V2 增加流式响应、Markdown 渲染、全局助手、内容安全过滤、家长学情摘要、管理员使用统计、学生学习路径推荐。V3 安全加固（原子配额/苏格拉底校验/重试机制）+ 竞品对标（知识图谱集成/苏格拉底式辅导强化）。

**架构定位**：
- 位于 `modules/` 层，通过 `shared/lib/ai` 调用底层 AI SDK
- 通过 `AiClientProvider`（React Context）向客户端组件注入 Server Action 引用
- 业务模块不直接 import `ai/actions`，仅通过 Context 消费
- V2：在 dashboard layout 全局注入 `AiClientProvider`，所有页面均可使用 AI 助手

**核心导出**：

| 类型 | 名称 | 文件 | 说明 |
|------|------|------|------|
| **Server Actions** | `aiChatAction` | `modules/ai/actions.ts` | AI 对话（权限：AI_CHAT） |
| **Server Actions** | `suggestSimilarQuestionsAction` | `modules/ai/actions.ts` | 相似题推荐（权限：AI_CHAT + ERROR_BOOK_READ） |
| **Server Actions** | `suggestGradingAction` | `modules/ai/actions.ts` | AI 辅助批改（权限：AI_CHAT + HOMEWORK_GRADE） |
| **Server Actions** | `generateLessonContentAction` | `modules/ai/actions.ts` | 备课内容生成（权限：AI_CHAT + LESSON_PLAN_READ） |
| **Server Actions** | `generateQuestionVariantAction` | `modules/ai/actions.ts` | 题目变体生成（权限：AI_CHAT + EXAM_AI_GENERATE） |
| **Server Actions** | `analyzeWeaknessAction` | `modules/ai/actions.ts` | 薄弱点分析（权限：AI_CHAT + ERROR_BOOK_READ） |
| **Server Actions** | `generateChildSummaryAction` | `modules/ai/actions.ts` | 家长学情摘要（权限：AI_CHAT）— V2 新增 |
| **Server Actions** | `recommendStudyPathAction` | `modules/ai/actions.ts` | 学习路径推荐（权限：AI_CHAT）— V2 新增 |
| **Server Actions** | `getAiUsageStatsAction` | `modules/ai/actions.ts` | AI 使用统计（权限：AI_CONFIGURE）— V2 新增 |
| **Server Actions** | `explainErrorAction` | `modules/ai/actions.ts` | 错题 AI 解释（权限：AI_CHAT + ERROR_BOOK_READ）— V3 P2-1 新增 |
| **SSE Route** | `POST /api/ai/chat/stream` | `app/api/ai/chat/stream/route.ts` | 流式 AI 对话（SSE）— V2 新增 |
| **SSE Route** | `GET /api/notifications/stream` | `app/api/notifications/stream/route.ts` | 通知实时推送（SSE，15s 心跳 + 5min 超时）— V2-P3 新增 |
| **Service** | `AiService` | `modules/ai/types.ts` | 服务端 AI 服务接口（含 8 个方法） |
| **Service** | `AiClientService` | `modules/ai/types.ts` | 客户端 AI 服务接口（Server Action 引用集合） |
| **Provider** | `AiClientProvider` | `modules/ai/context/ai-client-provider.tsx` | React Context Provider，注入 AiClientService |
| **Hook** | `useAiClient` | `modules/ai/context/ai-client-provider.tsx` | 消费 AiClientService（必须在 Provider 内使用） |
| **Hook** | `useAiClientOptional` | `modules/ai/context/ai-client-provider.tsx` | 可选消费 AiClientService（未注入返回 null） |
| **Hook** | `useAiChatStream` | `modules/ai/hooks/use-ai-chat-stream.ts` | 流式 AI 对话 Hook（SSE + AbortController）— V2 新增 |
| **Hook** | `useAiSuggestion` | `modules/ai/hooks/use-ai-suggestion.ts` | AI 建议 Hook |
| **Hook** | `useFloatingBall` | `modules/ai/hooks/use-floating-ball.ts` | 可拖拽悬浮球 Hook（边缘吸附+半隐藏+位置持久化）— V3 拆分 |
| **Hook** | `usePositionPersistence` | `modules/ai/hooks/use-position-persistence.ts` | 位置持久化 Hook（localStorage + resize 校正）— V3 新增 |
| **Hook** | `useDragPosition` | `modules/ai/hooks/use-drag-position.ts` | 拖拽位置 Hook（pointer 事件 + 拖拽状态）— V3 新增 |
| **Context** | `createFullAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建完整 AI 客户端服务工厂（含全部 9 个 Action）— V3 新增 |
| **Context** | `createCoreAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建核心 AI 客户端服务工厂（仅 6 个常用 Action）— V3 新增 |
| **Component** | `AiAssistantWidget` | `modules/ai/components/ai-assistant-widget.tsx` | 全局 AI 助手悬浮按钮（上下文感知）— V2 新增 |
| **Component** | `AiChatPanel` | `modules/ai/components/ai-chat-panel.tsx` | AI 对话面板（流式 + Markdown + 复制 + 停止 + 建议）— V2 增强 |
| **Component** | `AiMarkdownRenderer` | `modules/ai/components/ai-markdown-renderer.tsx` | Markdown 渲染器（GFM + 复制按钮）— V2 新增 |
| **Component** | `AiGradingAssist` | `modules/ai/components/ai-grading-assist.tsx` | AI 批改辅助（主观题预评分 + 反馈建议） |
| **Component** | `AiErrorBookAnalysis` | `modules/ai/components/ai-error-book-analysis.tsx` | 错题本 AI 分析（相似题 + 薄弱点） |
| **Component** | `AiLessonContentGenerator` | `modules/ai/components/ai-lesson-content-generator.tsx` | 备课内容生成器（活动/评估/讨论题/素材） |
| **Component** | `AiQuestionVariantGenerator` | `modules/ai/components/ai-question-variant-generator.tsx` | 题目变体生成器（同知识点/不同难度/不同题型） |
| **Component** | `AiChildSummary` | `modules/ai/components/ai-child-summary.tsx` | 家长 AI 学情摘要 — V2 新增 |
| **Component** | `AiUsageDashboard` | `modules/ai/components/ai-usage-dashboard.tsx` | 管理员 AI 使用统计仪表盘 — V2 新增 |
| **Component** | `AiStudyPath` | `modules/ai/components/ai-study-path.tsx` | 学生学习路径推荐 — V2 新增 |
| **Component** | `AiErrorBoundary` | `modules/ai/components/ai-error-boundary.tsx` | AI 功能错误边界 |
| **Component** | `AiSuggestionSkeleton` | `modules/ai/components/ai-skeleton.tsx` | AI 建议加载骨架屏 |
| **Component** | `AiChartRenderer` | `modules/ai/components/ai-chart-renderer.tsx` | AI 图表渲染器（bar/line/pie/radar）— V3 新增 |
| **Component** | `AiProviderSelector` | `modules/ai/components/ai-provider-selector.tsx` | AI 服务商选择器 |
| **Safety** | `filterUserInput` | `modules/ai/services/content-safety.ts` | 输入安全过滤 — V2 新增 |
| **Safety** | `filterAiOutput` | `modules/ai/services/content-safety.ts` | 输出安全过滤 — V2 新增 |
| **Safety** | `checkDailyLimit` | `modules/ai/services/content-safety.ts` | 每日交互限制 — V2 新增 |
| **Safety** | `tryConsumeDailyQuota` | `modules/ai/services/content-safety.ts` | 原子化每日限额（防 TOCTOU 竞态）— V3 新增 |
| **Safety** | `refundDailyQuota` | `modules/ai/services/content-safety.ts` | 配额回退（过滤/失败时不扣配额）— V3 新增 |
| **Safety** | `validateSocraticOutput` | `modules/ai/services/content-safety.ts` | 苏格拉底式辅导输出校验（问号结尾/连续陈述句限制）— V3 新增 |
| **Data Access** | `recordAiEvent` | `modules/ai/data-access.ts` | AI 事件记录（内存存储，生产环境替换为 DB）— V3 新增 |
| **Data Access** | `getAiUsageStats` | `modules/ai/data-access.ts` | AI 使用统计聚合（真实聚合，替代硬编码零）— V3 新增 |
| **Prompt** | `SOCRATIC_TUTOR_SYSTEM_PROMPT` | `modules/ai/services/prompt-templates.ts` | 苏格拉底式辅导专用提示词（3 级提示升级/禁止直接给答案）— V3 新增 |
| **Prompt** | `EXPLAIN_ERROR_SYSTEM_PROMPT` | `modules/ai/services/prompt-templates.ts` | 错题解释专用提示词（错误分析/正确思路/关键知识点/防范建议）— V3 P2-1 新增 |
| **Util** | `consumeSseStream` / `getStreamErrorKey` / `removeTrailingEmptyAssistant` | `modules/ai/hooks/stream-utils.ts` | SSE 流解析工具（从 hook 抽取）— V3 新增 |

**集成点**：

| 业务模块 | 集成组件 | 页面 | 说明 |
|---------|---------|------|------|
| homework | `AiGradingAssist` | `teacher/homework/submissions/[submissionId]` | 主观题辅助评分 |
| error-book | `AiErrorBookAnalysis` | `student/error-book` | 相似题推荐 + 薄弱点分析 |
| lesson-preparation | `AiLessonContentGenerator` | `teacher/lesson-plans/[planId]/edit` | 备课内容生成 |
| exams | `AiQuestionVariantGenerator` | `teacher/exams/[id]/build` | 题目变体生成 |
| **全局** | `AiAssistantWidget` | `app/(dashboard)/layout.tsx` | 全局 AI 助手悬浮按钮（所有页面）— V2 新增 |
| **parent** | `AiChildSummary` | `parent/dashboard` | 家长学情 AI 摘要 — V2 新增，V4 实际接入到 ParentDashboard 的 aiSummarySlot，按子女渲染 |
| **admin** | `AiUsageDashboard` | `admin/ai-settings` | AI 使用统计仪表盘 — V2 新增（V4 校正：原误记为 `admin/ai-usage`，实际位于 `/admin/ai-settings`） |
| **student** | `AiStudyPath` | `student/learning/study-path` | 学习路径推荐 — V2 新增，V4 实际接入页面（含 loading.tsx + error.tsx） |

**依赖关系**：
- `modules/ai` → `shared/lib/ai`（AI SDK 封装，含流式 `createAiChatCompletionStream`，V3 增加 `withRetry` 重试机制）
- `modules/ai` → `shared/lib/auth-guard`（权限校验）
- `modules/ai` → `shared/lib/track-event`（使用量埋点）
- `modules/ai` → `shared/types/permissions`（权限常量）
- `modules/ai` → `shared/types/action-state`（返回值类型）
- `modules/ai` → `modules/textbooks/data-access-graph`（知识图谱+掌握度查询）— V3 新增
- `app/(dashboard)/layout` → `modules/ai`（全局 Provider + Widget）— V2 新增
- `app/api/ai/chat/stream` → `modules/ai/services/content-safety`（SSE 端点安全过滤）— V3 加固
- 业务模块 → `modules/ai/context/ai-client-provider`（通过 Context 注入）
- 业务模块 → `modules/ai/components/*`（组合 AI 组件）

**安全机制（V2 新增 / V3 加固）**：
- 输入过滤：`filterUserInput` 拦截暴力/自残/色情/PII
- 输出过滤：`filterAiOutput` 扫描 AI 回复
- 每日限制：学生 50 次/天，教师 200 次/天，家长 30 次/天
- 学生 Socratic 模式：system prompt 强制不直接给答案
- SSE 端点权限校验：`requirePermission(AI_CHAT)`
- V3 原子配额：`tryConsumeDailyQuota` 解决 TOCTOU 竞态，`refundDailyQuota` 过滤/失败时回退
- V3 苏格拉底校验：`validateSocraticOutput` 检查问号结尾+连续陈述句限制，`SOCRATIC_TUTOR_SYSTEM_PROMPT` 3 级提示升级
- V3 SSE 端点加固：Zod 校验 + rate limit + 服务端强制 systemPrompt（忽略客户端）
- V3 重试机制：`withRetry` 指数退避（429/5xx，2 次重试，1s/2s/4s 延迟）
- V3 知识图谱集成：`recommendStudyPathAction` 自动从 textbooks 模块获取图谱+掌握度注入 prompt
- V4 超时优化：`shared/lib/ai/client.ts` 按场景分离超时（普通 30s / 测试连接 120s / 流式 180s），解决添加 Provider 时检测超时过短导致测试失败
- V4 规范修复：`ai-service.ts` 消除 `as unknown as Record` 双重断言（改用 `in` 类型缩小）；`actions.ts` 消除非空断言 `!`（改用 `flatMap`）；`ai-assistant-widget.tsx` 硬编码英文提示词迁移至 i18n（新增 `chat.suggestedPrompts.context.*` 翻译键）

**文件清单**：

| 文件 | 行数 | 职责 |
|------|------|------|
| `modules/ai/types.ts` | ~290 | 类型定义（8 个业务场景类型 + AiService/AiClientService，V3 新增 knowledgeGraph/textbookId） |
| `modules/ai/schema.ts` | ~230 | Zod 验证 schema（8 个输入 + 8 个输出，V3 新增 knowledgeGraph/textbookId） |
| `modules/ai/actions.ts` | ~400 | 9 个 Server Actions（含权限校验，V3 新增知识图谱获取，V4 消除非空断言） |
| `modules/ai/data-access.ts` | ~138 | AI 事件存储+统计聚合 — V3 新增 |
| `modules/ai/services/ai-service.ts` | ~430 | DefaultAiService 实现（8 个方法，V3 新增知识图谱上下文注入，V4 消除 as 断言） |
| `modules/ai/services/prompt-templates.ts` | ~250 | 9 个系统提示词模板（V3 新增 SOCRATIC_TUTOR_SYSTEM_PROMPT） |
| `modules/ai/services/usage-tracker.ts` | ~90 | AI 使用量埋点（V3 新增 child_summary/study_path 能力） |
| `modules/ai/services/content-safety.ts` | ~290 | 内容安全过滤（输入/输出/每日限制/原子配额/苏格拉底校验）— V2 新增 / V3 加固 |
| `modules/ai/context/ai-client-provider.tsx` | ~62 | React Context Provider + Hooks |
| `modules/ai/components/ai-assistant-widget.tsx` | ~170 | 全局 AI 助手悬浮按钮 — V2 新增 / V4 i18n 修复 |
| `modules/ai/components/ai-chat-panel.tsx` | ~305 | AI 对话面板（流式 + Markdown）— V2 增强 |
| `modules/ai/components/ai-markdown-renderer.tsx` | ~100 | Markdown 渲染器 — V2 新增 |
| `modules/ai/components/ai-child-summary.tsx` | ~170 | 家长学情摘要 — V2 新增 |
| `modules/ai/components/ai-usage-dashboard.tsx` | ~180 | 管理员使用统计 — V2 新增 |
| `modules/ai/components/ai-study-path.tsx` | ~170 | 学生学习路径 — V2 新增 |
| `modules/ai/components/ai-grading-assist.tsx` | 173 | AI 批改辅助组件 |
| `modules/ai/components/ai-error-book-analysis.tsx` | 246 | 错题本 AI 分析组件 |
| `modules/ai/components/ai-lesson-content-generator.tsx` | ~187 | 备课内容生成器 |
| `modules/ai/components/ai-question-variant-generator.tsx` | ~208 | 题目变体生成器 |
| `modules/ai/components/ai-error-boundary.tsx` | ~88 | AI 错误边界 |
| `modules/ai/components/ai-skeleton.tsx` | ~47 | AI 骨架屏 |
| `modules/ai/components/ai-provider-selector.tsx` | ~129 | 服务商选择器 |
| `modules/ai/hooks/use-ai-chat-stream.ts` | ~107 | 流式 AI 对话 Hook（V3 拆分至 ≤80 行函数体）— V2 新增 / V3 重构 |
| `modules/ai/hooks/stream-utils.ts` | ~120 | SSE 流解析工具（consumeSseStream/getStreamErrorKey 等）— V3 新增 |
| `modules/ai/hooks/use-ai-suggestion.ts` | ~72 | AI 建议 Hook |
| `app/api/ai/chat/stream/route.ts` | ~160 | SSE 流式端点 — V2 新增 |
| `app/api/notifications/stream/route.ts` | ~120 | 通知实时推送 SSE 端点 — V2-P3 新增 |

**i18n**：
- 翻译文件：`shared/i18n/messages/{locale}/ai.json`
- 命名空间：`ai`
- V2 新增键：`chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts`、`grading.description/batch*`、`lessonPrep.description/additionalContext/insertContent`、`exam.variantType.*/targetDifficulty/addVariant`、`parent.*`、`admin.*`、`studyPath.*`、`widget.*`、`safety.*`
- V4 新增键：`chat.suggestedPrompts.context.{teacherGrading,teacherLesson,teacherExam,studentHomework}`（上下文感知提示词 i18n 化）

---

## 2.30 adaptive-practice（专项练习模块）— ✅ 新增（核心教学链路闭环）

**职责**：实现"错题集 → 知识点 → 专项出题 → 学生答题 → 自动判分 → 教师/年级主任宏观数据分析"的完整闭环。支持四种出题策略：错题变式、知识点专项、薄弱章节、AI 推荐。提供教师端与年级主任端的宏观数据分析工作台。

**架构定位**：
- 位于 `modules/` 层，严格遵循三层架构
- 通过 `data-access.ts` 提供 CRUD 操作，通过 `data-access-strategy.ts` 实现出题策略，通过 `data-access-analytics.ts` 提供教师/年级分析查询
- 跨模块通信：通过 `modules/error-book/data-access` 获取错题数据，通过 `modules/questions/data-access` 获取题目数据，通过 `modules/classes/data-access` 获取班级/学生数据，通过 `modules/users/data-access` 获取用户数据
- Server Actions 通过 `requirePermission()` 校验 `ADAPTIVE_PRACTICE_READ` / `ADAPTIVE_PRACTICE_MANAGE` 权限

**核心导出**：

| 类型 | 名称 | 文件 | 说明 |
|------|------|------|------|
| **DB Schema** | `practiceSessions` | `shared/db/schema.ts` | 练习会话表（学生 ID/类型/状态/统计） |
| **DB Schema** | `practiceAnswers` | `shared/db/schema.ts` | 练习答题记录表（含变式题支持） |
| **Server Actions** | `getPracticeSessionsAction` | `modules/adaptive-practice/actions.ts` | 获取练习列表（权限：ADAPTIVE_PRACTICE_READ） |
| **Server Actions** | `getPracticeSessionDetailAction` | `modules/adaptive-practice/actions.ts` | 获取练习详情（权限：ADAPTIVE_PRACTICE_READ） |
| **Server Actions** | `getPracticeStatsAction` | `modules/adaptive-practice/actions.ts` | 获取练习统计（权限：ADAPTIVE_PRACTICE_READ） |
| **Server Actions** | `createPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 创建练习会话（权限：ADAPTIVE_PRACTICE_MANAGE） |
| **Server Actions** | `submitPracticeAnswerAction` | `modules/adaptive-practice/actions.ts` | 提交答案+自动判分（权限：ADAPTIVE_PRACTICE_MANAGE） |
| **Server Actions** | `completePracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 完成练习（权限：ADAPTIVE_PRACTICE_MANAGE） |
| **Server Actions** | `abandonPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 放弃练习（权限：ADAPTIVE_PRACTICE_MANAGE） |
| **Data Access** | `getPracticeSessions` / `getPracticeSessionById` | `data-access.ts` | 学生端 CRUD |
| **Data Access** | `createPracticeSession` | `data-access.ts` | 创建会话（事务：会话+答题记录） |
| **Data Access** | `submitPracticeAnswer` | `data-access.ts` | 提交答案+自动判分 |
| **Data Access** | `autoGradeAnswer` | `data-access.ts` | 自动判分逻辑（选择题/判断题自动，填空题返回 null） |
| **Strategy** | `selectQuestionsForPractice` | `data-access-strategy.ts` | 出题策略主入口 |
| **Strategy** | `selectForErrorVariant` | `data-access-strategy.ts` | 错题变式策略 |
| **Strategy** | `selectForKnowledgePoint` | `data-access-strategy.ts` | 知识点专项策略 |
| **Strategy** | `selectForWeakChapter` | `data-access-strategy.ts` | 薄弱章节策略 |
| **Strategy** | `selectForAiRecommended` | `data-access-strategy.ts` | AI 推荐策略 |
| **Analytics** | `getClassPracticeStats` | `data-access-analytics.ts` | 班级练习统计 |
| **Analytics** | `getClassStudentPracticeSummaries` | `data-access-analytics.ts` | 班级学生练习摘要 |
| **Analytics** | `getGradePracticeStats` | `data-access-analytics.ts` | 年级练习统计 |
| **Analytics** | `getPracticeTypeBreakdown` | `data-access-analytics.ts` | 练习类型分布 |
| **Analytics** | `getStudentsWithoutPractice` | `data-access-analytics.ts` | 识别未参与练习学生 |
| **Analytics** | `getTeacherClassPracticeOverviews` | `data-access-analytics.ts` | 教师所教班级练习概览 |
| **Analytics** | `getClassKnowledgePointWeakness` | `data-access-analytics.ts` | 班级知识点薄弱度（基于练习答题） |
| **Analytics** | `getGradeClassPracticeComparison` | `data-access-analytics.ts` | 年级各班级练习对比 |
| **Analytics** | `getClassLearningProfile` | `data-access-analytics.ts` | 班级综合学习画像（跨模块整合） |
| **Analytics** | `getGradePracticeOverview` | `data-access-analytics.ts` | 年级综合练习统计 |
| **Component** | `PracticeStarter` | `components/practice-starter.tsx` | 练习发起器（四种模式选择；V4 重构：通过 `usePracticeService()` 调用 actions，a11y 增强，原生 input → shadcn Checkbox，reason 改为枚举值） |
| **Component** | `PracticeStarterWithNav` | `components/practice-starter-with-nav.tsx` | V4 新增：客户端路由导航包装器，注入 `onSessionCreated` 回调，使 Server Component 页面可直接渲染 PracticeStarter |
| **Component** | `PracticeSessionView` | `components/practice-session-view.tsx` | 答题界面（逐题作答+自动判分；V4 重构：通过 `usePracticeService()` 调用 actions，埋点接入 `usePracticeAnalytics()`，`onAbandoned` 回调注入路由跳转） |
| **Component** | `PracticeHistory` | `components/practice-history.tsx` | 练习历史列表（V4：新增 `routePrefix?` 可选 prop，未传时渲染只读卡片，供家长端复用） |
| **Component** | `PracticeStatsCards` | `components/practice-stats-cards.tsx` | 学生端统计卡片 |
| **Component** | `PracticeOverviewStatsCards` | `components/practice-overview-stats-cards.tsx` | 教师/年级端统计卡片 — 新增 |
| **Component** | `ClassPracticeComparisonTable` | `components/class-practice-comparison-table.tsx` | 班级练习对比表格 — 新增 |
| **Component** | `PracticeTypeBreakdownChart` | `components/practice-type-breakdown-chart.tsx` | 练习类型分布柱状图 — 新增 |
| **Component** | `ClassKnowledgePointWeaknessChart` | `components/class-knowledge-point-weakness-chart.tsx` | 知识点薄弱度柱状图 — 新增 |
| **Component** | `StudentPracticeRankingTable` | `components/student-practice-ranking-table.tsx` | 学生练习排名表格 — 新增 |
| **Component** | `InactiveStudentsAlert` | `components/inactive-students-alert.tsx` | 未参与练习学生提醒 — 新增 |
| **Service (DI)** | `PracticeService` / `PracticeServiceProvider` / `usePracticeService` / `usePracticeAnalytics` | `services/practice-service.tsx` | V4 新增：依赖注入基础设施（接口 + 默认实现代理 actions + Provider + hooks），参考 dashboard-service.tsx 模式；未包裹 Provider 时回退默认实现 |
| **Lib** | `grading` / `source-meta` / `type-guards` | `lib/grading.ts` / `lib/source-meta.ts` / `lib/type-guards.ts` | V4 提取：纯函数库（判分逻辑、sourceMeta 类型守卫、PracticeType 类型守卫），便于单测 |

**集成点**：

| 业务模块 | 集成组件 | 页面 | 说明 |
|---------|---------|------|------|
| error-book | `createPracticeSessionAction` | `student/error-book` 详情弹窗 | 错题详情页"发起变式练习"按钮（通过 `onStartVariantPractice` props 注入，已解耦） |
| student | `PracticeStarterWithNav` / `PracticeSessionView` | `student/practice` / `student/practice/[sessionId]` | 学生端练习入口与答题（V4：通过 `PracticeServiceProvider` 注入 service） |
| teacher | `PracticeOverviewStatsCards` 等 | `teacher/practice` | 教师端专项练习分析（V4：班级筛选改用 `shared/components/class-filter`，移除对 error-book 的跨模块依赖） |
| management | `PracticeOverviewStatsCards` 等 | `management/grade/practice` | 年级主任宏观数据分析 |
| parent | `PracticeStatsCards` / `PracticeHistory`（`routePrefix=undefined`） | `parent/practice` | V4 新增（2026-06-25 P0-修复-11）：家长查看子女练习统计与历史，只读视图，PracticeServiceProvider 注入 |

**依赖关系**：
- `modules/adaptive-practice` → `shared/db`（schema: practiceSessions/practiceAnswers/questions/knowledgePoints 等）
- `modules/adaptive-practice` → `shared/lib/auth-guard`（权限校验）
- `modules/adaptive-practice` → `shared/types/permissions`（权限常量）
- `modules/adaptive-practice` → `shared/types/action-state`（返回值类型）
- `modules/adaptive-practice` → `modules/classes/data-access`（getActiveStudentIdsByClassId/getClassNameById/getClassesByGradeId/getClassIdsByGradeIds/getStudentIdsByClassIds）
- `modules/adaptive-practice` → `modules/users/data-access`（getUserIdsByGradeId/getUserNamesByIds）
- `modules/adaptive-practice` → `modules/questions/data-access`（题目查询，出题策略使用）
- `modules/adaptive-practice` → `modules/error-book/data-access`（错题数据，错题变式策略使用）
- `app/(dashboard)/student/practice` → `modules/adaptive-practice`（学生端页面；V4：通过 `PracticeServiceProvider` 注入 PracticeService）
- `app/(dashboard)/teacher/practice` → `modules/adaptive-practice`（教师端分析页面）— 新增；V4：班级筛选改用 `shared/components/class-filter`（通用组件，`ClassFilterItem` 接口 + 预翻译文本 props），**不再依赖** `modules/error-book/components/class-filter`
- `app/(dashboard)/management/grade/practice` → `modules/adaptive-practice`（年级主任分析页面）— 新增
- `app/(dashboard)/parent/practice` → `modules/adaptive-practice`（家长端查看子女练习）— V4 新增（2026-06-25 P0-修复-11）
- `modules/error-book/components/error-book-detail-dialog` → `modules/adaptive-practice/actions`（变式练习入口，通过 `onStartVariantPractice` props 注入，已解耦）

**权限点**：
- `ADAPTIVE_PRACTICE_READ`：查看练习数据（admin/teacher/student/parent/grade_head/teaching_head 均有）
- `ADAPTIVE_PRACTICE_MANAGE`：创建/提交/完成/放弃练习（仅 student 有）

**自动判分逻辑**：
- `single_choice`：学生答案 ID 与正确答案 ID 完全匹配
- `multiple_choice`：学生答案 ID 集合与正确答案 ID 集合完全匹配（集合比对）
- `judgment`：学生布尔答案与正确布尔答案一致
- `text`（填空题）：不自动判分，返回 `null`，待教师批阅

**出题策略**：
- `error_variant`：按难度升序选取原题（确保 AI 不可用时也能练习）
- `knowledge_point`：按知识点+难度筛选，随机抽取
- `weak_chapter`：排除已答题目，避免重复
- `ai_recommended`：从 AI 推荐知识点抽题

**文件清单**：

| 文件 | 行数 | 职责 |
|------|------|------|
| `modules/adaptive-practice/types.ts` | ~143 | 类型定义（PracticeType/PracticeSourceMeta 联合类型等；V4：WeakChapterSourceMeta.chapterId 改为可选） |
| `modules/adaptive-practice/schema.ts` | ~75 | Zod 验证 schema（V4：discriminatedUnion） |
| `modules/adaptive-practice/data-access.ts` | ~620 | CRUD 操作+自动判分逻辑（V4：类型守卫替换 as、submitPracticeAnswer 事务化、completePracticeSession 校验答题完整性、抛 PracticeError） |
| `modules/adaptive-practice/data-access-strategy.ts` | ~343 | 四种出题策略实现（V4：移除 as WeakChapterSourceMeta 断言） |
| `modules/adaptive-practice/data-access-analytics.ts` | ~634 | 教师/年级宏观数据分析查询（V4：N+1 优化，单 SQL GROUP BY） |
| `modules/adaptive-practice/actions.ts` | ~267 | 7 个 Server Actions（V4：i18n 错误码返回 + dataScope 下 studentId 归属校验） |
| `modules/adaptive-practice/lib/grading.ts` | ~40 | V4 新增：纯函数判分逻辑 |
| `modules/adaptive-practice/lib/source-meta.ts` | ~60 | V4 新增：sourceMeta 类型守卫 + parseSourceMeta |
| `modules/adaptive-practice/lib/type-guards.ts` | ~30 | V4 新增：isPracticeType / asPracticeType / asPracticeSourceMeta |
| `modules/adaptive-practice/services/practice-service.tsx` | ~150 | V4 新增：依赖注入基础设施（PracticeService 接口 + 默认实现 + Provider + hooks + PracticeAnalytics 接口） |
| `modules/adaptive-practice/components/practice-starter.tsx` | ~330 | 练习发起器（V4：usePracticeService 注入 + Checkbox + a11y + 枚举 reason + onSessionCreated 回调） |
| `modules/adaptive-practice/components/practice-starter-with-nav.tsx` | ~25 | V4 新增：路由导航包装器（注入 routePrefix 跳转） |
| `modules/adaptive-practice/components/practice-session-view.tsx` | ~550 | 答题界面（V4：拆分为 5 个子组件 + usePracticeService 注入 + 埋点） |
| `modules/adaptive-practice/components/practice-history.tsx` | ~110 | 练习历史列表（V4：新增 routePrefix 可选 prop） |
| `modules/adaptive-practice/components/practice-stats-cards.tsx` | ~73 | 学生端统计卡片 |
| `modules/adaptive-practice/components/practice-overview-stats-cards.tsx` | ~99 | 教师/年级端统计卡片 — 新增 |
| `modules/adaptive-practice/components/class-practice-comparison-table.tsx` | ~88 | 班级练习对比表格 — 新增 |
| `modules/adaptive-practice/components/practice-type-breakdown-chart.tsx` | ~123 | 练习类型分布柱状图 — 新增 |
| `modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx` | ~144 | 知识点薄弱度柱状图 — 新增 |
| `modules/adaptive-practice/components/student-practice-ranking-table.tsx` | ~112 | 学生练习排名表格 — 新增 |
| `modules/adaptive-practice/components/inactive-students-alert.tsx` | ~64 | 未参与练习学生提醒 — 新增 |
| `app/(dashboard)/student/practice/page.tsx` | - | 学生端练习列表页（V4：注入 PracticeServiceProvider + routePrefix） |
| `app/(dashboard)/student/practice/[sessionId]/page.tsx` | - | 学生端答题页（V4：注入 PracticeServiceProvider） |
| `app/(dashboard)/teacher/practice/page.tsx` | ~250 | 教师端专项练习分析页（V4：改用 shared/components/class-filter，移除 error-book 依赖） |
| `app/(dashboard)/teacher/practice/loading.tsx` | - | 教师端加载骨架 |
| `app/(dashboard)/teacher/practice/error.tsx` | - | 教师端错误边界（V4：i18n 化） |
| `app/(dashboard)/management/grade/practice/page.tsx` | ~140 | 年级主任宏观数据分析页 |
| `app/(dashboard)/management/grade/practice/loading.tsx` | - | 年级端加载骨架 |
| `app/(dashboard)/management/grade/practice/error.tsx` | - | 年级端错误边界（V4：i18n 化） |
| `app/(dashboard)/parent/practice/page.tsx` | ~135 | V4 新增（2026-06-25）：家长查看子女练习统计与历史 |
| `app/(dashboard)/parent/practice/loading.tsx` | - | V4 新增：家长端加载骨架 |
| `app/(dashboard)/parent/practice/error.tsx` | - | V4 新增：家长端错误边界（i18n） |
| `shared/components/class-filter.tsx` | ~50 | V4 新增：通用班级筛选器（`ClassFilterItem` 接口 + 预翻译文本 props），消除 teacher/practice 对 error-book 的跨模块依赖 |

**i18n**：
- 翻译文件：`shared/i18n/messages/{locale}/practice.json`
- 命名空间：`practice`
- 键：
  - `starter.*`（发起器；V4：a11y aria-label）
  - `session.*`、`result.*`、`history.*`、`stats.*`、`types.*`、`status.*`
  - `teacher.*`（含 overview/classComparison/typeBreakdown/knowledgePointWeakness/studentRanking/inactiveStudents）
  - `grade.*`（含 overview/classComparison）
  - V4 新增：`parent.*`（家长端：title/description/noChild/noChildDescription/stats/history/noChildSelected）
  - V4 新增：`errors.*`（结构化错误码：session_not_found/session_ended/answer_not_found/answer_already_submitted/question_not_found/session_not_complete/no_questions_found/forbidden_scope/invalid_input/validation_error/invalid_source_meta；页面错误：pageErrorPractice/pageErrorGrade/pageErrorParent + pageErrorTitle* + loadFailed/loadFailedDescription/retry）
  - V4 新增：`messages.*`（用户可见操作反馈：sessionCreated/answerSubmitted/answerCorrect/answerIncorrect/answerSkipped/sessionCompleted/sessionAbandoned）
  - V4 新增：`reasons.*`（AI 推荐理由枚举翻译：student_initiated/teacher_assigned/parent_suggested）
  - V4 新增：`classFilter.*`（班级筛选器徽章：sessionCount plural 规则）

**数据库表**：
- `practice_sessions`：练习会话表（id/studentId/subjectId/practiceType/sourceMeta/status/totalQuestions/answeredQuestions/correctCount/startedAt/completedAt）
- `practice_answers`：练习答题记录表（id/sessionId/studentId/questionId/variantContent/isVariant/orderIndex/status/studentAnswer/isCorrect/score/maxScore）

---

## 2.31 rbac（角色与权限管理模块）— ✅ 新增

**职责**：角色与权限管理（RBAC）。提供角色 CRUD、角色启停、角色-权限分配、用户-角色分配、权限目录查询能力，支撑管理员对系统权限体系的动态化管理。

**架构定位**：
- 位于 `modules/` 层，严格遵循三层架构
- 通过 `data-access.ts` 提供角色 CRUD，通过 `data-access-assignments.ts` 提供用户-角色分配查询，通过 `lib/permission-catalog.ts` 提供权限元数据目录
- Server Actions 通过 `requirePermission()` 校验 `ROLE_CREATE` / `ROLE_READ` / `ROLE_UPDATE` / `ROLE_DELETE` / `ROLE_ASSIGN` / `PERMISSION_READ` 权限
- 与 `shared/types/permissions.ts` 协作：`Role` 类型从固定联合类型改为 `string`，支持动态角色；`BUILTIN_ROLES` 常量 + `isBuiltinRole()` 类型守卫区分内置角色与自定义角色
- 与 `shared/lib/permissions.ts` 协作：`resolvePermissions` 改为 async 从 DB 读取角色权限；`ROLE_PERMISSIONS` 重命名为 `ROLE_PERMISSIONS_SEED`（保留旧名 re-export 兼容）

**导出函数**：
- Actions：`createRoleAction` / `updateRoleAction` / `deleteRoleAction` / `toggleRoleEnabledAction` / `setRolePermissionsAction` / `assignUserRolesAction` / `getRoleDetailAction` / `isAdminRole`（✅ 每个 Action 调用 `requirePermission()` 进行权限校验；内置角色不可删除/改名的校验在 data-access 层；P2-7 新增 `setRolePermissionsAction` / `assignUserRolesAction` 调用 `trackPermissionChange` 埋点）
- Data-access（`data-access.ts`）：`getRoles` / `getRoleById`（✅ P2-3 新增返回 `userCount` 字段）/ `getRoleByName` / `createRole` / `updateRole` / `deleteRole` / `setRoleEnabled` / `getRolePermissions` / `setRolePermissions` / `ADMIN_ROLE_NAME`
- Data-access（`data-access-assignments.ts`）：`getUserRoleNames` / `assignRolesToUser` / `getUserRoleAssignments`
- Lib（`lib/permission-catalog.ts`）：`PERMISSION_CATALOG`（权限目录常量，按模块分组）/ `getAllPermissionMetas`（获取所有权限元数据）/ `getPermissionMeta`（按权限点获取元数据）
- Lib（`lib/track.ts`）：`trackPermissionChange`（✅ P2-7 新增：RBAC 关键操作埋点接口，no-op 实现，预留未来 analytics 接入）/ `TrackPermissionChangeParams` 接口

**依赖关系**：
- 依赖：`shared/db`（schema: roles/usersToRoles/rolePermissions）、`shared/types/permissions`（权限常量与 Role 类型）、`shared/lib/auth-guard`（权限校验）、`shared/lib/audit-logger`（操作日志）、`shared/lib/change-logger`（数据变更日志）
- 被依赖：`app/(dashboard)/admin/roles`、`app/(dashboard)/admin/permissions`、`app/(dashboard)/admin/users`（用户-角色分配）、`modules/users`（通过 UserRoleAssignDialog 组件）

**已知问题**：
- ✅ 职责清晰，data-access 按职责拆分为 `data-access.ts`（角色 CRUD）与 `data-access-assignments.ts`（用户-角色分配）
- ✅ 内置角色保护：`ADMIN_ROLE_NAME` 常量标识管理员角色，`isSystem` 字段标记系统内置角色，禁止删除/改名
- ✅ 角色启停：`isEnabled` 字段控制角色启用/停用，停用角色不参与权限解析

**文件清单**：

| 文件 | 职责 |
|------|------|
| `modules/rbac/actions.ts` | 8 个 Server Action（含权限校验 + 审计日志 + 变更日志 + P2-7 埋点） |
| `modules/rbac/data-access.ts` | 角色 CRUD + 角色权限分配（getRoles/getRoleById[含userCount]/createRole/updateRole/deleteRole/setRoleEnabled/getRolePermissions/setRolePermissions） |
| `modules/rbac/data-access-assignments.ts` | 用户-角色分配（getUserRoleNames/assignRolesToUser/getUserRoleAssignments） |
| `modules/rbac/lib/permission-catalog.ts` | 权限目录常量 + 元数据查询（PERMISSION_CATALOG/getAllPermissionMetas/getPermissionMeta） |
| `modules/rbac/lib/track.ts` | ✅ P2-7 新增：RBAC 关键操作埋点接口（trackPermissionChange，no-op 实现） |

**组件清单**：

| 组件 | 职责 |
|------|------|
| `components/role-list.tsx` | 角色列表（含启停切换、删除；✅ P2-5 新增 caption/aria-label） |
| `components/role-form-dialog.tsx` | 角色创建/编辑表单对话框 |
| `components/role-permission-matrix.tsx` | 角色-权限分配矩阵（按模块分组的权限勾选；✅ P2-1 搜索+折叠；P2-3 userCount 影响提示；P2-5 fieldset/legend） |
| `components/permission-catalog-view.tsx` | 权限目录浏览视图（只读，展示所有权限点元数据；✅ P2-5 aria-label） |
| `components/user-role-assign-dialog.tsx` | 用户-角色分配对话框（导出 `UserRoleAssignDialog` + `AssignableRole` 类型；✅ P2-5 aria-label） |
| `components/role-management-view.tsx` | 角色管理主视图（列表 + 创建按钮） |
| `components/role-detail-edit-button.tsx` | 角色详情页编辑按钮 |

**路由清单**：

| 路由 | 文件 | 说明 |
|------|------|------|
| `/admin/roles` | `app/(dashboard)/admin/roles/page.tsx` | 角色管理列表页（权限：ROLE_READ） |
| `/admin/roles/[id]` | `app/(dashboard)/admin/roles/[id]/page.tsx` | 角色详情页（权限：ROLE_READ，含权限矩阵） |
| `/admin/permissions` | `app/(dashboard)/admin/permissions/page.tsx` | 权限目录浏览页（权限：PERMISSION_READ） |

**权限点**：
- `ROLE_CREATE`：创建角色（`role:create`）
- `ROLE_READ`：查看角色（`role:read`）
- `ROLE_UPDATE`：更新角色（`role:update`）
- `ROLE_DELETE`：删除角色（`role:delete`）
- `ROLE_ASSIGN`：分配用户角色（`role:assign`）
- `PERMISSION_READ`：查看权限目录（`permission:read`）

**数据库表**：
- `roles`（✅ 字段更新）：id / name / description / createdAt / updatedAt / **isSystem**（boolean DEFAULT FALSE，标记内置角色）/ **isEnabled**（boolean DEFAULT TRUE，角色启停开关）
- 迁移文件：`drizzle/0011_rbac_role_flags_and_seed.sql`

**i18n**：
- 翻译文件：`shared/i18n/messages/{zh-CN,en}/rbac.json`
- 命名空间：`rbac`

---

## 2.32 invitation-codes（邀请码模块）— ✅ audit-P2-3 新增

**职责**：批量生成邀请码替代开放注册，支持预分配角色/班级/限定邮箱/过期时间；注册流程集成邀请码校验与消费（乐观锁），管理员可生成、列表、删除邀请码。

**导出函数**：
- Actions（`actions.ts`）：
  - `generateInvitationCodesAction(input)` — 批量生成邀请码（USER_MANAGE 权限，Zod 校验 1-100 张/批，trackEvent `invitation_codes.generated`，返回明文 codes 仅此一次）
  - `listInvitationCodesAction()` — 列出邀请码（USER_MANAGE 权限，含已使用+未使用，按 createdAt desc，最多 200 条）
  - `deleteInvitationCodeAction(codeId)` — 删除邀请码（USER_MANAGE 权限，仅允许删除未使用，已使用保留作审计记录）
  - `validateInvitationCodeAction(code, email)` — 校验邀请码（公开接口，注册流程预检用，不返回明文）
- Data-access（`data-access.ts`）：
  - `generateInvitationCodes(input, createdBy)` — 批量插入 + 查询回记录
  - `listInvitationCodes(limit, includeUsed)` — 列表查询
  - `validateInvitationCode(code, email)` — 校验（存在/已用/过期/邮箱匹配 4 项）
  - `consumeInvitationCode(input)` — 乐观锁消费（`UPDATE ... WHERE usedBy IS NULL` + `affectedRows` 检查）
  - `deleteInvitationCode(codeId)` — 删除（仅未使用）
  - `purgeExpiredInvitationCodes()` — 清理已过期且未使用（cron 预留）
- Lib（`lib/code-generator.ts`）：
  - `generateInvitationCode()` — 8 字符邀请码（`node:crypto.randomBytes` + 安全字母表 `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`，剔除 I/O/0/1）
  - `generateUniqueInvitationCodes(count)` — 批量生成（Set 去重 + 最多 3× 重试）
  - `generateBatchId()` — 批次 ID（`batch_<timestamp36>_<random6hex>`）
- Schema（`schema.ts`）：`GenerateInvitationCodesSchema`（count 1-100、role enum、可选 email/classId/expiresAt/notes）、`InvitationCodeFieldSchema`（嵌入 RegisterSchema，8-64 字符大写字母数字）、`INVITATION_ROLE_VALUES`
- Types（`types.ts`）：`InvitationRole` / `InvitationCodeRecord` / `GenerateInvitationCodesInput` / `GenerateInvitationCodesResult` / `ValidateInvitationCodeResult` / `ConsumeInvitationCodeInput`
- Components：
  - `InvitationCodesView` — 管理视图（统计卡片 + 表格 + 删除确认 + 生成对话框入口，接受 `initialCodes` + `now` props 由 Server Component 注入避免 `Date.now()` 在 render 调用）
  - `GenerateInvitationCodesDialog` — 生成对话框（form/result 两态切换，result 视图仅此一次展示明文供复制）

**依赖关系**：
- 依赖 `shared/db`（drizzle 客户端）、`shared/db/schema`（invitationCodes 表）、`shared/lib/auth-guard`（requirePermission）、`shared/lib/track-event`、`shared/types/action-state`、`shared/types/permissions`
- 被依赖：`modules/auth/actions.ts`（registerAction 调用 validateInvitationCode + consumeInvitationCode）、`app/(dashboard)/admin/invitation-codes/page.tsx`（Server Component 调用 listInvitationCodes + 渲染 InvitationCodesView）

**已知问题**：
- ✅ audit-P2-3 已实施：~~系统开放注册缺少预分配角色/班级的机制~~ 完整邀请码模块 + 注册流程集成 + admin 管理页

**数据库表**：
- `invitation_codes`（✅ 新增）：id / code(varchar 64, unique) / email(varchar 255, nullable) / role(varchar 50) / class_id(varchar 128, nullable) / notes(varchar 500, nullable) / created_at(timestamp) / created_by(varchar 128, nullable) / expires_at(timestamp, nullable) / used_by(varchar 128, nullable) / used_at(timestamp, nullable)
- 索引：`invitation_codes_code_idx`(code) / `invitation_codes_email_idx`(email) / `invitation_codes_used_by_idx`(used_by)
- 迁移文件：`drizzle/0016_invitation_codes.sql`

**i18n**：
- 翻译文件：`shared/i18n/messages/{zh-CN,en}/invitation-codes.json`
- 命名空间：`invitationCodes`
- 注册位置：`src/i18n/request.ts`（Promise.all 数组 + messages 对象）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 135 | Server Actions（generate/list/delete/validate，USER_MANAGE 权限，trackEvent 埋点） |
| `data-access.ts` | 247 | 数据访问层（generate/list/validate/consume/delete/purge，乐观锁消费模式） |
| `schema.ts` | 45 | Zod 验证（GenerateInvitationCodesSchema + InvitationCodeFieldSchema + INVITATION_ROLE_VALUES） |
| `types.ts` | 65 | 类型定义（InvitationRole / InvitationCodeRecord / Generate/Validate/Consume 接口） |
| `lib/code-generator.ts` | 67 | 邀请码生成器（8 字符安全字母表 + Set 去重 + 批次 ID） |
| `components/invitation-codes-view.tsx` | 302 | 管理视图（统计卡片 + 表格 + 删除确认 + 生成对话框入口） |
| `components/generate-invitation-codes-dialog.tsx` | 387 | 生成对话框（form/result 两态，结果可复制全部/单个） |

**路由**：
- `/admin/invitation-codes` — 管理员邀请码管理页（USER_MANAGE 权限，Server Component → InvitationCodesView 客户端组件）

---

## 2.33 leave-requests（在线请假模块）— ✅ L-5 新增

**职责**：学生/家长代子女提交请假申请 → 教师/年级主任/教务主任审批 → 审批通过自动同步 excused 考勤记录（幂等）→ 申请人 pending 状态可撤销。

**状态机**：`pending → approved/rejected`；`pending/approved → cancelled`（仅 requester 本人可撤销 pending）。

**导出函数**：
- Actions（`actions.ts`）：
  - `createLeaveRequestAction(prevState, formData)` — 创建请假申请（LEAVE_REQUEST_CREATE 权限，Zod 校验；二次校验 requester-student 关系：家长代子女需在 `parentStudentRelations` 中、学生本人 `requesterId === studentId`；校验 classId 确为学生当前活跃班级；`hasOverlappingLeave` 防止日期重叠；trackEvent `leave_request.created`）
  - `reviewLeaveRequestAction(prevState, formData)` — 审批请假申请（LEAVE_REQUEST_REVIEW 权限；pending → approved/rejected，rejected 必填 reviewComment；审批通过后调用 `attendance/data-access.syncExcusedFromLeaveRequest` 同步考勤，try/catch 不阻断；`markAttendanceSynced` 标志位防重复；trackEvent `leave_request.reviewed`）
  - `cancelLeaveRequestAction(id)` — 撤销请假（LEAVE_REQUEST_CREATE 权限；仅 requester 本人的 pending 状态可撤销；trackEvent `leave_request.cancelled`）
- Data-access（`data-access.ts`）：
  - `getLeaveRequests(params)` — 分页查询（按 DataScope 过滤：all/class_taught/children/class_members/owned；class_members 由 currentUserId 过滤本人提交；批量查询学生/班级/提交人/审批人名称避免 N+1）
  - `getLeaveRequest(id, scope, currentUserId?)` — 单条详情（按 DataScope 校验访问权限）
  - `hasOverlappingLeave(studentId, startDate, endDate, excludeId?)` — 日期重叠校验（SQL `startDate <= endDate AND endDate >= startDate`，仅查 pending/approved）
  - `createLeaveRequest(data, requesterId)` — 创建（`new Date(startDate)` 转换字符串为 Date；状态默认 pending）
  - `reviewLeaveRequest(id, review, reviewerId)` — 审批（Drizzle `db.update()` 返回 ResultSetHeader，需重新 `db.select()` 获取更新行）
  - `cancelLeaveRequest(id, requesterId)` — 撤销（仅 requester 本人的 pending 状态）
  - `markAttendanceSynced(id)` — 标记 attendanceSynced=true，防止重复触发同步
  - `getLeaveRequestsByIds(ids)` — 按 ID 批量查询（按 startDate 升序）
  - `countPendingForClasses(classIds)` — 教师审批待办计数
- Schema（`schema.ts`）：`CreateLeaveRequestSchema`（含 `.refine()` 跨字段校验 endDate >= startDate）、`ReviewLeaveRequestSchema`（rejected 时 reviewComment 必填）
- Types（`types.ts`）：`LeaveType` / `LeaveStatus` / `LeaveRequest` / `LeaveRequestListItem` / `LeaveQueryParams` / `PaginatedLeaveResult` / `LeaveReviewInput`
- Components：
  - `LeaveRequestForm` — 请假表单（`useActionState` + `useFormStatus`）；家长模式 `childOptions` 下拉选择子女，学生模式 hidden input 写入 studentId/classId。**注意**：prop 名为 `childOptions` 而非 `children`，避免与 React 特殊 children prop 冲突触发 `react/no-children-prop` ESLint 规则
  - `LeaveRequestList` — 只读列表展示（STATUS_STYLES + STATUS_ICONS 状态徽章）
  - `LeaveReviewDialog` — 审批对话框（批准/拒绝按钮切换，拒绝必填 reviewComment）
  - `LeaveReviewList` — 教师审批列表（pending 项显示审批按钮，打开 LeaveReviewDialog）

**依赖关系**：
- 依赖：`shared/*`（db/auth-guard/track-event/types/permissions）、`@/auth`、`classes`（`getStudentActiveClass` 校验当前班级、`getClassNamesByIds` 列表展示）、`users`（`getUserNamesByIds` 列表展示学生/提交人/审批人姓名）、`parent`（`getParentStudentMapByStudentIds` 校验家长-子女关系）、`attendance`（`syncExcusedFromLeaveRequest` 审批通过后同步考勤）、`next-intl`
- 被依赖：`app/(dashboard)/{parent,student,teacher}/leave/page.tsx`（Server Component 直接调用 data-access 与渲染组件）

**数据库表**：
- `leave_requests`（✅ L-5 新增）：id / student_id / requester_id / class_id / leave_type(enum: sick/personal/family/other) / start_date(date) / end_date(date) / reason(text) / status(enum: pending/approved/rejected/cancelled, default pending) / reviewer_id(varchar, nullable) / review_comment(text, nullable) / reviewed_at(timestamp, nullable) / attendance_synced(boolean, default false) / created_at / updated_at
- 索引：`leave_requests_student_idx` / `leave_requests_requester_idx` / `leave_requests_class_idx` / `leave_requests_status_idx` / `leave_requests_reviewer_idx` / `leave_requests_class_status_idx`（复合）/ `leave_requests_student_status_idx`（复合）
- 外键：`lr_c_fk`(class_id → classes.id ON DELETE CASCADE) / `lr_s_fk`(student_id → users.id ON DELETE CASCADE) / `lr_rq_fk`(requester_id → users.id ON DELETE CASCADE) / `lr_rv_fk`(reviewer_id → users.id ON DELETE SET NULL)
- 迁移文件：`drizzle/0020_leave_requests.sql`（含 INSERT IGNORE role_permissions 为 admin/teacher/parent/student/grade_head/teaching_head 注入权限）

**权限**：
- `LEAVE_REQUEST_CREATE`（leave_request:create）— admin/parent/student
- `LEAVE_REQUEST_READ`（leave_request:read）— admin/teacher/parent/student/grade_head/teaching_head
- `LEAVE_REQUEST_REVIEW`（leave_request:review）— admin/teacher/grade_head/teaching_head

**i18n**：
- 翻译文件：`shared/i18n/messages/{zh-CN,en}/leave.json`
- 命名空间：`leave`
- 注册位置：`src/i18n/request.ts`（Promise.all 数组 + messages 对象）

**埋点事件**：
- `leave_request.created` / `leave_request.reviewed` / `leave_request.cancelled`（在 `shared/lib/track-event.ts` 的 EventName 联合类型中扩展）

**文件清单**：
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~180 | 3 个 Server Action（create/review/cancel，权限校验 + Zod + 跨模块校验 + trackEvent） |
| `data-access.ts` | ~330 | 数据访问层（CRUD + 分页 + DataScope 过滤 + 日期重叠校验 + 批量名称查询） |
| `schema.ts` | ~50 | Zod 校验（CreateLeaveRequestSchema + ReviewLeaveRequestSchema） |
| `types.ts` | ~70 | 类型定义（LeaveType/Status/Request/ListItem/Query/Result/ReviewInput） |
| `components/leave-request-form.tsx` | ~190 | 请假表单（useActionState + 家长/学生双模式） |
| `components/leave-request-list.tsx` | ~120 | 只读列表 + 状态徽章 |
| `components/leave-review-dialog.tsx` | ~150 | 审批对话框 |
| `components/leave-review-list.tsx` | ~100 | 教师审批列表 |

**路由**：
- `/parent/leave` — 家长请假页（LEAVE_REQUEST_CREATE + LEAVE_REQUEST_READ；DataScope.children；表单 childOptions 模式 + 列表）
- `/student/leave` — 学生请假页（LEAVE_REQUEST_CREATE + LEAVE_REQUEST_READ；DataScope.class_members 由 currentUserId 过滤；表单 defaultStudentId/classId 模式 + 列表）
- `/teacher/leave` — 教师审批页（LEAVE_REQUEST_REVIEW；DataScope.class_taught；LeaveReviewList + LeaveReviewDialog）

**设计要点**：
- ✅ **幂等考勤同步**：`syncExcusedFromLeaveRequest` 遍历日期区间，仅插入不存在的 excused 记录，避免重复同步；`attendanceSynced` 标志位防止审批重复触发
- ✅ **跨模块联动容错**：审批通过后调用考勤同步使用 try/catch 包裹，同步失败不阻断审批结果（审批已成功，考勤可由管理员后续手动补录）
- ✅ **日期重叠校验**：`hasOverlappingLeave` 使用 SQL 条件检测区间重叠，防止重复请假
- ✅ **DataScope 行级过滤**：`buildScopeFilter` 支持 5 种 scope（all/class_taught/children/class_members/owned），class_members 由 currentUserId 过滤本人提交
- ✅ **Drizzle update 返回值处理**：`db.update()` 返回 ResultSetHeader 而非行数据，需重新 `db.select()` 查询更新后的行
- ✅ **React children prop 冲突规避**：LeaveRequestForm 的 prop 名为 `childOptions` 而非 `children`，避免触发 `react/no-children-prop` ESLint 规则

---

# 第三部分：已知架构问题和技术债

## 3.1 P0 严重问题（必须立即修复）

### P0-1：文件超 1000 行硬上限（3 个文件）

| 文件 | 行数 | 问题 | 拆分建议 |
|------|------|------|---------|
| `classes/data-access.ts` | ~~2104~~ → 548 | ~~混入 homework/scheduling/grades 逻辑~~ ✅ 已拆分 | 已拆为 5 个文件：data-access.ts(548行) + data-access-stats.ts(531行) + data-access-schedule.ts(194行) + data-access-students.ts(244行) + data-access-admin.ts(406行)，通过 re-export 保持向后兼容 |
| `homework/data-access.ts` | ~~1038~~ → 626 | ~~混入排名计算业务逻辑~~ ✅ 已拆分 | 已拆为 data-access.ts(626行，2026-06-25 删除 re-export) + stats-service.ts(414行) + data-access-student.ts(293行) + data-access-scans.ts(44行)，统计函数迁移至 stats-service.ts |
| `shared/db/schema.ts` | 1111 | 54 张表混合 | 按业务域拆分为 schema/auth.ts + schema/academic.ts + schema/exam.ts + ...，通过 index.ts 聚合 |

### P0-2：shared/lib ↔ auth 循环依赖 ✅ 已修复

```
shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
```

**影响**：shared 层无法独立测试/复用；架构上基础设施不应反向依赖应用层。

**修复方案**（已实施）：
- ✅ 创建 `shared/lib/session.ts` 封装 session 获取（`getSession()`，server-only）
- ✅ `session.ts` 内部使用 `dynamic import("@/auth")` 打破模块级静态循环
- ✅ `audit-logger.ts` / `change-logger.ts` / `auth-guard.ts` 改为 `import { getSession } from "@/shared/lib/session"`，不再直接依赖 `@/auth`
- ✅ 运行时调用链保持不变，模块加载图无环

### P0-3：dashboard 跨模块直接查询 11 张表 ✅ 已修复

`dashboard/data-access.ts` 的 `getAdminDashboardData` 原直查 sessions/users/usersToRoles/roles/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions。

**修复方案**（已实施）：
- 各模块新增 dashboard stats 函数：
  - `users/data-access.ts` → `getUsersDashboardStats()`（userCount/activeSessionsCount/userRoleCounts/recentUsers）
  - `classes/data-access.ts` → `getClassesDashboardStats()`（classCount）
  - `textbooks/data-access.ts` → `getTextbooksDashboardStats()`（textbookCount/chapterCount）
  - `questions/data-access.ts` → `getQuestionsDashboardStats()`（questionCount）
  - `exams/data-access.ts` → `getExamsDashboardStats(scope?)`（examCount，支持 scope 过滤）
  - `homework/stats-service.ts` → `getHomeworkDashboardStats(scope?)`（4 个计数，支持 scope 过滤）
- dashboard 改为并行调用：`Promise.all([getUsersDashboardStats(), getClassesDashboardStats(), ...])`
- 返回值结构保持不变，调用方无需修改

### P0-4：messaging 绕过 notifications 直接写通知 ✅ 已修复

`messaging/actions.ts` 原直接调用 `createNotification` 写 `messageNotifications` 表，导致用户通知偏好失效、多渠道通知无效。`notifications/data-access.ts` 和 `in-app-channel.ts` 反向依赖 messaging 模块。

**修复方案**（已实施）：
- 将 `messageNotifications` 表的 CRUD 函数（`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount`）从 `messaging/data-access.ts` 迁移到 `notifications/data-access.ts`
- 将 `notificationPreferences` 表的 CRUD 函数（`getNotificationPreferences` / `upsertNotificationPreferences`）从 `messaging/notification-preferences.ts` 迁移到新文件 `notifications/preferences.ts`
- 将 `NotificationType` / `Notification` / `NotificationPreferences` / `UpdateNotificationPreferencesInput` / `CreateNotificationInput` / `GetNotificationsParams` / `PaginatedResult` 类型从 `messaging/types.ts` 迁移到 `notifications/types.ts`
- `notifications/channels/in-app-channel.ts` 改为静态导入本地 `createNotification`（不再动态 import messaging）
- `notifications/dispatcher.ts` 改为从 `notifications/preferences.ts` 导入 `getNotificationPreferences`（不再通过 `getUserNotificationPreferences` 包装器反向依赖 messaging）
- messaging 模块通过 re-export 保持向后兼容：`messaging/data-access.ts` re-export 通知 CRUD，`messaging/notification-preferences.ts` 转为 re-export shim，`messaging/types.ts` re-export 通知类型
- 依赖方向变为单向：messaging → notifications（messaging 调用 `sendNotification` dispatcher 发送通知）

### P0-5：classSchedule 表三处写入口 ✅ 已修复

- ~~`classes/data-access.ts`（createClassScheduleItem 等）~~
- ~~`scheduling/actions.ts`（applyAutoScheduleAction 直接 transaction 写入）~~
- ~~`scheduling/data-access.ts`（间接）~~

**影响**：数据完整性高风险。

**修复方案**（已实施）：
- 将 classSchedule 写函数（`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`）从 `classes/data-access-schedule.ts` 迁移到新文件 `scheduling/data-access-class-schedule.ts`
- classes 模块仅保留 READ 函数（`getStudentSchedule` / `getClassSchedule`），不再有任何 classSchedule 写入口
- scheduling 模块通过 `classes/data-access.verifyTeacherOwnsClass` 跨模块校验教师班级归属（合理依赖）
- `classes/actions.ts` 改为从 `@/modules/scheduling/data-access-class-schedule` 导入写函数
- 类型 `CreateClassScheduleItemInput` / `UpdateClassScheduleItemInput` 从 `classes/types.ts` 迁移到 `scheduling/types.ts`
- 所有 classSchedule DB 写入统一由 scheduling 模块管理（`insertClassScheduleItem` / `updateClassScheduleItemById` / `deleteClassScheduleItemById` / `replaceClassSchedule`）

### P0-6：proctoring 死代码与重复实现

- `exam-mode-config.tsx` 未集成到考试表单（监考功能无法启用）
- ~~事件上报存在 Server Action 与 REST API 双通道重复~~ ✅ 已修复

**修复方案**（已实施）：
- 将 `src/app/api/proctoring/event/route.ts` 移至 `deletes/api/proctoring/event/route.ts`，消除事件上报的 REST API 重复通道
- Server Action `recordProctoringEventAction`（`proctoring/actions.ts`）为唯一规范路径
- `exam-mode-config.tsx` 暂保留原位（集成到考试表单属于功能新增，不在本次修复范围）

### P0-7：proxy.ts 路由权限跨角色访问漏洞 ✅ 已修复（webapp-testing 发现）

**问题**：`proxy.ts` 中 `/teacher` 和 `/parent` 路由前缀都使用 `EXAM_READ` 权限校验，但 `student` 和 `parent` 角色也拥有 `EXAM_READ` 权限，导致：
- 学生可访问 `/teacher/*` 所有页面
- 教师可访问 `/parent/*` 所有页面

**修复方案**（已实施）：
- `/teacher` 改为 `Permissions.EXAM_CREATE`（仅 teacher/admin 拥有）
- `/parent` 改为 `Permissions.DASHBOARD_PARENT_READ`（仅 parent 拥有）
- 新增 `DASHBOARD_ROUTE_PERMISSIONS` 细粒度仪表盘权限表，覆盖各角色 dashboard 路由
- 跨角色访问测试验证：teacher/student/parent 访问其他角色路由均被重定向回各自 dashboard

### P0-8：list-pagination.tsx 客户端/服务端边界错误 ✅ 已修复（webapp-testing 发现）

**问题**：`shared/components/ui/list-pagination.tsx` 文件顶部声明 `"use client"`，但导出的 `computePagination()` 和 `paginate()` 纯工具函数被 4 个服务端组件页面直接调用：
- `teacher/attendance/page.tsx`
- `teacher/homework/assignments/page.tsx`
- `teacher/homework/submissions/page.tsx`
- `teacher/grades/page.tsx`

导致 Next.js 16 / React 19 抛出错误：`Attempted to call computePagination() from the server but computePagination is on the client`，页面渲染时触发 ErrorBoundary。

**修复方案**（已实施）：
- 移除 `list-pagination.tsx` 的 `"use client"` 指令
- `ListPagination` 组件仅使用 `Link`、`Button` 和图标，无需客户端交互，可作为服务端组件渲染
- `computePagination` 和 `paginate` 恢复为服务端可调用的纯函数

---

## 3.2 P1 较严重问题（短期执行）

### P1-1：跨模块直接 DB 查询普遍存在 ✅ 已修复

| 被访问表 | 访问次数 | 应归属模块 | 主要违规者 |
|---------|---------|-----------|-----------|
| `classes` | 8+ | classes | ✅ exams/homework/grades/dashboard 已改为通过 classes data-access |
| `classEnrollments` | 6+ | classes | ✅ homework/grades/attendance/users 已改为通过 classes data-access |
| `users` | 6+ | users | ✅ 多个模块已改为通过 users data-access |
| `subjects` | 6+ | school | ✅ exams/homework/questions/grades 已改为通过 school data-access |
| `exams` | 5+ | exams | ✅ homework/grades/dashboard/classes 已改为通过 exams data-access |
| `homeworkAssignments` | 5+ | homework | ✅ classes（反向直查）已改为通过 homework/data-access-classes |

**修复方案**（已实施）：
- ✅ 各模块 data-access 暴露查询接口：
  - `classes/data-access`：~~`getClassGradeIdsByClassIds`~~ ✅ 已实现 / `getClassStudentsByClassId` / `getActiveClassStudents` / `getClassExists` / `getClassNameById` / `getClassNamesByIds` / `getActiveStudentIdsByClassId` / `getStudentActiveClassId` / `getClassesByGradeId` / `verifyTeacherOwnsClass` / `getTeacherIdForMutations`
  - `exams/data-access`：~~`getExamForHomeworkCreation`~~ ✅ 已实现（`getExamIdsByGradeIds` / `getExamSubjectIdMap` / `getExamWithQuestionsForHomework` / `getExamSubmissionWithAnswers` / `getExamForProctoringCrossModule` / `getExamSubmissionForProctoringCrossModule` / `getExamSubmissionsForExam` / `getExamTitleById`）
  - `school/data-access`：~~`getSubjectOptions` / `getGradeOptions`~~ ✅ 已实现（含 `getSubjectNameMapByIds` 批量科目名称映射，P1-1 新增供 homework/data-access-classes 调用）
  - `users/data-access`：~~`getUserNameByIds` / `getStudentInfo`~~ ✅ 已实现（`getUserNamesByIds` / `getUserWithRole` / `getUserBasicInfo` / `getUserIdsByGradeId` / `getCurrentStudentUser`）
  - `textbooks/data-access`：~~`getKnowledgePointOptions`~~ ✅ 已实现
  - `questions/data-access`：~~`insertQuestionWithRelations`~~ ✅ 已通过 `createQuestionWithRelations` 供 exams 调用 / `getKnowledgePointsForQuestions`
  - `homework/data-access-classes`：✅ 新增 7 个函数供 classes 模块跨模块调用

### P1-2：actions 层混入数据访问逻辑 ✅ 已修复

**已完成修复**（2026-06-17，commit 84d6636）：4 个模块的 actions 层 DB 操作全部下沉到 data-access：

| 模块 | 问题 Action | 修复内容 |
|------|------------|---------|
| exams | `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` | ✅ 新增 7 个 data-access 函数（getExamCreatorId/updateExamWithQuestions/deleteExamById/duplicateExam/getExamPreview/getExamSubjects/getExamGrades），actions.ts 831→691 行，data-access.ts 374→471 行。✅ 2026-06-24 进一步拆分：actions.ts 1525→633 行（拆出 actions-helpers.ts/actions-rich-editor.ts/ai-pipeline/auto-mark.ts），P0-4 修复：新增 updateExamWithRichEditorData 下沉富文本编辑器更新事务 |
| homework | `createHomeworkAssignmentAction`（157 行）/ `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` | ✅ 新建 `data-access-write.ts`（285 行，10 个写函数），actions.ts 387→239 行 |
| questions | `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` | ✅ 新增 4 个 data-access 函数（createQuestionWithRelations/updateQuestionById/deleteQuestionByIdRecursive/getKnowledgePointOptions），actions.ts 294→149 行，data-access.ts 138→260 行；✅ 审计新增 3 个级联 data-access 函数（getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter）+ 3 个对应 Server Action + 批量删除/导入导出 data-access 与 Action，actions.ts 149→380+ 行，data-access.ts 260→600+ 行 |
| announcements | 所有写操作 Action | ✅ 新增 5 个 data-access 函数（insertAnnouncement/updateAnnouncementById/deleteAnnouncementById/publishAnnouncementById/archiveAnnouncementById），actions.ts 242→197 行，data-access.ts 120→171 行 |

**剩余未修复模块**（不在本次 P1-2 范围）：
- ✅ users：~~`updateUserProfileAction` 直接 db.update~~ 已下沉到 data-access（P1-1 修复）
- ✅ scheduling：~~`applyAutoScheduleAction` / `autoScheduleAction` 直接 db.transaction + db.select~~ 已改为调用 `replaceClassSchedule` 统一写入口；`autoScheduleAction` 直查 users 表已改为通过 users data-access（P0-5 / P1-1 修复）

### P1-3：auth.ts 混合 5 类职责 ✅ 已完成

`src/auth.ts` 原 293 行混合：NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数。

**已完成拆分**（auth.ts 现 193 行，仅保留 NextAuth 配置）：
- ✅ 密码安全 DB 操作 → `shared/lib/password-security-service.ts`（getOrCreatePasswordSecurity / recordFailedLogin / resetFailedLogin，server-only）
- ✅ 角色规范化 → `shared/lib/role-utils.ts`（normalizeRole / resolvePrimaryRole，纯函数）
- ✅ bcrypt 哈希规范化 → `shared/lib/bcrypt-utils.ts`（normalizeBcryptHash，纯函数）
- ✅ IP 解析 → `shared/lib/http-utils.ts`（resolveClientIp，server-only）

**后续可选优化**（audit-P1-3 已完成 ✅）：
- ✅ ~~`authorize` 回调可进一步拆分为 `checkRateLimit` / `checkAccountLockout` / `verifyPassword` / `loadUserRoles`，使 auth.ts 降至 ≤150 行~~ 已抽取至 `modules/auth/services/login-service.ts` 的 `authenticateUser()` 函数（7 步：输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载），auth.ts authorize 降至 3 行薄包装，整体文件 132 行

> **audit-P0 重构（2026-06-25）**：在 P1-3 拆分基础上进一步审计 auth.ts，识别并修复以下问题：
> - audit-P0-2：~~`auth.ts` 跨模块依赖 `modules/settings/actions-security.verifyTwoFactorForLogin`~~ 改为同模块 `modules/auth/services/two-factor-service.ts` 编排，2FA 仍通过 `modules/settings/data-access-two-factor` + `modules/settings/lib/totp` 获取数据（合理跨模块 data-access）
> - audit-P0-5：~~`auth.ts` 3 处 `as` 断言~~ 通过 `next-auth.d.ts` 扩展 `User` 接口（添加 `role?`/`roles?` 字段）+ `"token" in message` 类型收窄消除
> - audit-P0-6：~~`proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现角色路径解析~~ 提取 `shared/lib/route-resolver.ts` 纯函数 `resolveDefaultPath(roles)`，proxy.ts 与 onboarding/actions.ts 共用
> - audit-P0-7：~~e2e smoke-auth 测试选择器与实际中文 UI 不符~~ 修正为 `getByLabel("姓名")` / `getByLabel("邮箱")` / `getByLabel("密码")` / `getByRole("button", { name: "创建账户" })`

### P1-4：users/import-export.ts 四重职责 ✅ 已完成

`users/import-export.ts` 原 291 行混合：导入解析 + 导出 + 用户创建（含密码哈希）+ 班级注册（跨模块写 classEnrollments）。

**已完成拆分**（import-export.ts 现 157 行，仅保留文件解析/生成）：
- ✅ 用户创建（含密码哈希、角色分配）→ `user-service.ts`（`batchImportUsers`，82 行，server-only）
- ✅ 班级注册 → `class-registration.ts`（`registerStudentByInvitationCode`，21 行，server-only）
- ✅ `batchImportUsers` 不再直写 `classEnrollments`，改为调用 `classes/data-access.enrollStudentByInvitationCode`
- ✅ `import-export.ts` 通过 re-export `batchImportUsers` / `UserImportResult` 保持向后兼容（`actions.ts` 和 `app/api/export/route.ts` 无需修改）

### P1-5：notifications 反向依赖 messaging ✅ 已完成

`notifications/data-access.ts` 和 `in-app-channel.ts` 原反向依赖 messaging 模块的偏好和 createNotification。

**已完成修复**（与 P0-4 一并解决）：
- ✅ 将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块
- ✅ `notifications/data-access.ts` 不再 import messaging 模块
- ✅ `notifications/channels/in-app-channel.ts` 改为静态导入本地 `createNotification`（不再动态 import messaging）
- ✅ `notifications/dispatcher.ts` 改为从本地 `preferences.ts` 导入偏好函数
- ✅ messaging 模块通过 re-export 保持向后兼容

### P1-6：三个 logger 重复实现 IP/Header 提取 ✅ 已修复

`audit-logger.ts` / `change-logger.ts` / `login-logger.ts` / `auth.ts` 四处重复实现 IP/User-Agent 提取逻辑，且实现略有差异。

**修复方案**（已实施）：
- ✅ `shared/lib/http-utils.ts` 新增 `getUserAgent()` 函数（与已有 `resolveClientIp()` 配套）
- ✅ `audit-logger.ts` / `change-logger.ts` / `login-logger.ts` 改为从 `@/shared/lib/http-utils` 导入 `resolveClientIp` 和 `getUserAgent`，删除本地重复实现
- ✅ `auth.ts` 已在 P1-3 中改用 `resolveClientIp`
- ✅ 四处实现统一，消除不一致风险（`resolveClientIp` 取 `x-forwarded-for` 第一段，更准确）

---

## 3.3 P2 代码质量问题（机会修复）

| 序号 | 问题 | 模块 |
|------|------|------|
| P2-1 | `exams/ai-pipeline.ts` 857 行，混合 4 类职责 | exams |
| ~~P2-2~~ | ~~`exams/actions.ts` 832 行（超 800 建议）~~ ✅ 已修复（P1-2 后降至 691 行） | exams |
| ~~P2-3~~ | ~~`shared/lib/ai.ts` 218 行，混合 5 类职责~~ ✅ 已修复（P2-2 已拆分为 `ai/` 目录） | shared |
| ~~P2-4~~ | ~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ ✅ 已修复（迁移至 `modules/onboarding/`，改用独立路由 + Server Action + middleware 重定向） | shared/onboarding |
| P2-5 | `global-search.tsx` 业务类型硬编码在 shared | shared |
| ~~P2-6~~ | ~~`proxy.ts` 硬编码权限字符串，未复用 Permissions 常量~~ ✅ 已修复（改用 `Permissions` 常量） | proxy |
| ~~P2-7~~ | ~~`useA11yId` Hook 错放在 lib/ 而非 hooks/~~ ✅ 已修复（文件已不存在；`use-aria-live.ts` 已在 `hooks/` 目录） | shared |
| ~~P2-8~~ | ~~`schema.ts` 分节编号混乱（section 12 出现在 14b 之后）~~ ✅ 已修复（重新编号为连续 1-24） | shared/db |
| P2-9 | `audit/actions.ts` Excel 导出逻辑内联 | audit |
| P2-10 | school 模块审计日志不一致（仅 school 实体记录） | school |
| ~~P2-11~~ | ~~`announcements` 死代码 `void wasPublished`~~ ✅ 已修复（代码中已不存在） | announcements |
| ~~P2-12~~ | ~~`announcements` 权限模式不一致（requireAuth vs requirePermission）~~ ✅ 已修复 | announcements |
| P2-13 | ~~`files` try-catch 吞错误~~ ✅ 已修复（所有 catch 块已添加 console.error；conditions 隐式 any[] 改为 SQL[]） | files |
| ~~P2-14~~ | ~~`elective` runLottery 使用 Math.random~~ ✅ 已修复（改为 Fisher-Yates 无偏洗牌） | elective |
| ~~P2-15~~ | ~~`elective` selectCourse FCFS 并发超卖风险~~ ✅ 已修复（db.transaction + FOR UPDATE 行锁） | elective |
| P2-16 | `diagnostic` 班级报告 studentId 字段复用 | diagnostic |
| ~~P2-17~~ | ~~`layout` 用权限反推角色~~ ✅ 已修复（`app-sidebar.tsx` 改用 `hasRole()` 判断角色；N3 新增多角色切换机制：`SidebarContext.currentRole`/`setCurrentRole` + 侧边栏底部 Select 下拉） | layout |
| ~~P2-18~~ | ~~`scheduling/actions.ts` 末尾 re-export data-access~~ ✅ 已修复（移除 re-export，4 个页面改为从 `data-access` 导入） | scheduling |
| P2-19 | `ExamAssembly` / `ExamPreviewQuestionEditor` 10 个 props | exams |
| P2-20 | ~~`homework/data-access.getDemoStudentUser` 使用 `auth()` 而非 auth-guard~~ ✅ 已修复（已迁移至 `users/data-access.getCurrentStudentUser`，6 个 student 页面改用 users 模块；`elective` 页面改用 `getAuthContext()`；homework 保留 re-export 向后兼容） | homework |

---

## 3.4 解耦优先级路线图

### 立即执行（P0）
1. ~~拆分 `classes/data-access.ts`（2104 行 → 按职责拆 3-4 个文件）~~ ✅ 已完成（拆为 5 个文件：data-access.ts 548行 + data-access-stats.ts 531行 + data-access-schedule.ts 194行 + data-access-students.ts 244行 + data-access-admin.ts 406行）
2. ~~拆分 `homework/data-access.ts`（1038 行 → 分离排名逻辑）~~ ✅ 已完成（拆为 data-access.ts 626行 + stats-service.ts 414行 + data-access-student.ts 293行 + data-access-scans.ts 44行）
3. ~~修复 `shared/lib` ↔ `auth` 循环依赖~~ ✅ 已完成（新增 `shared/lib/session.ts` 单一入口，3 个 shared/lib 文件改为通过 getSession 获取 session，dynamic import 打破静态循环）
4. ~~dashboard 改为通过各模块 data-access 获取数据~~ ✅ 已完成（P0-3 修复：并行调用各模块 dashboard stats 函数）
5. ~~messaging 写通知改为通过 notifications dispatcher~~ ✅ 已完成（P0-4 / P1-5 修复：通知 CRUD 和偏好迁移至 notifications 模块，messaging 通过 dispatcher 发送通知）
6. ~~统一 classSchedule 写入口到 scheduling 模块~~ ✅ 已完成（P0-5 修复：classSchedule 写函数从 classes/data-access-schedule.ts 迁移至 scheduling/data-access-class-schedule.ts，classes 模块仅保留读函数）
7. ~~集成 proctoring/exam-mode-config 到考试表单~~ 部分完成（P0-6 修复：删除重复的 /api/proctoring/event REST 路由，Server Action 为唯一规范路径；exam-mode-config.tsx 集成属于功能新增，暂保留原位）

### 短期执行（P1）
8. ~~actions 层移除直接 DB 操作（exams/homework/questions/announcements/users/scheduling）~~ ✅ 已完成（全部 6 个模块均已修复）
9. ~~拆分 `auth.ts`~~ ✅ 已完成（4 个辅助函数组迁移至 shared/lib，auth.ts 保留 NextAuth 配置）
10. ~~拆分 `users/import-export.ts`~~ ✅ 已完成（拆为 import-export.ts 157行 + user-service.ts 82行 + class-registration.ts 21行，班级注册改为调用 classes/data-access）
11. ~~消除 notifications → messaging 反向依赖~~ ✅ 已完成（P0-4 / P1-5 修复：通知表所有权迁移至 notifications，in-app-channel 改为静态导入）
12. ~~提取 `shared/lib/http-utils.ts` 统一 IP 提取~~ ✅ 已完成（新增 `getUserAgent`，三个 logger 统一复用 `resolveClientIp` / `getUserAgent`）
13. ~~各模块暴露跨模块查询接口（见 P1-1）~~ ✅ 已完成（所有跨模块直查已改为通过对方 data-access 接口）

### 中期执行（P2）
14. ~~建立模块间数据访问规范（通过对方 data-access 或导出查询函数）~~ ✅ 已完成（P1-1 修复）
15. ~~`schema.ts` 按业务域分节~~ ✅ 已完成（P2-8 修复：重新编号为连续 1-24，消除 8b/14b/乱序问题）
16. 拆分 `exams/ai-pipeline.ts`
17. ~~拆分 `shared/lib/ai.ts`~~ ✅ 已完成（P2-2，commit 6588f74，拆分为 `ai/` 目录 6 个文件，原 ai.ts 保留为重导出）
18. shared 层业务逻辑下沉到 modules 层
19. 代码质量问题逐项修复（✅ 大部分已修复：React.cache 包装、Promise.all 并行化、错误吞没清理、非空断言清理、函数返回类型补齐、重复代码提取、合并 filter 遍历、Set/Map 优化、P2-6 proxy.ts 权限常量复用、P2-11 announcements 死代码清理、P2-17 layout 角色判断、P2-18 scheduling re-export 移除）

---

## 3.5 标杆实践（建议推广）

| 实践 | 模块 | 说明 |
|------|------|------|
| 算法纯函数化 | `scheduling/auto-scheduler.ts` | 无 DB 依赖，可独立测试，应作为算法抽取模板 |
| stats 文件拆分 | `attendance/data-access-stats.ts` | 统计逻辑独立成文件，classes 应效仿 |
| data-access 多文件拆分 | `grades/data-access*.ts` | 按职责拆分为 3 个文件（CRUD/分析/排名） |
| actions 辅助函数 | `course-plans/actions.ts` | handleError / revalidatePlanPaths 消除重复 |
| actions 编排模式 | `textbooks/actions.ts` | 权限校验 → 调用 data-access → revalidatePath（标杆） |
| DataScope 接入 | `attendance/actions.ts` | 6 种数据范围完整支持 |
| 权限统一接入 | school / attendance / course-plans | 全部 Action 使用 requirePermission |
| 跨模块解耦 | `grades` | 通过外键引用 exams/homework，不直接访问其表 |
| 渠道抽象 | `notifications/channels/` | 接口 + 工厂 + Mock 实现 |

---

## 3.6 教师端全模块审计修复（2026-06-23）

> 本次审计修复覆盖教师端 13 个模块组、约 80+ 个文件，聚焦越权访问漏洞、错误处理一致性、类型安全与 React 性能。以下仅记录涉及架构图同步的变更（函数签名/导出/新增文件），其余纯实现修复（如 try/catch 补齐、useMemo/useCallback、key prop、as 断言清理）不在此列出。

### 3.6.1 新增共享文件 `src/shared/lib/action-utils.ts`

统一 Server Action 错误处理与客户端 Action 调用模式，避免内部错误消息暴露给客户端。

| 导出 | 类型 | 签名 | 用途 |
|------|------|------|------|
| `handleActionError` | function | `(e: unknown) => ActionState<never>` | 统一 Server Action catch 块错误处理：PermissionDeniedError/BusinessError 返回其 message，其他 Error 返回通用消息并 console.error 记录 |
| `safeActionCall` | function | `<T>(action: () => Promise<ActionState<T>>, options?: { onError?, onFinally? }) => Promise<ActionState<T> \| null>` | 客户端调用 Server Action 的 try/catch/finally 包装器，防止 UI 永久卡 loading |
| `safeJsonParse` | function | `<T>(json: string, errorMessage: string) => T` | 安全 JSON.parse，失败抛 ValidationError（替代裸 JSON.parse） |
| `safeParseDate` | function | `(value: string, fieldName: string) => Date` | 校验日期字符串有效性，无效抛 ValidationError |
| `safeParseNumber` | function | `(value: string, fieldName: string) => number` | 校验数字字符串，无效抛 ValidationError |
| `escapeLikePattern` | function | `(input: string) => string` | 转义 SQL LIKE 通配符（% _ \\），防止用户输入干扰模糊查询 |
| `BusinessError` | class | `extends Error` | 已知业务错误基类，message 可安全返回客户端（含可选 code） |
| `NotFoundError` | class | `extends BusinessError` | 资源不存在错误（自动生成 `${resource} 不存在` 消息，code: not_found） |
| `ValidationError` | class | `extends BusinessError` | 输入校验错误（code: validation_error） |

### 3.6.2 grades 模块签名变更

**data-access 层新增 scope 参数**（P3 修复：所有查询函数应用 `buildScopeClassFilter` 进行行级 scope 过滤）：

| 函数 | 文件 | 旧签名 | 新签名 |
|------|------|--------|--------|
| `getGradeRecords` | `data-access.ts` | `(params: GradeQueryParams) => Promise<GradeRecordListItem[]>` | `(params: GradeQueryParams & { scope: DataScope; currentUserId?: string; limit?: number; offset?: number }) => Promise<PaginatedGradeRecords>` |
| `getClassGradeStats` | `data-access.ts` | `(classId, subjectId?, examId?) => Promise<GradeStats>` | `(classId, subjectId?, examId?, scope?: DataScope, currentUserId?: string) => Promise<GradeStats \| null>` |
| `getStudentGradeSummary` | `data-access.ts` | `(studentId) => Promise<StudentGradeSummary>` | `(studentId, scope?: DataScope) => Promise<StudentGradeSummary \| null>`（class_taught scope 校验学生归属） |
| `getClassRanking` | `data-access.ts` | `(classId, subjectId?, examId?) => Promise<ClassRankingItem[]>` | `(classId, subjectId?, examId?, scope?: DataScope, currentUserId?: string) => Promise<ClassRankingItem[]>`（含并列排名处理） |
| `getClassStudentsForEntry` | `data-access.ts` | `(classId) => Promise<{id,name}[]>` | `(classId, scope?: DataScope) => Promise<{id,name,email}[]>`（class_taught scope 校验 classId） |
| `getRankingTrend` | `data-access-ranking.ts` | `(studentId, subjectId?, semester?) => Promise<RankingTrendResult \| null>` | `(studentId, subjectId?, semester?, scope?: DataScope) => Promise<RankingTrendResult \| null>`（class_taught scope 校验） |

**lib 层签名变更**：
- `buildScopeClassFilter(scope: DataScope, currentUserId?: string): SQL | null`（新增 `currentUserId` 参数，`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤；P3-26：从 `lib/grade-utils.ts` 迁移至 `lib/scope-filter.ts`）

**新增导出**：
- `assertClassInScope(scope: DataScope, classId: string): string | null`（✅ v4-P2-6 从 `actions.ts` 迁移至 `lib/scope-check.ts`：校验 classId 是否在 scope 允许范围内，供 actions.ts 与 actions-analytics.ts 复用。迁移原因：actions.ts 是 "use server" 文件要求所有 export 为 async，而 assertClassInScope 是同步函数）
- `PaginatedGradeRecords` 接口（`data-access.ts`，`{ records: GradeRecordListItem[]; total: number }`，配合 DB 层分页）

### 3.6.3 homework 模块签名变更

**data-access 层新增 scope 参数**（P3 修复：教师仅查看自己创建的作业，学生/家长仅查看相关作业）：

| 函数 | 文件 | 新签名 |
|------|------|--------|
| `getHomeworkAssignments` | `data-access.ts` | `(params?: { creatorId?, ids?, classId?, scope?: DataScope }) => Promise<HomeworkAssignmentListItem[]>` |
| `getHomeworkAssignmentById` | `data-access.ts` | `(id: string, scope?: DataScope) => Promise<HomeworkAssignmentListItem \| null>` |
| `getHomeworkSubmissions` | `data-access.ts` | `(params?: { assignmentId?, classId?, creatorId?, scope?: DataScope }) => Promise<HomeworkSubmissionListItem[]>` |
| `getHomeworkAssignmentReviewList` | `data-access.ts` | `(params: { creatorId: string; scope?: DataScope }) => Promise<HomeworkReviewListItem[]>` |

### 3.6.4 lesson-preparation 模块变更

**新增 Zod schema**（`schema.ts`）：
- `publishLessonPlanHomeworkSchema`：发布作业输入校验（planId/blockId 必填，classIds 至少 1 个，availableAt/dueAt 日期格式校验）
- 导出类型 `PublishLessonPlanHomeworkInput = z.infer<typeof publishLessonPlanHomeworkSchema>`

**data-access-versions.ts 事务变更**：
- `revertToVersion` 包裹 `db.transaction` 确保原子性（回滚版本时同步更新 lessonPlans.content 与版本记录）
- `createLessonPlanVersion` 已使用 `db.transaction`（版本号自增 + 插入版本记录原子化）

### 3.6.5 其他修复（不涉及签名变更，仅记录范围）

- 越权访问修复：15+ 处页面添加 `requirePermission`，data-access 层添加 scope 过滤
- 功能性 BUG 修复：question-actions 删除字段名、question-columns 时间显示、textbook-card 删除按钮、block-renderer 拖拽
- 统一错误处理：所有 Server Action catch 块改用 `handleActionError`
- 客户端 Action 调用添加 try/catch/finally（20+ 处，使用 `safeActionCall`）
- `JSON.parse` 改用 `safeJsonParse`（8+ 处）
- Date 解析添加校验（15+ 处，使用 `safeParseDate`）
- 数据库事务添加：`batchCreateGradeRecords`、`deleteChapter`、`revertToVersion`
- 性能优化：`Promise.all` 并行化、DB 层分页（grades `getGradeRecords`）
- TypeScript `as` 断言清理（30+ 处，改用类型守卫）
- React 性能修复：`useMemo`、`useCallback`、`key` prop、渲染期间副作用移除

---

# 附录 A：模块间依赖矩阵

> 行表示使用方，列表示被使用方。`✅` 合理依赖，`❌` 违规直查，`⟳` 循环依赖。
> ✅ P1-1 已修复：所有跨模块直查已改为通过对方 data-access 接口。

| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | users | grades | messaging | notifications | lesson-prep | error-book | 其他 |
|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|-------|--------|-----------|---------------|-------------|------------|------|
| **shared** | - | ⟳✅已修复 | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **auth(root)** | ✅ db/lib | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **exams** | ✅ | ✅ | - | - | ✅data-access | - | ✅data-access | ✅data-access | - | - | - | - | - | - | - | - |
| **homework** | ✅ | ✅ | ✅data-access | - | ✅关系 | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **questions** | ✅ | ✅ | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - | - |
| **textbooks** | ✅ | ✅ | - | - | ✅UI | - | - | - | - | - | - | - | - | - | - | - |
| **classes** | ✅ | ✅ | ✅data-access | ✅data-access | - | - | - | ✅data-access | - | - | - | - | - | - | - | ✅scheduling（P0-5：data-access-class-schedule 写函数） |
| **school** | ✅ | ✅ | - | - | - | - | - | - | - | ⚠️可接受 | - | - | - | - | - | - |
| **grades** | ✅ | ✅ | ✅外键 | ✅外键 | - | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **dashboard** | ✅ | ✅ | ✅data-access | ✅data-access | ✅data-access | ✅data-access | ✅data-access | - | - | ✅data-access | - | - | - | - | - | - |
| **users** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | ✅rbac（UserRoleAssignDialog 组件） |
| **messaging** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | ✅dispatcher | - | - | - |
| **notifications** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **attendance** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **scheduling** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | ✅data-access | - | - | - | - | - | - |
| **proctoring** | ✅ | ✅ | ✅data-access | - | - | - | - | - | - | ✅data-access | - | - | - | - | - | - |
| **diagnostic** | ✅ | ✅ | ✅data-access | - | ✅data-access | - | ✅data-access | - | - | ✅data-access | - | - | ✅data-access | - | - | ✅parent/notifications（v2-P2-6/v4-P1-5） |
| **parent** | ✅ | ✅ | - | ✅data-access | - | - | ✅data-access | ✅data-access | - | ✅data-access | ✅data-access | - | - | - | - | - |
| **elective** | ✅ | ✅ | - | - | - | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **course-plans** | ✅ | ✅ | - | - | - | - | ✅ | ✅ | - | ✅ | - | - | - | - | - | - |
| **audit** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **announcements** | ✅ | ✅ | - | - | - | - | - | ✅ | - | - | - | - | - | - | - | - |
| **files** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **settings** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
| **layout** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
| **lesson-preparation** | ✅ | ✅ | ✅data-access | ✅data-access | ✅data-access | ✅data-access | ✅data-access | - | - | - | - | - | - | - | - | ✅files/ai |
| **error-book** | ✅ | ✅ | - | - | ✅actions | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **rbac** | ✅ db/types/lib | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |

---

# 附录 B：关键参数影响链

### `userId`
1. 由 `auth.ts` JWT callback 从 `users` 表查询产生，存入 JWT
2. 通过 `session.user.id` 传递到所有 Server/Client Components
3. 通过 `getAuthContext().userId` 传递到所有 Server Actions
4. 在 `auth-guard.ts` 中用于查询 `usersToRoles`（角色）和 `classSubjectTeachers`/`grades`（DataScope）
5. 在 `exams/actions.ts` 中作为 `creatorId` 写入 `exams` 表
6. 在 `homework/actions.ts` 中作为 `creatorId` 写入 `homeworkAssignments` 表
7. 在 `classes/data-access.ts` 中查询 `getTeacherClasses(teacherId)` / `getGradeManagedClasses(userId)`
8. 在 `elective/actions.ts` 中作为 `teacherId`/`studentId` 用于选课过滤

### `examId`
1. 由 `exams/actions.createExamAction` 产生（CUID2），写入 `exams` 表
2. 被 `exams/data-access.getExamById(id)` 读取
3. 被 `exams/actions` 的 `updateExamAction`/`deleteExamAction`/`duplicateExamAction` 用于定位考试
4. 传入 `homework/actions.createHomeworkAssignmentAction` 的 `sourceExamId` 参数
5. 在 `homeworkAssignments` 表中作为外键关联到源考试
6. 被 `homework/data-access.getHomeworkAssignmentAnalytics` 用于追溯作业来源

### `classId`
1. 由 `classes/actions` 的 `createTeacherClassAction`/`createAdminClassAction` 产生
2. 被 `classes/data-access.getClassStudents(classId)` 读取学生列表
3. 被 `classes/data-access.getClassSchedule(classId)` 读取课表
4. 被 `classes/data-access.getClassHomeworkInsights(classId)` 读取作业洞察（✅ P0-7 已修复：通过 `homework/data-access-classes` 获取数据）
5. 被 `homework/data-access.getHomeworkAssignments({ classId })` 过滤作业列表
6. 在 `auth-guard.ts` 中通过 `classSubjectTeachers` 查询教师关联的 classIds，构建 `DataScope.class_taught`

### `permission`
1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义（61 个权限点）
2. 在 `shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表
3. 在 `auth.ts` JWT callback 中通过 `resolvePermissions(roleNames)` 合并多角色权限，存入 JWT
4. 在 `proxy.ts` middleware 中通过 `token.permissions` 检查路由访问权限
5. 在 `shared/lib/auth-guard.ts` 中通过 `requirePermission(permission)` 在 Server Action 层断言权限
6. 在 `shared/hooks/use-permission.ts` 中通过 `hasPermission(permission)` 在客户端组件中条件渲染
7. 在 `layout/config/navigation.ts` 中作为 `NavItem.permission` 字段过滤侧边栏菜单

### `DataScope`
1. 由 `auth-guard.ts` 的 `resolveDataScope(userId, roles)` 根据用户角色和 DB 关系动态计算
2. 支持类型：`all` / `grade_managed` / `class_taught` / `class_members` / `children` / `owned`
3. 传递到 `exams`/`homework`/`grades`/`attendance`/`elective`/`dashboard` 的 data-access 进行行级过滤
4. 对 parent 角色，查询 `parentStudentRelations` 表构建 `{ type: "children", childrenIds: string[] }`
5. 在 `parent/children/[studentId]/page.tsx` 中通过 `ctx.dataScope.childrenIds.includes(studentId)` 二次校验

---

# 附录 C：核心函数签名索引

> 完整函数签名见 `005_architecture_data.json`。本附录仅列出关键函数。

### shared 层核心函数

```typescript
// shared/lib/auth-guard.ts
getAuthContext(): Promise<AuthContext>
requirePermission(permission: Permission): Promise<AuthContext>
requireAuth(): Promise<AuthContext>
checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }>
resolveDataScope(userId: string, roleNames: string[]): Promise<DataScope>

// shared/lib/permissions.ts
resolvePermissions(roleNames: string[]): Promise<Permission[]>

// shared/lib/audit-logger.ts
logAudit(params: LogAuditParams): Promise<void>

// shared/lib/login-logger.ts
logLoginEvent(params: LogLoginEventParams): Promise<void>

// shared/lib/change-logger.ts
logDataChange(params: LogDataChangeParams): Promise<void>

// shared/lib/session.ts (P0-2 新增，session 获取单一入口)
getSession(): Promise<AppSession>

// shared/lib/http-utils.ts (P1-6 完成，统一 IP/UA 提取)
resolveClientIp(): Promise<string>
getUserAgent(): Promise<string>

// shared/lib/ai/ (P2-2 已拆分，原 ai.ts 保留为重导出)
// ai/client.ts
createAiChatCompletion(input: AiChatRequest): Promise<{ content, usage }>
// ai/payload-parser.ts
parseAiChatPayload(body: unknown): AiChatRequest
// ai/api-key-crypto.ts
encryptAiApiKey(value: string): string
decryptAiApiKey(value: string): string

// shared/lib/password-policy.ts
validatePassword(password: string): { valid: boolean; errors: string[] }
getPasswordStrength(password: string): "weak" | "medium" | "strong"
isAccountLocked(failedAttempts: number, lastFailedAt: Date | null): boolean

// shared/lib/rate-limit/ (audit-P1-6 重构：单文件拆分为目录，抽象 RateLimiter 接口)
//   index.ts —— 门面：getRateLimiter() 按 env.RATE_LIMIT_DRIVER 选择实现；rateLimit()/resetRateLimit() 返回 Promise
//   types.ts —— RateLimiter 接口 + RateLimitParams/RateLimitResult 类型
//   rules.ts —— RATE_LIMIT_RULES / rateLimitKey / rateLimitHeaders 纯函数（同步）
//   memory-limiter.ts —— MemoryRateLimiter 类（默认，进程级 Map）
//   redis-limiter.ts —— RedisRateLimiter 类（基于 @upstash/ratelimit + @upstash/redis，懒加载）
//   upstash-modules.d.ts —— @upstash/* 可选依赖类型声明（仅 RATE_LIMIT_DRIVER=redis 时安装）
rateLimit(params: RateLimitParams): Promise<RateLimitResult>  // audit-P1-6：改为 Promise 返回
resetRateLimit(key: string): Promise<void>  // audit-P1-6：改为 Promise 返回
rateLimitKey(prefix: string, identifier: string): string  // 同步纯函数
rateLimitHeaders(result: RateLimitResult): Record<string, string>  // 同步纯函数
getRateLimiter(): Promise<RateLimiter>  // audit-P1-6 新增：返回当前进程的限流器实例；redis 驱动下动态 import redis-limiter 故为异步
RATE_LIMIT_RULES  // 同步常量（含 LOGIN/API/UPLOAD/AI_CHAT/PASSWORD_CHANGE/ONBOARDING_BIND；audit-P1-8 新增 ONBOARDING_BIND: 5次/小时）
// RateLimiter interface (audit-P1-6 新增)
//   limit(params: RateLimitParams): Promise<RateLimitResult>
//   reset(key: string): Promise<void>

// shared/lib/permission-bitmap.ts (audit-P1-7 新增：权限位图编解码)
//   纯函数无 server-only，可在 edge runtime 使用（proxy.ts 直接调用）
//   PERMISSION_BITMAP_ORDER: readonly Permission[] —— 67 个权限点的固定顺序（不可变，新增权限只能追加末尾）
//   编码：每个权限按位图中位置分配 bit (0-66)，67 bit 打包为 BigInt → base36 字符串（约 13 字符 vs JSON 数组 1.1KB，减少 99%）
//   BigInt 精度安全：手工按位累加 base36 → BigInt（parseBase36ToBigInt），避免 parseInt(bitmap, 36) 的 Number 53 bit 精度丢失
encodePermissionsBitmap(permissions: Permission[]): string  // 编码：Permission[] → base36 字符串
decodePermissionsBitmap(bitmap: string): Permission[]  // 解码：base36 字符串 → Permission[]（用于 session callback 还原数组）
hasPermissionInBitmap(bitmap: string, permission: Permission): boolean  // 单权限检查，不解码全部（proxy.ts 性能优化）

// shared/lib/excel.ts
exportToExcel(params: { sheets: ExcelSheet[] }): Promise<Buffer>
parseExcel(buffer: Buffer): Promise<ParsedSheet[]>
generateTemplate(params: { sheets: TemplateSheet[] }): Promise<Buffer>

// shared/lib/file-storage.ts
isAllowedMimeType(mimeType: string): boolean
generateStoragePath(originalName: string): string
formatFileSize(bytes: number): string

// shared/lib/utils.ts
cn(...inputs: ClassValue[]): string
formatDate(date: string | Date, locale?: string): string
getSearchParam(params: SearchParams, key: string): string | undefined
formatNumber(v: number | null | undefined, digits?: number): string

// shared/lib/search-params.ts (re-export from utils.ts)
getParam(params: SearchParams, key: string): string | undefined  // = getSearchParam
```

### 业务模块核心 Actions

```typescript
// exams/actions.ts
createExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<Exam>>
createAiExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ examId: string }>>
updateExamAction(prevState: ActionState, formData: FormData): Promise<ActionState>
deleteExamAction(prevState: ActionState, formData: FormData): Promise<ActionState>
duplicateExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ examId: string }>>

// homework/actions.ts
createHomeworkAssignmentAction(prevState: ActionState, formData: FormData): Promise<ActionState>
startHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ submissionId: string }>>
saveHomeworkAnswerAction(prevState: ActionState, formData: FormData): Promise<ActionState>
submitHomeworkAction(prevState: ActionState, formData: FormData): Promise<ActionState>
gradeHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise<ActionState>

// classes/actions.ts
createTeacherClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
createAdminClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
createGradeClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
// + update/delete 各 3 个，共 9 个

// grades/actions.ts
getGradeRecordsAction(params: GetGradeRecordsParams): Promise<ActionState<PaginatedGradeRecords>>
createGradeRecordAction(prevState: ActionState, formData: FormData): Promise<ActionState>
exportGradesAction(params: ExportGradesParams): Promise<ActionState<Buffer>>

// scheduling/actions.ts
autoScheduleAction(prevState: ActionState, formData: FormData): Promise<ActionState>
applyAutoScheduleAction(prevState: ActionState, formData: FormData): Promise<ActionState>
```

### 业务模块核心 Data-access

```typescript
// exams/data-access.ts
getExams(params: GetExamsParams & { scope: DataScope }): Promise<{ items: Exam[]; total: number }>
getExamById(id: string, scope: DataScope): Promise<Exam | null>
persistExamDraft(input: ExamDraftInput): Promise<{ examId: string }>
persistAiGeneratedExamDraft(input: AiExamDraftInput): Promise<{ examId: string }>

// homework/data-access.ts
getHomeworkAssignments(params: GetHomeworkAssignmentsParams & { scope: DataScope }): Promise<{ items, total }>
getStudentHomeworkAssignments(studentId: string): Promise<StudentHomeworkAssignment[]>
getStudentDashboardGrades(studentId: string): Promise<StudentDashboardGrades>
getHomeworkAssignmentAnalytics(assignmentId: string): Promise<HomeworkAnalytics>

// classes/data-access.ts
getAdminClasses(scope: DataScope): Promise<Class[]>
getTeacherClasses(teacherId: string): Promise<Class[]>
getStudentClasses(studentId: string): Promise<Class[]>
getClassStudents(classId: string): Promise<Student[]>
getClassHomeworkInsights(classId: string): Promise<ClassHomeworkInsights>  // ✅ P0-7 已修复：通过 homework/data-access-classes 获取数据

// grades/data-access.ts
getGradeRecords(params: GetGradeRecordsParams & { scope: DataScope; currentUserId?: string; limit?: number; offset?: number }): Promise<PaginatedGradeRecords>
getStudentGradeSummary(studentId: string, scope?: DataScope): Promise<StudentGradeSummary | null>
getClassRanking(classId: string, subjectId?: string, examId?: string, scope?: DataScope, currentUserId?: string): Promise<ClassRankingItem[]>

// scheduling/auto-scheduler.ts（纯函数，标杆）
findOptimalSlot(input: FindOptimalSlotInput): ScheduleSlot | null
validateSchedule(schedule: ScheduleItem[]): ValidationResult
autoSchedule(input: AutoScheduleInput): AutoScheduleResult
buildDefaultTimeSlots(): TimeSlot[]
```

---

## 相关文档

| 文档 | 说明 |
|------|------|
| [005_architecture_data.json](./005_architecture_data.json) | AI 友好格式的结构化架构数据 |
| [006_k12_feature_checklist.md](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 |
| [007_gap_audit_report.md](./007_gap_audit_report.md) | 差距审计报告 |
| [008_module_role_mapping.md](./008_module_role_mapping.md) | 模块-角色-功能映射总览 |

---

## 文档维护说明

- **修改源码后**：同步更新本文档对应模块章节 + `005_architecture_data.json`
- **新增模块**：在第二部分添加模块清单 + 更新 1.1 分层架构图 + 更新附录 A 依赖矩阵
- **新增/删除导出函数**：更新对应模块的"导出函数"清单 + 附录 C
- **修改依赖关系**：更新 1.2 模块依赖关系图 + 附录 A 依赖矩阵
- **新增路由**：更新 `005_architecture_data.json` 的 `routes` 节点
- **新增数据库表**：更新 `shared/db/schema.ts` 分节 + `005` 的 `dbTables` 节点

> 完整路由表、DevOps 脚本、E2E 测试等信息见 `005_architecture_data.json`。
