|
|
@@ -49,18 +49,22 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
private static final String ACTION_MARK_OFF = "MARK_OFF";
|
|
|
|
|
|
@Override
|
|
|
- public Map<String, Object> fullSync() {
|
|
|
+ public Map<String, Object> fullSync(Integer limitOverride) {
|
|
|
long start = System.currentTimeMillis();
|
|
|
- log.info("[PersonnelSync] 全量同步开始");
|
|
|
+ int limit = effectiveLimit(limitOverride);
|
|
|
+ boolean limited = limit > 0;
|
|
|
+ log.info("[PersonnelSync] 全量同步开始 limit={}", limited ? limit : "none");
|
|
|
|
|
|
List<Map> dingUsers = fetchAllDingUsers();
|
|
|
- Map<String, Map> dingUserMap = indexByUserid(dingUsers);
|
|
|
- log.info("[PersonnelSync] 钉钉全量拉取 {} 人 (去重后)", dingUserMap.size());
|
|
|
+ Map<String, Map> dingUserMap = applyLimit(indexByUserid(dingUsers), limit);
|
|
|
+ enrichManagers(dingUserMap);
|
|
|
+ log.info("[PersonnelSync] 钉钉拉取 {} 人 (去重后{})", dingUserMap.size(), limited ? ", 已截断前" + limit + "条" : "");
|
|
|
|
|
|
Map<String, YidaRecord> yidaMap = fetchAllYidaPersonnel();
|
|
|
log.info("[PersonnelSync] 宜搭人员档案 {} 条", yidaMap.size());
|
|
|
|
|
|
- List<Action> actions = diff(dingUserMap, yidaMap);
|
|
|
+ if (limited) log.info("[PersonnelSync] limit 模式: 本轮跳过 MARK_OFF (拉取非全量)");
|
|
|
+ List<Action> actions = diff(dingUserMap, yidaMap, limited);
|
|
|
Map<String, Long> actionStats = countActions(actions);
|
|
|
log.info("[PersonnelSync] diff 完成: create={}, update={}, markOff={}, skip={}",
|
|
|
actionStats.getOrDefault(ACTION_CREATE, 0L),
|
|
|
@@ -84,12 +88,15 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public Map<String, Object> dryRun() {
|
|
|
+ public Map<String, Object> dryRun(Integer limitOverride) {
|
|
|
long start = System.currentTimeMillis();
|
|
|
+ int limit = effectiveLimit(limitOverride);
|
|
|
+ boolean limited = limit > 0;
|
|
|
List<Map> dingUsers = fetchAllDingUsers();
|
|
|
- Map<String, Map> dingUserMap = indexByUserid(dingUsers);
|
|
|
+ Map<String, Map> dingUserMap = applyLimit(indexByUserid(dingUsers), limit);
|
|
|
+ enrichManagers(dingUserMap);
|
|
|
Map<String, YidaRecord> yidaMap = fetchAllYidaPersonnel();
|
|
|
- List<Action> actions = diff(dingUserMap, yidaMap);
|
|
|
+ List<Action> actions = diff(dingUserMap, yidaMap, limited);
|
|
|
|
|
|
Map<String, Long> stats = countActions(actions);
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
@@ -164,8 +171,8 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
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);
|
|
|
+ // 新 formData (在职判定改为存在性, probe 统一用 UPDATE)
|
|
|
+ Map<String, Object> newData = toYidaFormData(userid, ding, ACTION_UPDATE);
|
|
|
r.put("newFormData", newData);
|
|
|
// 字段对比
|
|
|
Map<String, Object> diff = new LinkedHashMap<>();
|
|
|
@@ -251,39 +258,35 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
|
|
|
// ==================== 内部: 差异计算 ====================
|
|
|
|
|
|
- private List<Action> diff(Map<String, Map> dingUserMap, Map<String, YidaRecord> yidaMap) {
|
|
|
+ private List<Action> diff(Map<String, Map> dingUserMap, Map<String, YidaRecord> yidaMap, boolean skipMarkOff) {
|
|
|
List<Action> actions = new ArrayList<>();
|
|
|
|
|
|
+ // 钉钉里查到的 -> 一律按"在职"写 (active 布尔不再参与)
|
|
|
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));
|
|
|
+ actions.add(new Action(ACTION_CREATE, userid, null, toYidaFormData(userid, ding, ACTION_CREATE)));
|
|
|
} 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));
|
|
|
+ Map<String, Object> formData = toYidaFormData(userid, ding, ACTION_UPDATE);
|
|
|
+ if (isSameAsYida(formData, yida.formData)) continue; // 幂等跳过
|
|
|
+ actions.add(new Action(ACTION_UPDATE, 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));
|
|
|
+ // 宜搭里有、钉钉里没有 -> 标记离职 (limit 模式下拉取非全量, 跳过此步)
|
|
|
+ if (!skipMarkOff) {
|
|
|
+ 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;
|
|
|
}
|
|
|
@@ -304,10 +307,18 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
// 离职软标记只更新状态字段, 保留其他原值
|
|
|
if (ACTION_MARK_OFF.equals(action)) return formData;
|
|
|
|
|
|
- // 员工编号 <- job_number
|
|
|
+ // 员工姓名 <- name (目标表该字段 READONLY, 仍按需求强写覆盖)
|
|
|
+ Object name = ding.get("name");
|
|
|
+ if (notBlank(conf.getFieldName()) && name != null && notBlank(String.valueOf(name))) {
|
|
|
+ formData.put(conf.getFieldName(), String.valueOf(name).trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 员工编号 / 员工工号 <- job_number (员工工号在目标表 READONLY, 按需求强写覆盖)
|
|
|
Object jobNumber = ding.get("job_number");
|
|
|
- if (jobNumber != null && !String.valueOf(jobNumber).isEmpty()) {
|
|
|
- formData.put(conf.getFieldJobNumber(), String.valueOf(jobNumber));
|
|
|
+ if (jobNumber != null && notBlank(String.valueOf(jobNumber))) {
|
|
|
+ String jn = String.valueOf(jobNumber).trim();
|
|
|
+ if (notBlank(conf.getFieldJobNumber())) formData.put(conf.getFieldJobNumber(), jn);
|
|
|
+ if (notBlank(conf.getFieldJobNumber2())) formData.put(conf.getFieldJobNumber2(), jn);
|
|
|
}
|
|
|
|
|
|
// 员工部门 <- dept_id_list 稳定排序后取首个 (钉钉返回顺序不固定, 避免 diff 抖动)
|
|
|
@@ -324,14 +335,26 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
}
|
|
|
|
|
|
// 入职时间 <- hired_date (毫秒时间戳, 需要钉钉花名册权限才返回)
|
|
|
- if (conf.getFieldHiredDate() != null && !conf.getFieldHiredDate().isEmpty()) {
|
|
|
+ if (notBlank(conf.getFieldHiredDate())) {
|
|
|
Object hired = ding.get("hired_date");
|
|
|
if (hired instanceof Number) {
|
|
|
formData.put(conf.getFieldHiredDate(), ((Number) hired).longValue());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 人员属性 <- extattr.<key> 或 部门白名单兜底
|
|
|
+ // Manager <- manager_userid (EmployeeField, 数组格式)
|
|
|
+ Object mgr = ding.get("manager_userid");
|
|
|
+ if (notBlank(conf.getFieldManager()) && mgr != null && notBlank(String.valueOf(mgr))) {
|
|
|
+ formData.put(conf.getFieldManager(), Collections.singletonList(String.valueOf(mgr).trim()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 北森编号 / 归属公司 / 是否CF / 成本中心 <- 钉钉 extattr 自定义字段
|
|
|
+ putExtAttr(formData, ding, conf.getFieldBeisenJobNo(), conf.getExtAttrKeyBeisen());
|
|
|
+ putExtAttr(formData, ding, conf.getFieldCompany(), conf.getExtAttrKeyCompany());
|
|
|
+ putExtAttr(formData, ding, conf.getFieldIsCf(), conf.getExtAttrKeyIsCf());
|
|
|
+ putExtAttr(formData, ding, conf.getFieldCostCenter(), conf.getExtAttrKeyCostCenter());
|
|
|
+
|
|
|
+ // 属性 <- 部门含 externalDeptIds ? 外部 : 内部 (extAttrKeyUserType 留空时走部门白名单)
|
|
|
String userType = resolveUserType(ding);
|
|
|
if (userType != null) {
|
|
|
formData.put(conf.getFieldUserType(), userType);
|
|
|
@@ -340,6 +363,49 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
return formData;
|
|
|
}
|
|
|
|
|
|
+ /** extattr 自定义字段取值并写入 (fieldId 或 extKey 为空 / 取不到值 则不写, 保留宜搭原值) */
|
|
|
+ private void putExtAttr(Map<String, Object> formData, Map ding, String fieldId, String extKey) {
|
|
|
+ if (!notBlank(fieldId) || !notBlank(extKey)) return;
|
|
|
+ String v = readExtAttr(ding, extKey);
|
|
|
+ if (notBlank(v)) formData.put(fieldId, v.trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 读钉钉自定义字段[key]:
|
|
|
+ * - topapi/v2/user/list & user/get 把自定义字段放在 extension (JSON 字符串 {key: value}) 里
|
|
|
+ * - 兼容旧式 extattr (Map, 值可能是纯字符串或 {text, value} 枚举)
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private String readExtAttr(Map ding, String key) {
|
|
|
+ Object ext = ding.get("extension");
|
|
|
+ if (ext instanceof String && !((String) ext).trim().isEmpty()) {
|
|
|
+ try {
|
|
|
+ Object parsed = JSON.parse((String) ext);
|
|
|
+ if (parsed instanceof Map) {
|
|
|
+ Object v = ((Map) parsed).get(key);
|
|
|
+ if (v != null && !String.valueOf(v).trim().isEmpty()) return String.valueOf(v);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ }
|
|
|
+ Object extattr = ding.get("extattr");
|
|
|
+ if (extattr instanceof Map) {
|
|
|
+ Object attr = ((Map) extattr).get(key);
|
|
|
+ if (attr instanceof Map) {
|
|
|
+ Map am = (Map) attr;
|
|
|
+ Object v = am.get("value");
|
|
|
+ if (v == null) v = am.get("text");
|
|
|
+ return v == null ? null : String.valueOf(v);
|
|
|
+ } else if (attr != null && !String.valueOf(attr).trim().isEmpty()) {
|
|
|
+ return String.valueOf(attr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean notBlank(String s) {
|
|
|
+ return s != null && !s.trim().isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
@SuppressWarnings("unchecked")
|
|
|
private String resolveUserType(Map ding) {
|
|
|
// 1. 优先读 extattr
|
|
|
@@ -515,6 +581,46 @@ public class PersonnelSyncServiceImpl implements PersonnelSyncService {
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
+ /** 解析生效的 limit: 入参优先, 否则用配置 limitFirstN; <=0 表示不限 */
|
|
|
+ private int effectiveLimit(Integer override) {
|
|
|
+ if (override != null) return Math.max(0, override);
|
|
|
+ return Math.max(0, conf.getLimitFirstN());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** limit>0 时按 userid 升序取前 N 条 (排序保证幂等可复现) */
|
|
|
+ private Map<String, Map> applyLimit(Map<String, Map> byUserid, int limit) {
|
|
|
+ if (limit <= 0 || byUserid.size() <= limit) return byUserid;
|
|
|
+ List<String> keys = new ArrayList<>(byUserid.keySet());
|
|
|
+ Collections.sort(keys);
|
|
|
+ Map<String, Map> limited = new LinkedHashMap<>();
|
|
|
+ for (int i = 0; i < limit; i++) limited.put(keys.get(i), byUserid.get(keys.get(i)));
|
|
|
+ return limited;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** topapi/v2/user/list 不返回 manager_userid, 按需逐人补 (仅当 fieldManager 已配置; 单人失败不影响整体) */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private void enrichManagers(Map<String, Map> dingUserMap) {
|
|
|
+ if (!notBlank(conf.getFieldManager()) || dingUserMap.isEmpty()) return;
|
|
|
+ String token = ddClient.getAccessToken();
|
|
|
+ int filled = 0;
|
|
|
+ for (Map u : dingUserMap.values()) {
|
|
|
+ if (u.get("manager_userid") != null) { filled++; continue; }
|
|
|
+ Object uid = u.get("userid");
|
|
|
+ if (uid == null) continue;
|
|
|
+ try {
|
|
|
+ Map detail = ddClient_contacts.getUserInfoById(token, String.valueOf(uid));
|
|
|
+ Object mgr = detail == null ? null : detail.get("manager_userid");
|
|
|
+ if (mgr != null && notBlank(String.valueOf(mgr))) {
|
|
|
+ u.put("manager_userid", String.valueOf(mgr).trim());
|
|
|
+ filled++;
|
|
|
+ }
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.warn("[PersonnelSync] 取 manager_userid 失败 userid={} err={}", uid, ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("[PersonnelSync] manager_userid 已就绪 {}/{} 人", filled, dingUserMap.size());
|
|
|
+ }
|
|
|
+
|
|
|
private Map<String, Long> countActions(List<Action> actions) {
|
|
|
Map<String, Long> stats = new HashMap<>();
|
|
|
for (Action a : actions) {
|