# 应报工时重复与离职后数据清理设计 > 日期:2026-07-15 > 状态:数据清理与删除后复查已完成,等待最终代码部署 ## 目标 清理「应填报工时」表中的两类无效数据: 1. 同一员工同一应填报日期存在多条记录时,只保留一条。 2. 员工已有离职日期时,删除应填报日期晚于离职日期的记录。 离职当天及之前的数据保留。无法匹配人员档案、人员档案公司或属性为空等其他情况继续沿用现有保留逻辑,不在本次清理范围内。 ## 已确认现状 - 每日增量同步在 03:30、12:45 执行,每次重新读取全量人员档案。 - 人员档案的离职日期字段为 `dateField_mh8xhqc7`。 - 写入前已有 `workDay > offlineDate` 过滤,离职日后不再新增应报工时。 - 状态为离职但离职日期为空时,代码会整人防御跳过;当前数据不存在这种情况。 - 本地隔离服务连接正式宜搭执行 dry-run:扫描 43,851 条,识别 74 名有离职日期的员工、796 条离职日后记录。 ## 重复记录判定 重复键为: ```text employeeId + "|" + workDay ``` - `employeeId` 从 `employeeField_mmd8onl4` 提取。 - `workDay` 从 `dateField_mmd8onl5` 解析。 - 员工或日期缺失时无法形成可靠重复键,该记录保留并计入 `skippedInvalidKey`。 - 仅键完全相同且数量大于 1 的记录视为重复组。 ## 重复组保留规则 每个重复组只保留一条,按以下顺序选择: 1. 保留业务字段非空数量最多的记录。 2. 完整度相同时,保留 `gmtModified` 最近的记录。 3. 修改时间仍相同时,保留 `gmtCreate` 最近的记录。 4. 仍相同时,按 `formInstanceId` 做稳定排序,保证重复执行结果一致。 完整度仅统计已有应报工时业务字段:工时、Manager、员工编号、属性、部门、归属公司、是否 CF 员工。核心键字段不计分。 不合并不同记录的字段,避免构造从未真实存在过的组合数据。此前公司和属性已完成统一回填,因此选择最完整记录即可。 ## 接口与执行流程 新增一次性接口: ```text GET /workhours/cleanup-duplicates?dryRun=true GET /workhours/cleanup-duplicates ``` 执行分为四步: 1. 按月份扫描从 2026-04 至当前月的应报工时,绕过宜搭搜索结果 30,000 条上限。 2. 在内存中按重复键分组并选择保留记录,收集其余实例 ID。 3. dry-run 返回统计与前 5 个重复组样本,不调用删除接口。 4. 正式模式按每批最多 100 条调用 `delete_batch`,返回实际删除数与失败数。 返回统计至少包含: - `total` - `uniqueKeys` - `duplicateGroups` - `duplicateRecords` - `toDelete` - `skippedInvalidKey` - `deleted` - `fail` - `samples` ## 删除安全门槛 正式删除前必须把以下范围复述给用户并取得二次确认: - 目标表单:应填报工时表。 - 重复组数量、重复记录总数和待删除实例数。 - 离职后待删除记录数,当前 dry-run 为 796。 - 重复记录保留规则。 正式删除后必须再次执行: 1. `/workhours/cleanup-duplicates?dryRun=true`,期望 `toDelete=0`。 2. `/workhours/cleanup-after-offline?dryRun=true`,期望 `toDelete=0`。 3. `/workhours/cleanup-future?dryRun=true`,期望 `toDelete=0`。 ## 实际执行结果 - 重复数据:35 个重复组、70 条重复记录,按保留规则删除 35 条,失败 0。 - 离职后数据:删除 796 条,失败 0;离职当天及之前记录保留。 - 删除后现存 43,020 条,员工+日期唯一键 43,020 个。 - 重复复查:`duplicateGroups=0`、`toDelete=0`、`skippedInvalidKey=0`。 - 离职后复查:`toDelete=0`。 - 未来日期复查:截止 2026-07-15,`toDelete=0`。 - 本地服务完成后已停止;本次代码尚未部署到生产服务。 ## 测试 采用测试驱动实现,至少覆盖: 1. 同组中保留业务字段最完整的记录。 2. 完整度相同时保留最近修改的记录。 3. 员工或日期缺失的记录不进入删除集合。 4. dry-run 只返回待删除数量,不调用 `delete_batch`。 5. 离职边界保持 `workDay > offlineDate`:离职当天保留,次日删除或跳过写入。 最终执行模块测试、完整打包、差异检查,再提交和部署。 ## 不在本次范围 - 不删除无法匹配人员档案的记录。 - 不清空人员档案公司或属性为空的历史字段。 - 不处理项目变更审批 Manager 回算。 - 不启用 `projectChangeSyncEnabled`。