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

61 KiB
Raw Blame History

考勤与选修课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-*.tsxsrc/shared/i18n/messages/** 参照规则:docs/architecture/004_architecture_impact_map.mddocs/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.tsxstudent/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.tsxstudent 列表骨架屏
错误边界 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-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 §2.10attendance与 §2.20elective以及 005_architecture_data.json 中对应节点,架构图记录存在以下偏差(详见第五节):

  • attendance 行数统计过期:图记 actions.ts 271 行 / data-access.ts 309 行,实际一致;但 data-access-stats.ts 图记 145 行,实际 145 行(一致)。组件文件数图记 5 个,实际 8 个组件文件(缺 attendance-record-list.tsxattendance-rules-form.tsxstudent-attendance-view.tsx)。
  • attendance 导出函数名不一致:图记 Actions 含 getAttendanceRecordsAction / createAttendanceRecordAction / updateAttendanceRecordAction / deleteAttendanceRecordAction / getStudentAttendanceAction / getAttendanceStatsAction,实际为 recordAttendanceAction / batchRecordAttendanceAction / updateAttendanceAction / deleteAttendanceAction / getAttendanceAction / getStudentAttendanceAction / getClassAttendanceStatsAction / getClassAttendanceForDateAction / saveAttendanceRulesAction / getAttendanceRulesAction10 个,名称与图不一致)。
  • attendance 缺失组件记录:图记 AttendanceStatsCards 一个组件,实际有 8 个组件(含 AttendanceSheetAttendanceRecordListAttendanceFiltersAttendanceStatsCardAttendanceStatsCardsAttendanceStatsClassSelectorAttendanceRulesFormStudentAttendanceView)。
  • 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-formelective-course-listelective-filters),实际 4 个(缺 student-selection-view.tsx)。
  • elective 缺失 usedBy 信息getStudentSelectionsAction / getAvailableCoursesActionusedBy 字段标注为"待扩展",实际已被 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 模块的 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/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

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

  • 位置
    • data-access.ts#L10-L11import { getGradeOptions, getSubjectOptions } from "@/modules/school/data-access"import { getUserNamesByIds } from "@/modules/users/data-access"
    • data-access-selections.ts#L12-L13import { 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-intli18n/request.ts),但考勤模块没有任何一处使用 useTranslations / getTranslations,所有文案硬编码,且中英文混杂:
    • 中文硬编码:"考勤总览""查看全校所有班级的考勤记录""统计分析""暂无考勤记录""系统中尚未产生任何考勤记录。""考勤记录""管理学生考勤记录。""录入考勤""统计""当前班级有未保存的考勤记录,确认切换班级?""总记录数""出勤""缺勤""迟到""早退""出勤率"admin/attendance/page.tsxteacher/attendance/page.tsxattendance-sheet.tsxattendance-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.tsxattendance-record-list.tsxattendance-stats-card.tsxattendance-rules-form.tsxstudent-attendance-view.tsxattendance-filters.tsx
    • 状态标签常量硬编码英文:ATTENDANCE_STATUS_LABELStypes.ts#L86-L92 直接写死 "Present" / "Absent" / "Late" / "Early Leave" / "Excused",未走 i18n。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
  • 原因:模块开发时未跟进 i18n 改造,文案随写随定。
  • 后果:无法切换语言;同一界面中英混杂(管理员页中文、教师点名页英文、统计卡片中文),专业度差;后续做国际化需返工全部组件。

问题 2.2.2 选修课模块零 i18n 覆盖P0

  • 位置:模块全部 10 个源文件
  • 现象:与考勤模块相同,选修课模块无任何 i18n 调用,文案中英混杂:
    • 中文硬编码:"选修课程""管理选修课程、开放/关闭选课与抽签。""新建选修课程""创建新的选修课程。""编辑选修课程""更新选修课程详情。"admin/elective/page.tsxadmin/elective/create/page.tsxadmin/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.tsxelective-course-form.tsxstudent-selection-view.tsxelective-filters.tsx
    • 状态标签常量硬编码英文:ELECTIVE_STATUS_LABELS / SELECTION_MODE_LABELS / COURSE_SELECTION_STATUS_LABELStypes.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#L204setSelectionMode(v as "fcfs" | "lottery") —— v 已是 string,应用类型守卫或 ElectiveSelectionModeEnum 校验。
    • elective-course-list.tsx#L54await action(null as never, formData) —— 用 as never 绕过 prevState 类型检查,是类型逃逸。
    • 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#L107if (!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 直查 classEnrollmentsP1

  • 位置data-access.ts#L208-L219
  • 现象:架构图 §2.10 标注" P1-1 已修复:getClassStudentsForAttendance 直查 classEnrollments 改为通过 classes data-access 获取",但实际代码仍直接查询 classEnrollmentsdb.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.tsxparent/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

问题 2.4.4 Server Action 错误消息英文硬编码P2

  • 位置
  • 现象:所有 Action 的 message 字段硬编码英文,未走 i18n。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果toast 提示无法本地化。

2.5 组件复用与组合

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

  • 位置
  • 现象:考勤状态枚举的标签、颜色、快捷键、样式在 4 个文件里各写一份。
  • 违反规则:项目规则"最大化复用……抽象为泛型组件和 hooks"。
  • 后果:新增状态需改 4 处;当前已出现不一致(ATTENDANCE_STATUS_COLORS"outline" 表示 early_leaveSTATUS_STYLESbg-blue-500)。

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

  • 位置
  • 现象:状态标签在 types.ts 集中定义,但表单/筛选组件未复用,重新硬编码。
  • 后果:标签变更需改 3 处i18n 改造时需同步多处。

问题 2.5.3 考勤页面布局重复P2

问题 2.5.4 选修课列表页布局重复P2

2.6 可访问性a11y

问题 2.6.1 考勤点名表单缺 aria-labelP2

  • 位置attendance-sheet.tsx#L215-L226
  • 现象:班级选择器 <Select>aria-label,日期输入框有 id="date" 但无 aria-label;状态按钮组有 aria-pressedaria-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

问题 2.7.2 零测试覆盖P1

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

2.8 性能

问题 2.8.1 getAttendanceStats 全表扫描但只统计前 20 条P0

  • 位置data-access.ts#L285-L308
  • 现象:见 2.3.3。getAttendanceRecords 默认 pageSize=20getAttendanceStats 调用它后只统计 items20 条),但管理员总览页展示的是"全校考勤统计"——数据严重失真
  • 后果:管理员看到的出勤率永远是前 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

  • 位置
  • 违反规则:项目规则"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紧急阻塞多角色上线或数据严重失真

  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.dataScopectx.userId 校验资源归属(教师只能操作自己班级/课程)。
  4. 全模块 i18n 改造:新增 shared/i18n/messages/{en,zh-CN}/attendance.jsonelective.json 命名空间,在 i18n/request.ts 注册加载;提取所有硬编码文案;状态标签常量改为 i18n key运行时通过 useTranslations 解析)。
  5. 补齐 Error Boundary:在 7 个页面目录下新增 error.tsxadmin/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.confirmAlertDialogattendance-sheet.tsx 的切换班级确认改为 AlertDialog,与模块其他删除操作一致。
  6. 补齐骨架屏:为 admin/teacher 考勤与选修课页面补 loading.tsx
  7. 统一空状态:内联空状态全部改用 EmptyState 组件。
  8. a11y 改进:考勤点名表单补 aria-label;选修课卡片补 role="article" + aria-label;考勤月历日期格子补 tabIndex + 方向键导航。
  9. 清理死代码 Action:删除无调用方的 6 个读 ActiongetAttendanceAction / getStudentAttendanceAction / getClassAttendanceStatsAction / getClassAttendanceForDateAction / getAttendanceRulesAction / getElectiveCoursesAction / getStudentSelectionsAction / getAvailableCoursesAction),或改为页面层调用(统一权限二次校验)。
  10. 埋点接口预留:在 data-accessactions 中预留 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 §2.10attendance与 §2.20elective以及 005_architecture_data.json 中对应节点存在以下偏差,需同步修正:

5.1 attendance 行数与组件统计偏差

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

5.2 attendance 已知问题记录偏差

架构图 §2.10 标注" P1-1 已修复:getClassStudentsForAttendance 直查 classEnrollments 改为通过 classes data-access 获取",但实际代码仍直接查询 classEnrollmentsdata-access.ts#L208-L219)。需将架构图改为" P1-1 未修复:getClassStudentsForAttendance 仍直查 classEnrollments"。

5.3 attendance 缺失功能记录

架构图未记录以下已实现的功能:

  • attendanceRules 表的 CRUDsaveAttendanceRulesAction / 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 4student-selection-view.tsx

5.5 elective usedBy 信息缺失

getStudentSelectionsAction / getAvailableCoursesActionusedBy 字段标注为"待扩展",实际已被 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.jsonmodules.attendancemodules.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 仍直查 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. 数据服务接口抽象

// 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>
}

通过 AttendanceDataProviderReact 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-accessactions 中预留 onAttendanceRecorded / onCourseSelected / onLotteryCompleted / onAttendanceRuleChanged 钩子
  • 钩子默认 no-op由后续监控模块通过 Context 注入实现