32 KiB
考勤(Attendance)模块审计报告
审计日期:2026-06-25 审计范围:
src/modules/attendance/**、src/app/(dashboard)/{admin,teacher,student,parent}/attendance/**、跨模块依赖src/modules/parent/components/parent-attendance-*.tsx及child-detail-panel.tsx、i18nsrc/shared/i18n/messages/{en,zh-CN}/attendance.json参照规则:docs/architecture/004_architecture_impact_map.md(第 2.10 节)、docs/architecture/005_architecture_data.json(L14681 起)、.trae/rules/project_rules.md
一、现有实现概要
1.1 文件分布
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 节)与 JSON(L14681 起)整体覆盖 attendance 模块,但存在 6 处信息过时/不准确(详见第五节),需同步更新。模块导出、权限点、依赖关系、路由已记录。
二、现存问题与原因分析
2.1 三层架构合规性
问题 2.1.1:data-access 层跨模块直接 JOIN 外部表【P0】
- 位置:data-access.ts:118-119、data-access.ts:77-81、data-access-stats.ts:87-91、data-access-stats.ts:161-165、data-access-stats.ts:177-178
- 描述:
getAttendanceRecords直接leftJoin(users)、leftJoin(classes);resolveRecorderNames直接select from users;getStudentAttendanceSummary/getClassAttendanceStats直接查询users/classes表。 - 违反规则:项目规则「
modules/之间通过对方 data-access 通信,不直接查询对方 DB 表」。 - 原因:为减少查询往返,在 attendance data-access 内联 JOIN 获取 studentName/className/recorderName。
- 后果:users/classes 模块 schema 变更(如
users.name重命名)会直接破坏 attendance 查询;模块边界失效,无法独立演进。
问题 2.1.2:parent 模块直接 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.3:parent-attendance-calendar 直接依赖 attendance/constants【P1】
- 位置:parent-attendance-calendar.tsx:9-12
- 描述:直接 import
ATTENDANCE_STATUS_DOT_COLORS、ATTENDANCE_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.1:teacher 子页面缺失权限校验【P0】
- 位置:teacher/attendance/sheet/page.tsx、teacher/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()未传入 scope(teacher/attendance/sheet/page.tsx:9),任何已登录用户访问 URL 即可获取教师班级学生列表;stats 页同理。存在数据越权风险。
问题 2.2.2:teacher/student/parent 主页面权限校验不一致【P1】
- 位置:teacher/attendance/page.tsx:39、student/attendance/page.tsx:11
- 描述:仅
getAuthContext(),未requirePermission(ATTENDANCE_READ);而 admin/attendance/page.tsx:30 已建立该惯例。 - 违反规则:权限校验应统一。
- 后果:权限点缺失,无法通过权限矩阵精确控制 teacher/student 是否可访问考勤页。
问题 2.2.3:前端删除按钮无权限点控制【P2】
- 位置:attendance-record-list.tsx:106-114
- 描述:删除按钮对所有能看到列表的用户可见,未使用
usePermission().hasPermission(Permissions.ATTENDANCE_MANAGE)控制显隐。 - 违反规则:项目规则「前端权限判断统一使用
usePermission().hasPermission()」。 - 后果:无权用户看到删除按钮,点击后才被 Server Action 拒绝,体验差。
2.3 i18n 国际化
问题 2.3.1:safeParseDate 中文 fieldName 硬编码【P0】
- 位置:data-access.ts:100-102、data-access.ts:167、data-access.ts:185、data-access.ts:308、data-access-stats.ts:95-96、data-access-stats.ts:169-170
- 描述:
safeParseDate(value, "日期")、safeParseDate(value, "开始日期")等 10 处中文 fieldName,经handleActionError返回客户端为用户可见错误消息。 - 违反规则:项目规则「所有用户可见文本必须适配 i18n」。
- 后果:英文环境下显示中文错误。
问题 2.3.2:action-utils shared 层中文兜底消息【P1】
- 位置:
shared/lib/action-utils.ts:32,70,74,143 - 描述:
NotFoundError(\${resource} 不存在`)、"操作失败,请稍后重试"、${fieldName} 格式无效` 等。 - 违反规则:同上。
- 后果:所有调用 shared 层的模块(含 attendance)均受影响。
问题 2.3.3:Excel 导出英文列头硬编码【P1】
- 位置:export.ts:83-84
- 描述:
"Metric"、"Value"硬编码。
问题 2.3.4:calendar 硬编码 en-US locale【P1】
- 位置:parent-attendance-calendar.tsx:102
- 描述:
toLocaleDateString("en-US"),未使用当前 locale。
问题 2.3.5:child-detail-panel 英文硬编码【P1】
- 位置:child-detail-panel.tsx:50-56、L111、L137-143、L152、L160-163、L182、L44、L186
- 描述:Tab 标签、区块标题、占位提示、按钮文案共 20+ 处英文硬编码。
问题 2.3.6:翻译键误用【P0】
- 位置:
- parent/attendance/error.tsx:17、teacher/attendance/error.tsx:17、admin/attendance/error.tsx:17:重试按钮使用
t("actions.save")而非t("actions.retry"),显示"保存"。 - attendance-record-list.tsx:127:删除确认对话框描述使用
t("errors.unexpected")("发生未知错误"),应为t("sheet.confirmDelete")。 - attendance-rules-form.tsx:32:保存按钮显示
t("rules.saved")("考勤规则已保存"),应为t("actions.save")。 - attendance-sheet.tsx:395:切换班级确认对话框标题误用
t("sheet.confirmDelete"),应为t("sheet.confirmClassSwitch")。
- parent/attendance/error.tsx:17、teacher/attendance/error.tsx:17、admin/attendance/error.tsx:17:重试按钮使用
- 违反规则:i18n 正确性。
- 后果:用户看到错误/误导文案。
问题 2.3.7:error.tsx title/description 重复【P2】
- 位置:4 个 error.tsx
- 描述:title 和 description 均用
t("errors.unexpected"),完全相同。
2.4 类型安全
问题 2.4.1:export.ts 无类型守卫的 as 断言【P1】
- 位置:export.ts:28-34
- 描述:
params.status as "present" | "absent" | ...,params.status为string | undefined,无类型守卫。 - 违反规则:项目规则「禁止
as断言(除非从unknown转换)」。 - 后果:非法 status 值绕过类型检查。
问题 2.4.2:child-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.1:teacher 子路由缺失 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.2:student 空状态文案错误【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_OPTIONS、STATUS_SHORTCUTS、createInitialStatusCounts与 constants.ts 重复(部分复用、部分重复的混乱状态)。 - 违反规则:DRY 原则。
- 后果:状态选项变更需同步两处,易遗漏。
问题 2.6.2:4 个 error.tsx 近乎完全重复【P1】
- 位置:4 个 error.tsx
- 描述:结构完全相同,共约 96 行重复代码。
- 违反规则:项目记忆「Shared components must be extracted when page duplication exceeds 90%」。
- 后果:修改一处需同步四处。
问题 2.6.3:两个 stats 卡片组件数据结构分裂【P2】
- 位置:attendance-stats-card.tsx、attendance-stats-cards.tsx
- 描述:同一概念两套数据结构(
AttendanceStatsvsAttendanceOverviewStats,stats.present/stats.presentRatevsstats.presentCount/stats.attendanceRate)。
2.7 数据注入与解耦
问题 2.7.1:无接口抽象与 Context 注入【P1】
- 描述:attendance 模块未定义任何
AttendanceDataService接口,无AttendanceContext/AttendanceProvider,无角色差异的接口多态实现。 - 违反规则:项目规则「通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务」。
- 后果:角色间无统一契约约束,参数/返回处理可能不一致;无法通过接口 mock 做单测。
2.8 可测试性
问题 2.8.1:data-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.1:getClassAttendanceStats 未用 SQL 聚合【P1】
- 位置:data-access-stats.ts:172-182
- 描述:仍用全量查询 + 内存
computeStats,而getStudentAttendanceSummary/getAttendanceStats已改用 SQL 聚合。 - 违反规则:性能最佳实践。
- 后果:大班级统计查询慢。
问题 2.10.2:teacher 分页基于截断数据计算【P0】
- 位置:teacher/attendance/page.tsx:46-63
- 描述:先获取
result.items(pageSize=20),再对仅 20 条做前端分页计算 totalPages。 - 后果:分页页数错误,用户无法访问第 2 页之后数据。
问题 2.10.3:parent 组件可降级为 RSC【P2】
- 位置:parent-attendance-warning.tsx、parent-attendance-rate-card.tsx
- 描述:仅因
useTranslations标记"use client",可用getTranslations改为 RSC。
问题 2.10.4:statusCounts 未 memoize【P2】
- 位置:attendance-sheet.tsx:151-157
- 描述:每次 render 全量 reduce,大班级性能损耗。
三、行业差距对比
基于 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、键盘导航 | 已实现且较完善 | ✅ 基本达标 |
核心差距总结:
- 功能完整性:缺请假流程、节次考勤、原因分类、趋势可视化、跨班级对比——这些是 K12 学校的刚需。
- 主动通知机制:被动展示→主动预警的转变。
- 数据联动:考勤与成绩、请假、通知模块的联动缺失。
四、改进优先级建议
P0(紧急,影响安全/数据正确性,立即修复)
| # | 问题 | 改进方向 |
|---|---|---|
| P0-1 | teacher/sheet、teacher/stats 缺权限校验且未传 scope(2.2.1) | 页面级增加 requirePermission(ATTENDANCE_READ) + 传入 dataScope;data-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-access、classes/data-access 提供姓名查询接口 |
P1(重要,影响可维护性/合规性,短期修复)
| # | 问题 | 改进方向 |
|---|---|---|
| P1-1 | teacher/student 主页面缺 requirePermission(2.2.2) | 增加 requirePermission(ATTENDANCE_READ) |
| P1-2 | parent 直接 import attendance 组件(2.1.2) | 通过接口抽象 + Context 注入,或将共享视图下沉为可注入组件 |
| P1-3 | parent-attendance-calendar 依赖 attendance/constants(2.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-US(2.3.4) | 使用 useLocale()/getLocale() |
| P1-8 | teacher 子路由缺 error.tsx(2.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 组件可降级 RSC(2.10.3) | 改用 getTranslations |
| P2-9 | statusCounts 未 memoize(2.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 |
已改为 AlertDialog(L392-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 深层嵌套。 - 逻辑复用抽取为 hooks:
useAttendanceSheet、useAttendanceStats、useAttendanceFilters。 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。 - 纯逻辑(
computeStats、buildWarnings、aggregateStats等)已分离,保持。
6.7 可扩展性
- 角色配置驱动:
attendanceRoleConfig[role]决定渲染哪些 Widget/子模块。 - 新增角色仅修改配置。
6.8 企业级补充
- a11y:保持现有 ARIA/键盘导航水平,优化 keydown 监听范围。
- 性能:RSC 获取初始数据,客户端组件最小化,支持流式渲染。
- 安全:data-access 层结合
dataScope过滤,Server Action 二次校验。 - 监控:预留
trackEvent埋点接口(点名/删除/规则保存等关键操作)。