# 考勤与选修课（Attendance & Elective）模块审计报告

> 审计日期：2026-06-22
> 审计范围：
> - `src/modules/attendance/**`、`src/app/(dashboard)/admin/attendance/**`、`src/app/(dashboard)/teacher/attendance/**`、`src/app/(dashboard)/student/attendance/**`、`src/app/(dashboard)/parent/attendance/**`
> - `src/modules/elective/**`、`src/app/(dashboard)/admin/elective/**`、`src/app/(dashboard)/teacher/elective/**`、`src/app/(dashboard)/student/elective/**`
> - 跨模块依赖：`src/modules/parent/components/parent-attendance-*.tsx`、`src/shared/i18n/messages/**`
> 参照规则：`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`

---

## 一、现有实现概要

### 1.1 文件分布

#### 考勤模块（attendance）

| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts) | 271 | 10 个 Server Action（含权限校验、Zod 校验） |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts) | 309 | 考勤记录 CRUD + 班级学生查询 + 规则 upsert + 总览统计 |
| 数据访问 | [data-access-stats.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts) | 145 | 学生/班级考勤汇总（拆分范例） |
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/attendance/schema.ts) | 43 | Zod 校验（5 个 schema） |
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/attendance/types.ts) | 103 | 类型定义 + 状态标签/颜色常量 |
| 组件 | [components/attendance-sheet.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx) | 353 | 批量点名表单（键盘快捷键、状态按钮组） |
| 组件 | [components/attendance-record-list.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx) | 130 | 考勤记录列表 + 删除对话框 |
| 组件 | [components/attendance-filters.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-filters.tsx) | 97 | URL 同步筛选器（班级/状态/日期） |
| 组件 | [components/attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx) | 81 | 单卡片统计（8 指标） |
| 组件 | [components/attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx) | 80 | 管理员总览 6 卡片网格 |
| 组件 | [components/attendance-stats-class-selector.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-class-selector.tsx) | 27 | 班级筛选 ChipNav |
| 组件 | [components/attendance-rules-form.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx) | 148 | 考勤规则配置表单 |
| 组件 | [components/student-attendance-view.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/student-attendance-view.tsx) | 104 | 学生/家长视图（统计 + 最近记录） |
| 页面 | [admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx) | 91 | 管理员考勤总览（RSC） |
| 页面 | [teacher/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx) | 116 | 教师考勤记录列表（RSC + 分页） |
| 页面 | [teacher/attendance/sheet/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx) | 44 | 教师点名页（RSC） |
| 页面 | [teacher/attendance/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx) | 85 | 教师班级考勤统计（RSC） |
| 页面 | [student/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx) | 40 | 学生考勤汇总（RSC） |
| 页面 | [parent/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx) | 66 | 家长多子女考勤聚合（RSC） |
| 骨架屏 | 2 个 `loading.tsx`（student/parent） | — | 列表骨架屏 |
| 错误边界 | 0 个 `error.tsx` | — | **完全缺失** |

#### 选修课模块（elective）

| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/elective/actions.ts) | 304 | 11 个 Server Action |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts) | 250 | 课程 CRUD + scope 过滤 + 显示名聚合 |
| 数据访问 | [data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts) | 245 | 选课/退课/抽签（事务 + FOR UPDATE 锁） |
| 数据访问 | [data-access-selections.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-selections.ts) | 149 | 选课记录查询 + 学生可选课程 |
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/elective/schema.ts) | 132 | Zod 校验（5 个 schema） |
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/elective/types.ts) | 108 | 类型定义 + 4 组标签/颜色常量 |
| 组件 | [components/elective-course-list.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx) | 233 | 课程卡片网格 + 管理操作 |
| 组件 | [components/elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx) | 293 | 课程创建/编辑表单 |
| 组件 | [components/elective-filters.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx) | 49 | nuqs 筛选栏（搜索 + 模式） |
| 组件 | [components/student-selection-view.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/student-selection-view.tsx) | 250 | 学生选课视图（已选 + 可选） |
| 页面 | [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx) | 46 | 管理员课程列表（RSC） |
| 页面 | [admin/elective/create/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/create/page.tsx) | 36 | 创建课程（RSC） |
| 页面 | [admin/elective/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx) | 48 | 编辑课程（RSC） |
| 页面 | [teacher/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx) | 53 | 教师我的课程（RSC） |
| 页面 | [student/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx) | 54 | 学生选课中心（RSC） |
| 骨架屏 | 1 个 `loading.tsx`（student） | — | 列表骨架屏 |
| 错误边界 | 0 个 `error.tsx` | — | **完全缺失** |

#### 跨模块依赖（parent 模块消费 attendance 类型）

| 文件 | 行数 | 职责 |
|------|------|------|
| [parent/components/parent-attendance-warning.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx) | 102 | 家长考勤异常预警横幅 |
| [parent/components/parent-attendance-rate-card.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx) | 114 | 家长出勤率汇总卡片 |
| [parent/components/parent-attendance-calendar.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx) | 194 | 家长考勤月历视图 |

### 1.2 数据流

#### 考勤数据流

```
page.tsx (RSC)
  └─ getAttendanceRecords / getStudentAttendanceSummary / getClassAttendanceStats  (data-access)
       └─ db (drizzle) → attendanceRecords / attendanceRules / classEnrollments / users / classes 表
  └─ <AttendanceSheet> (client) → batchRecordAttendanceAction
  └─ <AttendanceRecordList> (client) → deleteAttendanceAction
  └─ <AttendanceRulesForm> (client) → saveAttendanceRulesAction
  └─ <StudentAttendanceView> (server) — 学生/家长只读
  └─ <ParentAttendanceCalendar/Warning/RateCard> (server/client) — 家长聚合视图
```

#### 选修课数据流

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

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

经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.10（attendance）与 §2.20（elective）以及 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中对应节点，架构图记录**存在以下偏差**（详见第五节）：

- **attendance 行数统计过期**：图记 `actions.ts 271 行 / data-access.ts 309 行`，实际一致；但 `data-access-stats.ts` 图记 145 行，实际 145 行（一致）。组件文件数图记 5 个，实际 8 个组件文件（缺 `attendance-record-list.tsx`、`attendance-rules-form.tsx`、`student-attendance-view.tsx`）。
- **attendance 导出函数名不一致**：图记 Actions 含 `getAttendanceRecordsAction / createAttendanceRecordAction / updateAttendanceRecordAction / deleteAttendanceRecordAction / getStudentAttendanceAction / getAttendanceStatsAction`，实际为 `recordAttendanceAction / batchRecordAttendanceAction / updateAttendanceAction / deleteAttendanceAction / getAttendanceAction / getStudentAttendanceAction / getClassAttendanceStatsAction / getClassAttendanceForDateAction / saveAttendanceRulesAction / getAttendanceRulesAction`（10 个，名称与图不一致）。
- **attendance 缺失组件记录**：图记 `AttendanceStatsCards` 一个组件，实际有 8 个组件（含 `AttendanceSheet`、`AttendanceRecordList`、`AttendanceFilters`、`AttendanceStatsCard`、`AttendanceStatsCards`、`AttendanceStatsClassSelector`、`AttendanceRulesForm`、`StudentAttendanceView`）。
- **attendance 缺失规则功能记录**：架构图未记录 `attendanceRules` 表的 CRUD（实际已实现 `saveAttendanceRulesAction` / `getAttendanceRulesAction` + `upsertAttendanceRules` / `getAttendanceRules`）。
- **elective 行数统计过期**：图记 `actions.ts 304 行 / data-access.ts 250 行 / data-access-operations.ts 245 行 / data-access-selections.ts 189 行`，实际 `data-access-selections.ts` 为 149 行（减少 40 行）。
- **elective 缺失组件记录**：图记组件 3 个（`elective-course-form`、`elective-course-list`、`elective-filters`），实际 4 个（缺 `student-selection-view.tsx`）。
- **elective 缺失 usedBy 信息**：`getStudentSelectionsAction` / `getAvailableCoursesAction` 的 `usedBy` 字段标注为"待扩展"，实际已被 `student/elective/page.tsx` 通过 data-access 直接调用（绕过 Action）。
- **parent 跨模块 UI 依赖未记录**：parent 模块的 3 个 attendance 组件直接 import `@/modules/attendance/types`，架构图未在 parent 模块的依赖关系中标注此 UI 层依赖。

---

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

### 2.1 架构解耦

#### 问题 2.1.1 ｜ parent 模块跨模块 import attendance 类型（P1）

- **位置**：
  - [parent-attendance-warning.tsx#L5](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx#L5)：`import type { StudentAttendanceSummary } from "@/modules/attendance/types"`
  - [parent-attendance-rate-card.tsx#L5](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx#L5)：同上
  - [parent-attendance-calendar.tsx#L6-L10](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L6)：`import type { AttendanceListItem, AttendanceStatus, StudentAttendanceSummary } from "@/modules/attendance/types"`
- **现象**：parent 模块的 3 个组件直接依赖 attendance 模块的类型定义，且 `parent-attendance-calendar.tsx` 内部重新定义了 `STATUS_LABEL` / `STATUS_DOT` 常量（与 attendance 模块的 `ATTENDANCE_STATUS_LABELS` / `ATTENDANCE_STATUS_COLORS` 重复）。
- **违反规则**：项目规则"该模块必须作为独立功能单元……模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access（只能通过注入的接口调用）"。虽然此处仅 import 类型，但 parent 模块应通过自身定义的视图模型接口解耦，而非直接消费 attendance 内部类型。
- **原因**：家长考勤视图需要展示 attendance 数据，开发时直接复用 attendance 类型，未做视图模型隔离。
- **后果**：attendance 模块修改 `StudentAttendanceSummary` 字段会破坏 parent 模块编译；parent 模块无法独立测试；新增角色时无法替换 attendance 数据源。

#### 问题 2.1.2 ｜ 考勤页面层绕过 Action 直接调用 data-access（P2）

- **位置**：
  - [admin/attendance/page.tsx#L12](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L12)：`import { getAttendanceRecords, getAttendanceStats } from "@/modules/attendance/data-access"`
  - [teacher/attendance/page.tsx#L10](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L10)：`import { getAttendanceRecords } from "@/modules/attendance/data-access"`
  - [teacher/attendance/sheet/page.tsx#L3](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx#L3)：`import { getClassStudentsForAttendance } from "@/modules/attendance/data-access"`
  - [teacher/attendance/stats/page.tsx#L3](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx#L3)：`import { getClassAttendanceStats } from "@/modules/attendance/data-access-stats"`
  - [student/attendance/page.tsx#L2](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx#L2)：`import { getStudentAttendanceSummary } from "@/modules/attendance/data-access-stats"`
  - [parent/attendance/page.tsx#L2](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx#L2)：同上
- **现象**：所有读操作页面（admin/teacher/student/parent）均直接调用 data-access，未走 `getAttendanceAction` / `getStudentAttendanceAction` / `getClassAttendanceStatsAction` 等 Server Action。
- **违反规则**：项目规则"`app/` 只能调用 `modules/` 的 Server Actions 和 data-access"——此处虽合规（data-access 允许被 app 调用），但架构图 §2.10 标注的 10 个 Action 中有 6 个读 Action 实际无调用方（死代码），且页面层未享受 Action 的统一错误处理与权限二次校验。
- **原因**：RSC 页面直接调 data-access 性能更优（少一层包装），但导致 Action 层读函数成为死代码。
- **后果**：Action 层 6 个读函数（`getAttendanceAction` / `getStudentAttendanceAction` / `getClassAttendanceStatsAction` / `getClassAttendanceForDateAction` / `getAttendanceRulesAction`）无调用方，维护成本浪费；权限二次校验形同虚设。

#### 问题 2.1.3 ｜ elective 页面层同样绕过 Action（P2）

- **位置**：
  - [admin/elective/page.tsx#L4](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx#L4)：`import { getElectiveCourses } from "@/modules/elective/data-access"`
  - [admin/elective/[id]/edit/page.tsx#L5](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx#L5)：`import { getElectiveCourseById } from "@/modules/elective/data-access"`
  - [teacher/elective/page.tsx#L4](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx#L4)：同 admin
  - [student/elective/page.tsx#L3](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx#L3)：`import { getAvailableCoursesForStudent, getStudentSelections } from "@/modules/elective/data-access-selections"`
- **现象**：与考勤相同，elective 的 3 个读 Action（`getElectiveCoursesAction` / `getStudentSelectionsAction` / `getAvailableCoursesAction`）无调用方。
- **后果**：同 2.1.2。

#### 问题 2.1.4 ｜ elective data-access 跨模块依赖未通过接口抽象（P2）

- **位置**：
  - [data-access.ts#L10-L11](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L10)：`import { getGradeOptions, getSubjectOptions } from "@/modules/school/data-access"`、`import { getUserNamesByIds } from "@/modules/users/data-access"`
  - [data-access-selections.ts#L12-L13](file:///e:/Desktop/CICD/src/modules/elective/data-access-selections.ts#L12)：`import { getStudentActiveGradeId } from "@/modules/classes/data-access"`、`import { getUserNamesByIds } from "@/modules/users/data-access"`
- **现象**：elective data-access 直接静态 import school/users/classes 模块的 data-access。
- **违反规则**：项目规则"模块间只能通过对方 data-access 通信"——此处合规（data-access 层通信），但未通过接口抽象，导致 elective 模块无法独立测试（mock 需拦截具体路径）。
- **原因**：架构图 §2.20 已标注这些跨模块依赖为"已修复"（从直查表改为 data-access），但未进一步抽象为接口。
- **后果**：单测 elective 时需 mock 3 个模块的 data-access 函数；未来替换 school/users/classes 实现需改 elective 源码。

### 2.2 国际化（i18n）

#### 问题 2.2.1 ｜ 考勤模块零 i18n 覆盖（P0）

- **位置**：模块全部 13 个源文件
- **现象**：项目已接入 next-intl（见 [i18n/request.ts](file:///e:/Desktop/CICD/src/i18n/request.ts)），但考勤模块**没有任何一处**使用 `useTranslations` / `getTranslations`，所有文案硬编码，且中英文混杂：
  - 中文硬编码：`"考勤总览"`、`"查看全校所有班级的考勤记录"`、`"统计分析"`、`"暂无考勤记录"`、`"系统中尚未产生任何考勤记录。"`、`"考勤记录"`、`"管理学生考勤记录。"`、`"录入考勤"`、`"统计"`、`"当前班级有未保存的考勤记录，确认切换班级？"`、`"总记录数"`、`"出勤"`、`"缺勤"`、`"迟到"`、`"早退"`、`"出勤率"`（[admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx)、[teacher/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx)、[attendance-sheet.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx)、[attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx)）
  - 英文硬编码：`"Attendance Sheet"`、`"Save Attendance"`、`"Saving..."`、`"Class"`、`"Date"`、`"Student"`、`"Email"`、`"Status"`、`"Mark All Present"`、`"Search student..."`、`"No students in this class..."`、`"Attendance Statistics"`、`"Present"`、`"Absent"`、`"Late"`、`"Early Leave"`、`"Excused"`、`"Total Records"`、`"Present Rate"`、`"Late Rate"`、`"No attendance data available."`、`"Recent Attendance"`、`"Attendance Rules"`、`"Save Rules"`、`"Late Threshold (minutes)"`、`"Early Leave Threshold (minutes)"`、`"Enable auto-marking..."`、`"Delete Attendance Record"`、`"Are you sure..."`、`"My Attendance"`、`"View your attendance records and statistics."`、`"No attendance records found."`、`"No data"`、`"Student attendance summary is not available."`、`"Recorded By"`、`"Created"`（[attendance-sheet.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx)、[attendance-record-list.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx)、[attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx)、[attendance-rules-form.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx)、[student-attendance-view.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/student-attendance-view.tsx)、[attendance-filters.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-filters.tsx)）
  - 状态标签常量硬编码英文：`ATTENDANCE_STATUS_LABELS` 在 [types.ts#L86-L92](file:///e:/Desktop/CICD/src/modules/attendance/types.ts#L86) 直接写死 `"Present"` / `"Absent"` / `"Late"` / `"Early Leave"` / `"Excused"`，未走 i18n。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键"。
- **原因**：模块开发时未跟进 i18n 改造，文案随写随定。
- **后果**：无法切换语言；同一界面中英混杂（管理员页中文、教师点名页英文、统计卡片中文），专业度差；后续做国际化需返工全部组件。

#### 问题 2.2.2 ｜ 选修课模块零 i18n 覆盖（P0）

- **位置**：模块全部 10 个源文件
- **现象**：与考勤模块相同，选修课模块无任何 i18n 调用，文案中英混杂：
  - 中文硬编码：`"选修课程"`、`"管理选修课程、开放/关闭选课与抽签。"`、`"新建选修课程"`、`"创建新的选修课程。"`、`"编辑选修课程"`、`"更新选修课程详情。"`（[admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx)、[admin/elective/create/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/create/page.tsx)、[admin/elective/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx)）
  - 英文硬编码：`"My Elective Courses"`、`"View and manage the elective courses you teach."`、`"Elective Courses"`、`"Browse available electives and manage your selections."`、`"New Course"`、`"No elective courses"`、`"There are no elective courses available."`、`"Credit"`、`"Teacher"`、`"Mode"`、`"Capacity"`、`"Room"`、`"Schedule"`、`"Open"`、`"Close"`、`"Lottery"`、`"Edit"`、`"Delete"`、`"New Elective Course"`、`"Edit Elective Course"`、`"Course Name *"`、`"Subject"`、`"Grade"`、`"Capacity"`、`"Classroom"`、`"Schedule"`、`"Credit"`、`"Selection Mode"`、`"First Come First Served"`、`"Lottery"`、`"Start Date"`、`"End Date"`、`"Selection Start"`、`"Selection End"`、`"Description"`、`"Cancel"`、`"Create"`、`"Save"`、`"Saving..."`、`"My Selections"`、`"Available Courses"`、`"No selections yet"`、`"Browse available courses below..."`、`"No available courses"`、`"Drop"`、`"Drop this course?"`、`"You are about to drop..."`、`"Yes, drop course"`、`"Already selected"`、`"Select"`、`"Selecting..."`、`"Search by course name, teacher..."`、`"All Modes"`、`"Selection Mode"`（[elective-course-list.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx)、[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)、[student-selection-view.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/student-selection-view.tsx)、[elective-filters.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx)）
  - 状态标签常量硬编码英文：`ELECTIVE_STATUS_LABELS` / `SELECTION_MODE_LABELS` / `COURSE_SELECTION_STATUS_LABELS` 在 [types.ts#L69-L97](file:///e:/Desktop/CICD/src/modules/elective/types.ts#L69) 直接写死英文。
- **违反规则**：同 2.2.1。
- **后果**：同 2.2.1。

#### 问题 2.2.3 ｜ i18n 翻译文件未注册新命名空间（P1）

- **位置**：[src/i18n/request.ts](file:///e:/Desktop/CICD/src/i18n/request.ts)
- **现象**：`request.ts` 加载了 12 个命名空间（common/auth/onboarding/classes/errors/dashboard/examHomework/announcements/messages/settings/textbooks/grade），但**未加载 attendance/elective 命名空间**（这两个文件也不存在）。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：即使组件层加了 `useTranslations("attendance")`，运行时也会因消息缺失而回退到 key 本身。

### 2.3 类型安全

#### 问题 2.3.1 ｜ `as` 断言与 `as never` 类型逃逸（P1）

- **位置**：
  - [elective-course-form.tsx#L204](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx#L204)：`setSelectionMode(v as "fcfs" | "lottery")` —— `v` 已是 `string`，应用类型守卫或 `ElectiveSelectionModeEnum` 校验。
  - [elective-course-list.tsx#L54](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L54)：`await action(null as never, formData)` —— 用 `as never` 绕过 `prevState` 类型检查，是类型逃逸。
  - [attendance-sheet.tsx#L126](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L126)：`{} as Record<AttendanceStatus, number>` —— 空对象断言为完整 Record，运行时 `statusCounts[status]` 在未初始化时会 `undefined`。
- **违反规则**：项目规则"禁止 `as` 断言（除非从 `unknown` 转换或测试中，需注释原因）"。
- **后果**：类型系统无法保护运行时错误；`as never` 让编译器失去对 `prevState` 的校验。

#### 问题 2.3.2 ｜ `attendance-sheet.tsx` 使用 `window.confirm` 阻塞 UI（P2）

- **位置**：[attendance-sheet.tsx#L107](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L107)：`if (!window.confirm("当前班级有未保存的考勤记录，确认切换班级？"))`
- **现象**：使用浏览器原生 `confirm`，与模块内其他删除操作使用的 `AlertDialog`/`Dialog` 不一致。
- **违反规则**：项目规则"组合优先"与 UI 一致性；`confirm()` 阻塞主线程且不可定制样式。
- **后果**：交互体验割裂；移动端 `confirm` 表现不一；i18n 文案无法替换。

#### 问题 2.3.3 ｜ `getAttendanceStats` 实现低效且类型不精确（P2）

- **位置**：[data-access.ts#L285-L308](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L285)
- **现象**：`getAttendanceStats` 注释写"简化实现：基于已有查询统计"，实际是先调 `getAttendanceRecords`（默认 pageSize=20）取前 20 条，再 `filter` 统计——**统计结果只基于前 20 条记录**，不是全量。
- **违反规则**：项目规则"函数返回值必须显式标注"（此处已标注，但语义错误）。
- **后果**：管理员考勤总览页的 6 卡片统计**永远是前 20 条记录的统计**，不是全校考勤统计，数据严重失真。

#### 问题 2.3.4 ｜ `getClassStudentsForAttendance` 直查 `classEnrollments`（P1）

- **位置**：[data-access.ts#L208-L219](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L208)
- **现象**：架构图 §2.10 标注"✅ P1-1 已修复：~~`getClassStudentsForAttendance` 直查 `classEnrollments`~~ 改为通过 classes data-access 获取"，但**实际代码仍直接查询 `classEnrollments` 表**（`db.select(...).from(classEnrollments).innerJoin(users, ...)`）。
- **违反规则**：项目规则"模块间只能通过对方 data-access 通信，禁止跨模块直接查询数据库表"。架构图记录与实际代码不一致。
- **原因**：架构图记录错误，或修复后被回退。
- **后果**：classes 模块修改 `classEnrollments` schema 会破坏 attendance 模块；架构图可信度受损。

### 2.4 错误与边界处理

#### 问题 2.4.1 ｜ 完全缺失 React Error Boundary（P0）

- **位置**：
  - 考勤：`src/app/(dashboard)/admin/attendance/`、`src/app/(dashboard)/teacher/attendance/`、`src/app/(dashboard)/student/attendance/`、`src/app/(dashboard)/parent/attendance/` 均无 `error.tsx`
  - 选修课：`src/app/(dashboard)/admin/elective/`、`src/app/(dashboard)/teacher/elective/`、`src/app/(dashboard)/student/elective/` 均无 `error.tsx`
- **现象**：7 个页面目录均无错误边界，DB 查询失败、Server Action 抛错时整页白屏。
- **违反规则**：项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **后果**：一次 DB 抖动导致整个考勤/选修课页面崩溃，无法隔离故障域；用户只能手动刷新。

#### 问题 2.4.2 ｜ 骨架屏覆盖不全（P2）

- **位置**：
  - 考勤：仅 `student/attendance/loading.tsx`、`parent/attendance/loading.tsx` 存在；`admin/attendance/`、`teacher/attendance/`、`teacher/attendance/sheet/`、`teacher/attendance/stats/` 均无骨架屏。
  - 选修课：仅 `student/elective/loading.tsx` 存在；`admin/elective/`、`admin/elective/create/`、`admin/elective/[id]/edit/`、`teacher/elective/` 均无骨架屏。
- **违反规则**：项目规则"异步数据使用 React Suspense + 骨架屏"。
- **后果**：管理员/教师端首屏白屏时间长，体验差。

#### 问题 2.4.3 ｜ 空状态文案与组件不统一（P2）

- **位置**：
  - [attendance-record-list.tsx#L54-L60](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx#L54)：内联 `<div>No attendance records found.</div>`
  - [attendance-sheet.tsx#L245-L248](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L245)：内联 `<p>No students in this class...</p>`
  - 列表页则用 `EmptyState` 组件
- **后果**：同一模块内空状态有两种写法，维护成本高，a11y 属性缺失。

#### 问题 2.4.4 ｜ Server Action 错误消息英文硬编码（P2）

- **位置**：
  - [attendance/actions.ts#L56](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts#L56)：`"Attendance recorded"`、`"Invalid form data"`、`"Unexpected error"`
  - [elective/actions.ts#L88](file:///e:/Desktop/CICD/src/modules/elective/actions.ts#L88)：`"Elective course created"`、`"Course not found"`、`"Invalid form data"`
- **现象**：所有 Action 的 `message` 字段硬编码英文，未走 i18n。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：toast 提示无法本地化。

### 2.5 组件复用与组合

#### 问题 2.5.1 ｜ 考勤状态标签/颜色常量重复定义（P1）

- **位置**：
  - [attendance/types.ts#L86-L103](file:///e:/Desktop/CICD/src/modules/attendance/types.ts#L86)：`ATTENDANCE_STATUS_LABELS` / `ATTENDANCE_STATUS_COLORS`
  - [parent-attendance-calendar.tsx#L14-L28](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L14)：`STATUS_DOT` / `STATUS_LABEL`（与 attendance 重复）
  - [attendance-sheet.tsx#L39-L61](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L39)：`STATUS_OPTIONS` / `STATUS_SHORTCUTS` / `STATUS_STYLES`（部分重复）
  - [attendance-filters.tsx#L21-L27](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-filters.tsx#L21)：`STATUS_OPTIONS`（与 sheet 重复）
- **现象**：考勤状态枚举的标签、颜色、快捷键、样式在 4 个文件里各写一份。
- **违反规则**：项目规则"最大化复用……抽象为泛型组件和 hooks"。
- **后果**：新增状态需改 4 处；当前已出现不一致（`ATTENDANCE_STATUS_COLORS` 用 `"outline"` 表示 early_leave，但 `STATUS_STYLES` 用 `bg-blue-500`）。

#### 问题 2.5.2 ｜ 选修课状态标签/颜色常量分散（P1）

- **位置**：
  - [elective/types.ts#L69-L108](file:///e:/Desktop/CICD/src/modules/elective/types.ts#L69)：4 组常量（`ELECTIVE_STATUS_LABELS` / `ELECTIVE_STATUS_COLORS` / `SELECTION_MODE_LABELS` / `COURSE_SELECTION_STATUS_LABELS` / `COURSE_SELECTION_STATUS_COLORS`）
  - [elective-course-form.tsx#L208-L213](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx#L208)：Select 选项硬编码 `"First Come First Served"` / `"Lottery"`（未复用 `SELECTION_MODE_LABELS`）
  - [elective-filters.tsx#L40-L44](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx#L40)：Select 选项硬编码（同上）
- **现象**：状态标签在 types.ts 集中定义，但表单/筛选组件未复用，重新硬编码。
- **后果**：标签变更需改 3 处；i18n 改造时需同步多处。

#### 问题 2.5.3 ｜ 考勤页面布局重复（P2）

- **位置**：
  - [admin/attendance/page.tsx#L62-L89](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L62)
  - [teacher/attendance/page.tsx#L63-L114](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L63)
- **现象**：两个页面的标题区 + 筛选区 + 列表区结构几乎相同，仅按钮和分页略有差异。
- **违反规则**：项目规则"最大化复用"。
- **后果**：UI 调整需改多处。

#### 问题 2.5.4 ｜ 选修课列表页布局重复（P2）

- **位置**：
  - [admin/elective/page.tsx#L30-L45](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx#L30)
  - [teacher/elective/page.tsx#L37-L52](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx#L37)
- **现象**：admin 和 teacher 列表页结构完全相同，仅 `createHref` 不同。
- **后果**：同 2.5.3。

### 2.6 可访问性（a11y）

#### 问题 2.6.1 ｜ 考勤点名表单缺 aria-label（P2）

- **位置**：[attendance-sheet.tsx#L215-L226](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L215)
- **现象**：班级选择器 `<Select>` 无 `aria-label`，日期输入框有 `id="date"` 但无 `aria-label`；状态按钮组有 `aria-pressed` 和 `aria-label`（✅ 良好），但表格行 `<TableRow>` 缺 `role="button"` 与 `tabIndex`。
- **违反规则**：项目规则"可访问性（a11y）：语义化标签、ARIA 属性、键盘导航"。
- **后果**：屏幕阅读器用户无法理解筛选区用途。

#### 问题 2.6.2 ｜ 选修课卡片缺语义化标签（P2）

- **位置**：[elective-course-list.tsx#L110-L227](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L110)
- **现象**：课程卡片用 `<Card>` 但无 `role="article"` 或 `aria-label`；"Open"/"Close"/"Lottery"/"Delete" 按钮有图标但 `aria-label` 缺失（仅有 `variant` 文本）。
- **后果**：屏幕阅读器用户无法快速定位卡片内容。

#### 问题 2.6.3 ｜ 考勤月历键盘导航缺失（P2）

- **位置**：[parent-attendance-calendar.tsx#L143-L177](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L143)
- **现象**：月历日期格子用 `<div>`，无 `tabIndex`、无方向键导航；月份切换按钮有 `aria-label`（✅ 良好），但日期格子不可聚焦。
- **后果**：键盘用户无法浏览具体日期的考勤状态。

### 2.7 可测试性

#### 问题 2.7.1 ｜ 纯逻辑未导出，无法单测（P1）

- **位置**：
  - [attendance/data-access-stats.ts#L26-L39](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L26) `computeStats`（模块内未导出）
  - [parent-attendance-warning.tsx#L14-L55](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx#L14) `buildWarnings`（模块内未导出）
  - [parent-attendance-rate-card.tsx#L14-L30](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx#L14) `aggregate` / `rateTone`（模块内未导出）
  - [parent-attendance-calendar.tsx#L30-L62](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L30) `formatDateKey` / `parseDateKey` / `buildCalendarDays` / `isSameDay`（模块内未导出）
  - [elective/data-access-operations.ts#L14-L19](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L14) `buildLotteryRankCase`（模块内未导出）
- **现象**：这些纯函数（统计计算、预警规则、聚合、日期工具、SQL 构造）是核心逻辑，但未导出，无法写单测；两个模块目录下无任何 `__tests__` 或 `*.test.ts`。
- **违反规则**：项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks，与 UI 分离；导出清晰的接口类型以便 mock"。
- **后果**：考勤统计、预警阈值、抽签算法这类容易出 bug 的逻辑无回归保护。

#### 问题 2.7.2 ｜ 零测试覆盖（P1）

- **位置**：两个模块整体
- **现象**：无单元测试、无集成测试、无 e2e 测试。
- **后果**：重构高风险。

### 2.8 性能

#### 问题 2.8.1 ｜ `getAttendanceStats` 全表扫描但只统计前 20 条（P0）

- **位置**：[data-access.ts#L285-L308](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L285)
- **现象**：见 2.3.3。`getAttendanceRecords` 默认 `pageSize=20`，`getAttendanceStats` 调用它后只统计 `items`（20 条），但管理员总览页展示的是"全校考勤统计"——**数据严重失真**。
- **后果**：管理员看到的出勤率永远是前 20 条记录的出勤率，决策失误。

#### 问题 2.8.2 ｜ `getStudentAttendanceSummary` 一次拉全量记录（P2）

- **位置**：[data-access-stats.ts#L60-L68](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L60)
- **现象**：学生汇总页一次性加载该学生所有考勤记录（无分页），仅 `recentRecords` 截取前 20 条，但 `stats` 基于全量。
- **后果**：考勤记录多的学生首屏慢。

#### 问题 2.8.3 ｜ `resolveCourseDisplayNames` 每次调用都全量拉取科目/年级/教师（P2）

- **位置**：[elective/data-access.ts#L100-L122](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L100)
- **现象**：每次查询课程列表都调用 `getSubjectOptions()` / `getGradeOptions()` / `getUserNamesByIds()`，无缓存（虽然 `getElectiveCourses` 用了 `cache()`，但内部 `resolveCourseDisplayNames` 仍会执行）。
- **后果**：高频访问时重复查询。

### 2.9 安全性

#### 问题 2.9.1 ｜ Server Action 未校验资源归属（P0）

- **位置**：
  - [attendance/actions.ts#L98-L128](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts#L98) `updateAttendanceAction(id, ...)`：仅校验 `ATTENDANCE_MANAGE` 权限，未校验 `id` 对应的考勤记录是否属于当前教师所教班级。
  - [attendance/actions.ts#L130-L143](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts#L130) `deleteAttendanceAction(id)`：同上。
  - [elective/actions.ts#L94-L134](file:///e:/Desktop/CICD/src/modules/elective/actions.ts#L94) `updateElectiveCourseAction(id, ...)`：仅校验 `ELECTIVE_MANAGE`，未校验 `id` 对应课程是否属于当前教师（admin 可改全部，teacher 应只能改自己的课程）。
  - [elective/actions.ts#L136-L153](file:///e:/Desktop/CICD/src/modules/elective/actions.ts#L136) `deleteElectiveCourseAction`：同上。
- **违反规则**：项目规则"Server Action 二次校验"、"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
- **后果**：教师 A 可通过改 `id` 篡改/删除教师 B 的考勤记录或选修课（越权写）。

#### 问题 2.9.2 ｜ `getClassAttendanceForDateAction` 未校验班级归属（P1）

- **位置**：[attendance/actions.ts#L212-L225](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts#L212)
- **现象**：仅校验 `ATTENDANCE_READ`，未校验 `classId` 是否属于当前教师所教班级。
- **后果**：教师可查看任意班级的考勤明细。

#### 问题 2.9.3 ｜ `saveAttendanceRulesAction` 未校验班级归属（P1）

- **位置**：[attendance/actions.ts#L227-L257](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts#L227)
- **现象**：仅校验 `ATTENDANCE_MANAGE`，未校验 `classId` 是否属于当前教师所教班级。
- **后果**：教师可修改任意班级的考勤规则。

#### 问题 2.9.4 ｜ `runLotteryAction` / `openSelectionAction` / `closeSelectionAction` 未校验课程归属（P1）

- **位置**：[elective/actions.ts#L155-L211](file:///e:/Desktop/CICD/src/modules/elective/actions.ts#L155)
- **现象**：仅校验 `ELECTIVE_MANAGE`，未校验 `courseId` 是否属于当前教师。
- **后果**：教师可对他人课程执行抽签/开放/关闭。

### 2.10 监控与埋点

#### 问题 2.10.1 ｜ 关键操作无埋点接口（P2）

- **位置**：两个模块全部 Action
- **现象**：考勤录入、选课、抽签这类关键操作无任何埋点钩子。
- **违反规则**：项目规则"监控：方案中预留关键操作埋点接口"。
- **后果**：无法统计考勤录入率、选课转化率、抽签冲突率等业务指标。

---

## 三、行业差距对比

对标国内外主流 K12 教育平台（如校宝在线、ClassIn、Seewo、PowerSchool、Veracross、Khan Academy）在考勤与选修课模块的设计，本模块存在以下差距：

### 3.1 考勤模块

| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 多维度考勤：按课节/全天/活动考勤 | 仅按"班级+日期"考勤，无课节维度 | 无法支撑"上午缺勤/下午缺勤"细分，K12 排课制场景受限 |
| 自动考勤：对接校园卡/人脸/蓝牙签到 | 仅手动点名 | 教师负担重，数据滞后 |
| 考勤异常自动通知家长（SMS/微信/站内信） | 仅家长端被动查看 | 家长无法及时获知孩子缺勤 |
| 考勤趋势图表（按周/月/学期） | 仅静态统计卡片 | 无法发现出勤规律（如每周五缺勤多） |
| 考勤预警规则可配置（连续缺勤 N 次触发） | 仅 `attendanceRules` 表存阈值，无触发逻辑 | 规则形同虚设 |
| 请假申请流程（学生/家长发起→教师审批→自动标记 excused） | 无请假流程，`excused` 状态需手动录入 | 请销假流程断裂 |
| 补签/改签审计日志 | 无审计 | 无法追溯考勤篡改 |
| 班级出勤热力图（哪天缺勤多） | 无 | 教师无法快速定位异常日 |

### 3.2 选修课模块

| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 课程目录：分类/标签/搜索/筛选/排序 | 仅按状态/模式筛选，无分类标签 | 学生发现课程困难 |
| 课程详情页：大纲/教师介绍/评价/历史选课数据 | 仅卡片展示基本信息 | 学生决策信息不足 |
| 选课优先级多志愿（第一志愿/第二志愿）+ 智能分配 | `priority` 字段存在但抽签仅按 priority 升序，无多志愿匹配算法 | 抽签结果可能让学生一无所获 |
| 候补队列实时通知（有人退课自动递补+通知） | FCFS 模式有递补逻辑但无通知 | 候补学生不知道自己被录取 |
| 选课时间窗口冲突检测（与必修课/其他选修课冲突） | 无 | 学生可能选到时间冲突的课程 |
| 学分上限/下限校验 | 无 | 学生可能选课过多或过少 |
| 教师端：选课名单管理/成绩录入/导出 | 教师端仅列表，无名单/成绩 | 教师无法管理已选学生 |
| 课程评价/满意度调查 | 无 | 无法改进课程质量 |
| 历史选课数据归档 | 无 | 无法分析选课趋势 |

### 3.3 多角色协作层

| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| admin：考勤全校热力图 + 异常班级排名 + 选课数据大盘 | admin 考勤仅 6 卡片（且统计失真），选课无大盘 | 管理员无法宏观决策 |
| teacher：考勤批量补签 + 选课名单导出 Excel | 考勤无补签，选课无导出 | 教师日常操作低效 |
| parent：考勤异常推送 + 请假申请 + 选课结果通知 | parent 仅被动查看，无请假/通知 | 家长参与度低 |
| student：考勤自查 + 请假申请 + 选课推荐 | student 仅查看，无请假/推荐 | 学生自主性差 |

### 3.4 交互体验层

| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 考勤点名：一键全到/批量按状态/键盘快捷键 | ✅ 已实现（快捷键 P/A/L/E/X） | 良好 |
| 考勤点名：学生头像/学号排序/拼音搜索 | 仅按 name 排序，搜索按 name includes | 中文环境拼音搜索缺失 |
| 选课：课程对比/收藏/愿望清单 | 无 | 学生难以比较课程 |
| 选课：移动端优化（卡片瀑布流） | 响应式但未针对移动端优化 | 平板/手机体验一般 |
| 空状态/加载骨架屏/错误重试 | 部分页面有骨架屏，错误边界完全缺失 | 体验不稳定 |

### 3.5 数据分析层

| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 考勤与成绩关联分析（缺勤多→成绩下降） | 无 | 无法预警学业风险 |
| 选课与升学路径关联（选某课→升某专业） | 无 | 无法指导学生规划 |
| 考勤/选课数据导出 Excel/PDF | 考勤无导出，选课无导出 | 无法离线分析 |

---

## 四、改进优先级建议

### P0（紧急，阻塞多角色上线或数据严重失真）

1. **修复 `getAttendanceStats` 统计失真**：改为基于 `COUNT` 聚合查询，而非取前 20 条 `items` 统计；或直接在 data-access 层用 `db.select({ count, status }).groupBy(status)` 一次查询。
2. **修复 `getClassStudentsForAttendance` 跨模块直查**：改为调用 `classes/data-access.getActiveStudentIdsByClassId` 或新增 `classes/data-access.getClassStudentsForAttendance`，与架构图记录一致。
3. **Server Action 资源归属校验**：在 `updateAttendanceAction` / `deleteAttendanceAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `runLotteryAction` / `openSelectionAction` / `closeSelectionAction` / `saveAttendanceRulesAction` / `getClassAttendanceForDateAction` 内，结合 `ctx.dataScope` 与 `ctx.userId` 校验资源归属（教师只能操作自己班级/课程）。
4. **全模块 i18n 改造**：新增 `shared/i18n/messages/{en,zh-CN}/attendance.json` 与 `elective.json` 命名空间，在 `i18n/request.ts` 注册加载；提取所有硬编码文案；状态标签常量改为 i18n key（运行时通过 `useTranslations` 解析）。
5. **补齐 Error Boundary**：在 7 个页面目录下新增 `error.tsx`（admin/teacher/student/parent × attendance/elective），复用现有 `EmptyState` + `AlertCircle` 模式。

### P1（重要，影响正确性与可维护性）

1. **解耦 parent 模块对 attendance 类型的直接依赖**：在 parent 模块定义视图模型接口（`ParentAttendanceSummary`），由 `parent/attendance/page.tsx` 在 RSC 层做映射；或抽取共享类型到 `shared/types/attendance.ts`。
2. **消除状态常量重复**：新建 `attendance/constants.ts` 集中导出 `ATTENDANCE_STATUS_OPTIONS`（含 value/label-key/color/shortcut/icon），供 sheet/filters/stats/calendar 复用；elective 同理。
3. **抽取纯函数并补单测**：导出 `computeStats` / `buildWarnings` / `aggregate` / `rateTone` / `formatDateKey` / `parseDateKey` / `buildCalendarDays` / `isSameDay` / `buildLotteryRankCase`，补 Vitest 单测覆盖空数组、边界值、闰年、跨月等。
4. **修复类型断言**：用类型守卫替换 `as "fcfs" | "lottery"`（用 `ElectiveSelectionModeEnum.safeParse`）；用 `Object.fromEntries(STATUS_OPTIONS.map(s => [s, 0]))` 替换 `{} as Record<...>`；删除 `as never`，改为泛型约束 `prevState`。
5. **统一 `window.confirm` 为 `AlertDialog`**：`attendance-sheet.tsx` 的切换班级确认改为 `AlertDialog`，与模块其他删除操作一致。
6. **补齐骨架屏**：为 admin/teacher 考勤与选修课页面补 `loading.tsx`。
7. **统一空状态**：内联空状态全部改用 `EmptyState` 组件。
8. **a11y 改进**：考勤点名表单补 `aria-label`；选修课卡片补 `role="article"` + `aria-label`；考勤月历日期格子补 `tabIndex` + 方向键导航。
9. **清理死代码 Action**：删除无调用方的 6 个读 Action（`getAttendanceAction` / `getStudentAttendanceAction` / `getClassAttendanceStatsAction` / `getClassAttendanceForDateAction` / `getAttendanceRulesAction` / `getElectiveCoursesAction` / `getStudentSelectionsAction` / `getAvailableCoursesAction`），或改为页面层调用（统一权限二次校验）。
10. **埋点接口预留**：在 `data-access` 与 `actions` 中预留 `onAttendanceRecorded` / `onCourseSelected` / `onLotteryCompleted` 钩子，供后续接入监控。

### P2（优化，提升体验与专业度）

1. **页面布局复用**：抽取 `AttendancePageLayout` / `ElectivePageLayout` 组件，admin/teacher 页面复用。
2. **考勤统计图表**：接入 recharts，按周/月展示出勤趋势线、缺勤热力图。
3. **选修课课程详情页**：新增 `/student/elective/[id]` 详情页，展示大纲/教师/评价。
4. **选课时间冲突检测**：在 `selectCourse` 内校验学生已有选课的 schedule 是否冲突。
5. **学分上限校验**：在 `selectCourse` 内校验学生本学期已选学分 + 当前课程学分是否超过上限。
6. **考勤/选课数据导出**：复用 `shared/lib/excel.ts`，新增导出 Action。
7. **移动端优化**：选修课卡片改为瀑布流，考勤点名表单窄屏优化。
8. **补全架构图同步**（见第五节）。

---

## 五、架构图同步说明

本次审计发现 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.10（attendance）与 §2.20（elective）以及 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中对应节点存在以下偏差，需同步修正：

### 5.1 attendance 行数与组件统计偏差

| 项 | 图记 | 实际 |
|------|------|------|
| `actions.ts` 行数 | 271 | 271（一致） |
| `data-access.ts` 行数 | 309 | 309（一致） |
| `data-access-stats.ts` 行数 | 145 | 145（一致） |
| 组件文件数 | 5（仅列 `AttendanceStatsCards`） | 8（`AttendanceSheet` / `AttendanceRecordList` / `AttendanceFilters` / `AttendanceStatsCard` / `AttendanceStatsCards` / `AttendanceStatsClassSelector` / `AttendanceRulesForm` / `StudentAttendanceView`） |
| Actions 名称 | `getAttendanceRecordsAction` / `createAttendanceRecordAction` / `updateAttendanceRecordAction` / `deleteAttendanceRecordAction` / `getStudentAttendanceAction` / `getAttendanceStatsAction` | `recordAttendanceAction` / `batchRecordAttendanceAction` / `updateAttendanceAction` / `deleteAttendanceAction` / `getAttendanceAction` / `getStudentAttendanceAction` / `getClassAttendanceStatsAction` / `getClassAttendanceForDateAction` / `saveAttendanceRulesAction` / `getAttendanceRulesAction`（10 个） |

### 5.2 attendance 已知问题记录偏差

架构图 §2.10 标注"✅ P1-1 已修复：~~`getClassStudentsForAttendance` 直查 `classEnrollments`~~ 改为通过 classes data-access 获取"，但**实际代码仍直接查询 `classEnrollments` 表**（[data-access.ts#L208-L219](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L208)）。需将架构图改为"❌ P1-1 未修复：`getClassStudentsForAttendance` 仍直查 `classEnrollments`"。

### 5.3 attendance 缺失功能记录

架构图未记录以下已实现的功能：
- `attendanceRules` 表的 CRUD（`saveAttendanceRulesAction` / `getAttendanceRulesAction` + `upsertAttendanceRules` / `getAttendanceRules`）
- `AttendanceRulesForm` 组件
- `AttendanceRecordList` 组件（含删除对话框）
- `StudentAttendanceView` 组件（学生/家长视图）
- `AttendanceStatsClassSelector` 组件（ChipNav 筛选）

### 5.4 elective 行数与组件统计偏差

| 项 | 图记 | 实际 |
|------|------|------|
| `actions.ts` 行数 | 304 | 304（一致） |
| `data-access.ts` 行数 | 250 | 250（一致） |
| `data-access-operations.ts` 行数 | 245 | 245（一致） |
| `data-access-selections.ts` 行数 | 189 | 149（减少 40 行） |
| 组件文件数 | 3 | 4（缺 `student-selection-view.tsx`） |

### 5.5 elective usedBy 信息缺失

`getStudentSelectionsAction` / `getAvailableCoursesAction` 的 `usedBy` 字段标注为"待扩展"，实际已被 `student/elective/page.tsx` 通过 data-access 直接调用（绕过 Action）。应改为"无调用方（页面层直接调 data-access）"或删除这两个 Action。

### 5.6 parent 跨模块 UI 依赖未记录

架构图 §2.19（parent）的依赖关系未标注 parent 模块对 attendance 模块类型的直接 import：
- `parent/components/parent-attendance-warning.tsx` → `@/modules/attendance/types`
- `parent/components/parent-attendance-rate-card.tsx` → `@/modules/attendance/types`
- `parent/components/parent-attendance-calendar.tsx` → `@/modules/attendance/types`

应在 004 的 parent 依赖关系与 005 的 `dependencyMatrix` 中补充该 UI 层依赖，并标注为"待解耦（P1）"。

### 5.7 建议的 JSON 节点更新

`005_architecture_data.json` 中 `modules.attendance` 与 `modules.elective` 节点建议补充/修正：

```jsonc
{
  "attendance": {
    "exports": {
      "actions": [
        "recordAttendanceAction", "batchRecordAttendanceAction",
        "updateAttendanceAction", "deleteAttendanceAction",
        "getAttendanceAction", "getStudentAttendanceAction",
        "getClassAttendanceStatsAction", "getClassAttendanceForDateAction",
        "saveAttendanceRulesAction", "getAttendanceRulesAction"
      ],
      "dataAccess": [
        "getAttendanceRecords", "getClassAttendanceForDate",
        "createAttendanceRecord", "batchCreateAttendanceRecords",
        "updateAttendanceRecord", "deleteAttendanceRecord",
        "getClassStudentsForAttendance",  // ❌ 仍直查 classEnrollments
        "getAttendanceRules", "upsertAttendanceRules",
        "getStudentAttendanceSummary", "getClassAttendanceStats",
        "getAttendanceStats"  // ❌ 统计失真，仅基于前 20 条
      ],
      "components": [
        "AttendanceSheet", "AttendanceRecordList", "AttendanceFilters",
        "AttendanceStatsCard", "AttendanceStatsCards",
        "AttendanceStatsClassSelector", "AttendanceRulesForm",
        "StudentAttendanceView"
      ]
    },
    "knownIssues": [
      "getClassStudentsForAttendance 仍直查 classEnrollments（P1）",
      "getAttendanceStats 统计失真，仅基于前 20 条（P0）",
      "Server Action 未校验资源归属（P0）",
      "全模块零 i18n（P0）",
      "缺 Error Boundary（P0）",
      "parent 模块跨模块 import attendance 类型（P1）",
      "状态常量重复定义（P1）",
      "纯逻辑未导出，零单测（P1）"
    ]
  },
  "elective": {
    "exports": {
      "actions": [
        "createElectiveCourseAction", "updateElectiveCourseAction",
        "deleteElectiveCourseAction", "openSelectionAction",
        "closeSelectionAction", "runLotteryAction",
        "selectCourseAction", "dropCourseAction",
        "getElectiveCoursesAction",  // ❌ 无调用方
        "getStudentSelectionsAction",  // ❌ 无调用方
        "getAvailableCoursesAction"  // ❌ 无调用方
      ],
      "components": [
        "ElectiveCourseList", "ElectiveCourseForm",
        "ElectiveFilters", "StudentSelectionView"
      ]
    },
    "knownIssues": [
      "Server Action 未校验课程归属（P0）",
      "全模块零 i18n（P0）",
      "缺 Error Boundary（P0）",
      "3 个读 Action 无调用方（P1）",
      "状态常量分散，表单未复用（P1）",
      "纯逻辑未导出，零单测（P1）"
    ]
  }
}
```

---

## 附：重构方案设计要点（不写实现代码）

为满足"完全解耦 / 组合优先 / 国际化就绪 / 最大化复用 / 错误与边界处理 / 可测试性 / 可扩展性 / 企业级补充"八项原则，建议按以下方向重构（详细实现留待后续任务）：

### A. 数据服务接口抽象

```ts
// attendance/services/types.ts
export interface AttendanceDataService {
  listRecords(query: AttendanceQuery): Promise<PaginatedAttendanceResult>
  getStudentSummary(studentId: string, range?: DateRange): Promise<StudentAttendanceSummary | null>
  getClassStats(classId: string, range?: DateRange): Promise<ClassAttendanceSummary | null>
  getClassStudents(classId: string): Promise<Student[]>
  getRules(classId?: string): Promise<AttendanceRule[]>
}

export interface AttendanceMutationService {
  record(input: RecordAttendanceInput): Promise<ActionState>
  batchRecord(input: BatchRecordAttendanceInput): Promise<ActionState>
  update(id: string, input: UpdateAttendanceInput): Promise<ActionState>
  delete(id: string): Promise<ActionState>
  saveRules(input: AttendanceRuleInput): Promise<ActionState>
}
```

通过 `AttendanceDataProvider`（React Context）注入不同角色实现：teacher 实现 = 按 `class_taught` scope 过滤 + 可写；student 实现 = 按 `owned` scope 过滤 + 只读；admin 实现 = 全量 + 可写；parent 实现 = 按 `children` scope 过滤 + 只读。

elective 模块同理定义 `ElectiveDataService` / `ElectiveMutationService`。

### B. 配置驱动角色渲染

```ts
// attendance/config/role-config.ts
export const ATTENDANCE_ROLE_CONFIG: Record<Role, AttendanceRoleConfig> = {
  admin:   { widgets: ['stats', 'filters', 'list'], canManage: true, scope: 'all' },
  teacher: { widgets: ['stats', 'filters', 'list', 'sheet', 'rules'], canManage: true, scope: 'class_taught' },
  student: { widgets: ['summary'], canManage: false, scope: 'owned' },
  parent:  { widgets: ['summary', 'calendar', 'warning', 'rateCard'], canManage: false, scope: 'children' },
}
```

页面根据 `useRoleConfig()` 决定渲染哪些 Widget，新增角色只改配置。

### C. 组合式 UI

- `AttendancePage` 改为 `children`-based 组合：`<AttendancePage><StatsCards /><Filters /><RecordList /></AttendancePage>`
- parent 模块的考勤视图改为 render prop：`<ParentAttendanceView renderSummary={(summary) => <CustomCalendar summary={summary} />} />`，由页面层注入 calendar/warning/rateCard 组件，parent 模块内部不 import attendance 类型。

### D. i18n 翻译文件结构示例

```
shared/i18n/messages/
├─ en/attendance.json
├─ en/elective.json
├─ zh-CN/attendance.json
└─ zh-CN/elective.json
```

```jsonc
// zh-CN/attendance.json
{
  "title": { "admin": "考勤总览", "teacher": "考勤记录", "student": "我的考勤", "parent": "子女考勤" },
  "subtitle": { "admin": "查看全校所有班级的考勤记录", "teacher": "管理学生考勤记录" },
  "action": {
    "record": "录入考勤", "stats": "统计", "markAllPresent": "全部标记到场",
    "save": "保存", "cancel": "取消", "delete": "删除", "edit": "编辑"
  },
  "field": {
    "class": "班级", "date": "日期", "student": "学生", "status": "状态",
    "remark": "备注", "recordedBy": "记录人", "createdAt": "创建时间",
    "lateThreshold": "迟到阈值（分钟）", "earlyLeaveThreshold": "早退阈值（分钟）",
    "enableAutoMark": "启用自动标记（学生按时签到则自动标记到场）"
  },
  "status": {
    "present": "到场", "absent": "缺勤", "late": "迟到",
    "early_leave": "早退", "excused": "请假"
  },
  "stats": {
    "total": "总记录数", "present": "出勤", "absent": "缺勤",
    "late": "迟到", "earlyLeave": "早退", "excused": "请假",
    "presentRate": "出勤率", "lateRate": "迟到率"
  },
  "empty": {
    "noRecords": "暂无考勤记录", "noStudents": "该班级暂无学生",
    "noData": "暂无数据", "noClasses": "您还没有班级"
  },
  "dialog": {
    "deleteTitle": "删除考勤记录", "deleteDesc": "确定要删除这条考勤记录吗？此操作无法撤销。",
    "confirmSwitchClass": "当前班级有未保存的考勤记录，确认切换班级？"
  },
  "error": { "loadFailed": "考勤数据加载失败", "retry": "重试" }
}
```

```jsonc
// zh-CN/elective.json
{
  "title": { "admin": "选修课程", "teacher": "我的选修课", "student": "选课中心" },
  "subtitle": { "admin": "管理选修课程、开放/关闭选课与抽签" },
  "action": {
    "create": "新建课程", "edit": "编辑", "delete": "删除",
    "open": "开放选课", "close": "关闭选课", "lottery": "抽签",
    "select": "选择", "drop": "退课", "cancel": "取消", "save": "保存"
  },
  "field": {
    "name": "课程名称", "subject": "学科", "grade": "年级", "teacher": "教师",
    "capacity": "容量", "classroom": "教室", "schedule": "上课时间",
    "credit": "学分", "selectionMode": "选课模式",
    "startDate": "开始日期", "endDate": "结束日期",
    "selectionStart": "选课开始", "selectionEnd": "选课结束",
    "description": "课程简介"
  },
  "status": {
    "draft": "草稿", "open": "开放中", "closed": "已关闭", "cancelled": "已取消"
  },
  "selectionMode": { "fcfs": "先到先得", "lottery": "抽签" },
  "selectionStatus": {
    "selected": "已选", "enrolled": "已录取", "waitlist": "候补",
    "dropped": "已退课", "rejected": "未录取"
  },
  "section": { "mySelections": "我的选课", "available": "可选课程" },
  "empty": {
    "noCourses": "暂无选修课程", "noSelections": "暂无选课",
    "noAvailable": "暂无可选课程"
  },
  "dialog": {
    "dropTitle": "确认退课？", "dropDesc": "您即将退课 {course}，此操作无法撤销，且若课程已满，您可能失去名额。",
    "confirmDrop": "确认退课"
  },
  "error": { "loadFailed": "选修课数据加载失败", "retry": "重试" }
}
```

### E. 错误边界与骨架屏

- 每个独立数据区块（统计卡片、筛选栏、记录列表、点名表单、规则表单、课程列表、选课视图）用 `<ErrorBoundary fallback={<ErrorState />}>` 包裹
- 异步加载用 `<Suspense fallback={<AttendancePageSkeleton />}>`
- 空状态、无权限、网络异常统一用 `EmptyState` / `ForbiddenState` / `ErrorState` 三套标准组件

### F. 可测试性

- 纯逻辑（`computeStats` / `buildWarnings` / `aggregate` / `rateTone` / `formatDateKey` / `parseDateKey` / `buildCalendarDays` / `isSameDay` / `buildLotteryRankCase`）抽到 `*/utils/` 并导出
- 数据服务接口便于 mock，组件测试时注入 stub service
- 补 Vitest 单测 + Playwright e2e（考勤点名、选课、抽签三条核心路径）

### G. 监控埋点

- 在 `data-access` 与 `actions` 中预留 `onAttendanceRecorded` / `onCourseSelected` / `onLotteryCompleted` / `onAttendanceRuleChanged` 钩子
- 钩子默认 no-op，由后续监控模块通过 Context 注入实现
