## [ERR-20260427-008] ADP 同时段重复 PTO request：同人同天出现 2 条 approved

**日期**: 2026-04-27
**类别**: ADP 数据 / 业务逻辑
**严重度**: 低（管理员列表显示重复，会议出勤功能不受影响）

### 问题描述
管理员看到 Admin PTO 列表里 Aaron Ma 的 4-20 ~ 4-24 每天都有 2 条记录。

### 根因（不是 bug，是 ADP 数据特征）
ADP 同一员工同一时段可以有**多个 approved 状态的 timeOffRequest**。常见场景：
1. 员工先申请 5 天 PTO，被批
2. 后又改/扩展为 10 天，提交新申请，又被批
3. 老申请没有自动 cancel/rejection，所以**两个 request 都是 approved**

ADP 在数据层不强制唯一，需要 client side 去重。

### 修复
PTO sync upsert 前按 `(startTime, endTime)` 去重，同一时段保留 `adpEntryId` **字典序最小**的那条：
```ts
const slotByTime = new Map<string, Slot>();
for (...) {
  const key = `${start.toISOString()}|${end.toISOString()}`;
  const existing = slotByTime.get(key);
  if (!existing || adpEntryId < existing.adpEntryId) {
    slotByTime.set(key, { adpEntryId, start, end, leaveDate });
  }
}
// upsert from slotByTime.values()
```

**为什么按字典序最小**：保证每次同步选同一条，不会让 DB 里 entry_id 来回漂移、触发无意义的 update + delete-not-seen 删旧重建。

### 启示
- 处理任何 HR / 工作流系统的数据时，**别假设业务实体不重叠**。同人同时段的多份 approved 记录是历史遗留导致的常态
- 客户端去重要"稳定"：选取规则要确定性的（字典序、最早创建时间等），不能用"第一个遇到"这种依赖 API 返回顺序的
- 会议出勤标记功能本来就用 `findFirst`，所以重叠对核心功能无影响 —— 但 admin 列表是直接 enum 全部 PTO，重叠会暴露给运营看到，所以必须去重

---
