# 课程二态考勤与截止自动结算 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` 在教师视图归为已打卡、陈浩无记录归为未打卡；不修改数据库数据。
