|
|
@@ -49,6 +49,9 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
// prd 离职时间字段(人员档案侧, 用于过滤离职后不再生成/清理历史应报工时)
|
|
|
private static final String PERSONNEL_OFFLINE_DATE = "dateField_mh8xhqc7";
|
|
|
+ // prd 在职状态字段 + 离职取值 (防御开关: status=离职 且 offlineDate=空 → 数据不一致, 该员工不写工时)
|
|
|
+ private static final String PERSONNEL_STATUS = "radioField_mp1sngq1";
|
|
|
+ private static final String STATUS_INACTIVE = "离职";
|
|
|
|
|
|
// prd 外部员工关联项目经理:项目档案成员子表字段
|
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
|
@@ -180,6 +183,12 @@ public class WorkHoursCalcService {
|
|
|
result.put("error", "员工已离职(offlineDate=" + offlineDate + "), 按业务规则跳过写入");
|
|
|
return result;
|
|
|
}
|
|
|
+ // 防御: status=离职 但 offlineDate=空 → 不一致态, 拒绝写入
|
|
|
+ if (STATUS_INACTIVE.equals(String.valueOf(info.get(PERSONNEL_STATUS))) && offlineDate == null) {
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("error", "员工 status=离职 但 offlineDate=空, 数据不一致, 拒绝写入");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
|
|
|
// 2. Manager 取值: 内部=[钉钉直属主管], 外部=当天参与项目的 PM 合并
|
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
|
@@ -544,6 +553,7 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
AtomicInteger skippedOffline = new AtomicInteger(0);
|
|
|
AtomicInteger skippedNoPm = new AtomicInteger(0);
|
|
|
+ AtomicInteger skippedInconsistent = new AtomicInteger(0);
|
|
|
for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
|
|
|
String empId = entry.getKey();
|
|
|
Map<String, Object> info = entry.getValue();
|
|
|
@@ -553,6 +563,14 @@ public class WorkHoursCalcService {
|
|
|
List<Assignment> externalAssignments = isInternal ? null : managerData.external.get(empId);
|
|
|
// prd 离职时间: 若有离职时间, 该日之后的应报工时不再生成
|
|
|
LocalDate offlineDate = parseToLocalDate(info.get(PERSONNEL_OFFLINE_DATE));
|
|
|
+ // prd 防御开关: 人员档案 status=离职 但 offlineDate 空 → 数据不一致态, 该员工整月跳过, 避免误写
|
|
|
+ boolean inconsistentOffline = STATUS_INACTIVE.equals(String.valueOf(info.get(PERSONNEL_STATUS)))
|
|
|
+ && offlineDate == null;
|
|
|
+ if (inconsistentOffline) {
|
|
|
+ log.warn("[防御跳过] 员工{} status=离职 但 offlineDate=空, 整月跳过写入(工作日{}天)", empId, workingDays.size());
|
|
|
+ skippedInconsistent.addAndGet(workingDays.size());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
futures.add(executor.submit(() -> {
|
|
|
for (LocalDate workDay : workingDays) {
|
|
|
@@ -616,6 +634,9 @@ public class WorkHoursCalcService {
|
|
|
if (skippedNoPm.get() > 0) {
|
|
|
log.info("外部员工无 PM 过滤: 跳过{}条 当天无活跃项目 PM 的记录", skippedNoPm.get());
|
|
|
}
|
|
|
+ if (skippedInconsistent.get() > 0) {
|
|
|
+ log.warn("[防御跳过统计] 累计跳过{}条 status=离职 但 offlineDate=空 的记录", skippedInconsistent.get());
|
|
|
+ }
|
|
|
} finally {
|
|
|
executor.shutdown();
|
|
|
}
|
|
|
@@ -656,6 +677,7 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
// 2. 按月分区扫应报工时, 收集离职日之后的 instanceIds
|
|
|
List<String> toDelete = new ArrayList<>();
|
|
|
+ List<Map<String, Object>> samples = new ArrayList<>(); // dryRun 抽样前 5 条便于人工核对
|
|
|
int total = 0;
|
|
|
int pageSize = YDConf.PAGE_SIZE_LIMIT;
|
|
|
ZoneId zone = ZoneId.systemDefault();
|
|
|
@@ -700,6 +722,14 @@ public class WorkHoursCalcService {
|
|
|
// 离职日当天及之前保留 (员工在职期间工时有效, 不动)
|
|
|
if (workDay != null && workDay.isAfter(offlineDate)) {
|
|
|
toDelete.add(String.valueOf(instId));
|
|
|
+ if (samples.size() < 5) {
|
|
|
+ Map<String, Object> s = new LinkedHashMap<>();
|
|
|
+ s.put("instanceId", String.valueOf(instId));
|
|
|
+ s.put("empId", empId);
|
|
|
+ s.put("workDay", workDay.toString());
|
|
|
+ s.put("offlineDate", offlineDate.toString());
|
|
|
+ samples.add(s);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
currentPage++;
|
|
|
@@ -710,6 +740,7 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
stats.put("total", total);
|
|
|
stats.put("toDelete", toDelete.size());
|
|
|
+ stats.put("samples", samples);
|
|
|
if (dryRun) {
|
|
|
stats.put("deleted", 0);
|
|
|
stats.put("fail", 0);
|
|
|
@@ -854,6 +885,8 @@ public class WorkHoursCalcService {
|
|
|
info.put("textField_mpp7a2k7", formData.get("textField_mow9w7d8"));
|
|
|
// prd 离职时间: 携带原值供 concurrentUpsert 过滤 workDay > offlineDate
|
|
|
info.put(PERSONNEL_OFFLINE_DATE, formData.get(PERSONNEL_OFFLINE_DATE));
|
|
|
+ // prd 在职状态: 携带供 concurrentUpsert 防御 (status=离职 且 offlineDate=空 时跳过写入)
|
|
|
+ info.put(PERSONNEL_STATUS, formData.get(PERSONNEL_STATUS));
|
|
|
personnelMap.put(empId, info);
|
|
|
}
|
|
|
}
|
|
|
@@ -913,6 +946,8 @@ public class WorkHoursCalcService {
|
|
|
info.put("textField_mpp7a2k7", formData.get("textField_mow9w7d8"));
|
|
|
// prd 离职时间: 携带原值供 concurrentUpsert 过滤 workDay > offlineDate
|
|
|
info.put(PERSONNEL_OFFLINE_DATE, formData.get(PERSONNEL_OFFLINE_DATE));
|
|
|
+ // prd 在职状态: 携带供 concurrentUpsert 防御 (status=离职 且 offlineDate=空 时跳过写入)
|
|
|
+ info.put(PERSONNEL_STATUS, formData.get(PERSONNEL_STATUS));
|
|
|
personnelMap.put(empId, info);
|
|
|
}
|
|
|
}
|