|
@@ -62,6 +62,11 @@ public class WorkHoursCalcService {
|
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
|
// prd 应填报日期文本,用于跨表按 yyyyMMdd 精确关联,避免日期组件时区偏移
|
|
// prd 应填报日期文本,用于跨表按 yyyyMMdd 精确关联,避免日期组件时区偏移
|
|
|
private static final String REQUIRED_HOURS_DATE_TEXT = "textField_mtseezhf";
|
|
private static final String REQUIRED_HOURS_DATE_TEXT = "textField_mtseezhf";
|
|
|
|
|
+ // prd 正式工时汇总按填报人+日期匹配;创建应报工时时累计当天日变更工时。
|
|
|
|
|
+ private static final String SUMMARY_SUBMITTER_ID = "textField_mmbffvd9";
|
|
|
|
|
+ private static final String SUMMARY_DAY_DATE = "dateField_mmbffvd8";
|
|
|
|
|
+ private static final String SUMMARY_DAY_TEXT = "textField_mmbffvdb";
|
|
|
|
|
+ private static final String SUMMARY_DAILY_CHANGE = "numberField_mmeakgi5";
|
|
|
|
|
|
|
|
// prd 外部员工项目参与关系 + 项目经理:项目档案字段
|
|
// prd 外部员工项目参与关系 + 项目经理:项目档案字段
|
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
@@ -116,11 +121,14 @@ public class WorkHoursCalcService {
|
|
|
LocalDate monthStart = LocalDate.of(year, month, 1);
|
|
LocalDate monthStart = LocalDate.of(year, month, 1);
|
|
|
LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
|
|
LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
|
|
|
Set<String> existKeys = queryExistingHoursKeys(monthStart, monthEnd);
|
|
Set<String> existKeys = queryExistingHoursKeys(monthStart, monthEnd);
|
|
|
|
|
+ Map<String, Double> summaryChangeHours = querySummaryChangeHours(monthStart, monthEnd);
|
|
|
log.info("全量: 当月已存在记录{}条", existKeys.size());
|
|
log.info("全量: 当月已存在记录{}条", existKeys.size());
|
|
|
stats.put("existingCount", existKeys.size());
|
|
stats.put("existingCount", existKeys.size());
|
|
|
|
|
+ stats.put("summaryChangeCount", summaryChangeHours.size());
|
|
|
|
|
|
|
|
// 6. 多线程并发写入:按员工维度分任务
|
|
// 6. 多线程并发写入:按员工维度分任务
|
|
|
- int[] counts = concurrentUpsert(personnelMap, managerData, workingDays, existKeys);
|
|
|
|
|
|
|
+ int[] counts = concurrentUpsert(
|
|
|
|
|
+ personnelMap, managerData, workingDays, existKeys, summaryChangeHours);
|
|
|
stats.put("success", counts[0]);
|
|
stats.put("success", counts[0]);
|
|
|
stats.put("fail", counts[1]);
|
|
stats.put("fail", counts[1]);
|
|
|
log.info("应填报工时写入完成: 成功{}条, 失败{}条({}名员工 × {}个工作日, 已存在{}条 skip)",
|
|
log.info("应填报工时写入完成: 成功{}条, 失败{}条({}名员工 × {}个工作日, 已存在{}条 skip)",
|
|
@@ -175,10 +183,12 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
|
|
// 5. 预查窗口内已存在记录 (empId|yyyy-MM-dd 集合), 传入 concurrentUpsert 命中即 skip
|
|
// 5. 预查窗口内已存在记录 (empId|yyyy-MM-dd 集合), 传入 concurrentUpsert 命中即 skip
|
|
|
Set<String> existKeys = queryExistingHoursKeys(fromDate, today);
|
|
Set<String> existKeys = queryExistingHoursKeys(fromDate, today);
|
|
|
|
|
+ Map<String, Double> summaryChangeHours = querySummaryChangeHours(fromDate, today);
|
|
|
log.info("增量: 窗口内已存在记录{}条", existKeys.size());
|
|
log.info("增量: 窗口内已存在记录{}条", existKeys.size());
|
|
|
|
|
|
|
|
// 6. 并发写入
|
|
// 6. 并发写入
|
|
|
- int[] counts = concurrentUpsert(personnelMap, managerData, windowDays, existKeys);
|
|
|
|
|
|
|
+ int[] counts = concurrentUpsert(
|
|
|
|
|
+ personnelMap, managerData, windowDays, existKeys, summaryChangeHours);
|
|
|
log.info("增量同步完成: 成功{}条, 失败{}条 ({}名员工 × {}天工作日, 已存在{}条 skip)",
|
|
log.info("增量同步完成: 成功{}条, 失败{}条 ({}名员工 × {}天工作日, 已存在{}条 skip)",
|
|
|
counts[0], counts[1], personnelMap.size(), windowDays.size(), existKeys.size());
|
|
counts[0], counts[1], personnelMap.size(), windowDays.size(), existKeys.size());
|
|
|
}
|
|
}
|
|
@@ -193,17 +203,18 @@ public class WorkHoursCalcService {
|
|
|
* @return Map{userId, workDay, personnelInfo, managerId, success, error?}
|
|
* @return Map{userId, workDay, personnelInfo, managerId, success, error?}
|
|
|
*/
|
|
*/
|
|
|
public Map<String, Object> syncOneEmployeeOneDay(String userId, LocalDate workDay) {
|
|
public Map<String, Object> syncOneEmployeeOneDay(String userId, LocalDate workDay) {
|
|
|
- return syncOneEmployeeOneDay(userId, workDay, DAILY_HOURS);
|
|
|
|
|
|
|
+ return syncOneEmployeeOneDay(userId, workDay, DAILY_HOURS, true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object> syncOneEmployeeOneDayForRepair(String userId, LocalDate workDay) {
|
|
Map<String, Object> syncOneEmployeeOneDayForRepair(String userId, LocalDate workDay) {
|
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(workDay.getYear()));
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(workDay.getYear()));
|
|
|
return syncOneEmployeeOneDay(userId, workDay,
|
|
return syncOneEmployeeOneDay(userId, workDay,
|
|
|
- resolveRequiredHours(workDay, holidayRules));
|
|
|
|
|
|
|
+ resolveRequiredHours(workDay, holidayRules), false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private Map<String, Object> syncOneEmployeeOneDay(String userId, LocalDate workDay,
|
|
private Map<String, Object> syncOneEmployeeOneDay(String userId, LocalDate workDay,
|
|
|
- double requiredHours) {
|
|
|
|
|
|
|
+ double baseRequiredHours,
|
|
|
|
|
+ boolean includeSummaryChange) {
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
result.put("userId", userId);
|
|
result.put("userId", userId);
|
|
|
result.put("workDay", workDay == null ? null : workDay.toString());
|
|
result.put("workDay", workDay == null ? null : workDay.toString());
|
|
@@ -280,6 +291,12 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
|
|
// 3. upsert 写一条
|
|
// 3. upsert 写一条
|
|
|
try {
|
|
try {
|
|
|
|
|
+ double requiredHours = baseRequiredHours;
|
|
|
|
|
+ if (includeSummaryChange) {
|
|
|
|
|
+ double changeHours = querySummaryChangeHours(workDay, workDay)
|
|
|
|
|
+ .getOrDefault(requiredHoursKey(userId, workDay), 0d);
|
|
|
|
|
+ requiredHours = resolveInitialRequiredHours(baseRequiredHours, changeHours);
|
|
|
|
|
+ }
|
|
|
upsertDailyHours(userId, managerIds, workDay, info, requiredHours);
|
|
upsertDailyHours(userId, managerIds, workDay, info, requiredHours);
|
|
|
result.put("success", true);
|
|
result.put("success", true);
|
|
|
log.info("单条验证写入成功: userId={}, workDay={}, 归属公司={}",
|
|
log.info("单条验证写入成功: userId={}, workDay={}, 归属公司={}",
|
|
@@ -348,7 +365,11 @@ public class WorkHoursCalcService {
|
|
|
stats.put("workingDays", workingDays.size());
|
|
stats.put("workingDays", workingDays.size());
|
|
|
|
|
|
|
|
// 5. 并发写入 (小批量验证入口: existKeys 传空集, 保持"必写"语义便于回归验证)
|
|
// 5. 并发写入 (小批量验证入口: existKeys 传空集, 保持"必写"语义便于回归验证)
|
|
|
- int[] counts = concurrentUpsert(subset, managerData, workingDays, Collections.emptySet());
|
|
|
|
|
|
|
+ LocalDate monthStart = LocalDate.of(year, month, 1);
|
|
|
|
|
+ LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
|
|
|
|
|
+ Map<String, Double> summaryChangeHours = querySummaryChangeHours(monthStart, monthEnd);
|
|
|
|
|
+ int[] counts = concurrentUpsert(
|
|
|
|
|
+ subset, managerData, workingDays, Collections.emptySet(), summaryChangeHours);
|
|
|
stats.put("success", counts[0]);
|
|
stats.put("success", counts[0]);
|
|
|
stats.put("fail", counts[1]);
|
|
stats.put("fail", counts[1]);
|
|
|
log.info("小批量同步完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
|
|
log.info("小批量同步完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
|
|
@@ -836,7 +857,8 @@ public class WorkHoursCalcService {
|
|
|
private int[] concurrentUpsert(Map<String, Map<String, Object>> personnelMap,
|
|
private int[] concurrentUpsert(Map<String, Map<String, Object>> personnelMap,
|
|
|
ManagerData managerData,
|
|
ManagerData managerData,
|
|
|
List<LocalDate> workingDays,
|
|
List<LocalDate> workingDays,
|
|
|
- Set<String> existKeys) {
|
|
|
|
|
|
|
+ Set<String> existKeys,
|
|
|
|
|
+ Map<String, Double> summaryChangeHours) {
|
|
|
AtomicInteger successCount = new AtomicInteger(0);
|
|
AtomicInteger successCount = new AtomicInteger(0);
|
|
|
AtomicInteger failCount = new AtomicInteger(0);
|
|
AtomicInteger failCount = new AtomicInteger(0);
|
|
|
// fixme 宜搭写接口失败时不能原地重试:首个请求可能已成功落库但响应丢失,连续 upsert 会生成重复记录。
|
|
// fixme 宜搭写接口失败时不能原地重试:首个请求可能已成功落库但响应丢失,连续 upsert 会生成重复记录。
|
|
@@ -898,6 +920,10 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
}
|
|
|
// prd 内部员工允许空 Manager;外部员工仍必须存在启用项目及有效项目 PM
|
|
// prd 内部员工允许空 Manager;外部员工仍必须存在启用项目及有效项目 PM
|
|
|
List<String> managerIds = null;
|
|
List<String> managerIds = null;
|
|
|
|
|
+ double requiredHours = resolveInitialRequiredHours(
|
|
|
|
|
+ DAILY_HOURS,
|
|
|
|
|
+ summaryChangeHours.getOrDefault(
|
|
|
|
|
+ requiredHoursKey(empId, workDay), 0d));
|
|
|
try {
|
|
try {
|
|
|
// prd 外部员工须在启用项目中且当日未下线;内部员工不校验项目
|
|
// prd 外部员工须在启用项目中且当日未下线;内部员工不校验项目
|
|
|
if (!isInternal && !hasActiveProject(externalAssignments, workDay)) {
|
|
if (!isInternal && !hasActiveProject(externalAssignments, workDay)) {
|
|
@@ -916,11 +942,13 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
yidaLimiter.acquire();
|
|
yidaLimiter.acquire();
|
|
|
- upsertDailyHours(empId, managerIds, workDay, info);
|
|
|
|
|
|
|
+ upsertDailyHours(empId, managerIds, workDay, info, requiredHours);
|
|
|
successCount.incrementAndGet();
|
|
successCount.incrementAndGet();
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
boolean rateLimited = isRateLimitError(e);
|
|
boolean rateLimited = isRateLimitError(e);
|
|
|
- deferredWrites.add(new DeferredWrite(empId, managerIds, workDay, info, externalAssignments));
|
|
|
|
|
|
|
+ deferredWrites.add(new DeferredWrite(
|
|
|
|
|
+ empId, managerIds, workDay, info,
|
|
|
|
|
+ externalAssignments, requiredHours));
|
|
|
log.warn("员工{} {} 首轮处理失败,已记录并跳过,待首轮全部完成后查询再补写: rateLimited={}, code={}, message={}",
|
|
log.warn("员工{} {} 首轮处理失败,已记录并跳过,待首轮全部完成后查询再补写: rateLimited={}, code={}, message={}",
|
|
|
empId, workDay, rateLimited, queryErrorCode(e), e.getMessage());
|
|
empId, workDay, rateLimited, queryErrorCode(e), e.getMessage());
|
|
|
if (rateLimited) {
|
|
if (rateLimited) {
|
|
@@ -1021,7 +1049,9 @@ public class WorkHoursCalcService {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
yidaLimiter.acquire();
|
|
yidaLimiter.acquire();
|
|
|
- upsertDailyHours(item.employeeId, managerIds, item.workDay, item.personnelInfo);
|
|
|
|
|
|
|
+ upsertDailyHours(
|
|
|
|
|
+ item.employeeId, managerIds, item.workDay,
|
|
|
|
|
+ item.personnelInfo, item.requiredHours);
|
|
|
successCount.incrementAndGet();
|
|
successCount.incrementAndGet();
|
|
|
log.info("员工{} {} 延后补写成功", item.employeeId, item.workDay);
|
|
log.info("员工{} {} 延后补写成功", item.employeeId, item.workDay);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
@@ -1588,6 +1618,91 @@ public class WorkHoursCalcService {
|
|
|
return stats;
|
|
return stats;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询日期范围内正式工时汇总的日变更工时,按员工+日期累计。
|
|
|
|
|
+ * ppExt: 只供常规应报工时新增使用;补填专用创建仍由调用方按原有基础值处理。
|
|
|
|
|
+ */
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private Map<String, Double> querySummaryChangeHours(LocalDate fromDate, LocalDate toDate) {
|
|
|
|
|
+ Map<String, Double> changeHours = new HashMap<>();
|
|
|
|
|
+ if (fromDate == null || toDate == null || fromDate.isAfter(toDate)) {
|
|
|
|
|
+ return changeHours;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (whConf.getFormUuidWorkHoursSummary() == null
|
|
|
|
|
+ || whConf.getFormUuidWorkHoursSummary().trim().isEmpty()) {
|
|
|
|
|
+ throw new IllegalStateException("工时汇总表未配置");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ZoneId zone = ZoneId.systemDefault();
|
|
|
|
|
+ long startMs = fromDate.atStartOfDay(zone).toInstant().toEpochMilli();
|
|
|
|
|
+ long endMs = toDate.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1;
|
|
|
|
|
+ Map<String, Object> search = new HashMap<>();
|
|
|
|
|
+ search.put(SUMMARY_DAY_DATE, Arrays.asList(startMs, endMs));
|
|
|
|
|
+
|
|
|
|
|
+ int currentPage = 1;
|
|
|
|
|
+ int pageSize = YDConf.PAGE_SIZE_LIMIT;
|
|
|
|
|
+ long totalCount;
|
|
|
|
|
+ Map<String, Integer> matchedCounts = new HashMap<>();
|
|
|
|
|
+ do {
|
|
|
|
|
+ DDR_New result = ydClient.queryData(YDParam.builder()
|
|
|
|
|
+ .appType(whConf.getYidaAppType())
|
|
|
|
|
+ .systemToken(whConf.getYidaSystemToken())
|
|
|
|
|
+ .formUuid(whConf.getFormUuidWorkHoursSummary())
|
|
|
|
|
+ .searchFieldJson(JSON.toJSONString(search))
|
|
|
|
|
+ .currentPage(currentPage)
|
|
|
|
|
+ .pageSize(pageSize)
|
|
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ throw new IllegalStateException("查询工时汇总变更工时返回空响应");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ totalCount = result.getTotalCount();
|
|
|
|
|
+ List<Map> dataList = (List<Map>) result.getData();
|
|
|
|
|
+ if (dataList == null || dataList.isEmpty()) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (Map item : dataList) {
|
|
|
|
|
+ Map<String, Object> formData = (Map<String, Object>) item.get("formData");
|
|
|
|
|
+ if (formData == null) continue;
|
|
|
|
|
+ String employeeId = textValue(formData.get(SUMMARY_SUBMITTER_ID));
|
|
|
|
|
+ String dayText = textValue(formData.get(SUMMARY_DAY_TEXT));
|
|
|
|
|
+ if (employeeId.isEmpty() || !dayText.matches("\\d{8}")) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String key = employeeId + "|" + LocalDate.parse(
|
|
|
|
|
+ dayText, DateTimeFormatter.BASIC_ISO_DATE);
|
|
|
|
|
+ double dailyChange = hoursValue(formData.get(SUMMARY_DAILY_CHANGE));
|
|
|
|
|
+ changeHours.merge(key, dailyChange, Double::sum);
|
|
|
|
|
+ matchedCounts.merge(key, 1, Integer::sum);
|
|
|
|
|
+ }
|
|
|
|
|
+ currentPage++;
|
|
|
|
|
+ } while ((long) (currentPage - 1) * pageSize < totalCount);
|
|
|
|
|
+
|
|
|
|
|
+ matchedCounts.forEach((key, count) -> {
|
|
|
|
|
+ if (count > 1) {
|
|
|
|
|
+ log.warn("工时汇总同员工同日期存在{}条记录,日变更工时按合计读取: key={}", count, key);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return changeHours;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static double resolveInitialRequiredHours(double baseHours, double changeHours) {
|
|
|
|
|
+ return baseHours + changeHours;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String requiredHoursKey(String employeeId, LocalDate workDay) {
|
|
|
|
|
+ return employeeId + "|" + workDay;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String textValue(Object value) {
|
|
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static double hoursValue(Object value) {
|
|
|
|
|
+ if (value == null || String.valueOf(value).trim().isEmpty()) return 0d;
|
|
|
|
|
+ if (value instanceof Number) return ((Number) value).doubleValue();
|
|
|
|
|
+ return Double.parseDouble(String.valueOf(value).trim());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 预查指定日期范围内已存在的应报工时记录, 返回 "empId|yyyy-MM-dd" 键集合
|
|
* 预查指定日期范围内已存在的应报工时记录, 返回 "empId|yyyy-MM-dd" 键集合
|
|
|
* ppExt: 供 concurrentUpsert 补漏语义使用 (命中即 skip); 范围内不含重复员工+日期 (upsert 唯一键)
|
|
* ppExt: 供 concurrentUpsert 补漏语义使用 (命中即 skip); 范围内不含重复员工+日期 (upsert 唯一键)
|
|
@@ -1807,12 +1922,14 @@ public class WorkHoursCalcService {
|
|
|
private final List<Assignment> externalAssignments;
|
|
private final List<Assignment> externalAssignments;
|
|
|
private final LocalDate workDay;
|
|
private final LocalDate workDay;
|
|
|
private final Map<String, Object> personnelInfo;
|
|
private final Map<String, Object> personnelInfo;
|
|
|
|
|
+ private final double requiredHours;
|
|
|
|
|
|
|
|
private DeferredWrite(String employeeId,
|
|
private DeferredWrite(String employeeId,
|
|
|
List<String> managerIds,
|
|
List<String> managerIds,
|
|
|
LocalDate workDay,
|
|
LocalDate workDay,
|
|
|
Map<String, Object> personnelInfo,
|
|
Map<String, Object> personnelInfo,
|
|
|
- List<Assignment> externalAssignments) {
|
|
|
|
|
|
|
+ List<Assignment> externalAssignments,
|
|
|
|
|
+ double requiredHours) {
|
|
|
this.employeeId = employeeId;
|
|
this.employeeId = employeeId;
|
|
|
this.managerIds = managerIds == null
|
|
this.managerIds = managerIds == null
|
|
|
? null
|
|
? null
|
|
@@ -1820,6 +1937,7 @@ public class WorkHoursCalcService {
|
|
|
this.externalAssignments = externalAssignments;
|
|
this.externalAssignments = externalAssignments;
|
|
|
this.workDay = workDay;
|
|
this.workDay = workDay;
|
|
|
this.personnelInfo = personnelInfo;
|
|
this.personnelInfo = personnelInfo;
|
|
|
|
|
+ this.requiredHours = requiredHours;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|