Files
NextEdu/docs/architecture/audit/archive/elective-audit-report.md

340 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 选修课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 P0parent 角色缺失选课页面
- **位置**`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 P0admin/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.990%),可由 `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 模块章节的「已知问题」存在以下过时项,本次审计已核实并修复:
| 架构图记录 | 实际情况 | 处理 |
|---|---|---|
| "❌ P03 个读 Action 无调用方" | 实际并不存在这 3 个 Action页面直接调用 data-access项目规则允许 `app/` 调用 data-access | 删除该项 |
| "❌ P0i18n 完全缺失" | i18n 资源完整zh-CN + en 双语Server Action 错误消息已 i18n 化 | 修正为「P0data-access-operations 的 throw 错误消息仍为英文」 |
| "❌ P0错误边界完全缺失3 个角色目录均无 `error.tsx`" | 3 个 `error.tsx` 已存在 | 删除该项改为「P1error.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` 节点