# 选修课(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 └─ (client) → deleteElectiveCourseAction / openSelectionAction / closeSelectionAction / runLotteryAction └─ (client) → createElectiveCourseAction / updateElectiveCourseAction └─ (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` 使用 `` 而非 `` - **位置**: - [elective-course-list.tsx:92](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L92) `` - [elective-course-list.tsx:177](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L177) `` - **违反规则**:项目记忆「Link navigation must use Next.js `` component instead of raw `` tags」。 - **后果**:原生 `` 触发整页刷新,丢失客户端导航状态、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:`),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` 一次性等待所有数据,没有 `` 边界,无法流式渲染局部内容。 - **违反规则**:「异步数据使用 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. **`` → ``**:`elective-course-list.tsx` 中 2 处替换为 Next.js ``。 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` 节点