|
|
@@ -129,6 +129,130 @@ public class WorkHoursCalcService {
|
|
|
counts[0], counts[1], personnelMap.size(), workingDays.size());
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 单员工单日 upsert 验证入口(清空后做一条验证用,非日常流程)
|
|
|
+ * ppExt: 复用 queryAllPersonnelDetails + 钉钉主管查询 + upsertDailyHours;
|
|
|
+ * 返回体回显 personnelInfo(含 textField_mmekrcji 归属公司)供核对
|
|
|
+ *
|
|
|
+ * @param userId 员工钉钉 userId
|
|
|
+ * @param workDay 目标日期
|
|
|
+ * @return Map{userId, workDay, personnelInfo, managerId, success, error?}
|
|
|
+ */
|
|
|
+ public Map<String, Object> syncOneEmployeeOneDay(String userId, LocalDate workDay) {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("userId", userId);
|
|
|
+ result.put("workDay", workDay == null ? null : workDay.toString());
|
|
|
+
|
|
|
+ if (userId == null || userId.isEmpty() || workDay == null) {
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("error", "userId 和 workDay 不能为空");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 全量查人员档案,取目标员工信息
|
|
|
+ Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
|
|
|
+ Map<String, Object> info = personnelMap.get(userId);
|
|
|
+ if (info == null) {
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("error", "人员档案未找到该 userId: " + userId);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ result.put("personnelInfo", info);
|
|
|
+
|
|
|
+ // 2. 内部员工查主管(外部员工跳过)
|
|
|
+ String managerId = null;
|
|
|
+ if ("内部".equals(String.valueOf(info.get("radioField_mkow4ydo")))) {
|
|
|
+ try {
|
|
|
+ String accessToken = ddClient.getAccessToken();
|
|
|
+ Map userInfo = ddClient_contacts.getUserInfoById(accessToken, userId);
|
|
|
+ if (userInfo != null && userInfo.get("manager_userid") != null) {
|
|
|
+ String mgrId = String.valueOf(userInfo.get("manager_userid"));
|
|
|
+ if (!mgrId.isEmpty() && !"null".equals(mgrId)) {
|
|
|
+ managerId = mgrId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取员工{}直属主管失败: {}", userId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ result.put("managerId", managerId);
|
|
|
+
|
|
|
+ // 3. upsert 写一条
|
|
|
+ try {
|
|
|
+ upsertDailyHours(userId, managerId, workDay, info);
|
|
|
+ result.put("success", true);
|
|
|
+ log.info("单条验证写入成功: userId={}, workDay={}, 归属公司={}",
|
|
|
+ userId, workDay, info.get("textField_mmekrcji"));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("单条验证写入失败: userId={}, workDay={}", userId, workDay, e);
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("error", e.getMessage());
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小批量同步验证入口(清空后取前 N 个内部员工写入当月数据,便于核对归属公司与 Manager)
|
|
|
+ * ppExt: 走完整流程(人员档案→主管→节假日→工作日→并发写入),仅人员集合缩成前 N 个内部员工
|
|
|
+ *
|
|
|
+ * @param limit 前 N 个内部员工
|
|
|
+ * @param targetMonth 目标月份,null 则默认当前月
|
|
|
+ * @return Map{employeeCount, workingDays, success, fail, sampleEmployees}
|
|
|
+ */
|
|
|
+ public Map<String, Object> syncBatchEmployees(int limit, LocalDate targetMonth) {
|
|
|
+ Map<String, Object> stats = new LinkedHashMap<>();
|
|
|
+ if (targetMonth == null) {
|
|
|
+ targetMonth = LocalDate.now();
|
|
|
+ }
|
|
|
+ if (limit <= 0) {
|
|
|
+ limit = 5;
|
|
|
+ }
|
|
|
+ int year = targetMonth.getYear();
|
|
|
+ int month = targetMonth.getMonthValue();
|
|
|
+ log.info("开始小批量同步: 前{}个内部员工, {}年{}月", limit, year, month);
|
|
|
+
|
|
|
+ // 1. 全量查人员档案
|
|
|
+ Map<String, Map<String, Object>> all = queryAllPersonnelDetails();
|
|
|
+
|
|
|
+ // 2. 截取前 limit 个内部员工(保持档案原始顺序)
|
|
|
+ Map<String, Map<String, Object>> subset = new LinkedHashMap<>();
|
|
|
+ List<Map<String, Object>> sample = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Map<String, Object>> entry : all.entrySet()) {
|
|
|
+ Map<String, Object> info = entry.getValue();
|
|
|
+ if (!"内部".equals(String.valueOf(info.get("radioField_mkow4ydo")))) continue;
|
|
|
+ subset.put(entry.getKey(), info);
|
|
|
+ Map<String, Object> s = new LinkedHashMap<>();
|
|
|
+ s.put("userId", entry.getKey());
|
|
|
+ s.put("textField_mh8xhqc1", info.get("textField_mh8xhqc1"));
|
|
|
+ s.put("textField_mmekrcji", info.get("textField_mmekrcji"));
|
|
|
+ sample.add(s);
|
|
|
+ if (subset.size() >= limit) break;
|
|
|
+ }
|
|
|
+ stats.put("employeeCount", subset.size());
|
|
|
+ stats.put("sampleEmployees", sample);
|
|
|
+ if (subset.isEmpty()) {
|
|
|
+ log.warn("未取到任何内部员工,跳过");
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 主管
|
|
|
+ Map<String, String> managerMap = queryManagerMap(subset);
|
|
|
+ stats.put("managerCount", managerMap.size());
|
|
|
+
|
|
|
+ // 4. 节假日 + 工作日
|
|
|
+ Map<LocalDate, String> rules = queryHolidayRules(String.valueOf(year));
|
|
|
+ List<LocalDate> workingDays = getWorkingDays(year, month, rules);
|
|
|
+ stats.put("workingDays", workingDays.size());
|
|
|
+
|
|
|
+ // 5. 并发写入
|
|
|
+ int[] counts = concurrentUpsert(subset, managerMap, workingDays);
|
|
|
+ stats.put("success", counts[0]);
|
|
|
+ stats.put("fail", counts[1]);
|
|
|
+ log.info("小批量同步完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
|
|
|
+ counts[0], counts[1], subset.size(), workingDays.size());
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 批量删除应填报工时全部数据(验证时使用,非日常流程)
|
|
|
*/
|
|
|
@@ -185,8 +309,12 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 删除后总数变化,始终查第1页
|
|
|
- if (totalDeleted >= totalCount) break;
|
|
|
+ // fixme: 原判断 `totalDeleted >= totalCount` 会提前 break(累计已删 vs 当次剩余总数语义不同)
|
|
|
+ // 改为仅靠下一轮 dataList.isEmpty() 判空;加安全阈值防死循环
|
|
|
+ if (totalDeleted > 200000) {
|
|
|
+ log.warn("已删除{}条超过安全阈值,终止批量删除", totalDeleted);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
log.info("应填报工时数据清空完成, 共删除{}条", totalDeleted);
|
|
|
}
|
|
|
@@ -355,7 +483,8 @@ public class WorkHoursCalcService {
|
|
|
info.put("radioField_mkow4ydo", formData.get("radioField_mkow4ydo"));
|
|
|
info.put("textField_mh8xhqc1", formData.get("textField_mh8xhqc1"));
|
|
|
info.put("departmentSelectField_mkow4ydr", formData.get("departmentSelectField_mkow4ydr_id"));
|
|
|
- info.put("textField_mmekrcji", formData.get("textField_mmekrcji"));
|
|
|
+ // 归属公司:人员档案是 SelectField 下拉,直接取字符串;目标表单写入仍用 textField_mmekrcji
|
|
|
+ info.put("textField_mmekrcji", formData.get("selectField_mh8xhqc4"));
|
|
|
personnelMap.put(empId, info);
|
|
|
}
|
|
|
}
|
|
|
@@ -409,7 +538,8 @@ public class WorkHoursCalcService {
|
|
|
info.put("radioField_mkow4ydo", formData.get("radioField_mkow4ydo"));
|
|
|
info.put("textField_mh8xhqc1", formData.get("textField_mh8xhqc1"));
|
|
|
info.put("departmentSelectField_mkow4ydr", formData.get("departmentSelectField_mkow4ydr_id"));
|
|
|
- info.put("textField_mmekrcji", formData.get("textField_mmekrcji"));
|
|
|
+ // 归属公司:人员档案是 SelectField 下拉,直接取字符串;目标表单写入仍用 textField_mmekrcji
|
|
|
+ info.put("textField_mmekrcji", formData.get("selectField_mh8xhqc4"));
|
|
|
personnelMap.put(empId, info);
|
|
|
}
|
|
|
}
|