审计报告: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 字段、更新依赖矩阵
61 KiB
考勤与选修课(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 | 271 | 10 个 Server Action(含权限校验、Zod 校验) |
| 数据访问 | data-access.ts | 309 | 考勤记录 CRUD + 班级学生查询 + 规则 upsert + 总览统计 |
| 数据访问 | data-access-stats.ts | 145 | 学生/班级考勤汇总(拆分范例) |
| Schema | schema.ts | 43 | Zod 校验(5 个 schema) |
| Types | types.ts | 103 | 类型定义 + 状态标签/颜色常量 |
| 组件 | components/attendance-sheet.tsx | 353 | 批量点名表单(键盘快捷键、状态按钮组) |
| 组件 | components/attendance-record-list.tsx | 130 | 考勤记录列表 + 删除对话框 |
| 组件 | components/attendance-filters.tsx | 97 | URL 同步筛选器(班级/状态/日期) |
| 组件 | components/attendance-stats-card.tsx | 81 | 单卡片统计(8 指标) |
| 组件 | components/attendance-stats-cards.tsx | 80 | 管理员总览 6 卡片网格 |
| 组件 | components/attendance-stats-class-selector.tsx | 27 | 班级筛选 ChipNav |
| 组件 | components/attendance-rules-form.tsx | 148 | 考勤规则配置表单 |
| 组件 | components/student-attendance-view.tsx | 104 | 学生/家长视图(统计 + 最近记录) |
| 页面 | admin/attendance/page.tsx | 91 | 管理员考勤总览(RSC) |
| 页面 | teacher/attendance/page.tsx | 116 | 教师考勤记录列表(RSC + 分页) |
| 页面 | teacher/attendance/sheet/page.tsx | 44 | 教师点名页(RSC) |
| 页面 | teacher/attendance/stats/page.tsx | 85 | 教师班级考勤统计(RSC) |
| 页面 | student/attendance/page.tsx | 40 | 学生考勤汇总(RSC) |
| 页面 | parent/attendance/page.tsx | 66 | 家长多子女考勤聚合(RSC) |
| 骨架屏 | 2 个 loading.tsx(student/parent) |
— | 列表骨架屏 |
| 错误边界 | 0 个 error.tsx |
— | 完全缺失 |
选修课模块(elective)
| 层 | 文件 | 行数 | 职责 |
|---|---|---|---|
| Server Actions | actions.ts | 304 | 11 个 Server Action |
| 数据访问 | data-access.ts | 250 | 课程 CRUD + scope 过滤 + 显示名聚合 |
| 数据访问 | data-access-operations.ts | 245 | 选课/退课/抽签(事务 + FOR UPDATE 锁) |
| 数据访问 | data-access-selections.ts | 149 | 选课记录查询 + 学生可选课程 |
| Schema | schema.ts | 132 | Zod 校验(5 个 schema) |
| Types | types.ts | 108 | 类型定义 + 4 组标签/颜色常量 |
| 组件 | components/elective-course-list.tsx | 233 | 课程卡片网格 + 管理操作 |
| 组件 | components/elective-course-form.tsx | 293 | 课程创建/编辑表单 |
| 组件 | components/elective-filters.tsx | 49 | nuqs 筛选栏(搜索 + 模式) |
| 组件 | components/student-selection-view.tsx | 250 | 学生选课视图(已选 + 可选) |
| 页面 | admin/elective/page.tsx | 46 | 管理员课程列表(RSC) |
| 页面 | admin/elective/create/page.tsx | 36 | 创建课程(RSC) |
| 页面 | admin/elective/[id]/edit/page.tsx | 48 | 编辑课程(RSC) |
| 页面 | teacher/elective/page.tsx | 53 | 教师我的课程(RSC) |
| 页面 | student/elective/page.tsx | 54 | 学生选课中心(RSC) |
| 骨架屏 | 1 个 loading.tsx(student) |
— | 列表骨架屏 |
| 错误边界 | 0 个 error.tsx |
— | 完全缺失 |
跨模块依赖(parent 模块消费 attendance 类型)
| 文件 | 行数 | 职责 |
|---|---|---|
| parent/components/parent-attendance-warning.tsx | 102 | 家长考勤异常预警横幅 |
| parent/components/parent-attendance-rate-card.tsx | 114 | 家长出勤率汇总卡片 |
| parent/components/parent-attendance-calendar.tsx | 194 | 家长考勤月历视图 |
1.2 数据流
考勤数据流
page.tsx (RSC)
└─ getAttendanceRecords / getStudentAttendanceSummary / getClassAttendanceStats (data-access)
└─ db (drizzle) → attendanceRecords / attendanceRules / classEnrollments / users / classes 表
└─ <AttendanceSheet> (client) → batchRecordAttendanceAction
└─ <AttendanceRecordList> (client) → deleteAttendanceAction
└─ <AttendanceRulesForm> (client) → saveAttendanceRulesAction
└─ <StudentAttendanceView> (server) — 学生/家长只读
└─ <ParentAttendanceCalendar/Warning/RateCard> (server/client) — 家长聚合视图
选修课数据流
page.tsx (RSC)
└─ getElectiveCourses / getElectiveCourseById / getAvailableCoursesForStudent / getStudentSelections (data-access)
└─ db (drizzle) → electiveCourses / courseSelections 表
└─ 跨模块 data-access:school.getSubjectOptions / school.getGradeOptions / users.getUserNamesByIds / classes.getStudentActiveGradeId
└─ <ElectiveCourseList> (client) → deleteElectiveCourseAction / openSelectionAction / closeSelectionAction / runLotteryAction
└─ <ElectiveCourseForm> (client) → createElectiveCourseAction / updateElectiveCourseAction
└─ <StudentSelectionView> (client) → selectCourseAction / dropCourseAction
1.3 架构图记录完整性
经核对 004_architecture_impact_map.md §2.10(attendance)与 §2.20(elective)以及 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:
import type { StudentAttendanceSummary } from "@/modules/attendance/types" - parent-attendance-rate-card.tsx#L5:同上
- parent-attendance-calendar.tsx#L6-L10:
import type { AttendanceListItem, AttendanceStatus, StudentAttendanceSummary } from "@/modules/attendance/types"
- parent-attendance-warning.tsx#L5:
- 现象:parent 模块的 3 个组件直接依赖 attendance 模块的类型定义,且
parent-attendance-calendar.tsx内部重新定义了STATUS_LABEL/STATUS_DOT常量(与 attendance 模块的ATTENDANCE_STATUS_LABELS/ATTENDANCE_STATUS_COLORS重复)。 - 违反规则:项目规则"该模块必须作为独立功能单元……模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)"。虽然此处仅 import 类型,但 parent 模块应通过自身定义的视图模型接口解耦,而非直接消费 attendance 内部类型。
- 原因:家长考勤视图需要展示 attendance 数据,开发时直接复用 attendance 类型,未做视图模型隔离。
- 后果:attendance 模块修改
StudentAttendanceSummary字段会破坏 parent 模块编译;parent 模块无法独立测试;新增角色时无法替换 attendance 数据源。
问题 2.1.2 | 考勤页面层绕过 Action 直接调用 data-access(P2)
- 位置:
- admin/attendance/page.tsx#L12:
import { getAttendanceRecords, getAttendanceStats } from "@/modules/attendance/data-access" - teacher/attendance/page.tsx#L10:
import { getAttendanceRecords } from "@/modules/attendance/data-access" - teacher/attendance/sheet/page.tsx#L3:
import { getClassStudentsForAttendance } from "@/modules/attendance/data-access" - teacher/attendance/stats/page.tsx#L3:
import { getClassAttendanceStats } from "@/modules/attendance/data-access-stats" - student/attendance/page.tsx#L2:
import { getStudentAttendanceSummary } from "@/modules/attendance/data-access-stats" - parent/attendance/page.tsx#L2:同上
- admin/attendance/page.tsx#L12:
- 现象:所有读操作页面(admin/teacher/student/parent)均直接调用 data-access,未走
getAttendanceAction/getStudentAttendanceAction/getClassAttendanceStatsAction等 Server Action。 - 违反规则:项目规则"
app/只能调用modules/的 Server Actions 和 data-access"——此处虽合规(data-access 允许被 app 调用),但架构图 §2.10 标注的 10 个 Action 中有 6 个读 Action 实际无调用方(死代码),且页面层未享受 Action 的统一错误处理与权限二次校验。 - 原因:RSC 页面直接调 data-access 性能更优(少一层包装),但导致 Action 层读函数成为死代码。
- 后果:Action 层 6 个读函数(
getAttendanceAction/getStudentAttendanceAction/getClassAttendanceStatsAction/getClassAttendanceForDateAction/getAttendanceRulesAction)无调用方,维护成本浪费;权限二次校验形同虚设。
问题 2.1.3 | elective 页面层同样绕过 Action(P2)
- 位置:
- admin/elective/page.tsx#L4:
import { getElectiveCourses } from "@/modules/elective/data-access" - admin/elective/[id]/edit/page.tsx#L5:
import { getElectiveCourseById } from "@/modules/elective/data-access" - teacher/elective/page.tsx#L4:同 admin
- student/elective/page.tsx#L3:
import { getAvailableCoursesForStudent, getStudentSelections } from "@/modules/elective/data-access-selections"
- admin/elective/page.tsx#L4:
- 现象:与考勤相同,elective 的 3 个读 Action(
getElectiveCoursesAction/getStudentSelectionsAction/getAvailableCoursesAction)无调用方。 - 后果:同 2.1.2。
问题 2.1.4 | elective data-access 跨模块依赖未通过接口抽象(P2)
- 位置:
- data-access.ts#L10-L11:
import { getGradeOptions, getSubjectOptions } from "@/modules/school/data-access"、import { getUserNamesByIds } from "@/modules/users/data-access" - data-access-selections.ts#L12-L13:
import { getStudentActiveGradeId } from "@/modules/classes/data-access"、import { getUserNamesByIds } from "@/modules/users/data-access"
- data-access.ts#L10-L11:
- 现象: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),但考勤模块没有任何一处使用
useTranslations/getTranslations,所有文案硬编码,且中英文混杂:- 中文硬编码:
"考勤总览"、"查看全校所有班级的考勤记录"、"统计分析"、"暂无考勤记录"、"系统中尚未产生任何考勤记录。"、"考勤记录"、"管理学生考勤记录。"、"录入考勤"、"统计"、"当前班级有未保存的考勤记录,确认切换班级?"、"总记录数"、"出勤"、"缺勤"、"迟到"、"早退"、"出勤率"(admin/attendance/page.tsx、teacher/attendance/page.tsx、attendance-sheet.tsx、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、attendance-record-list.tsx、attendance-stats-card.tsx、attendance-rules-form.tsx、student-attendance-view.tsx、attendance-filters.tsx) - 状态标签常量硬编码英文:
ATTENDANCE_STATUS_LABELS在 types.ts#L86-L92 直接写死"Present"/"Absent"/"Late"/"Early Leave"/"Excused",未走 i18n。
- 中文硬编码:
- 违反规则:项目规则"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。
- 原因:模块开发时未跟进 i18n 改造,文案随写随定。
- 后果:无法切换语言;同一界面中英混杂(管理员页中文、教师点名页英文、统计卡片中文),专业度差;后续做国际化需返工全部组件。
问题 2.2.2 | 选修课模块零 i18n 覆盖(P0)
- 位置:模块全部 10 个源文件
- 现象:与考勤模块相同,选修课模块无任何 i18n 调用,文案中英混杂:
- 中文硬编码:
"选修课程"、"管理选修课程、开放/关闭选课与抽签。"、"新建选修课程"、"创建新的选修课程。"、"编辑选修课程"、"更新选修课程详情。"(admin/elective/page.tsx、admin/elective/create/page.tsx、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、elective-course-form.tsx、student-selection-view.tsx、elective-filters.tsx) - 状态标签常量硬编码英文:
ELECTIVE_STATUS_LABELS/SELECTION_MODE_LABELS/COURSE_SELECTION_STATUS_LABELS在 types.ts#L69-L97 直接写死英文。
- 中文硬编码:
- 违反规则:同 2.2.1。
- 后果:同 2.2.1。
问题 2.2.3 | i18n 翻译文件未注册新命名空间(P1)
- 位置: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:
setSelectionMode(v as "fcfs" | "lottery")——v已是string,应用类型守卫或ElectiveSelectionModeEnum校验。 - elective-course-list.tsx#L54:
await action(null as never, formData)—— 用as never绕过prevState类型检查,是类型逃逸。 - attendance-sheet.tsx#L126:
{} as Record<AttendanceStatus, number>—— 空对象断言为完整 Record,运行时statusCounts[status]在未初始化时会undefined。
- elective-course-form.tsx#L204:
- 违反规则:项目规则"禁止
as断言(除非从unknown转换或测试中,需注释原因)"。 - 后果:类型系统无法保护运行时错误;
as never让编译器失去对prevState的校验。
问题 2.3.2 | attendance-sheet.tsx 使用 window.confirm 阻塞 UI(P2)
- 位置:attendance-sheet.tsx#L107:
if (!window.confirm("当前班级有未保存的考勤记录,确认切换班级?")) - 现象:使用浏览器原生
confirm,与模块内其他删除操作使用的AlertDialog/Dialog不一致。 - 违反规则:项目规则"组合优先"与 UI 一致性;
confirm()阻塞主线程且不可定制样式。 - 后果:交互体验割裂;移动端
confirm表现不一;i18n 文案无法替换。
问题 2.3.3 | getAttendanceStats 实现低效且类型不精确(P2)
- 位置:data-access.ts#L285-L308
- 现象:
getAttendanceStats注释写"简化实现:基于已有查询统计",实际是先调getAttendanceRecords(默认 pageSize=20)取前 20 条,再filter统计——统计结果只基于前 20 条记录,不是全量。 - 违反规则:项目规则"函数返回值必须显式标注"(此处已标注,但语义错误)。
- 后果:管理员考勤总览页的 6 卡片统计永远是前 20 条记录的统计,不是全校考勤统计,数据严重失真。
问题 2.3.4 | getClassStudentsForAttendance 直查 classEnrollments(P1)
- 位置:data-access.ts#L208-L219
- 现象:架构图 §2.10 标注"✅ P1-1 已修复:
改为通过 classes data-access 获取",但实际代码仍直接查询getClassStudentsForAttendance直查classEnrollmentsclassEnrollments表(db.select(...).from(classEnrollments).innerJoin(users, ...))。 - 违反规则:项目规则"模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"。架构图记录与实际代码不一致。
- 原因:架构图记录错误,或修复后被回退。
- 后果:classes 模块修改
classEnrollmentsschema 会破坏 attendance 模块;架构图可信度受损。
2.4 错误与边界处理
问题 2.4.1 | 完全缺失 React Error Boundary(P0)
- 位置:
- 考勤:
src/app/(dashboard)/admin/attendance/、src/app/(dashboard)/teacher/attendance/、src/app/(dashboard)/student/attendance/、src/app/(dashboard)/parent/attendance/均无error.tsx - 选修课:
src/app/(dashboard)/admin/elective/、src/app/(dashboard)/teacher/elective/、src/app/(dashboard)/student/elective/均无error.tsx
- 考勤:
- 现象:7 个页面目录均无错误边界,DB 查询失败、Server Action 抛错时整页白屏。
- 违反规则:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- 后果:一次 DB 抖动导致整个考勤/选修课页面崩溃,无法隔离故障域;用户只能手动刷新。
问题 2.4.2 | 骨架屏覆盖不全(P2)
- 位置:
- 考勤:仅
student/attendance/loading.tsx、parent/attendance/loading.tsx存在;admin/attendance/、teacher/attendance/、teacher/attendance/sheet/、teacher/attendance/stats/均无骨架屏。 - 选修课:仅
student/elective/loading.tsx存在;admin/elective/、admin/elective/create/、admin/elective/[id]/edit/、teacher/elective/均无骨架屏。
- 考勤:仅
- 违反规则:项目规则"异步数据使用 React Suspense + 骨架屏"。
- 后果:管理员/教师端首屏白屏时间长,体验差。
问题 2.4.3 | 空状态文案与组件不统一(P2)
- 位置:
- attendance-record-list.tsx#L54-L60:内联
<div>No attendance records found.</div> - attendance-sheet.tsx#L245-L248:内联
<p>No students in this class...</p> - 列表页则用
EmptyState组件
- attendance-record-list.tsx#L54-L60:内联
- 后果:同一模块内空状态有两种写法,维护成本高,a11y 属性缺失。
问题 2.4.4 | Server Action 错误消息英文硬编码(P2)
- 位置:
- attendance/actions.ts#L56:
"Attendance recorded"、"Invalid form data"、"Unexpected error" - elective/actions.ts#L88:
"Elective course created"、"Course not found"、"Invalid form data"
- attendance/actions.ts#L56:
- 现象:所有 Action 的
message字段硬编码英文,未走 i18n。 - 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
- 后果:toast 提示无法本地化。
2.5 组件复用与组合
问题 2.5.1 | 考勤状态标签/颜色常量重复定义(P1)
- 位置:
- attendance/types.ts#L86-L103:
ATTENDANCE_STATUS_LABELS/ATTENDANCE_STATUS_COLORS - parent-attendance-calendar.tsx#L14-L28:
STATUS_DOT/STATUS_LABEL(与 attendance 重复) - attendance-sheet.tsx#L39-L61:
STATUS_OPTIONS/STATUS_SHORTCUTS/STATUS_STYLES(部分重复) - attendance-filters.tsx#L21-L27:
STATUS_OPTIONS(与 sheet 重复)
- attendance/types.ts#L86-L103:
- 现象:考勤状态枚举的标签、颜色、快捷键、样式在 4 个文件里各写一份。
- 违反规则:项目规则"最大化复用……抽象为泛型组件和 hooks"。
- 后果:新增状态需改 4 处;当前已出现不一致(
ATTENDANCE_STATUS_COLORS用"outline"表示 early_leave,但STATUS_STYLES用bg-blue-500)。
问题 2.5.2 | 选修课状态标签/颜色常量分散(P1)
- 位置:
- elective/types.ts#L69-L108:4 组常量(
ELECTIVE_STATUS_LABELS/ELECTIVE_STATUS_COLORS/SELECTION_MODE_LABELS/COURSE_SELECTION_STATUS_LABELS/COURSE_SELECTION_STATUS_COLORS) - elective-course-form.tsx#L208-L213:Select 选项硬编码
"First Come First Served"/"Lottery"(未复用SELECTION_MODE_LABELS) - elective-filters.tsx#L40-L44:Select 选项硬编码(同上)
- elective/types.ts#L69-L108:4 组常量(
- 现象:状态标签在 types.ts 集中定义,但表单/筛选组件未复用,重新硬编码。
- 后果:标签变更需改 3 处;i18n 改造时需同步多处。
问题 2.5.3 | 考勤页面布局重复(P2)
- 位置:
- 现象:两个页面的标题区 + 筛选区 + 列表区结构几乎相同,仅按钮和分页略有差异。
- 违反规则:项目规则"最大化复用"。
- 后果:UI 调整需改多处。
问题 2.5.4 | 选修课列表页布局重复(P2)
- 位置:
- 现象:admin 和 teacher 列表页结构完全相同,仅
createHref不同。 - 后果:同 2.5.3。
2.6 可访问性(a11y)
问题 2.6.1 | 考勤点名表单缺 aria-label(P2)
- 位置:attendance-sheet.tsx#L215-L226
- 现象:班级选择器
<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
- 现象:课程卡片用
<Card>但无role="article"或aria-label;"Open"/"Close"/"Lottery"/"Delete" 按钮有图标但aria-label缺失(仅有variant文本)。 - 后果:屏幕阅读器用户无法快速定位卡片内容。
问题 2.6.3 | 考勤月历键盘导航缺失(P2)
- 位置:parent-attendance-calendar.tsx#L143-L177
- 现象:月历日期格子用
<div>,无tabIndex、无方向键导航;月份切换按钮有aria-label(✅ 良好),但日期格子不可聚焦。 - 后果:键盘用户无法浏览具体日期的考勤状态。
2.7 可测试性
问题 2.7.1 | 纯逻辑未导出,无法单测(P1)
- 位置:
- attendance/data-access-stats.ts#L26-L39
computeStats(模块内未导出) - parent-attendance-warning.tsx#L14-L55
buildWarnings(模块内未导出) - parent-attendance-rate-card.tsx#L14-L30
aggregate/rateTone(模块内未导出) - parent-attendance-calendar.tsx#L30-L62
formatDateKey/parseDateKey/buildCalendarDays/isSameDay(模块内未导出) - elective/data-access-operations.ts#L14-L19
buildLotteryRankCase(模块内未导出)
- attendance/data-access-stats.ts#L26-L39
- 现象:这些纯函数(统计计算、预警规则、聚合、日期工具、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
- 现象:见 2.3.3。
getAttendanceRecords默认pageSize=20,getAttendanceStats调用它后只统计items(20 条),但管理员总览页展示的是"全校考勤统计"——数据严重失真。 - 后果:管理员看到的出勤率永远是前 20 条记录的出勤率,决策失误。
问题 2.8.2 | getStudentAttendanceSummary 一次拉全量记录(P2)
- 位置:data-access-stats.ts#L60-L68
- 现象:学生汇总页一次性加载该学生所有考勤记录(无分页),仅
recentRecords截取前 20 条,但stats基于全量。 - 后果:考勤记录多的学生首屏慢。
问题 2.8.3 | resolveCourseDisplayNames 每次调用都全量拉取科目/年级/教师(P2)
- 位置:elective/data-access.ts#L100-L122
- 现象:每次查询课程列表都调用
getSubjectOptions()/getGradeOptions()/getUserNamesByIds(),无缓存(虽然getElectiveCourses用了cache(),但内部resolveCourseDisplayNames仍会执行)。 - 后果:高频访问时重复查询。
2.9 安全性
问题 2.9.1 | Server Action 未校验资源归属(P0)
- 位置:
- attendance/actions.ts#L98-L128
updateAttendanceAction(id, ...):仅校验ATTENDANCE_MANAGE权限,未校验id对应的考勤记录是否属于当前教师所教班级。 - attendance/actions.ts#L130-L143
deleteAttendanceAction(id):同上。 - elective/actions.ts#L94-L134
updateElectiveCourseAction(id, ...):仅校验ELECTIVE_MANAGE,未校验id对应课程是否属于当前教师(admin 可改全部,teacher 应只能改自己的课程)。 - elective/actions.ts#L136-L153
deleteElectiveCourseAction:同上。
- attendance/actions.ts#L98-L128
- 违反规则:项目规则"Server Action 二次校验"、"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
- 后果:教师 A 可通过改
id篡改/删除教师 B 的考勤记录或选修课(越权写)。
问题 2.9.2 | getClassAttendanceForDateAction 未校验班级归属(P1)
- 位置:attendance/actions.ts#L212-L225
- 现象:仅校验
ATTENDANCE_READ,未校验classId是否属于当前教师所教班级。 - 后果:教师可查看任意班级的考勤明细。
问题 2.9.3 | saveAttendanceRulesAction 未校验班级归属(P1)
- 位置:attendance/actions.ts#L227-L257
- 现象:仅校验
ATTENDANCE_MANAGE,未校验classId是否属于当前教师所教班级。 - 后果:教师可修改任意班级的考勤规则。
问题 2.9.4 | runLotteryAction / openSelectionAction / closeSelectionAction 未校验课程归属(P1)
- 位置:elective/actions.ts#L155-L211
- 现象:仅校验
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(紧急,阻塞多角色上线或数据严重失真)
- 修复
getAttendanceStats统计失真:改为基于COUNT聚合查询,而非取前 20 条items统计;或直接在 data-access 层用db.select({ count, status }).groupBy(status)一次查询。 - 修复
getClassStudentsForAttendance跨模块直查:改为调用classes/data-access.getActiveStudentIdsByClassId或新增classes/data-access.getClassStudentsForAttendance,与架构图记录一致。 - Server Action 资源归属校验:在
updateAttendanceAction/deleteAttendanceAction/updateElectiveCourseAction/deleteElectiveCourseAction/runLotteryAction/openSelectionAction/closeSelectionAction/saveAttendanceRulesAction/getClassAttendanceForDateAction内,结合ctx.dataScope与ctx.userId校验资源归属(教师只能操作自己班级/课程)。 - 全模块 i18n 改造:新增
shared/i18n/messages/{en,zh-CN}/attendance.json与elective.json命名空间,在i18n/request.ts注册加载;提取所有硬编码文案;状态标签常量改为 i18n key(运行时通过useTranslations解析)。 - 补齐 Error Boundary:在 7 个页面目录下新增
error.tsx(admin/teacher/student/parent × attendance/elective),复用现有EmptyState+AlertCircle模式。
P1(重要,影响正确性与可维护性)
- 解耦 parent 模块对 attendance 类型的直接依赖:在 parent 模块定义视图模型接口(
ParentAttendanceSummary),由parent/attendance/page.tsx在 RSC 层做映射;或抽取共享类型到shared/types/attendance.ts。 - 消除状态常量重复:新建
attendance/constants.ts集中导出ATTENDANCE_STATUS_OPTIONS(含 value/label-key/color/shortcut/icon),供 sheet/filters/stats/calendar 复用;elective 同理。 - 抽取纯函数并补单测:导出
computeStats/buildWarnings/aggregate/rateTone/formatDateKey/parseDateKey/buildCalendarDays/isSameDay/buildLotteryRankCase,补 Vitest 单测覆盖空数组、边界值、闰年、跨月等。 - 修复类型断言:用类型守卫替换
as "fcfs" | "lottery"(用ElectiveSelectionModeEnum.safeParse);用Object.fromEntries(STATUS_OPTIONS.map(s => [s, 0]))替换{} as Record<...>;删除as never,改为泛型约束prevState。 - 统一
window.confirm为AlertDialog:attendance-sheet.tsx的切换班级确认改为AlertDialog,与模块其他删除操作一致。 - 补齐骨架屏:为 admin/teacher 考勤与选修课页面补
loading.tsx。 - 统一空状态:内联空状态全部改用
EmptyState组件。 - a11y 改进:考勤点名表单补
aria-label;选修课卡片补role="article"+aria-label;考勤月历日期格子补tabIndex+ 方向键导航。 - 清理死代码 Action:删除无调用方的 6 个读 Action(
getAttendanceAction/getStudentAttendanceAction/getClassAttendanceStatsAction/getClassAttendanceForDateAction/getAttendanceRulesAction/getElectiveCoursesAction/getStudentSelectionsAction/getAvailableCoursesAction),或改为页面层调用(统一权限二次校验)。 - 埋点接口预留:在
data-access与actions中预留onAttendanceRecorded/onCourseSelected/onLotteryCompleted钩子,供后续接入监控。
P2(优化,提升体验与专业度)
- 页面布局复用:抽取
AttendancePageLayout/ElectivePageLayout组件,admin/teacher 页面复用。 - 考勤统计图表:接入 recharts,按周/月展示出勤趋势线、缺勤热力图。
- 选修课课程详情页:新增
/student/elective/[id]详情页,展示大纲/教师/评价。 - 选课时间冲突检测:在
selectCourse内校验学生已有选课的 schedule 是否冲突。 - 学分上限校验:在
selectCourse内校验学生本学期已选学分 + 当前课程学分是否超过上限。 - 考勤/选课数据导出:复用
shared/lib/excel.ts,新增导出 Action。 - 移动端优化:选修课卡片改为瀑布流,考勤点名表单窄屏优化。
- 补全架构图同步(见第五节)。
五、架构图同步说明
本次审计发现 004_architecture_impact_map.md §2.10(attendance)与 §2.20(elective)以及 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 已修复: 改为通过 classes data-access 获取",但实际代码仍直接查询 getClassStudentsForAttendance 直查 classEnrollmentsclassEnrollments 表(data-access.ts#L208-L219)。需将架构图改为"❌ P1-1 未修复:getClassStudentsForAttendance 仍直查 classEnrollments"。
5.3 attendance 缺失功能记录
架构图未记录以下已实现的功能:
attendanceRules表的 CRUD(saveAttendanceRulesAction/getAttendanceRulesAction+upsertAttendanceRules/getAttendanceRules)AttendanceRulesForm组件AttendanceRecordList组件(含删除对话框)StudentAttendanceView组件(学生/家长视图)AttendanceStatsClassSelector组件(ChipNav 筛选)
5.4 elective 行数与组件统计偏差
| 项 | 图记 | 实际 |
|---|---|---|
actions.ts 行数 |
304 | 304(一致) |
data-access.ts 行数 |
250 | 250(一致) |
data-access-operations.ts 行数 |
245 | 245(一致) |
data-access-selections.ts 行数 |
189 | 149(减少 40 行) |
| 组件文件数 | 3 | 4(缺 student-selection-view.tsx) |
5.5 elective usedBy 信息缺失
getStudentSelectionsAction / getAvailableCoursesAction 的 usedBy 字段标注为"待扩展",实际已被 student/elective/page.tsx 通过 data-access 直接调用(绕过 Action)。应改为"无调用方(页面层直接调 data-access)"或删除这两个 Action。
5.6 parent 跨模块 UI 依赖未记录
架构图 §2.19(parent)的依赖关系未标注 parent 模块对 attendance 模块类型的直接 import:
parent/components/parent-attendance-warning.tsx→@/modules/attendance/typesparent/components/parent-attendance-rate-card.tsx→@/modules/attendance/typesparent/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 节点建议补充/修正:
{
"attendance": {
"exports": {
"actions": [
"recordAttendanceAction", "batchRecordAttendanceAction",
"updateAttendanceAction", "deleteAttendanceAction",
"getAttendanceAction", "getStudentAttendanceAction",
"getClassAttendanceStatsAction", "getClassAttendanceForDateAction",
"saveAttendanceRulesAction", "getAttendanceRulesAction"
],
"dataAccess": [
"getAttendanceRecords", "getClassAttendanceForDate",
"createAttendanceRecord", "batchCreateAttendanceRecords",
"updateAttendanceRecord", "deleteAttendanceRecord",
"getClassStudentsForAttendance", // ❌ 仍直查 classEnrollments
"getAttendanceRules", "upsertAttendanceRules",
"getStudentAttendanceSummary", "getClassAttendanceStats",
"getAttendanceStats" // ❌ 统计失真,仅基于前 20 条
],
"components": [
"AttendanceSheet", "AttendanceRecordList", "AttendanceFilters",
"AttendanceStatsCard", "AttendanceStatsCards",
"AttendanceStatsClassSelector", "AttendanceRulesForm",
"StudentAttendanceView"
]
},
"knownIssues": [
"getClassStudentsForAttendance 仍直查 classEnrollments(P1)",
"getAttendanceStats 统计失真,仅基于前 20 条(P0)",
"Server Action 未校验资源归属(P0)",
"全模块零 i18n(P0)",
"缺 Error Boundary(P0)",
"parent 模块跨模块 import attendance 类型(P1)",
"状态常量重复定义(P1)",
"纯逻辑未导出,零单测(P1)"
]
},
"elective": {
"exports": {
"actions": [
"createElectiveCourseAction", "updateElectiveCourseAction",
"deleteElectiveCourseAction", "openSelectionAction",
"closeSelectionAction", "runLotteryAction",
"selectCourseAction", "dropCourseAction",
"getElectiveCoursesAction", // ❌ 无调用方
"getStudentSelectionsAction", // ❌ 无调用方
"getAvailableCoursesAction" // ❌ 无调用方
],
"components": [
"ElectiveCourseList", "ElectiveCourseForm",
"ElectiveFilters", "StudentSelectionView"
]
},
"knownIssues": [
"Server Action 未校验课程归属(P0)",
"全模块零 i18n(P0)",
"缺 Error Boundary(P0)",
"3 个读 Action 无调用方(P1)",
"状态常量分散,表单未复用(P1)",
"纯逻辑未导出,零单测(P1)"
]
}
}
附:重构方案设计要点(不写实现代码)
为满足"完全解耦 / 组合优先 / 国际化就绪 / 最大化复用 / 错误与边界处理 / 可测试性 / 可扩展性 / 企业级补充"八项原则,建议按以下方向重构(详细实现留待后续任务):
A. 数据服务接口抽象
// 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. 配置驱动角色渲染
// 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
// 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": "重试" }
}
// 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 注入实现