|
|
@@ -0,0 +1,553 @@
|
|
|
+package com.malk.service.personnel.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.malk.server.aliwork.YDConf;
|
|
|
+import com.malk.server.aliwork.YDParam;
|
|
|
+import com.malk.server.dingtalk.DDR_New;
|
|
|
+import com.malk.server.personnel.PersonnelSyncConf;
|
|
|
+import com.malk.service.aliwork.YDClient;
|
|
|
+import com.malk.service.dingtalk.DDClient;
|
|
|
+import com.malk.service.dingtalk.DDClient_Contacts;
|
|
|
+import com.malk.service.personnel.PersonnelSyncService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+import java.util.concurrent.Future;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private YDClient ydClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private DDClient ddClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private DDClient_Contacts ddClient_contacts;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PersonnelSyncConf conf;
|
|
|
+
|
|
|
+ private static final String ACTION_CREATE = "CREATE";
|
|
|
+ private static final String ACTION_UPDATE = "UPDATE";
|
|
|
+ private static final String ACTION_MARK_OFF = "MARK_OFF";
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> fullSync() {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ log.info("[PersonnelSync] 全量同步开始");
|
|
|
+
|
|
|
+ List<Map> dingUsers = fetchAllDingUsers();
|
|
|
+ Map<String, Map> dingUserMap = indexByUserid(dingUsers);
|
|
|
+ log.info("[PersonnelSync] 钉钉全量拉取 {} 人 (去重后)", dingUserMap.size());
|
|
|
+
|
|
|
+ Map<String, YidaRecord> yidaMap = fetchAllYidaPersonnel();
|
|
|
+ log.info("[PersonnelSync] 宜搭人员档案 {} 条", yidaMap.size());
|
|
|
+
|
|
|
+ List<Action> actions = diff(dingUserMap, yidaMap);
|
|
|
+ Map<String, Long> actionStats = countActions(actions);
|
|
|
+ log.info("[PersonnelSync] diff 完成: create={}, update={}, markOff={}, skip={}",
|
|
|
+ actionStats.getOrDefault(ACTION_CREATE, 0L),
|
|
|
+ actionStats.getOrDefault(ACTION_UPDATE, 0L),
|
|
|
+ actionStats.getOrDefault(ACTION_MARK_OFF, 0L),
|
|
|
+ (long) dingUserMap.size() + yidaMap.size() - actions.size());
|
|
|
+
|
|
|
+ WriteStats writeStats = concurrentWrite(actions);
|
|
|
+
|
|
|
+ long cost = System.currentTimeMillis() - start;
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("fetched", dingUserMap.size());
|
|
|
+ result.put("yidaExisting", yidaMap.size());
|
|
|
+ result.put("created", writeStats.created.get());
|
|
|
+ result.put("updated", writeStats.updated.get());
|
|
|
+ result.put("markedInactive", writeStats.markedInactive.get());
|
|
|
+ result.put("failed", writeStats.failed.get());
|
|
|
+ result.put("durationMs", cost);
|
|
|
+ log.info("[PersonnelSync] 全量同步完成 {}", result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> dryRun() {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ List<Map> dingUsers = fetchAllDingUsers();
|
|
|
+ Map<String, Map> dingUserMap = indexByUserid(dingUsers);
|
|
|
+ Map<String, YidaRecord> yidaMap = fetchAllYidaPersonnel();
|
|
|
+ List<Action> actions = diff(dingUserMap, yidaMap);
|
|
|
+
|
|
|
+ Map<String, Long> stats = countActions(actions);
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("fetched", dingUserMap.size());
|
|
|
+ result.put("yidaExisting", yidaMap.size());
|
|
|
+ Map<String, Object> actionCounts = new LinkedHashMap<>();
|
|
|
+ actionCounts.put("create", stats.getOrDefault(ACTION_CREATE, 0L));
|
|
|
+ actionCounts.put("update", stats.getOrDefault(ACTION_UPDATE, 0L));
|
|
|
+ actionCounts.put("markOff", stats.getOrDefault(ACTION_MARK_OFF, 0L));
|
|
|
+ result.put("actions", actionCounts);
|
|
|
+ result.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ // 抽样 10 条展示预期动作
|
|
|
+ List<Map<String, Object>> sample = new ArrayList<>();
|
|
|
+ for (int i = 0; i < Math.min(10, actions.size()); i++) {
|
|
|
+ Action a = actions.get(i);
|
|
|
+ Map<String, Object> s = new LinkedHashMap<>();
|
|
|
+ s.put("action", a.type);
|
|
|
+ s.put("userid", a.userid);
|
|
|
+ s.put("formData", a.formData);
|
|
|
+ sample.add(s);
|
|
|
+ }
|
|
|
+ result.put("sample", sample);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> probeDingtalkUsers(int sampleSize) {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ List<Long> deptIds = ddClient_contacts.getDepartmentId_all(token, true);
|
|
|
+ List<Map> users = ddClient_contacts.getAllUserDetails(token, true);
|
|
|
+ Map<String, Map> byUserid = indexByUserid(users);
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("total", byUserid.size());
|
|
|
+ result.put("deptCount", deptIds.size());
|
|
|
+ result.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+
|
|
|
+ List<Map> sample = new ArrayList<>();
|
|
|
+ int n = Math.min(sampleSize <= 0 ? 3 : sampleSize, users.size());
|
|
|
+ for (int i = 0; i < n; i++) sample.add(users.get(i));
|
|
|
+ result.put("sample", sample);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<Map> fetchAllDingUsers() {
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ return ddClient_contacts.getAllUserDetails(token, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map probeSingleUser(String userid) {
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ return ddClient_contacts.getUserInfoById(token, userid);
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public Map<String, Object> probeDiff(String userid) {
|
|
|
+ Map<String, Object> r = new LinkedHashMap<>();
|
|
|
+ // 钉钉
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ Map ding = ddClient_contacts.getUserInfoById(token, userid);
|
|
|
+ r.put("dingUser", ding);
|
|
|
+ // 宜搭
|
|
|
+ DDR_New result = ydClient.queryData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .searchFieldJson("{\"" + conf.getFieldEmployee() + "\":\"" + userid + "\"}")
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
+ List<Map> list = (List<Map>) result.getData();
|
|
|
+ Map<String, Object> yidaFormData = (list != null && !list.isEmpty()) ? (Map<String, Object>) list.get(0).get("formData") : null;
|
|
|
+ r.put("yidaFormData", yidaFormData);
|
|
|
+ // 新 formData
|
|
|
+ Map<String, Object> newData = toYidaFormData(userid, ding, isActive(ding) ? ACTION_UPDATE : ACTION_MARK_OFF);
|
|
|
+ r.put("newFormData", newData);
|
|
|
+ // 字段对比
|
|
|
+ Map<String, Object> diff = new LinkedHashMap<>();
|
|
|
+ if (yidaFormData != null) {
|
|
|
+ for (Map.Entry<String, Object> e : newData.entrySet()) {
|
|
|
+ String f = e.getKey();
|
|
|
+ Object nv = e.getValue();
|
|
|
+ Object ov = yidaFormData.get(f);
|
|
|
+ Object ovId = yidaFormData.get(f + "_id");
|
|
|
+ Map<String, Object> d = new LinkedHashMap<>();
|
|
|
+ d.put("new", nv);
|
|
|
+ d.put("newClass", nv == null ? null : nv.getClass().getSimpleName());
|
|
|
+ d.put("old", ov);
|
|
|
+ d.put("oldClass", ov == null ? null : ov.getClass().getSimpleName());
|
|
|
+ d.put("oldId", ovId);
|
|
|
+ d.put("oldIdClass", ovId == null ? null : ovId.getClass().getSimpleName());
|
|
|
+ diff.put(f, d);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ r.put("diff", diff);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> probeStats() {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ List<Map> users = fetchAllDingUsers();
|
|
|
+ int total = users.size();
|
|
|
+ int active = 0, inactive = 0, emptyDept = 0, hasExtattr = 0, emptyJobNumber = 0;
|
|
|
+ for (Map u : users) {
|
|
|
+ if (isActive(u)) active++; else inactive++;
|
|
|
+ Object dept = u.get("dept_id_list");
|
|
|
+ if (!(dept instanceof List) || ((List) dept).isEmpty()) emptyDept++;
|
|
|
+ Object ext = u.get("extattr");
|
|
|
+ if (ext instanceof Map && !((Map) ext).isEmpty()) hasExtattr++;
|
|
|
+ Object job = u.get("job_number");
|
|
|
+ if (job == null || String.valueOf(job).isEmpty()) emptyJobNumber++;
|
|
|
+ }
|
|
|
+ Map<String, Object> res = new LinkedHashMap<>();
|
|
|
+ res.put("total", total);
|
|
|
+ res.put("active", active);
|
|
|
+ res.put("inactive", inactive);
|
|
|
+ res.put("emptyDeptIdList", emptyDept);
|
|
|
+ res.put("hasExtattr", hasExtattr);
|
|
|
+ res.put("emptyJobNumber", emptyJobNumber);
|
|
|
+ res.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 数据抓取 ====================
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, YidaRecord> fetchAllYidaPersonnel() {
|
|
|
+ Map<String, YidaRecord> yidaMap = new LinkedHashMap<>();
|
|
|
+ int currentPage = 1;
|
|
|
+ long totalCount;
|
|
|
+ do {
|
|
|
+ DDR_New result = ydClient.queryData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .currentPage(currentPage)
|
|
|
+ .pageSize(YDConf.PAGE_SIZE_LIMIT)
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
+ 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 userid = extractEmployeeId(formData, conf.getFieldEmployee());
|
|
|
+ if (userid == null || userid.isEmpty()) continue;
|
|
|
+ YidaRecord rec = new YidaRecord();
|
|
|
+ rec.instanceId = String.valueOf(item.get("formInstanceId"));
|
|
|
+ rec.formData = formData;
|
|
|
+ yidaMap.put(userid, rec);
|
|
|
+ }
|
|
|
+ currentPage++;
|
|
|
+ } while ((long) (currentPage - 1) * YDConf.PAGE_SIZE_LIMIT < totalCount);
|
|
|
+ return yidaMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 差异计算 ====================
|
|
|
+
|
|
|
+ private List<Action> diff(Map<String, Map> dingUserMap, Map<String, YidaRecord> yidaMap) {
|
|
|
+ List<Action> actions = new ArrayList<>();
|
|
|
+
|
|
|
+ for (Map.Entry<String, Map> e : dingUserMap.entrySet()) {
|
|
|
+ String userid = e.getKey();
|
|
|
+ Map ding = e.getValue();
|
|
|
+ YidaRecord yida = yidaMap.get(userid);
|
|
|
+ boolean active = isActive(ding);
|
|
|
+
|
|
|
+ if (yida == null) {
|
|
|
+ Map<String, Object> formData = toYidaFormData(userid, ding, active ? ACTION_CREATE : ACTION_MARK_OFF);
|
|
|
+ actions.add(new Action(ACTION_CREATE, userid, null, formData));
|
|
|
+ } else {
|
|
|
+ String actionType = active ? ACTION_UPDATE : ACTION_MARK_OFF;
|
|
|
+ Map<String, Object> formData = toYidaFormData(userid, ding, actionType);
|
|
|
+ if (isSameAsYida(formData, yida.formData)) {
|
|
|
+ // 幂等跳过 (UPDATE/MARK_OFF 都适用)
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ actions.add(new Action(actionType, userid, yida.instanceId, formData));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 钉钉已无, 宜搭还有 -> 标记离职
|
|
|
+ for (Map.Entry<String, YidaRecord> e : yidaMap.entrySet()) {
|
|
|
+ if (dingUserMap.containsKey(e.getKey())) continue;
|
|
|
+ YidaRecord yida = e.getValue();
|
|
|
+ // 已是离职状态则跳过
|
|
|
+ Object currentStatus = yida.formData.get(conf.getFieldStatus());
|
|
|
+ if (conf.getStatusValueInactive().equals(String.valueOf(currentStatus))) continue;
|
|
|
+ Map<String, Object> formData = new LinkedHashMap<>();
|
|
|
+ formData.put(conf.getFieldStatus(), conf.getStatusValueInactive());
|
|
|
+ actions.add(new Action(ACTION_MARK_OFF, e.getKey(), yida.instanceId, formData));
|
|
|
+ }
|
|
|
+ return actions;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 字段映射 ====================
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Object> toYidaFormData(String userid, Map ding, String action) {
|
|
|
+ Map<String, Object> formData = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ // 人员 (唯一键, 永远写入)
|
|
|
+ formData.put(conf.getFieldEmployee(), Collections.singletonList(userid));
|
|
|
+
|
|
|
+ // 在职状态
|
|
|
+ String statusValue = ACTION_MARK_OFF.equals(action) ? conf.getStatusValueInactive() : conf.getStatusValueActive();
|
|
|
+ formData.put(conf.getFieldStatus(), statusValue);
|
|
|
+
|
|
|
+ // 离职软标记只更新状态字段, 保留其他原值
|
|
|
+ if (ACTION_MARK_OFF.equals(action)) return formData;
|
|
|
+
|
|
|
+ // 员工编号 <- job_number
|
|
|
+ Object jobNumber = ding.get("job_number");
|
|
|
+ if (jobNumber != null && !String.valueOf(jobNumber).isEmpty()) {
|
|
|
+ formData.put(conf.getFieldJobNumber(), String.valueOf(jobNumber));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 员工部门 <- dept_id_list 稳定排序后取首个 (钉钉返回顺序不固定, 避免 diff 抖动)
|
|
|
+ Object deptObj = ding.get("dept_id_list");
|
|
|
+ if (deptObj instanceof List && !((List) deptObj).isEmpty()) {
|
|
|
+ List<Long> sorted = new ArrayList<>();
|
|
|
+ for (Object d : (List) deptObj) {
|
|
|
+ if (d != null) sorted.add(((Number) d).longValue());
|
|
|
+ }
|
|
|
+ Collections.sort(sorted);
|
|
|
+ if (!sorted.isEmpty()) {
|
|
|
+ formData.put(conf.getFieldDepartment(), Collections.singletonList(String.valueOf(sorted.get(0))));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 入职时间 <- hired_date (毫秒时间戳, 需要钉钉花名册权限才返回)
|
|
|
+ if (conf.getFieldHiredDate() != null && !conf.getFieldHiredDate().isEmpty()) {
|
|
|
+ Object hired = ding.get("hired_date");
|
|
|
+ if (hired instanceof Number) {
|
|
|
+ formData.put(conf.getFieldHiredDate(), ((Number) hired).longValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 人员属性 <- extattr.<key> 或 部门白名单兜底
|
|
|
+ String userType = resolveUserType(ding);
|
|
|
+ if (userType != null) {
|
|
|
+ formData.put(conf.getFieldUserType(), userType);
|
|
|
+ }
|
|
|
+
|
|
|
+ return formData;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private String resolveUserType(Map ding) {
|
|
|
+ // 1. 优先读 extattr
|
|
|
+ String key = conf.getExtAttrKeyUserType();
|
|
|
+ if (key != null && !key.isEmpty()) {
|
|
|
+ Object extattrObj = ding.get("extattr");
|
|
|
+ if (extattrObj instanceof Map) {
|
|
|
+ Map extattr = (Map) extattrObj;
|
|
|
+ Object attr = extattr.get(key);
|
|
|
+ String raw = null;
|
|
|
+ if (attr instanceof Map) {
|
|
|
+ Map am = (Map) attr;
|
|
|
+ Object value = am.get("value");
|
|
|
+ if (value == null) value = am.get("text");
|
|
|
+ if (value != null) raw = String.valueOf(value);
|
|
|
+ } else if (attr != null) {
|
|
|
+ raw = String.valueOf(attr);
|
|
|
+ }
|
|
|
+ if (raw != null && !raw.isEmpty()) {
|
|
|
+ if (conf.getExtAttrValueInternal().equals(raw)) return conf.getExtAttrValueInternal();
|
|
|
+ if (conf.getExtAttrValueExternal().equals(raw)) return conf.getExtAttrValueExternal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 2. 兜底: 部门白名单 (外部部门列表 + 默认规则)
|
|
|
+ List<Long> externalDepts = conf.getExternalDeptIds();
|
|
|
+ Object deptObj = ding.get("dept_id_list");
|
|
|
+ if (deptObj instanceof List && !((List) deptObj).isEmpty()) {
|
|
|
+ if (externalDepts != null && !externalDepts.isEmpty()) {
|
|
|
+ boolean isExternal = false;
|
|
|
+ for (Object d : (List) deptObj) {
|
|
|
+ if (d == null) continue;
|
|
|
+ long deptId = ((Number) d).longValue();
|
|
|
+ if (externalDepts.contains(deptId)) { isExternal = true; break; }
|
|
|
+ }
|
|
|
+ return isExternal ? conf.getExtAttrValueExternal() : conf.getExtAttrValueInternal();
|
|
|
+ }
|
|
|
+ if (conf.isFallbackInternalByDefault()) {
|
|
|
+ return conf.getExtAttrValueInternal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isActive(Map ding) {
|
|
|
+ Object active = ding.get("active");
|
|
|
+ if (active instanceof Boolean) return (Boolean) active;
|
|
|
+ if (active == null) return true;
|
|
|
+ return Boolean.parseBoolean(String.valueOf(active));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 比较钉钉侧构造的 formData 与宜搭已有 formData 是否所有字段都相等
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private boolean isSameAsYida(Map<String, Object> newData, Map<String, Object> yidaData) {
|
|
|
+ for (Map.Entry<String, Object> e : newData.entrySet()) {
|
|
|
+ String fieldId = e.getKey();
|
|
|
+ Object newVal = e.getValue();
|
|
|
+ Object oldVal = yidaData.get(fieldId);
|
|
|
+ if (newVal instanceof List) {
|
|
|
+ // 成员/部门类字段: 宜搭返回时用 _id 后缀取 id 列表
|
|
|
+ Object oldIdList = yidaData.get(fieldId + "_id");
|
|
|
+ if (oldIdList != null) oldVal = oldIdList;
|
|
|
+ if (!listEquals((List) newVal, oldVal)) return false;
|
|
|
+ } else {
|
|
|
+ if (!Objects.equals(String.valueOf(newVal), String.valueOf(oldVal))) return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private boolean listEquals(List newList, Object oldObj) {
|
|
|
+ List<String> newStr = new ArrayList<>();
|
|
|
+ for (Object o : newList) newStr.add(String.valueOf(o));
|
|
|
+ List<String> oldStr = new ArrayList<>();
|
|
|
+ if (oldObj instanceof List) {
|
|
|
+ for (Object o : (List) oldObj) oldStr.add(String.valueOf(o));
|
|
|
+ } else if (oldObj != null) {
|
|
|
+ oldStr.add(String.valueOf(oldObj));
|
|
|
+ }
|
|
|
+ if (newStr.size() != oldStr.size()) return false;
|
|
|
+ for (String s : newStr) if (!oldStr.contains(s)) return false;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 写入 ====================
|
|
|
+
|
|
|
+ private WriteStats concurrentWrite(List<Action> actions) {
|
|
|
+ WriteStats stats = new WriteStats();
|
|
|
+ if (actions.isEmpty()) return stats;
|
|
|
+
|
|
|
+ ExecutorService pool = Executors.newFixedThreadPool(Math.max(1, conf.getConcurrency()));
|
|
|
+ List<Future<?>> futures = new ArrayList<>();
|
|
|
+ for (Action a : actions) {
|
|
|
+ futures.add(pool.submit(() -> executeAction(a, stats)));
|
|
|
+ }
|
|
|
+ for (Future<?> f : futures) {
|
|
|
+ try { f.get(); } catch (Exception ex) { log.warn("[PersonnelSync] future 异常", ex); }
|
|
|
+ }
|
|
|
+ pool.shutdown();
|
|
|
+ try { pool.awaitTermination(10, TimeUnit.MINUTES); } catch (InterruptedException ignored) {}
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void executeAction(Action a, WriteStats stats) {
|
|
|
+ int attempt = 0;
|
|
|
+ while (true) {
|
|
|
+ try {
|
|
|
+ if (ACTION_CREATE.equals(a.type)) {
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .formDataJson(JSON.toJSONString(a.formData))
|
|
|
+ .build(), YDConf.FORM_OPERATION.create);
|
|
|
+ stats.created.incrementAndGet();
|
|
|
+ } else {
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .formInstanceId(a.instanceId)
|
|
|
+ .updateFormDataJson(JSON.toJSONString(a.formData))
|
|
|
+ .ignoreEmpty(false)
|
|
|
+ .useLatestVersion(true)
|
|
|
+ .build(), YDConf.FORM_OPERATION.update);
|
|
|
+ if (ACTION_MARK_OFF.equals(a.type)) {
|
|
|
+ stats.markedInactive.incrementAndGet();
|
|
|
+ } else {
|
|
|
+ stats.updated.incrementAndGet();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ } catch (Exception ex) {
|
|
|
+ attempt++;
|
|
|
+ if (attempt > conf.getMaxRetry()) {
|
|
|
+ log.warn("[PersonnelSync] 写入失败 userid={} action={} err={}", a.userid, a.type, ex.getMessage());
|
|
|
+ stats.failed.incrementAndGet();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 工具 ====================
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private String extractEmployeeId(Map<String, Object> formData, String fieldId) {
|
|
|
+ Object raw = formData.get(fieldId + "_id");
|
|
|
+ if (raw == null) raw = formData.get(fieldId);
|
|
|
+ if (raw == null) return null;
|
|
|
+ if (raw instanceof List) {
|
|
|
+ List list = (List) raw;
|
|
|
+ return list.isEmpty() ? null : String.valueOf(list.get(0));
|
|
|
+ }
|
|
|
+ String str = String.valueOf(raw).trim();
|
|
|
+ if (str.startsWith("[") && str.endsWith("]")) {
|
|
|
+ Object parsed = JSON.parse(str);
|
|
|
+ if (parsed instanceof List) {
|
|
|
+ List pl = (List) parsed;
|
|
|
+ return pl.isEmpty() ? null : String.valueOf(pl.get(0));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return str.isEmpty() ? null : str;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Map> indexByUserid(List<Map> users) {
|
|
|
+ Map<String, Map> map = new LinkedHashMap<>();
|
|
|
+ for (Map u : users) {
|
|
|
+ Object uid = u.get("userid");
|
|
|
+ if (uid != null) map.putIfAbsent(String.valueOf(uid), u);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Long> countActions(List<Action> actions) {
|
|
|
+ Map<String, Long> stats = new HashMap<>();
|
|
|
+ for (Action a : actions) {
|
|
|
+ stats.merge(a.type, 1L, Long::sum);
|
|
|
+ }
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部: 数据结构 ====================
|
|
|
+
|
|
|
+ private static class YidaRecord {
|
|
|
+ String instanceId;
|
|
|
+ Map<String, Object> formData;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class Action {
|
|
|
+ final String type;
|
|
|
+ final String userid;
|
|
|
+ final String instanceId;
|
|
|
+ final Map<String, Object> formData;
|
|
|
+
|
|
|
+ Action(String type, String userid, String instanceId, Map<String, Object> formData) {
|
|
|
+ this.type = type;
|
|
|
+ this.userid = userid;
|
|
|
+ this.instanceId = instanceId;
|
|
|
+ this.formData = formData;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class WriteStats {
|
|
|
+ AtomicInteger created = new AtomicInteger();
|
|
|
+ AtomicInteger updated = new AtomicInteger();
|
|
|
+ AtomicInteger markedInactive = new AtomicInteger();
|
|
|
+ AtomicInteger failed = new AtomicInteger();
|
|
|
+ }
|
|
|
+}
|