feat: settle course attendance automatically

This commit is contained in:
2026-07-13 10:26:38 +08:00
parent b9b295c997
commit 1c8bd08be4
11 changed files with 730 additions and 87 deletions

View File

@@ -0,0 +1,76 @@
# 课程二态考勤与截止自动结算 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 教师课程考勤只显示已打卡/未打卡,并在课程截止后自动最终拉取、落库和完成场次。
**Architecture:** 保留钉钉原始迟到状态;课程服务根据“是否存在实际打卡时间”生成临时二态结果和最终 `present/absent`。新增 Attendance 模块内的 NestJS 定时结算服务,每分钟扫描到期课程并复用导入与课程考勤服务,失败留待下一轮补偿。
**Tech Stack:** NestJS 11、@nestjs/schedule 6、TypeORM 0.3、Jest、React 19、Ant Design 6。
## Global Constraints
- 仅调整排课关联的课程考勤。
- 不迁移历史记录,不改变钉钉原始记录。
- 不新增依赖、队列、兼容层或重复状态模型。
- 课程截止后有实际打卡写 `present`,无实际打卡写 `absent`
- 单节失败不得阻断其他课程,后续扫描必须可补偿。
---
### Task 1: 课程二态映射
**Files:**
- Modify: `apps/server/src/attendance/attendance.service.ts`
- Test: `apps/server/src/attendance/attendance.lesson-session.spec.ts`
**Interfaces:**
- Produces: `createLessonAttendanceFromDingTalk(scheduleId, lessonDate, userId, finalize?)``finalize=false` 返回临时二态,`finalize=true` 写最终二态并完成场次。
- [ ] 添加失败测试:`Late` 且有 `checkInTime` 应为 `present`;无实际时间应为 `pending`;最终结算时无时间应为 `absent` 且 session 为 `completed`
- [ ] 运行 `npm test -- attendance.lesson-session.spec.ts --runInBand`,确认新增断言按预期失败。
- [ ] 将课程状态映射改为只检查课程窗口内是否存在 `checkInTime``checkOutTime`;最终结算参数控制无打卡为 `absent`,并在同一事务完成场次。
- [ ] 再次运行相同测试,确认通过。
### Task 2: 截止自动结算
**Files:**
- Create: `apps/server/src/attendance/attendance-settlement.service.ts`
- Create: `apps/server/src/attendance/attendance-settlement.service.spec.ts`
- Modify: `apps/server/src/attendance/attendance.module.ts`
- Modify: `apps/server/src/app.module.ts`
**Interfaces:**
- Consumes: `AttendanceImportService.importFromDingTalk(...)``AttendanceService.getTeacherClassDingUserIds(...)``AttendanceService.createLessonAttendanceFromDingTalk(..., true)`
- Produces: `AttendanceSettlementService.settleEndedLessons(now?: Date): Promise<void>`,由 `@Cron('* * * * *')` 调用。
- [ ] 添加失败测试:未截止不处理、已完成不处理、到期课程最终拉取并结算、一个课程失败后继续处理下一个、昨日跨午夜课程可结算。
- [ ] 运行 `npm test -- attendance-settlement.service.spec.ts --runInBand`,确认因服务不存在而失败。
- [ ] 实现每分钟扫描今天普通到期课程及昨日跨午夜到期课程;逐课程捕获异常并记录;使用课程 `teacherId` 作为自动导入审计用户。
- [ ]`AttendanceModule` 注册服务,在根模块启用 `ScheduleModule.forRoot()`
- [ ] 再次运行相同测试,确认通过。
### Task 3: 教师二态界面
**Files:**
- Modify: `apps/admin/src/pages/Attendance/attendance-workspace.ts`
- Modify: `apps/admin/src/pages/Attendance/index.tsx`
- Test: `apps/admin/src/pages/Attendance/attendance-workspace.test.ts`
**Interfaces:**
- Produces: `summarizeLessonCheckins(records)`,将 `present`/`late` 归为已打卡,将其余归为未打卡。
- [ ] 添加失败测试:`present` 与遗留 `late` 均计入已打卡,`pending`/`absent` 计入未打卡。
- [ ] 运行 admin 的定向测试命令并确认失败。
- [ ] 教师抽屉改为“已打卡 / 未打卡”标签、汇总和手动修改选项;管理员档案保持原五态。
- [ ] 再次运行定向测试,确认通过。
### Task 4: 聚焦验证
**Files:**
- Verify only; no planned production edits.
- [ ] 运行 server 两个定向 Jest 测试文件。
- [ ] 运行 server `npm run typecheck`
- [ ] 运行 admin 定向测试与 `npm run typecheck`
- [ ] 用现有数据库场景确认王子琪的 `Late` 在教师视图归为已打卡、陈浩无记录归为未打卡;不修改数据库数据。

View File

@@ -0,0 +1,63 @@
# 课程二态考勤与截止自动结算设计
## 目标
课程考勤仅向教师展示“已打卡 / 未打卡”。迟到属于已打卡。每节课程到达截止时间后,系统自动从钉钉做最后一次拉取并将最终结果写入课程考勤记录;截止仍无实际打卡的学生记为缺勤。
## 范围
- 仅调整排课关联的课程考勤。
- 不迁移历史记录。
- 不改变后台其他考勤来源、迟到统计或钉钉原始数据。
- 不引入队列或新依赖,复用 NestJS Schedule 与现有导入、匹配、课程考勤服务。
## 状态规则
### 教师当前课程页面
- 存在课程时间窗口内的实际打卡时间:显示“已打卡”。
- 不存在实际打卡时间:显示“未打卡”。
- `Late``SeriousLate``Normal` 均显示为“已打卡”。
- 页面汇总仅显示已打卡数、未打卡数和总人数。
### 最终记录
- 截止时存在实际打卡时间:`present`
- 截止时不存在实际打卡时间:`absent`
- 钉钉原始记录继续保留 `timeResult`,因此不会丢失迟到信息。
- 自动结算完成后,课程考勤场次状态改为 `completed`,不再被后续拉取覆盖。
## 自动结算
后台任务每分钟扫描:
1. 当天有效的内部课程;
2. 当前时间已经达到课程 `endTime`
3. 对应日期的课程考勤场次尚未完成或尚未创建。
对每节符合条件的课程:
1. 获取该班在读学生的钉钉用户 ID
2. 拉取当天最终钉钉考勤并自动匹配;
3. 创建或刷新课程考勤记录;
4. 将有实际打卡的记录归为 `present`,其余归为 `absent`
5. 将场次标记为 `completed`
任务按课程独立处理。单节课拉取失败只记录错误,其他课程继续;下一分钟继续补偿失败课程。现有 `(scheduleId, lessonDate)` 唯一约束和完成状态保证重复扫描幂等。
跨午夜课程以结束时间不晚于开始时间判断为次日截止;扫描同时覆盖昨日跨午夜课程。
## 手动查看
课程开始后,教师点击“查看当前考勤”仍会拉取最新数据。课程截止前结果是临时二态视图;课程截止后读取自动结算的最终记录。若自动任务尚未成功,手动查看可继续拉取,但只有自动结算或明确完成操作会冻结最终结果。
## 测试
- 迟到且存在实际打卡时间时,当前课程视图为“已打卡”。
- 无实际打卡时间时,当前课程视图为“未打卡”。
- 截止结算把迟到和正常打卡写为 `present`
- 截止结算把无打卡写为 `absent` 并完成场次。
- 未截止课程不结算。
- 重复扫描已完成课程不重复拉取或写入。
- 单节课程失败不阻断其他课程,后续扫描可补偿。
- 跨午夜课程在次日截止后结算。