Files
NextEdu/docs/architecture/audit/attendance-elective-audit-report.md
SpecialX 4833930834 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 字段、更新依赖矩阵
2026-06-22 16:17:00 +08:00

770 lines
61 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 考勤与选修课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 注入实现