Files
NextEdu/docs/architecture/audit/archive/attendance-audit-report.md

32 KiB
Raw Blame History

考勤Attendance模块审计报告

审计日期2026-06-25 审计范围:src/modules/attendance/**src/app/(dashboard)/{admin,teacher,student,parent}/attendance/**、跨模块依赖 src/modules/parent/components/parent-attendance-*.tsxchild-detail-panel.tsx、i18n src/shared/i18n/messages/{en,zh-CN}/attendance.json 参照规则:docs/architecture/004_architecture_impact_map.md(第 2.10 节)、docs/architecture/005_architecture_data.jsonL14681 起)、.trae/rules/project_rules.md


一、现有实现概要

1.1 文件分布

文件 行数 职责
Server Actions actions.ts 258 5 个写 Action含权限校验、Zod 校验、归属校验)
数据访问 data-access.ts 340 考勤记录 CRUD + 规则 upsert + 总览统计 + recorder 解析
数据访问 data-access-stats.ts 206 学生/班级考勤汇总(纯函数 computeStats + SQL 聚合)
Schema schema.ts 43 Zod 校验5 个 schema
Types types.ts 103 类型定义
Constants constants.ts 64 状态选项/快捷键/颜色映射
Export export.ts 90 Excel 导出
组件 components/attendance-page-layout.tsx 38 admin/teacher 共用布局插槽
组件 components/attendance-sheet.tsx 418 批量点名表单快捷键、AlertDialog 确认)
组件 components/attendance-record-list.tsx 142 记录列表 + 删除对话框
组件 components/attendance-filters.tsx 94 URL 同步筛选器
组件 components/attendance-stats-card.tsx 82 单卡片 8 指标
组件 components/attendance-stats-cards.tsx 83 admin 总览 6 卡片网格
组件 components/attendance-stats-class-selector.tsx 27 班级筛选 ChipNav
组件 components/attendance-rules-form.tsx 155 规则配置表单
组件 components/student-attendance-view.tsx 111 学生/家长视图
页面 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
错误边界 4 个 error.tsxadmin/teacher/student/parent ~96 重复严重
跨模块 parent/components/parent-attendance-calendar.tsx 220 家长月历视图
跨模块 parent/components/parent-attendance-warning.tsx 110 异常预警横幅
跨模块 parent/components/parent-attendance-rate-card.tsx 114 出勤率汇总卡片
跨模块 parent/components/child-detail-panel.tsx 190 子女详情面板(含考勤 Tab

1.2 数据流

page.tsx (RSC)
  └─ getAttendanceRecords / getStudentAttendanceSummary / getClassAttendanceStats  (data-access)
       └─ db (drizzle) → attendanceRecords / attendanceRules 表
       └─ ⚠ 直接 JOIN users / classes 表(跨模块表查询)
       └─ 调用 classes/data-access.getClassActiveStudentsWithInfo合规
  └─ <AttendanceSheet> (client) → batchRecordAttendanceAction
  └─ <AttendanceRecordList> (client) → deleteAttendanceAction
  └─ <AttendanceRulesForm> (client) → saveAttendanceRulesAction

1.3 架构图完整性评估

架构影响地图004 第 2.10 节)与 JSONL14681 起)整体覆盖 attendance 模块,但存在 6 处信息过时/不准确(详见第五节),需同步更新。模块导出、权限点、依赖关系、路由已记录。


二、现存问题与原因分析

2.1 三层架构合规性

问题 2.1.1data-access 层跨模块直接 JOIN 外部表【P0】

  • 位置data-access.ts:118-119data-access.ts:77-81data-access-stats.ts:87-91data-access-stats.ts:161-165data-access-stats.ts:177-178
  • 描述getAttendanceRecords 直接 leftJoin(users)leftJoin(classes)resolveRecorderNames 直接 select from usersgetStudentAttendanceSummary/getClassAttendanceStats 直接查询 users/classes 表。
  • 违反规则:项目规则「modules/ 之间通过对方 data-access 通信,不直接查询对方 DB 表」。
  • 原因:为减少查询往返,在 attendance data-access 内联 JOIN 获取 studentName/className/recorderName。
  • 后果users/classes 模块 schema 变更(如 users.name 重命名)会直接破坏 attendance 查询;模块边界失效,无法独立演进。

问题 2.1.2parent 模块直接 import attendance 组件【P1】

  • 位置parent/attendance/page.tsx:4
  • 描述parent/attendance/page.tsx 直接 import { StudentAttendanceView } from "@/modules/attendance/components/student-attendance-view",跨模块 UI 组件依赖。
  • 违反规则:项目规则「该模块必须作为独立功能单元,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access」。
  • 原因parent 复用 student 视图组件以减少重复。
  • 后果parent 模块与 attendance 模块 UI 强耦合attendance 调整 StudentAttendanceView 会影响 parent 页面。

问题 2.1.3parent-attendance-calendar 直接依赖 attendance/constants【P1】

  • 位置parent-attendance-calendar.tsx:9-12
  • 描述:直接 import ATTENDANCE_STATUS_DOT_COLORSATTENDANCE_STATUS_LABEL_KEYS
  • 违反规则:同上「完全解耦」原则。
  • 原因parent 类型已解耦(parent/types.ts 自声明类型),但常量仍直接依赖。
  • 后果attendance 常量变更影响 parent 月历渲染。

问题 2.1.4架构图信息过时【P2】

  • 位置:架构图 004 第 1152、1154、1159、1175、1176、1195 行
  • 描述6 处描述与实际代码不符(详见第五节)。
  • 违反规则:项目规则「改码必同步图」。
  • 后果:架构图可信度下降,误导后续开发。

2.2 权限校验

问题 2.2.1teacher 子页面缺失权限校验【P0】

  • 位置teacher/attendance/sheet/page.tsxteacher/attendance/stats/page.tsx
  • 描述:两个页面无任何权限校验,未调用 requirePermission(Permissions.ATTENDANCE_READ),也未通过 getAuthContext().dataScope 过滤。
  • 违反规则:项目规则「所有 Server Action 必须调用 requirePermission() 进行权限校验」+ 项目记忆「Parent routes must include permission checks with both parentId and studentId」。
  • 原因RSC 页面非 Server Action开发者认为 data-access 内的 buildScopeFilter 会兜底。
  • 后果sheet 页调用 getTeacherClasses() 未传入 scopeteacher/attendance/sheet/page.tsx:9),任何已登录用户访问 URL 即可获取教师班级学生列表stats 页同理。存在数据越权风险

问题 2.2.2teacher/student/parent 主页面权限校验不一致【P1】

问题 2.2.3前端删除按钮无权限点控制【P2】

  • 位置attendance-record-list.tsx:106-114
  • 描述:删除按钮对所有能看到列表的用户可见,未使用 usePermission().hasPermission(Permissions.ATTENDANCE_MANAGE) 控制显隐。
  • 违反规则:项目规则「前端权限判断统一使用 usePermission().hasPermission()」。
  • 后果:无权用户看到删除按钮,点击后才被 Server Action 拒绝,体验差。

2.3 i18n 国际化

问题 2.3.1safeParseDate 中文 fieldName 硬编码【P0】

问题 2.3.2action-utils shared 层中文兜底消息【P1】

  • 位置shared/lib/action-utils.ts:32,70,74,143
  • 描述NotFoundError(\${resource} 不存在`)"操作失败,请稍后重试"${fieldName} 格式无效` 等。
  • 违反规则:同上。
  • 后果:所有调用 shared 层的模块(含 attendance均受影响。

问题 2.3.3Excel 导出英文列头硬编码【P1】

问题 2.3.4calendar 硬编码 en-US locale【P1】

问题 2.3.5child-detail-panel 英文硬编码【P1】

  • 位置child-detail-panel.tsx:50-56、L111、L137-143、L152、L160-163、L182、L44、L186
  • 描述Tab 标签、区块标题、占位提示、按钮文案共 20+ 处英文硬编码。

问题 2.3.6翻译键误用【P0】

问题 2.3.7error.tsx title/description 重复【P2】

  • 位置4 个 error.tsx
  • 描述title 和 description 均用 t("errors.unexpected"),完全相同。

2.4 类型安全

问题 2.4.1export.ts 无类型守卫的 as 断言【P1】

  • 位置export.ts:28-34
  • 描述params.status as "present" | "absent" | ...params.statusstring | undefined,无类型守卫。
  • 违反规则:项目规则「禁止 as 断言(除非从 unknown 转换)」。
  • 后果:非法 status 值绕过类型检查。

问题 2.4.2child-detail-panel as 断言【P2】

  • 位置child-detail-panel.tsx:29、L65
  • 描述(VALID_TABS as string[]).includes(v)v as ChildDetailTab,已有 isTab 守卫但未在 onValueChange 使用。

2.5 错误处理与边界

问题 2.5.1teacher 子路由缺失 error.tsx【P1】

  • 位置teacher/attendance/sheet/teacher/attendance/stats/
  • 描述:缺失 error.tsx运行时错误冒泡到 teacher/attendance/error.tsx,错误上下文不准确。
  • 违反规则项目记忆「All student routes must include loading.tsx and error.tsx」。

问题 2.5.2student 空状态文案错误【P2】

  • 位置student/attendance/page.tsx:24-25
  • 描述summary 为 null 时 EmptyState description 用 t("errors.unexpected")"发生未知错误"),实际原因可能是无考勤记录。

2.6 组件复用性

问题 2.6.1:常量在 constants.ts 与 attendance-sheet.tsx 重复定义【P1】

  • 位置attendance-sheet.tsx:50-83
  • 描述STATUS_OPTIONSSTATUS_SHORTCUTScreateInitialStatusCounts 与 constants.ts 重复(部分复用、部分重复的混乱状态)。
  • 违反规则DRY 原则。
  • 后果:状态选项变更需同步两处,易遗漏。

问题 2.6.24 个 error.tsx 近乎完全重复【P1】

  • 位置4 个 error.tsx
  • 描述:结构完全相同,共约 96 行重复代码。
  • 违反规则项目记忆「Shared components must be extracted when page duplication exceeds 90%」。
  • 后果:修改一处需同步四处。

问题 2.6.3:两个 stats 卡片组件数据结构分裂【P2】

2.7 数据注入与解耦

问题 2.7.1:无接口抽象与 Context 注入【P1】

  • 描述attendance 模块未定义任何 AttendanceDataService 接口,无 AttendanceContext/AttendanceProvider,无角色差异的接口多态实现。
  • 违反规则:项目规则「通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务」。
  • 后果:角色间无统一契约约束,参数/返回处理可能不一致;无法通过接口 mock 做单测。

2.8 可测试性

问题 2.8.1data-access 无接口类型可供 mock【P2】

  • 描述data-access 函数直接导出为具体函数,无 AttendanceRepository 接口。
  • 违反规则:项目规则「导出清晰的接口类型以便 mock」。

2.9 a11y 可访问性

问题 2.9.1:全局 keydown 监听可能冲突【P2】

  • 位置attendance-sheet.tsx:164-186
  • 描述keydown 绑定在 window仅排除 input/textarea未排除 Select 等可交互组件。

2.10 性能

问题 2.10.1getClassAttendanceStats 未用 SQL 聚合【P1】

  • 位置data-access-stats.ts:172-182
  • 描述:仍用全量查询 + 内存 computeStats,而 getStudentAttendanceSummary/getAttendanceStats 已改用 SQL 聚合。
  • 违反规则:性能最佳实践。
  • 后果:大班级统计查询慢。

问题 2.10.2teacher 分页基于截断数据计算【P0】

  • 位置teacher/attendance/page.tsx:46-63
  • 描述:先获取 result.itemspageSize=20再对仅 20 条做前端分页计算 totalPages。
  • 后果:分页页数错误,用户无法访问第 2 页之后数据。

问题 2.10.3parent 组件可降级为 RSC【P2】

问题 2.10.4statusCounts 未 memoize【P2】


三、行业差距对比

基于 K12 教育系统考勤模块的主流实践(参照 PowerSchool、Infinite Campus、Veracross、校长推荐系统等当前模块相比优秀实践的差距

维度 优秀实践 当前状态 差距影响
考勤状态细分 present/absent/late/early-leave/excused-absent/school-activity + 事假/病假/公假原因 仅 present/absent/late/excused 4 态,无原因字段 学校无法区分病假/事假,无法生成请假原因统计
实时家长通知 学生缺席自动推送家长 App 通知/短信 仅被动查看,无主动推送 家长无法及时获知子女缺勤,错过干预窗口
出勤率阈值预警 自动识别出勤率低于阈值的学生并通知班主任 有 parent-attendance-warning 但仅家长端展示,教师端缺失 教师无法主动获取需关注学生名单
考勤趋势可视化 折线图展示个人/班级出勤率周/月趋势 仅数字统计,无趋势图 无法直观看出勤率变化趋势
请假申请流程 家长在线提交请假申请,教师/管理员审批,自动同步考勤 完全缺失 请假流程线下化,考勤数据与请假记录脱节
补签/补录 学生事后提交补签申请,教师审核修正 缺失,仅管理员/教师可手动修改 考勤纠错流程繁琐
跨日/跨节次考勤 按课节(早读/上午/下午/晚自习)多次点名 仅按日单次记录 无法精确到节次,缺勤定位不精确
考勤与成绩关联 出勤率与学业成绩相关性分析 无关联 无法识别"低出勤→低成绩"风险学生
班级对比分析 同年级班级出勤率横向对比 缺失 管理员无法横向评估各班考勤管理水平
导出报告多样性 PDF 周报/月报、Excel 明细、家长签字单 仅 Excel 单一导出 无法满足不同场景报告需求
移动端适配 移动端点名(平板/手机) 未验证移动端体验 教师课堂点名不便携
考勤日历视图 学生/家长端月历视图(已有 parent-attendance-calendar 已实现且 a11y 良好 已达行业水准
批量操作效率 一键全勤、快捷键、批量按学号录入 已实现快捷键 + 一键全勤 已达行业水准
空状态/骨架屏 每个数据区块 EmptyState + Skeleton 已基本覆盖 基本达标
a11y 可访问性 语义化、ARIA、键盘导航 已实现且较完善 基本达标

核心差距总结

  1. 功能完整性:缺请假流程、节次考勤、原因分类、趋势可视化、跨班级对比——这些是 K12 学校的刚需。
  2. 主动通知机制:被动展示→主动预警的转变。
  3. 数据联动:考勤与成绩、请假、通知模块的联动缺失。

四、改进优先级建议

P0紧急影响安全/数据正确性,立即修复)

# 问题 改进方向
P0-1 teacher/sheet、teacher/stats 缺权限校验且未传 scope2.2.1 页面级增加 requirePermission(ATTENDANCE_READ) + 传入 dataScopedata-access 函数强制要求 scope 参数
P0-2 teacher 分页基于截断数据计算2.10.2 分页 totalPages 使用后端返回的 total勿基于 items 长度计算
P0-3 safeParseDate 中文 fieldName 硬编码2.3.1 改用 i18n key 或错误 code前端按 code 本地化
P0-4 翻译键误用error 重试按钮显示"保存"2.3.6 3 个 error.tsx 改用 t("actions.retry")
P0-5 data-access 跨模块直 JOIN users/classes 表2.1.1 委托 users/data-accessclasses/data-access 提供姓名查询接口

P1重要影响可维护性/合规性,短期修复)

# 问题 改进方向
P1-1 teacher/student 主页面缺 requirePermission2.2.2 增加 requirePermission(ATTENDANCE_READ)
P1-2 parent 直接 import attendance 组件2.1.2 通过接口抽象 + Context 注入,或将共享视图下沉为可注入组件
P1-3 parent-attendance-calendar 依赖 attendance/constants2.1.3 常量下沉到 shared 或通过 props 注入
P1-4 action-utils shared 中文兜底2.3.2 返回错误 code 而非中文消息
P1-5 child-detail-panel 英文硬编码2.3.5 提取 i18n 键
P1-6 export.ts 英文列头 + as 断言2.3.3、2.4.1 i18n + 类型守卫
P1-7 calendar 硬编码 en-US2.3.4 使用 useLocale()/getLocale()
P1-8 teacher 子路由缺 error.tsx2.5.1 新增 error.tsx
P1-9 常量重复定义2.6.1 attendance-sheet 统一使用 constants.ts
P1-10 4 个 error.tsx 重复2.6.2 抽取 shared ErrorBoundary 组件
P1-11 getClassAttendanceStats 未用 SQL 聚合2.10.1 改用 SQL GROUP BY 聚合
P1-12 无接口抽象/Context 注入2.7.1 定义 AttendanceDataService 接口 + Provider

P2优化提升体验/性能,中期演进)

# 问题 改进方向
P2-1 删除按钮无前端权限控制2.2.3 usePermission().hasPermission(ATTENDANCE_MANAGE)
P2-2 error.tsx title/description 重复2.3.7 区分 title/description 键
P2-3 student 空状态文案错误2.5.2 改用 t("list.emptyDescription")
P2-4 child-detail-panel as 断言2.4.2 使用 isTab 守卫
P2-5 stats 卡片组件数据结构分裂2.6.3 统一为单一数据结构
P2-6 data-access 无接口类型2.8.1 导出 AttendanceRepository 接口
P2-7 全局 keydown 冲突2.9.1 限制监听范围
P2-8 parent 组件可降级 RSC2.10.3 改用 getTranslations
P2-9 statusCounts 未 memoize2.10.4 useMemo
P2-10 架构图同步2.1.4 更新 004/005 文档

中长期演进(功能补齐,对齐行业实践)

# 功能 方向
L-1 考勤状态细分 + 原因字段 扩展 schema 增加 reason 字段,新增 early-leave/school-activity 状态
L-2 实时家长通知 接入 notifications 模块,缺勤自动推送
L-3 出勤率阈值预警(教师端) 配置驱动阈值,自动生成需关注学生名单
L-4 考勤趋势可视化 折线图组件,周/月趋势
L-5 在线请假流程 新增 leave-requests 子模块,审批流 + 考勤同步
L-6 节次考勤 扩展数据模型支持按节次记录
L-7 跨班级对比分析 同年级班级出勤率横向对比图
L-8 导出报告多样化 PDF 周报/月报 + 家长签字单
L-9 考勤与成绩关联分析 跨模块数据联动分析

五、架构图同步说明

本次审计发现架构图004 第 2.10 节、005 JSON L14681 起)存在以下不一致,需要同步更新

# 架构图描述 实际代码 更新动作
1 004 L1159: getClassStudentsForAttendance 仍直查 classEnrollments data-access.ts:226 已委托 classes/data-access.getClassActiveStudentsWithInfo 更新 004 描述为"已委托 classes data-access"
2 004 L1152: 10 个 Actions含 5 个读 Action actions.ts 仅 5 个写 Action 更新 Actions 计数为 5读操作标注为直接调 data-access
3 004 L1154: getClassAttendanceStats 改用 SQL 聚合 data-access-stats.ts:172-182 仍用全量查询 + computeStats 待本次重构改为 SQL 聚合后同步更新
4 004 L1175: attendance-sheet.tsx 使用 window.confirm 已改为 AlertDialogL392-415 更新为 AlertDialog
5 004 L1176: 存在 {} as Record 断言 已改为 createInitialStatusCounts() 函数L75-83 更新描述
6 004 L1195: attendance-stats-cards.tsx 硬编码中文 已全部使用 t() i18n 更新为已 i18n

JSON 同步005 中 attendance 节点的 exports、dependencies、permissions 需在本次重构后统一更新(新增 AttendanceDataService 接口、AttendanceProvider、抽取的 shared ErrorBoundary、新增 error.tsx 等)。


六、重构方案设计

6.1 完全解耦:接口抽象 + Context 注入

设计目标attendance 模块作为独立功能单元parent/student 等消费方通过接口契约消费,不直接 import 业务实现。

// src/modules/attendance/services/attendance-data-service.ts
// 接口抽象:定义数据契约,可被不同角色实现
export interface AttendanceDataService {
  getStudentSummary(studentId: string, range?: DateRange): Promise<AttendanceStats | null>;
  getRecentRecords(studentId: string, limit: number): Promise<AttendanceListItem[]>;
  getRecordsByDate(date: string, classId?: string): Promise<AttendanceListItem[]>;
  getClassStats(classId: string, range?: DateRange): Promise<AttendanceStats | null>;
}

// src/modules/attendance/services/attendance-context.tsx
// React Context 注入
const AttendanceServiceContext = createContext<AttendanceDataService | null>(null);

export function AttendanceProvider({ service, children }: {
  service: AttendanceDataService;
  children: ReactNode;
}) {
  return (
    <AttendanceServiceContext.Provider value={service}>
      {children}
    </AttendanceServiceContext.Provider>
  );
}

export function useAttendanceService(): AttendanceDataService {
  const service = useContext(AttendanceServiceContext);
  if (!service) throw new Error("AttendanceProvider missing");
  return service;
}

// 角色实现(示例)
// src/modules/attendance/services/student-service.ts —— 学生/家长视角实现
// src/modules/attendance/services/teacher-service.ts —— 教师视角实现
// src/modules/attendance/services/admin-service.ts —— 管理员视角实现

消费方改造parent/attendance/page.tsx 注入 StudentAttendanceService 实现后渲染 <StudentAttendanceView>,不再直接 import attendance 组件;或 attendance 导出纯展示组件,由 parent 通过 props 注入数据。

6.2 组合优先:组件组合与 hooks

  • 所有 UI 通过 children/slots/render props 组合,禁止 HOC 深层嵌套。
  • 逻辑复用抽取为 hooksuseAttendanceSheetuseAttendanceStatsuseAttendanceFilters
  • AttendancePageLayout 已采用插槽模式header/stats/filters/children推广至所有角色页面。

6.3 国际化就绪

翻译文件结构示例

// src/shared/i18n/messages/zh-CN/attendance.json
{
  "title": "考勤管理",
  "status": { "present": "出勤", "absent": "缺勤", "late": "迟到", "excused": "请假" },
  "stats": { "total": "总人次", "presentRate": "出勤率", ... },
  "errors": {
    "invalidDate": "日期格式无效",
    "invalidStartDate": "开始日期格式无效",
    "unexpected": "发生未知错误,请稍后重试",
    "forbidden": "无权限执行此操作",
    "notFound": "考勤记录不存在"
  },
  "actions": { "save": "保存", "retry": "重试", "delete": "删除", ... }
}

shared 层错误改造action-utils.ts 返回 { code: "INVALID_DATE", field: "date" } 结构化错误,前端按 code 查 i18n key 本地化,消除中文硬编码。

6.4 最大化复用

  • 抽取 shared ErrorBoundary 组件(替代 4 个重复 error.tsx
  • 统一 stats 数据结构为单一 AttendanceStats
  • 常量统一从 constants.ts 导出。
  • StudentAttendanceView 改为接收 AttendanceDataService 注入,支持 student/parent 复用。

6.5 错误与边界处理

  • 每个独立数据区块用 React Error Boundary 包裹。
  • 异步数据用 Suspense + 骨架屏。
  • 明确处理空数据、无权限、网络异常。

6.6 可测试性

  • data-access 导出 AttendanceRepository 接口类型供 mock。
  • 纯逻辑(computeStatsbuildWarningsaggregateStats 等)已分离,保持。

6.7 可扩展性

  • 角色配置驱动:attendanceRoleConfig[role] 决定渲染哪些 Widget/子模块。
  • 新增角色仅修改配置。

6.8 企业级补充

  • a11y:保持现有 ARIA/键盘导航水平,优化 keydown 监听范围。
  • 性能RSC 获取初始数据,客户端组件最小化,支持流式渲染。
  • 安全data-access 层结合 dataScope 过滤Server Action 二次校验。
  • 监控:预留 trackEvent 埋点接口(点名/删除/规则保存等关键操作)。