feat(attendance,elective): 考勤与选修课模块审计重构 — P0 修复 + i18n + Error Boundary

审计报告:docs/architecture/audit/attendance-elective-audit-report.md

P0 修复:
- attendance: getAttendanceStats 统计失真(仅基于前 20 条记录)改为 SQL 聚合查询
- attendance: getClassStudentsForAttendance 跨模块直查 classEnrollments 改为调用 classes data-access
- attendance: update/delete Action 新增资源归属校验(assertRecordOwnership)
- elective: update/delete/openSelection/closeSelection/runLottery Action 新增资源归属校验(assertCourseOwnership)

i18n 接入:
- 新增 attendance/elective 命名空间(zh-CN + en)
- attendance-stats-cards 接入 useTranslations
- elective-course-list/form 接入 useTranslations

类型安全(P1):
- elective-course-form: 移除 as 断言,改用类型守卫 isSelectionMode
- elective-course-list: 移除 null as never 类型逃逸,改用泛型

Error Boundary:
- 新增 admin/teacher attendance error.tsx
- 新增 admin/student elective error.tsx

架构图同步:
- 004: 修正 attendance/elective/parent 章节的导出函数、文件清单、已知问题
- 005: 修正 actions 的 usedBy(标记无调用方的死代码)、新增 issues 字段、更新依赖矩阵
This commit is contained in:
SpecialX
2026-06-22 16:17:00 +08:00
parent 5d42495480
commit 4833930834
16 changed files with 1431 additions and 48 deletions

View File

@@ -0,0 +1,769 @@
# 考勤与选修课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-accessschool.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.10attendance与 §2.20elective以及 [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-accessP2
- **位置**
- [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 页面层同样绕过 ActionP2
- **位置**
- [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` 阻塞 UIP2
- **位置**[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 BoundaryP0
- **位置**
- 考勤:`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-labelP2
- **位置**[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.10attendance与 §2.20elective以及 [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.19parent的依赖关系未标注 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 仍直查 classEnrollmentsP1",
"getAttendanceStats 统计失真,仅基于前 20 条P0",
"Server Action 未校验资源归属P0",
"全模块零 i18nP0",
"缺 Error BoundaryP0",
"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",
"全模块零 i18nP0",
"缺 Error BoundaryP0",
"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 注入实现