|
|
@@ -21,10 +21,12 @@ import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.Collections;
|
|
|
import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
import java.util.concurrent.Executors;
|
|
|
import java.util.concurrent.Future;
|
|
|
@@ -54,14 +56,18 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
|
|
|
@PostConstruct
|
|
|
private void initRateLimiters() {
|
|
|
- ddRateLimiter = RateLimiter.create(conf.getDdApiQps());
|
|
|
- yidaRateLimiter = RateLimiter.create(conf.getYidaApiQps());
|
|
|
+ ddRateLimiter = RateLimiter.create(DD_API_QPS);
|
|
|
+ yidaRateLimiter = RateLimiter.create(YIDA_API_QPS);
|
|
|
}
|
|
|
|
|
|
private static final String ACTION_CREATE = "CREATE";
|
|
|
private static final String ACTION_UPDATE = "UPDATE";
|
|
|
private static final String ACTION_MARK_OFF = "MARK_OFF";
|
|
|
|
|
|
+ // 钉钉官方 QPS 上限 60, 留 10 余量; 宜搭写接口 10 并发需整体压低
|
|
|
+ private static final double DD_API_QPS = 50.0;
|
|
|
+ private static final double YIDA_API_QPS = 30.0;
|
|
|
+
|
|
|
@Override
|
|
|
public Map<String, Object> fullSync(Integer limitOverride) {
|
|
|
long start = System.currentTimeMillis();
|
|
|
@@ -159,7 +165,34 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
@Override
|
|
|
public List<Map> fetchAllDingUsers() {
|
|
|
String token = ddClient.getAccessToken();
|
|
|
- return ddClient_contacts.getAllUserDetails(token, true);
|
|
|
+ List<Map> users = new ArrayList<>(ddClient_contacts.getAllUserDetails(token, true));
|
|
|
+ // fixme 外部部门(如 1066052389)不在组织树根部门 1 的递归子树内,getAllUserDetails 拉不到 → 显式补抓
|
|
|
+ List<Long> extDepts = conf.getExternalDeptIds();
|
|
|
+ if (extDepts != null && !extDepts.isEmpty()) {
|
|
|
+ Set<String> seen = new HashSet<>();
|
|
|
+ for (Map u : users) {
|
|
|
+ Object uid = u.get("userid");
|
|
|
+ if (uid != null) seen.add(String.valueOf(uid));
|
|
|
+ }
|
|
|
+ for (Long dept : extDepts) {
|
|
|
+ if (dept == null) continue;
|
|
|
+ try {
|
|
|
+ List<Map> ext = ddClient_contacts.listDepartmentUserDetail_all(token, dept);
|
|
|
+ int added = 0;
|
|
|
+ for (Map u : ext) {
|
|
|
+ Object uid = u.get("userid");
|
|
|
+ if (uid != null && seen.add(String.valueOf(uid))) {
|
|
|
+ users.add(u);
|
|
|
+ added++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("[PersonnelSync] 补抓外部部门 dept={} 新增 {} 人 (该部门共 {})", dept, added, ext.size());
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.warn("[PersonnelSync] 抓外部部门失败 dept={} err={}", dept, ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return users;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -210,6 +243,267 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
return r;
|
|
|
}
|
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> probeYidaByUserid(String userid) {
|
|
|
+ Map<String, Object> r = new LinkedHashMap<>();
|
|
|
+ r.put("userid", userid);
|
|
|
+ DDR_New result = ydClient.queryData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .searchFieldJson("{\"" + conf.getFieldEmployee() + "\":\"" + userid + "\"}")
|
|
|
+ .pageSize(YDConf.PAGE_SIZE_LIMIT)
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
+ List<Map> list = (List<Map>) result.getData();
|
|
|
+ r.put("count", list == null ? 0 : list.size());
|
|
|
+ List<Map<String, Object>> records = new ArrayList<>();
|
|
|
+ if (list != null) {
|
|
|
+ for (Map item : list) {
|
|
|
+ Map<String, Object> rec = new LinkedHashMap<>();
|
|
|
+ rec.put("instanceId", item.get("formInstanceId"));
|
|
|
+ rec.put("createTime", item.get("gmtCreate"));
|
|
|
+ rec.put("modifyTime", item.get("gmtModified"));
|
|
|
+ rec.put("creator", item.get("creator"));
|
|
|
+ rec.put("formData", item.get("formData"));
|
|
|
+ records.add(rec);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ r.put("records", records);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> probeYidaDuplicates() {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ Map<String, List<Map<String, Object>>> byUserid = new LinkedHashMap<>();
|
|
|
+ List<Map<String, Object>> emptyEmployee = new ArrayList<>();
|
|
|
+ int currentPage = 1;
|
|
|
+ long totalCount;
|
|
|
+ int total = 0;
|
|
|
+ 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) {
|
|
|
+ total++;
|
|
|
+ Map<String, Object> formData = (Map<String, Object>) item.get("formData");
|
|
|
+ String uid = formData == null ? null : extractEmployeeId(formData, conf.getFieldEmployee());
|
|
|
+ Map<String, Object> brief = new LinkedHashMap<>();
|
|
|
+ brief.put("instanceId", item.get("formInstanceId"));
|
|
|
+ brief.put("createTime", item.get("gmtCreate"));
|
|
|
+ brief.put("modifyTime", item.get("gmtModified"));
|
|
|
+ if (formData != null) {
|
|
|
+ brief.put("name", formData.get(conf.getFieldName()));
|
|
|
+ brief.put("status", formData.get(conf.getFieldStatus()));
|
|
|
+ brief.put("userType", formData.get(conf.getFieldUserType()));
|
|
|
+ }
|
|
|
+ if (uid == null || uid.isEmpty()) {
|
|
|
+ emptyEmployee.add(brief);
|
|
|
+ } else {
|
|
|
+ byUserid.computeIfAbsent(uid, k -> new ArrayList<>()).add(brief);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ currentPage++;
|
|
|
+ } while ((long) (currentPage - 1) * YDConf.PAGE_SIZE_LIMIT < totalCount);
|
|
|
+
|
|
|
+ List<Map<String, Object>> dups = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, List<Map<String, Object>>> e : byUserid.entrySet()) {
|
|
|
+ if (e.getValue().size() > 1) {
|
|
|
+ Map<String, Object> g = new LinkedHashMap<>();
|
|
|
+ g.put("userid", e.getKey());
|
|
|
+ g.put("count", e.getValue().size());
|
|
|
+ g.put("records", e.getValue());
|
|
|
+ dups.add(g);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<String, Object> r = new LinkedHashMap<>();
|
|
|
+ r.put("totalRecords", total);
|
|
|
+ r.put("uniqueUseridCount", byUserid.size());
|
|
|
+ r.put("emptyEmployeeCount", emptyEmployee.size());
|
|
|
+ r.put("duplicateGroupCount", dups.size());
|
|
|
+ r.put("duplicateGroups", dups);
|
|
|
+ r.put("emptyEmployeeRecords", emptyEmployee);
|
|
|
+ r.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ log.info("[PersonnelSync] probeYidaDuplicates total={} unique={} empty={} duplicateGroups={}",
|
|
|
+ total, byUserid.size(), emptyEmployee.size(), dups.size());
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一次性清理重复条(本期任务):
|
|
|
+ * - jingzhao: 直接删后建条 FHC66571O325JF0LK0EB54NKPOZV3YWZQ98OMOG7
|
|
|
+ * - 626967876(吴超): 把同步条 4XC66W81HA43...的钉钉字段 merge 到业务条 XRD66E7127A5... 再删同步条
|
|
|
+ * 钉钉字段以同步条为准:属性/员工编号/归属公司/Manager/是否CF/成本中心/入职时间/在职状态
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> cleanupKnownDuplicatesOnce(boolean dryRun) {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ Map<String, Object> r = new LinkedHashMap<>();
|
|
|
+ r.put("dryRun", dryRun);
|
|
|
+ List<Map<String, Object>> actions = new ArrayList<>();
|
|
|
+
|
|
|
+ // ===== Group A: jingzhao =====
|
|
|
+ String jingzhaoDeleteId = "FINST-FHC66571O325JF0LK0EB54NKPOZV3YWZQ98OMOG7";
|
|
|
+ Map<String, Object> aDel = new LinkedHashMap<>();
|
|
|
+ aDel.put("type", "DELETE");
|
|
|
+ aDel.put("userid", "jingzhao");
|
|
|
+ aDel.put("instanceId", jingzhaoDeleteId);
|
|
|
+ aDel.put("reason", "保留早建条 4XC66W81C9Z40Q...; 删除后建空字段重复条");
|
|
|
+ if (!dryRun) {
|
|
|
+ try {
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .formInstanceId(jingzhaoDeleteId)
|
|
|
+ .build(), YDConf.FORM_OPERATION.delete);
|
|
|
+ aDel.put("ok", true);
|
|
|
+ } catch (Exception ex) {
|
|
|
+ aDel.put("ok", false);
|
|
|
+ aDel.put("err", ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ actions.add(aDel);
|
|
|
+
|
|
|
+ // ===== Group B: 626967876 (吴超) merge then delete =====
|
|
|
+ String wuchaoKeepId = "FINST-XRD66E7127A51G39GBQX1AG83I8525SWNWKOMCZB";
|
|
|
+ String wuchaoDeleteId = "FINST-4XC66W81HA43S9G1H3ELL664BSEN23DTEE4NMS703";
|
|
|
+
|
|
|
+ Map<String, Object> mergeFields = new LinkedHashMap<>();
|
|
|
+ // 员工编号 textField_mh8xhqc1 = userid 字符串(同步代码 v1.2 后规范)
|
|
|
+ mergeFields.put(conf.getFieldJobNumber(), "626967876");
|
|
|
+ // 属性: 内部 (钉钉 dept_id=[1057430958] 内部部门, 同步条已写"内部")
|
|
|
+ mergeFields.put(conf.getFieldUserType(), conf.getExtAttrValueInternal());
|
|
|
+ // 归属公司: 上海 (同步条值)
|
|
|
+ mergeFields.put(conf.getFieldCompany(), "上海");
|
|
|
+ // Manager: Kevin Xu (640891109) 数组格式
|
|
|
+ mergeFields.put(conf.getFieldManager(), Collections.singletonList("640891109"));
|
|
|
+ // 是否CF: false (同步条值)
|
|
|
+ mergeFields.put(conf.getFieldIsCf(), "false");
|
|
|
+ // 成本中心: 35 (同步条值)
|
|
|
+ mergeFields.put(conf.getFieldCostCenter(), "35");
|
|
|
+ // 入职时间: 1716134400000 (同步条值)
|
|
|
+ mergeFields.put(conf.getFieldHiredDate(), 1716134400000L);
|
|
|
+ // 在职状态: 在职
|
|
|
+ mergeFields.put(conf.getFieldStatus(), conf.getStatusValueActive());
|
|
|
+ // 部门: 1057430958 (钉钉同步主部门)
|
|
|
+ mergeFields.put(conf.getFieldDepartment(), Collections.singletonList("1057430958"));
|
|
|
+
|
|
|
+ Map<String, Object> bUpd = new LinkedHashMap<>();
|
|
|
+ bUpd.put("type", "UPDATE_MERGE");
|
|
|
+ bUpd.put("userid", "626967876");
|
|
|
+ bUpd.put("instanceId", wuchaoKeepId);
|
|
|
+ bUpd.put("mergeFields", mergeFields);
|
|
|
+ bUpd.put("note", "钉钉字段贴到业务条,业务字段(客户类型/合同号/numberField/北森编号)保留不动");
|
|
|
+ if (!dryRun) {
|
|
|
+ try {
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .formInstanceId(wuchaoKeepId)
|
|
|
+ .updateFormDataJson(JSON.toJSONString(mergeFields))
|
|
|
+ .ignoreEmpty(false)
|
|
|
+ .useLatestVersion(true)
|
|
|
+ .build(), YDConf.FORM_OPERATION.update);
|
|
|
+ bUpd.put("ok", true);
|
|
|
+ } catch (Exception ex) {
|
|
|
+ bUpd.put("ok", false);
|
|
|
+ bUpd.put("err", ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ actions.add(bUpd);
|
|
|
+
|
|
|
+ Map<String, Object> bDel = new LinkedHashMap<>();
|
|
|
+ bDel.put("type", "DELETE");
|
|
|
+ bDel.put("userid", "626967876");
|
|
|
+ bDel.put("instanceId", wuchaoDeleteId);
|
|
|
+ bDel.put("reason", "同步条信息已 merge 到业务条 XRD66E7127A5...");
|
|
|
+ if (!dryRun) {
|
|
|
+ try {
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(conf.getYidaAppType())
|
|
|
+ .systemToken(conf.getYidaSystemToken())
|
|
|
+ .formUuid(conf.getFormUuidPersonnel())
|
|
|
+ .formInstanceId(wuchaoDeleteId)
|
|
|
+ .build(), YDConf.FORM_OPERATION.delete);
|
|
|
+ bDel.put("ok", true);
|
|
|
+ } catch (Exception ex) {
|
|
|
+ bDel.put("ok", false);
|
|
|
+ bDel.put("err", ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ actions.add(bDel);
|
|
|
+
|
|
|
+ r.put("actions", actions);
|
|
|
+ r.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ log.info("[PersonnelSync] cleanupKnownDuplicatesOnce dryRun={} result={}", dryRun, r);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> syncSingle(String userid) {
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("userid", userid);
|
|
|
+ // 1. 钉钉取这个人
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ Map ding = ddClient_contacts.getUserInfoById(token, userid);
|
|
|
+ if (ding == null || ding.get("userid") == null) {
|
|
|
+ result.put("action", "NOT_FOUND");
|
|
|
+ result.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ log.warn("[PersonnelSync] syncSingle 钉钉查无此人 userid={}", userid);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ // 2. 宜搭按 userid 查现有记录
|
|
|
+ DDR_New q = 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>) q.getData();
|
|
|
+ String instanceId = null;
|
|
|
+ Map<String, Object> existFormData = null;
|
|
|
+ if (list != null && !list.isEmpty()) {
|
|
|
+ instanceId = String.valueOf(list.get(0).get("formInstanceId"));
|
|
|
+ existFormData = (Map<String, Object>) list.get(0).get("formData");
|
|
|
+ }
|
|
|
+ // 3. 构造 formData 并选 action
|
|
|
+ String action = (instanceId == null) ? ACTION_CREATE : ACTION_UPDATE;
|
|
|
+ Map<String, Object> formData = toYidaFormData(userid, ding, action);
|
|
|
+ if (ACTION_UPDATE.equals(action) && isSameAsYida(formData, existFormData)) {
|
|
|
+ result.put("action", "SKIP");
|
|
|
+ result.put("instanceId", instanceId);
|
|
|
+ result.put("formData", formData);
|
|
|
+ result.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ log.info("[PersonnelSync] syncSingle 幂等跳过 userid={}", userid);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ // 4. 写入 (复用 executeAction 的指数退避 + 限速)
|
|
|
+ WriteStats stats = new WriteStats();
|
|
|
+ executeAction(new Action(action, userid, instanceId, formData), stats);
|
|
|
+ result.put("action", action);
|
|
|
+ result.put("instanceId", instanceId);
|
|
|
+ result.put("formData", formData);
|
|
|
+ result.put("created", stats.created.get());
|
|
|
+ result.put("updated", stats.updated.get());
|
|
|
+ result.put("failed", stats.failed.get());
|
|
|
+ result.put("durationMs", System.currentTimeMillis() - start);
|
|
|
+ log.info("[PersonnelSync] syncSingle 完成 userid={} action={} result={}", userid, action, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public Map<String, Object> probeStats() {
|
|
|
long start = System.currentTimeMillis();
|
|
|
@@ -263,6 +557,16 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
YidaRecord rec = new YidaRecord();
|
|
|
rec.instanceId = String.valueOf(item.get("formInstanceId"));
|
|
|
rec.formData = formData;
|
|
|
+ // fixme 同 userid 重复条防御: 保留早建条 (按 instanceId 字典序较小者) 并 WARN, 避免静默覆盖
|
|
|
+ YidaRecord exist = yidaMap.get(userid);
|
|
|
+ if (exist != null) {
|
|
|
+ log.warn("[PersonnelSync] 宜搭检测到重复 userid={} 已扫到 instanceId={} 又扫到 instanceId={}",
|
|
|
+ userid, exist.instanceId, rec.instanceId);
|
|
|
+ if (exist.instanceId != null && rec.instanceId != null
|
|
|
+ && rec.instanceId.compareTo(exist.instanceId) > 0) {
|
|
|
+ continue; // 已有的更早建,保留它
|
|
|
+ }
|
|
|
+ }
|
|
|
yidaMap.put(userid, rec);
|
|
|
}
|
|
|
currentPage++;
|