# 选修课（Elective）模块审计报告

> 审计日期：2026-06-25
> 审计范围：
> - `src/modules/elective/**`
> - `src/app/(dashboard)/admin/elective/**`、`src/app/(dashboard)/teacher/elective/**`、`src/app/(dashboard)/student/elective/**`
> - 跨模块依赖：`school` / `users` / `classes` 的 data-access，`rbac` 的 `ELECTIVE_*` 权限点
> - i18n 资源：`src/shared/i18n/messages/{zh-CN,en}/elective.json`
> 参照规则：`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`

---

## 一、现有实现概要

### 1.1 文件分布

| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/elective/actions.ts) | 348 | 8 个写 Action（权限校验 + Zod + trackEvent + 资源归属校验） |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts) | 258 | 课程 CRUD + scope 过滤 + 显示名聚合 + 共享映射函数 |
| 数据访问 | [data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts) | 374 | 选课/退课/抽签（事务 + FOR UPDATE 锁 + Fisher-Yates） + 时间冲突/学分上限校验 |
| 数据访问 | [data-access-selections.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-selections.ts) | 147 | 选课记录查询 + 学生可选课程 |
| 跨模块抽象 | [resolvers.ts](file:///e:/Desktop/CICD/src/modules/elective/resolvers.ts) | 83 | CourseDisplayResolver / StudentGradeResolver 接口 + 注入函数（测试 mock 友好） |
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/elective/schema.ts) | 179 | Zod 校验（5 个 schema） |
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/elective/types.ts) | 73 | 类型定义 |
| Constants | [constants.ts](file:///e:/Desktop/CICD/src/modules/elective/constants.ts) | 55 | i18n key 映射 + Badge variant + 类型守卫 |
| Import-export | [export.ts](file:///e:/Desktop/CICD/src/modules/elective/export.ts) | 102 | Excel 导出（课程列表 + 选课名单） |
| 组件 | [components/elective-page-layout.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-page-layout.tsx) | 30 | 页面布局骨架（header/children 插槽） |
| 组件 | [components/elective-course-list.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx) | 236 | 课程卡片网格 + 管理操作 |
| 组件 | [components/elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx) | 301 | 课程创建/编辑表单 |
| 组件 | [components/elective-filters.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx) | 51 | nuqs 筛选栏（搜索 + 模式） |
| 组件 | [components/student-selection-view.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/student-selection-view.tsx) | 248 | 学生选课视图（已选 + 可选） |
| 页面 | [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx) | 47 | 管理员课程列表（RSC） |
| 页面 | [admin/elective/create/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/create/page.tsx) | 32 | 创建课程（RSC） |
| 页面 | [admin/elective/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx) | 44 | 编辑课程（RSC） |
| 页面 | [teacher/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx) | 58 | 教师我的课程（RSC） |
| 页面 | [student/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx) | 48 | 学生选课中心（RSC） |
| 骨架屏 | 5 个 `loading.tsx`（admin/admin-create/admin-edit/teacher/student） | — | 列表/表单骨架屏 |
| 错误边界 | 3 个 `error.tsx`（admin/teacher/student） | — | 错误兜底 |
| i18n | [zh-CN/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/elective.json) | 114 | 中文翻译 |
| i18n | [en/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/elective.json) | 114 | 英文翻译 |

### 1.2 数据流

```
page.tsx (RSC)
  └─ getElectiveCourses / getElectiveCourseById / getAvailableCoursesForStudent / getStudentSelections  (data-access)
       └─ db (drizzle) → electiveCourses / courseSelections 表
       └─ 跨模块 data-access（通过 resolvers.ts 接口抽象）：
            school.getSubjectOptions / school.getGradeOptions
            users.getUserNamesByIds
            classes.getStudentActiveGradeId
  └─ <ElectiveCourseList> (client) → deleteElectiveCourseAction / openSelectionAction / closeSelectionAction / runLotteryAction
  └─ <ElectiveCourseForm> (client) → createElectiveCourseAction / updateElectiveCourseAction
  └─ <StudentSelectionView> (client) → selectCourseAction / dropCourseAction
```

### 1.3 架构图记录完整性

`docs/architecture/004_architecture_impact_map.md` 与 `005_architecture_data.json` 已覆盖 elective 模块（章节 2.20），包含完整的 exports / 依赖关系 / 权限点 / 文件清单。但「已知问题」段存在过时信息（详见第五部分），需同步修正。

---

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

### 2.1 P0：教师页面跳转到 admin 路由（跨角色越权 + 404）

- **位置**：[teacher/elective/page.tsx:53-54](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx#L53-L54)
- **现象**：教师页面渲染 `ElectiveCourseList` 时传入 `createHref="/admin/elective/create"`、`editBaseHref="/admin/elective"`。教师点击"创建课程"或"编辑"按钮后会被路由到 `/admin/*`，由于中间件对 admin 角色做了路由保护，教师实际看到的是 403 / 重定向到首页。
- **原因**：`ElectiveCourseList` 只支持单一 `createHref`/`editBaseHref`，admin 与 teacher 共用一份组件时硬编码了 admin 路径。
- **违反规则**：「Server Action 必须使用 `requirePermission()` 进行权限校验」（前端入口虽然校验了权限，但跳转到无权访问的路由等同于绕过校验）；UX 上不可达。
- **后果**：教师角色虽然被授予 `ELECTIVE_MANAGE` 权限，却无法实际创建/编辑课程，功能完全不可用。

### 2.2 P0：parent 角色缺失选课页面

- **位置**：`src/app/(dashboard)/parent/elective/**`（目录不存在）
- **现象**：[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中 parent 角色被授予 `ELECTIVE_READ` 权限，但没有对应的 parent 页面。家长无法查看子女的选课情况。
- **违反规则**：「所有用户可见文本必须适配 i18n」「最大化复用：识别四个角色共用的 UI 块和业务逻辑块」——当前只覆盖 3 个角色，遗漏 parent。
- **后果**：家长对子女选课缺乏监督，无法及时发现选课异常（如未选满学分、错选时间冲突课程）。

### 2.3 P0：admin/student 页面缺少 `requirePermission()`

- **位置**：
  - [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx)（无任何权限校验，直接调用 `getElectiveCourses`）
  - [student/elective/page.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx#L17) 使用 `getAuthContext()` 仅获取 userId，未做权限校验
- **违反规则**：「Server Action 必须使用 `requirePermission()` 进行权限校验」 + 项目记忆中的硬约束「Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage」。
- **后果**：仅依赖中间件的路由级保护，缺少纵深防御；若中间件配置出现疏漏（如新增动态路由），会直接导致越权读他人数据。teacher 页面已经做了示范（`requirePermission(Permissions.ELECTIVE_READ)`），admin/student 不应例外。

### 2.4 P0：选课/退课错误消息未走 i18n

- **位置**：[data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts)
  - L233 `throw new Error("Course selection is not open")`
  - L237 `throw new Error("Selection has not started yet")`
  - L241 `throw new Error("Selection has ended")`
  - L254 `throw new Error("Already selected this course")`
  - L259 `throw new Error("Schedule conflicts with your existing courses")`
  - L265 `throw new Error(\`Credit limit exceeded (${creditCheck.current}/${creditCheck.max})\`)`
  - L301-303 `message: "Enrolled successfully" / "Added to waitlist" / "Selection submitted"`
- **现象**：上述英文 throw 出去后经由 `handleActionError` 包装为 `{ success: false, message: <英文> }` 返回前端，toast 直接显示英文。
- **违反规则**：「所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键」。
- **后果**：中文用户看到英文错误提示，i18n 资源中已存在对应的中文键（`errors.selectionClosed`、`errors.alreadySelected`、`errors.scheduleConflict`、`errors.creditExceeded` 等）却完全没被复用。

### 2.5 P0：`export.ts` 存在 `as` 类型断言

- **位置**：[export.ts:21](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L21)
  ```ts
  status: params.status as "draft" | "open" | "closed" | "cancelled" | undefined,
  ```
- **违反规则**：「禁止 `as` 断言（除非从 `unknown` 转换或测试中，需注释原因）」。
- **后果**：未做类型守卫即强转，传入非法字符串（如 `"foo"`）会被静默接受，运行时引发 SQL 类型不匹配。

### 2.6 P1：`elective-course-form.tsx` 大量硬编码英文文案

- **位置**：[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
  - L95 `"New Elective Course"` / `"Edit Elective Course"`
  - L102 `"Course Name *"`
  - L112 `"Subject"`
  - L129 `"Grade"`
  - L146 `"Teacher"`
  - L163 `"Capacity"`
  - L175 `"Classroom"`
  - L184 `"Schedule"`
  - L188 `placeholder="e.g. Mon 14:00-15:30"`
  - L194 `"Credit"`
  - L225 `"Start Date"`
  - L235 `"End Date"`
  - L245 `"Selection Start"`
  - L258 `"Selection End"`
  - L274 `"Description"`
  - L278 `placeholder="Course description..."`
  - L291 `"Cancel"`
  - L294 `"Saving..."` / `"Create"` / `"Save"`
  - L72-85 `"Invalid form state"` / `"Failed to save course"` 等 toast 回退文案
- **违反规则**：「所有用户可见文本必须适配 i18n」+ i18n 资源已存在 `form.createTitle` / `form.editTitle` / `form.namePlaceholder` / `form.descriptionPlaceholder` 等键。
- **后果**：中文用户在创建/编辑课程表单中看到全英文界面，体验割裂。

### 2.7 P1：`elective-course-list.tsx` 与 `elective-course-form.tsx` 使用 `<a>` 而非 `<Link>`

- **位置**：
  - [elective-course-list.tsx:92](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L92) `<a href={createHref}>`
  - [elective-course-list.tsx:177](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L177) `<a href={...edit...}>`
- **违反规则**：项目记忆「Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags」。
- **后果**：原生 `<a>` 触发整页刷新，丢失客户端导航状态、prefetch 优化、Layout 复用。

### 2.8 P1：错误边界文案与按钮文案错误

- **位置**：[admin/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/error.tsx) 与 [teacher/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/error.tsx)
- **现象**：
  - `title` 与 `description` 都用 `t("errors.unexpected")`，重复且无信息量；
  - 重试按钮 `action.label` 用 `t("actions.save")`（"保存"），但学生页用 `t("actions.retry")`（"重试"）—— admin/teacher 文案错误。
- **违反规则**：i18n 完整性 + UX 一致性。
- **后果**：用户在错误页看到"保存"按钮且语义与"重试"不符。

### 2.9 P1：表单未使用 React Hook Form

- **位置**：[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
- **现象**：使用 4 个独立 `useState` 管理 Select 状态，没有统一表单状态管理、字段校验、脏值检查。项目 tech stack 明确包含 `React Hook Form`。
- **违反规则**：技术栈一致性 + 可维护性。
- **后果**：字段一多需要每个都加 `useState`，扩展性差；与服务端 Zod 校验形成两套校验，难以保持一致。

### 2.10 P1：`export.ts` 表头混用英文硬编码

- **位置**：[export.ts:56](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L56) `"Status"`、L93 `"Status"`、L94 `"Priority"`、L95 `"Selected At"`、L96 `"Enrolled At"`
- **现象**：课程列表 sheet 中 `status` 列的 header 是英文硬编码 `"Status"`，而其他列都走 i18n；选课名单 sheet 中 `status`/`priority`/`selectedAt`/`enrolledAt` 4 列 header 全英文硬编码。
- **违反规则**：「所有用户可见文本必须适配 i18n」—— Excel 导出也是用户可见文本。
- **后果**：中文用户下载 Excel 后表头混杂中英文。

### 2.11 P1：`getElectiveCourses` 静默吞错

- **位置**：[data-access.ts:161-164](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L161-L164)
  ```ts
  } catch (error) {
    console.error("getElectiveCourses failed:", error)
    return []
  }
  ```
- **现象**：DB 查询失败时返回空数组，页面无任何错误提示，用户以为"暂无数据"。
- **违反规则**：「错误与边界处理：明确处理空数据、无权限、网络异常等边界状态」。
- **后果**：DB 异常被掩盖，运维无法及时发现，用户误判为"无课程"，无法触发错误边界。

### 2.12 P1：`parseSchedule` 不支持完整英文星期与多时段

- **位置**：[data-access-operations.ts:35](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L35)
  ```ts
  const match = schedule.match(/^(周[一二三四五六日天]|[MonTueWedThuFriSatSun]+)\s+.../)
  ```
- **现象**：
  - 字符类 `[MonTueWedThuFriSatSun]+` 匹配任意 M/o/n/T/u/e 字符组合（如 `"Mon"`、`"oMenT"` 都会通过），不严谨；
  - 不支持 `"Monday"`、`"周一 14:00-15:30, 周三 16:00-17:30"` 多时段；
  - `normalizeDay` 表只有 `mon/tue/...`，没有 `monday/tuesday/...`。
- **后果**：教师在 schedule 输入 `"Monday 14:00-15:30"` 时不会触发冲突检测，存在隐性排课冲突。

### 2.13 P1：学分上限与候补人数硬编码（✅ 2026-06-25 已修复）

- **位置**：[data-access-operations.ts:15](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L15) `const MAX_CREDIT_PER_TERM = 10`
- **现象**：所有年级/学校共用同一个上限，无法配置。
- **违反规则**：「可扩展性：采用配置驱动设计」。
- **后果**：K12 不同年级（如高一 8 学分 vs 高三 12 学分）无法差异化配置。
- **修复说明**：新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts)，复用 `systemSettings` 表（category="elective"）作为配置存储。`getElectiveCreditLimit(gradeId?)` 支持按年级覆盖（key=`creditLimit:grade:<gradeId>`），fallback 到全局（key=`creditLimit:default`），均未配置返回默认值 10。React `cache()` 包装请求级去重。`checkCreditLimit` 新增 `studentGradeId: string | null` 参数并调用 `getElectiveCreditLimit(studentGradeId)`，`selectCourse` 在事务前通过 `getStudentGradeId(studentId)` 拿到年级 ID 并透传。

### 2.14 P1：抽签结果不可重跑

- **位置**：[data-access-operations.ts:212](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L212)
  ```ts
  await tx.update(electiveCourses).set({ enrolledCount, status: "closed", updatedAt: now })...
  ```
- **现象**：抽签完成立即把课程状态置为 `closed`，管理员若发现结果异常无法重新抽签（重抽需要先把状态手动改回 `open`）。
- **后果**：管理员缺乏"试抽 + 调整 + 正式抽"的灵活度，K12 学校在抽签争议时无法快速复核。

### 2.15 P1：管理员缺少课程统计概览

- **位置**：缺失
- **现象**：admin/teacher 页面只有平铺的课程卡片，缺少总览统计（如总课程数、总选课人数、热门科目分布、容量使用率）。
- **违反规则**：行业最佳实践「K12 admin 应有数据驾驶舱」。
- **后果**：管理员难以从全局角度掌握选课运行情况。

### 2.16 P2：`getCachedSubjectOptions` / `getCachedGradeOptions` 缓存全量选项

- **位置**：[data-access.ts:104-105](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L104-L105)
- **现象**：即使只需要 1 个科目的名称，也会拉取全部 subject/grade 选项。
- **后果**：高并发场景下存在 N+1 缓存膨胀风险（虽 React `cache()` 限单次请求内，但单次请求若涉及 1000+ 课程仍冗余）。

### 2.17 P2：缺少 Suspense 流式渲染（✅ 2026-06-25 已修复）

- **位置**：所有 RSC 页面
- **现象**：admin/teacher/student 页面用 `Promise.all` 一次性等待所有数据，没有 `<Suspense>` 边界，无法流式渲染局部内容。
- **违反规则**：「异步数据使用 React Suspense + 骨架屏」「性能：支持流式渲染」。
- **后果**：单条慢查询会拖累整页加载时间，用户长时间看到白屏。
- **修复说明**：
  - student 页面拆分为 `MySelectionsLoader`（Suspense）+ `AvailableCoursesLoader`（Suspense），分别对应"我的选课"与"可选课程"
  - admin 页面拆分为 `StatsCardsLoader`（Suspense）+ `CourseListLoader`（Suspense），统计卡片与列表分离
  - `StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件
  - `loading.tsx` 新增 `MySelectionsSkeleton` / `AvailableCoursesSkeleton` / `StatsCardsSkeleton` / `CourseListSkeleton` 分段骨架屏
  - React `cache()` 自动去重 `getStudentSelections` 调用，两个 Suspense 边界共享同一份数据

### 2.18 P2：缺少课程先修/容量阈值通知（✅ 2026-06-25 部分修复）

- **位置**：缺失
- **现象**：K12 学校通常需要：
  - 课程先修要求（如"必须先选 Python 入门才能选 Python 进阶"）；
  - 容量阈值通知（如课程满 90% 时通知管理员考虑扩容）；
  - 选课截止前提醒（如截止前 24h 通知未选课学生）。
- **后果**：缺少这些企业级功能会降低 K12 学校的运营效率。
- **修复说明（容量阈值通知）**：
  - 新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts) 中的 `getCapacityNotifyThreshold()`，默认 0.9（90%），可由 `systemSettings` 表配置（key=`capacityNotifyThreshold`，category=`elective`）。
  - `data-access-operations.ts` 新增 `notifyCapacityThresholdIfNeeded()`：仅当 `newEnrolledCount === Math.ceil(capacity * threshold)` 时触发一次通知（避免每次递增都发通知）。Fire-and-forget 设计：catch 中吞错并 `console.error`，不阻塞主流程。通过 `notifications.sendNotification` 发送给 `course.teacherId`，type=`"warning"`，附带 `actionUrl` 指向课程详情。i18n 标题/内容通过 `next-intl getTranslations("elective")` 翻译。
  - `selectCourse` 事务成功后调用 `notifyCapacityThresholdIfNeeded`。
- **未实施项**：课程先修要求与选课截止前提醒属于更长期规划，本次审计范围内不实施，后续可在 P3 阶段补齐。

### 2.19 P2：无单元测试（✅ 2026-06-25 已修复）

- **位置**：`tests/elective/`（不存在）
- **现象**：`buildLotteryRankCase`、`parseSchedule`、`isScheduleConflict`、`buildScopeFilter` 等纯函数已被精心设计为可测试，但没有任何测试文件。
- **修复说明**：新增 `tests/integration/elective/elective-pure-functions.test.ts`，35 个单测覆盖 `normalizeDay` / `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `mapCourseRow`；`vitest.config.ts` 添加 `server-only` 别名 stub。
- **违反规则**：「可测试性：数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks，与 UI 分离；导出清晰的接口类型以便 mock」——架构已就绪，但测试缺失。
- **后果**：未来重构无回归保障。

---

## 三、行业差距对比

| 维度 | 行业优秀实践（如 PowerSchool、Veracross、睿睿云、校园钉钉选修） | 当前实现 | 差距影响 |
|------|---|---|---|
| 角色覆盖 | 4 角色全覆盖：admin 全局管理 / teacher 创建维护 / student 选退课 / parent 查看子女 | 3 角色覆盖，缺 parent | 家长无法监督子女选课，错失家校协同点 |
| 时间冲突检测 | 结构化时段编辑器（周几 + 节次），可视化冲突预览 | 纯文本 schedule + 正则解析 | 教师/学生易输入错误格式，冲突检测可能失效 |
| 抽签可重跑 | 支持"预抽 + 公示 + 正式抽签"，结果可回滚 | 抽完立即 close，不可重抽 | 学校难以应对抽签争议 |
| 学分上限 | 按年级/学校可配置 | 全局硬编码 `MAX_CREDIT_PER_TERM=10` | 不同年级无法差异化 |
| 选课截止提醒 | 截止前 24h 短信/站内信通知未选课学生 | 无 | 学生错过选课窗口 |
| 容量阈值通知 | 满 90% 通知 admin 考虑扩容 | 无 | 热门课程爆满后才发现 |
| 课程详情页 | 独立课程详情页 + 教师介绍 + 评价聚合 + 历年选课人数趋势 | 仅卡片展示，无详情页 | 学生选课决策信息不足 |
| 选课概览驾驶舱 | admin 数据驾驶舱：选课率、热门科目、班级分布、未选名单 | 仅平铺课程卡片 | admin 缺乏全局视图，决策低效 |
| 候补转正通知 | 候补转正时通知学生 | 仅事务内自动转正，无通知 | 学生不知道自己已转正，可能错过上课 |
| 课程先修 | 标记先修关系，选课时校验 | 无 | 学生可能跳级选课失败 |
| 退课理由 | 退课时要求填写理由 + 期限 | 直接退课，无理由 | 学校无法分析退课原因改进课程 |

---

## 四、改进优先级建议

### P0（必须立即修复，影响功能可用或安全）

1. **教师页面跳转修复**：把 `createHref`/`editBaseHref` 改为参数化，teacher 页面传入 `/teacher/elective/...`，并新增 teacher 路由 `/teacher/elective/create` 与 `/teacher/elective/[id]/edit`（或共享 admin 路由但放开教师访问）。
2. **新增 parent 选课页面**：复用 `StudentSelectionView` 的只读变体，展示子女的已选/可选课程；通过 `parentId + studentId` 双重校验防止信息泄露。
3. **admin/student 页面加 `requirePermission()`**：补齐 `ELECTIVE_READ` 校验，与 teacher 一致。
4. **data-access 错误消息 i18n 化**：把 `throw new Error("...")` 改为带 i18n key + 参数的结构化错误，由 actions 层用 `getTranslations("elective")` 翻译后再返回。
5. **`export.ts` 移除 `as` 断言**：用类型守卫替代。

### P1（应在本次实施，影响质量与体验）

6. **`elective-course-form.tsx` 全量 i18n 化**：替换所有硬编码英文为 i18n 键，新增 `form.*` 翻译键。
7. **`<a>` → `<Link>`**：`elective-course-list.tsx` 中 2 处替换为 Next.js `<Link>`。
8. **错误边界文案修正**：admin/teacher error.tsx 重试按钮改用 `t("actions.retry")`；title/description 分离为 `errors.title` / `errors.description`。
9. **`export.ts` 表头全量 i18n**：新增 `export.statusHeader` / `export.priorityHeader` / `export.selectedAtHeader` / `export.enrolledAtHeader` 翻译键。
10. **`getElectiveCourses` 不再静默吞错**：移除 try-catch，让异常冒泡到 RSC 触发 error.tsx。
11. **`parseSchedule` 支持完整星期 + 多时段**：重写正则与归一化函数。
12. **抽签可重跑**：抽签后保留 `status="open"`，仅更新 `enrolledCount`；增加"已抽签"标记字段或独立的 `lotteryRunAt` 字段。
13. **管理员选课概览**：在 admin 页面顶部增加统计卡片网格（总课程数、总选课人数、平均容量使用率、待抽签课程数），复用 `attendance-stats-cards.tsx` 模式。

### P2（中长期改进，提升企业级能力）

14. **配置化学分上限**（✅ 2026-06-25 已修复）：抽离为 `data-access-settings.ts` 中的 `getElectiveCreditLimit(gradeId?)`，复用 `systemSettings` 表按年级可设，未配置 fallback 到全局默认值 10。
15. **Suspense 流式渲染**（✅ 2026-06-25 已修复）：student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界，admin 列表与统计卡片分离。
16. **容量阈值通知**（✅ 2026-06-25 已修复）：选课时若 `enrolledCount >= capacity * threshold`，触发 `notifications` 模块通知教师（type=`"warning"`），阈值通过 `getCapacityNotifyThreshold()` 可配置，默认 0.9。
17. **退课期限与理由**（✅ 2026-06-25 已修复）：新增 `electiveCourses.dropDeadline`（datetime）与 `courseSelections.dropReason`（varchar 255）字段，`dropCourse` 校验截止时间并抛 `ElectiveBusinessError("dropDeadlinePassed")`，退课对话框可选填理由，trim 后非空才入库。
18. **单元测试**（✅ 2026-06-25 已修复）：补齐 `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `buildScopeFilter` / `mapCourseRow` 的单元测试。
19. **课程详情页**（✅ 2026-06-25 已修复）：新增 `/admin/elective/[id]` 与 `/student/elective/[id]` 详情页。

---

## 五、架构图同步说明

### 5.1 需要修正的过时信息

`004_architecture_impact_map.md` 与 `005_architecture_data.json` 中 elective 模块章节的「已知问题」存在以下过时项，本次审计已核实并修复：

| 架构图记录 | 实际情况 | 处理 |
|---|---|---|
| "❌ P0：3 个读 Action 无调用方" | 实际并不存在这 3 个 Action；页面直接调用 data-access（项目规则允许 `app/` 调用 data-access） | 删除该项 |
| "❌ P0：i18n 完全缺失" | i18n 资源完整（zh-CN + en 双语），Server Action 错误消息已 i18n 化 | 修正为「P0：data-access-operations 的 throw 错误消息仍为英文」 |
| "❌ P0：错误边界完全缺失（3 个角色目录均无 `error.tsx`）" | 3 个 `error.tsx` 已存在 | 删除该项；改为「P1：error.tsx 文案错误（title=description=unexpected，按钮文案错误）」 |
| "⚠️ P1：`elective-course-form.tsx` 存在 `v as "fcfs" | "lottery"` 类型断言" | 已用 `isSelectionMode` 类型守卫替代，无 `as` | 删除该项 |
| "⚠️ P1：`elective-course-list.tsx` 存在 `null as never` 类型逃逸" | 当前代码无 `null as never` | 删除该项 |
| "⚠️ P1：`buildLotteryRankCase` 未导出，无法单测" | 已 `export function buildLotteryRankCase` | 删除该项 |
| "⚠️ P1：`SELECTION_MODE_LABELS` 已定义但表单未复用" | 表单已使用 `isSelectionMode` 守卫 + 直接渲染 `t("selectionMode.fcfs/lottery")` | 删除该项 |

### 5.2 需要新增的节点

| 新增项 | 004 章节 | 005 节点 |
|---|---|---|
| 新增 `parent/elective/page.tsx`（家长查看子女选课） | 2.20 文件清单新增一行 | `appRoutes.parent.elective` 节点 |
| 新增 `teacher/elective/create/page.tsx` 与 `teacher/elective/[id]/edit/page.tsx` | 2.20 文件清单新增 2 行 | `appRoutes.teacher.electiveCreate` / `electiveEdit` 节点 |
| 新增 `data-access-stats.ts`（管理员选课统计） | 2.20 文件清单新增一行 | `modules.elective.exports.dataAccess` 新增函数 |
| 新增 `tests/elective/*.test.ts`（单元测试） | 2.20 文件清单新增测试说明 | 无需 005 节点（测试不属导出） |

### 5.3 实施完成后的同步动作

代码修改完成后，将上述变更同步写入：
- `docs/architecture/004_architecture_impact_map.md` 第 2.20 节
- `docs/architecture/005_architecture_data.json` 的 `modules.elective` 与 `appRoutes` 节点
