diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java index d92f413b..2e7e75f3 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java @@ -61,6 +61,9 @@ public class OaPersonSyncPageVO implements Serializable { @Schema(description = "本页跳过条数") private Integer skippedCount; + @Schema(description = "全量同步结束时删除的、不在本次 OA 数据中的 is_oa=2 人员数") + private Integer removedCount; + @Schema(description = "是否已到最后一页") private Boolean finished; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml index 76c059bf..24b2d01f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml @@ -13,7 +13,7 @@ bud.user_id from blade_user_dept bud - left join blade_dept bd on bd.dept_code = bud.dept_code + left join blade_dept bd on bd.id = bud.dept_id and bd.is_deleted = 0 where bd.id is not null and bud.is_deleted = 0 diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 90588373..7978f376 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -277,6 +277,11 @@ public class OASyncServiceImpl implements IOASyncService { }, 10000, ComposeLogUtil.getLastLog()::info).accept(oaPersons::addAll); OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons); OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex); + if (startTime == null) { + oaUserListSyncHelper.removeOaUsersAbsentFrom( + oaUserListSyncHelper.extractOaLoginIds(oaPersons), + oaUserListSyncHelper.extractWorkcodes(oaPersons)); + } CacheUtil.clear(USER_CACHE); CacheUtil.clear(SYS_CACHE); CacheUtil.clear(SYS_CACHE, Boolean.FALSE); @@ -295,9 +300,10 @@ public class OASyncServiceImpl implements IOASyncService { private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size, boolean incremental, String modifiedSince) { int pageNo = current < 1 ? 1 : current; int pageSize = size < 1 ? 20 : Math.min(size, 200); - // 全量同步首页开启原始数据收集,结束后按父级 id 写入 JSON + // 全量同步首页开启原始数据收集,并开始累积登录名,最后一页再删除本地多余人员 if (!incremental && pageNo == 1) { oaSyncRawDumpHelper.beginFullDump(); + oaUserListSyncHelper.beginFullSyncReconcile(); } IncrementalStart incrementalStart = resolvePersonIncrementalStart(incremental, modifiedSince); OAPersonSearch personSearch = buildPersonSearch(incrementalStart.getStartTime()); @@ -314,9 +320,17 @@ public class OASyncServiceImpl implements IOASyncService { long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize(); if (!incremental) { oaSyncRawDumpHelper.collectPersons(oaPersons); + oaUserListSyncHelper.collectFullSyncPersons(oaPersons); } OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons); OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex); + boolean finished = oaPersons.isEmpty() + || oaPersons.size() < pageSize + || (long) pageNo * pageSize >= totalSize; + int removedCount = personSyncCount.getRemovedCount(); + if (!incremental && finished) { + removedCount += oaUserListSyncHelper.finishFullSyncReconcile(); + } CacheUtil.clear(USER_CACHE); CacheUtil.clear(SYS_CACHE); CacheUtil.clear(SYS_CACHE, Boolean.FALSE); @@ -327,17 +341,15 @@ public class OASyncServiceImpl implements IOASyncService { pageVO.setFetchedCount(oaPersons.size()); pageVO.setSyncedCount(personSyncCount.getSyncedCount()); pageVO.setSkippedCount(personSyncCount.getSkippedCount()); + pageVO.setRemovedCount(removedCount); pageVO.setModifiedSince(incrementalStart.getModifiedSince()); - boolean finished = oaPersons.isEmpty() - || oaPersons.size() < pageSize - || (long) pageNo * pageSize >= totalSize; pageVO.setFinished(finished); if (!incremental && finished) { pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishFullDump()); } - ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{}", + ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{},删除{}", incremental ? "incremental" : "full", pageNo, totalSize, - personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount()); + personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount(), removedCount); return pageVO; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java index 46f8af2b..f7382827 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java @@ -30,6 +30,7 @@ import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springblade.common.constant.CommonConstant; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.constant.BladeConstant; import org.springblade.core.tool.utils.DigestUtil; @@ -55,10 +56,12 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD; @@ -76,6 +79,22 @@ public class OaUserListSyncHelper { private static final int MAX_DEPT_ID_LENGTH = 2000; + private static final int OA_LOGIN_ID_BATCH_SIZE = 500; + + /** + * OA 人员状态:5 离职。离职人员不入库,已有 is_oa=2 账号直接删除。 + */ + private static final String OA_PERSON_STATUS_RESIGNED = "5"; + + /** + * 全量同步跨页累积的 OA 登录名。分页接口每页独立事务,删除必须等最后一页再执行。 + */ + private final Set fullSyncLoginIds = ConcurrentHashMap.newKeySet(); + + private final Set fullSyncWorkcodes = ConcurrentHashMap.newKeySet(); + + private volatile boolean fullSyncCollecting; + private final IDeptService deptService; private final IUserService userService; private final UserConvert userConvert; @@ -390,48 +409,325 @@ public class OaUserListSyncHelper { } /** - * 同步人员并绑定到三级部门 + * 全量同步从第一页开始收集 OA 登录名,最后一页再删除本地多余的 is_oa=2 人员。 + */ + public synchronized void beginFullSyncReconcile() { + this.fullSyncLoginIds.clear(); + this.fullSyncWorkcodes.clear(); + this.fullSyncCollecting = true; + } + + /** + * 累积本页 OA 人员登录名,供全量同步结束后比对删除。 + * + * @param oaPersons 本页 OA 人员 + */ + public synchronized void collectFullSyncPersons(List oaPersons) { + if (!this.fullSyncCollecting || CollectionUtil.isEmpty(oaPersons)) { + return; + } + this.fullSyncLoginIds.addAll(this.extractOaLoginIds(oaPersons)); + this.fullSyncWorkcodes.addAll(this.extractWorkcodes(oaPersons)); + } + + /** + * 全量同步结束后,删除登录名不在本次 OA 数据中的 is_oa=2 人员。 + * + * @return 删除人数;未开始收集或登录名集合为空时返回 0,避免误删全部 OA 人员 + */ + public synchronized int finishFullSyncReconcile() { + if (!this.fullSyncCollecting) { + return 0; + } + this.fullSyncCollecting = false; + Set oaLoginIds = new LinkedHashSet<>(this.fullSyncLoginIds); + Set workcodes = new LinkedHashSet<>(this.fullSyncWorkcodes); + this.fullSyncLoginIds.clear(); + this.fullSyncWorkcodes.clear(); + return this.removeOaUsersAbsentFrom(oaLoginIds, workcodes); + } + + /** + * 提取仍在职的 OA 人员登录名。离职人员不计入,全量同步结束时会删掉对应本地账号。 + * + * @param oaPersons OA人员 + * @return 去重后的登录名 + */ + public Set extractOaLoginIds(List oaPersons) { + Set oaLoginIds = new LinkedHashSet<>(); + if (CollectionUtil.isEmpty(oaPersons)) { + return oaLoginIds; + } + for (OAPersonResponse oaPerson : oaPersons) { + String oaLoginId = this.resolveOaLoginId(oaPerson); + if (oaLoginId != null && !this.isOaResigned(oaPerson)) { + oaLoginIds.add(oaLoginId); + } + } + return oaLoginIds; + } + + /** + * 提取仍在职人员的工号。没有登录名时靠工号保留本地账号。 + * + * @param oaPersons OA人员 + * @return 去重后的工号 + */ + public Set extractWorkcodes(List oaPersons) { + Set workcodes = new LinkedHashSet<>(); + if (CollectionUtil.isEmpty(oaPersons)) { + return workcodes; + } + for (OAPersonResponse oaPerson : oaPersons) { + String workcode = this.resolveWorkcode(oaPerson); + if (workcode != null && !this.isOaResigned(oaPerson)) { + workcodes.add(workcode); + } + } + return workcodes; + } + + /** + * 删除当前租户中 is_oa=2、且既不在本次登录名里、也不是无登录名工号人员的账号。 + * 登录名和工号都没收集到时不删除。当前登录账号即使不在同步数据中也保留。 + * + * @param oaLoginIds 本次全量同步出现过的 OA 登录名 + * @param workcodes 本次全量同步出现过的工号 + * @return 删除人数 + */ + public int removeOaUsersAbsentFrom(Set oaLoginIds, Set workcodes) { + boolean noLoginIds = CollectionUtil.isEmpty(oaLoginIds); + boolean noWorkcodes = CollectionUtil.isEmpty(workcodes); + if (noLoginIds && noWorkcodes) { + ComposeLogUtil.getLastLog().warn("全量同步未收集到OA登录名和工号,跳过删除本地 is_oa=2 人员"); + return 0; + } + Set loginIdSet = noLoginIds ? Set.of() : oaLoginIds; + Set workcodeSet = noWorkcodes ? Set.of() : workcodes; + String tenantId = this.resolveTenantId(); + List oaUsers = this.userService.list(Wrappers.lambdaQuery() + .select(User::getId, User::getOaLoginId, User::getCode) + .eq(User::getTenantId, tenantId) + .eq(User::getIsOa, CommonConstant.YES)); + Long currentUserId = AuthUtil.getUserId(); + List removeIds = new ArrayList<>(); + for (User oaUser : oaUsers) { + if (oaUser.getId() == null) { + continue; + } + if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) { + ComposeLogUtil.getLastLog().warn("全量同步跳过删除当前登录账号 userId={}", oaUser.getId()); + continue; + } + String oaLoginId = StringUtils.trimToNull(oaUser.getOaLoginId()); + String workcode = StringUtils.trimToNull(oaUser.getCode()); + boolean keptByLoginId = oaLoginId != null && loginIdSet.contains(oaLoginId); + boolean keptByWorkcode = oaLoginId == null && workcode != null && workcodeSet.contains(workcode); + if (!keptByLoginId && !keptByWorkcode) { + removeIds.add(oaUser.getId()); + } + } + if (removeIds.isEmpty()) { + ComposeLogUtil.getLastLog().info("全量同步无需删除 is_oa=2 人员"); + return 0; + } + return this.deleteOaUsers(removeIds, "全量同步删除不在OA数据中的 is_oa=2 人员"); + } + + /** + * 按 OA 登录名删除已有 is_oa=2 人员。用于离职人员,全量和增量同步都会执行。 + * + * @param tenantId 租户 + * @param oaLoginIds 离职人员登录名 + * @return 删除人数 + */ + private int removeOaUsersByLoginIds(String tenantId, Collection oaLoginIds) { + if (CollectionUtil.isEmpty(oaLoginIds)) { + return 0; + } + List oaLoginIdList = oaLoginIds.stream() + .filter(StringUtils::isNotBlank) + .distinct() + .toList(); + if (oaLoginIdList.isEmpty()) { + return 0; + } + Long currentUserId = AuthUtil.getUserId(); + List removeIds = new ArrayList<>(); + for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size())); + this.userService.list(Wrappers.lambdaQuery() + .select(User::getId, User::getOaLoginId) + .eq(User::getTenantId, tenantId) + .eq(User::getIsOa, CommonConstant.YES) + .in(User::getOaLoginId, oaLoginIdBatch)) + .forEach(oaUser -> { + if (oaUser.getId() == null) { + return; + } + if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) { + ComposeLogUtil.getLastLog().warn("同步跳过删除当前登录账号 userId={}", oaUser.getId()); + return; + } + removeIds.add(oaUser.getId()); + }); + } + return this.deleteOaUsers(removeIds, "同步删除OA离职人员"); + } + + /** + * 删除没有登录名、且工号属于离职人员的 is_oa=2 账号。 + * + * @param tenantId 租户 + * @param workcodes 离职人员工号 + * @return 删除人数 + */ + private int removeOaUsersByBlankLoginWorkcodes(String tenantId, Collection workcodes) { + if (CollectionUtil.isEmpty(workcodes)) { + return 0; + } + List workcodeList = workcodes.stream() + .filter(StringUtils::isNotBlank) + .distinct() + .toList(); + if (workcodeList.isEmpty()) { + return 0; + } + Long currentUserId = AuthUtil.getUserId(); + List removeIds = new ArrayList<>(); + for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size())); + this.userService.list(Wrappers.lambdaQuery() + .select(User::getId, User::getOaLoginId, User::getCode) + .eq(User::getTenantId, tenantId) + .eq(User::getIsOa, CommonConstant.YES) + .in(User::getCode, workcodeBatch) + .and(wrapper -> wrapper.isNull(User::getOaLoginId).or().eq(User::getOaLoginId, ""))) + .forEach(oaUser -> { + if (oaUser.getId() == null) { + return; + } + if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) { + ComposeLogUtil.getLastLog().warn("同步跳过删除当前登录账号 userId={}", oaUser.getId()); + return; + } + removeIds.add(oaUser.getId()); + }); + } + return this.deleteOaUsers(removeIds, "同步删除无登录名的OA离职人员"); + } + + /** + * 逻辑删除用户及其部门关系。 + * + * @param removeIds 用户 id + * @param logMessage 日志说明 + * @return 删除人数 + */ + private int deleteOaUsers(List removeIds, String logMessage) { + if (CollectionUtil.isEmpty(removeIds)) { + return 0; + } + for (int fromIndex = 0; fromIndex < removeIds.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List removeIdBatch = removeIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, removeIds.size())); + this.userService.deleteLogic(removeIdBatch); + this.userDeptService.remove(Wrappers.lambdaQuery().in(UserDept::getUserId, removeIdBatch)); + } + ComposeLogUtil.getLastLog().info("{}:{}", logMessage, removeIds.size()); + return removeIds.size(); + } + + /** + * OA 状态 5 表示离职。 + */ + private boolean isOaResigned(OAPersonResponse oaPerson) { + return oaPerson != null && OA_PERSON_STATUS_RESIGNED.equals(StringUtils.trimToEmpty(oaPerson.getStatus())); + } + + /** + * 同步人员并绑定到三级部门。有登录名按登录名更新,没有登录名但有工号则按工号更新且不回填登录名。离职人员直接删除。 * * @param oaPersons OA人员 * @param orgIndex 组织索引 - * @return 本批同步成功与跳过数量 + * @return 本批同步成功、跳过与删除数量 */ public PersonSyncCount handlePerson(List oaPersons, OaOrgIndex orgIndex) { if (CollectionUtil.isEmpty(oaPersons)) { - return new PersonSyncCount(0, 0); + return new PersonSyncCount(0, 0, 0); } Map uniquePersonMap = new LinkedHashMap<>(); + Set resignedLoginIds = new LinkedHashSet<>(); + Set resignedWorkcodes = new LinkedHashSet<>(); int skippedCount = 0; for (OAPersonResponse oaPerson : oaPersons) { - String oaLoginId = resolveOaLoginId(oaPerson); - if (StringUtils.isBlank(oaLoginId)) { + String syncKey = this.resolveSyncKey(oaPerson); + if (syncKey == null) { skippedCount++; continue; } - uniquePersonMap.put(oaLoginId, oaPerson); - } - if (uniquePersonMap.isEmpty()) { - ComposeLogUtil.getLastLog().warn("OA人员均缺少登录名,跳过人员同步"); - return new PersonSyncCount(0, skippedCount); + String oaLoginId = this.resolveOaLoginId(oaPerson); + String workcode = this.resolveWorkcode(oaPerson); + if (this.isOaResigned(oaPerson)) { + uniquePersonMap.remove(syncKey); + if (oaLoginId != null) { + resignedLoginIds.add(oaLoginId); + } else if (workcode != null) { + resignedWorkcodes.add(workcode); + } + continue; + } + if (oaLoginId != null) { + resignedLoginIds.remove(oaLoginId); + } else if (workcode != null) { + resignedWorkcodes.remove(workcode); + } + uniquePersonMap.put(syncKey, oaPerson); } String tenantId = resolveTenantId(); + int removedCount = this.removeOaUsersByLoginIds(tenantId, resignedLoginIds) + + this.removeOaUsersByBlankLoginWorkcodes(tenantId, resignedWorkcodes); + if (uniquePersonMap.isEmpty()) { + if (resignedLoginIds.isEmpty() && resignedWorkcodes.isEmpty()) { + ComposeLogUtil.getLastLog().warn("OA人员均缺少登录名和工号,跳过人员同步"); + } + return new PersonSyncCount(0, skippedCount, removedCount); + } String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD)); - List users = uniquePersonMap.entrySet().stream() - .map(entry -> { - User user = userConvert.person2user(entry.getValue(), defaultPassword); - user.setTenantId(tenantId); - user.setOaLoginId(entry.getKey()); - user.setAccount(entry.getKey()); - return user; - }) - .toList(); - Map existingByOaLoginId = this.loadUsersByOaLoginId(tenantId, uniquePersonMap.keySet()); + Set oaLoginIds = new LinkedHashSet<>(); + Set workcodes = new LinkedHashSet<>(); + for (OAPersonResponse oaPerson : uniquePersonMap.values()) { + String oaLoginId = this.resolveOaLoginId(oaPerson); + if (oaLoginId != null) { + oaLoginIds.add(oaLoginId); + continue; + } + String workcode = this.resolveWorkcode(oaPerson); + if (workcode != null) { + workcodes.add(workcode); + } + } + Map existingByOaLoginId = this.loadUsersByOaLoginId(tenantId, oaLoginIds); + Map existingByWorkcode = this.loadUsersByBlankLoginWorkcode(tenantId, workcodes); + List users = new ArrayList<>(); Map userMap = new HashMap<>(); Set existsUserIds = new HashSet<>(); - users.forEach(user -> { - User existingUser = existingByOaLoginId.get(user.getOaLoginId()); + for (Map.Entry entry : uniquePersonMap.entrySet()) { + OAPersonResponse oaPerson = entry.getValue(); + String oaLoginId = this.resolveOaLoginId(oaPerson); + String workcode = this.resolveWorkcode(oaPerson); + User user = this.userConvert.person2user(oaPerson, defaultPassword); + user.setTenantId(tenantId); + User existingUser = oaLoginId != null ? existingByOaLoginId.get(oaLoginId) : existingByWorkcode.get(workcode); + if (oaLoginId != null) { + user.setOaLoginId(oaLoginId); + user.setAccount(oaLoginId); + } else { + user.setOaLoginId(null); + user.setAccount(existingUser == null ? workcode : null); + } if (existingUser != null) { user.setId(existingUser.getId()); + // 全局更新策略为 not_null,置空后不会回写密码和登录名 user.setPassword(null); existsUserIds.add(existingUser.getId()); } else { @@ -442,9 +738,10 @@ public class OaUserListSyncHelper { user.setDataLevelRange(1); user.setIncludeNewCustomer(0); } - userService.encryptPhone(user); - userMap.put(user.getOaLoginId(), user.getId()); - }); + this.userService.encryptPhone(user); + users.add(user); + userMap.put(entry.getKey(), user.getId()); + } List addUsers = users.stream() .filter(user -> !existsUserIds.contains(user.getId())) @@ -460,33 +757,58 @@ public class OaUserListSyncHelper { ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size()); userService.updateBatchById(updateUsers); } - ComposeLogUtil.getLastLog().info("缺少OA登录名已跳过的人员:{}", skippedCount); + ComposeLogUtil.getLastLog().info("缺少登录名和工号已跳过的人员:{}", skippedCount); int syncedCount = addUsers.size() + updateUsers.size(); if (userMap.isEmpty()) { - return new PersonSyncCount(0, skippedCount); + return new PersonSyncCount(0, skippedCount, removedCount); } - Map userDeptMap = userDeptService.list(Wrappers.lambdaQuery() - .in(UserDept::getUserId, userMap.values()) - ).stream() + List existingUserDeptList = userDeptService.list(Wrappers.lambdaQuery() + .in(UserDept::getUserId, userMap.values())); + Map userDeptMap = existingUserDeptList.stream() .collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second)); Set existsUserDeptIds = new HashSet<>(userDeptMap.values()); - List userDeptList = oaPersons.stream() + Map currentUserDeptMap = new LinkedHashMap<>(); + oaPersons.stream() .filter(oaPerson -> { - String oaLoginId = resolveOaLoginId(oaPerson); - return StringUtils.isNotBlank(oaLoginId) && userMap.containsKey(oaLoginId); + String syncKey = this.resolveSyncKey(oaPerson); + return syncKey != null && userMap.containsKey(syncKey) && !this.isOaResigned(oaPerson); }) .map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex)) .filter(Objects::nonNull) + .forEach(userDept -> { + String userDeptKey = getUserDeptKey(userDept); + if (userDeptMap.containsKey(userDeptKey)) { + userDept.setId(userDeptMap.get(userDeptKey)); + } else { + UserDept duplicated = currentUserDeptMap.get(userDeptKey); + userDept.setId(duplicated == null + ? DefaultIdentifierGenerator.getInstance().nextId(null) + : duplicated.getId()); + } + currentUserDeptMap.put(userDeptKey, userDept); + }); + List userDeptList = new ArrayList<>(currentUserDeptMap.values()); + Set currentUserDeptIds = userDeptList.stream() + .map(UserDept::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set reboundUserIds = userDeptList.stream() + .map(UserDept::getUserId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + List staleUserDeptIds = existingUserDeptList.stream() + .filter(userDept -> userDept.getId() != null && reboundUserIds.contains(userDept.getUserId())) + .map(UserDept::getId) + .filter(userDeptId -> !currentUserDeptIds.contains(userDeptId)) .toList(); - userDeptList.forEach(userDept -> { - String userDeptKey = getUserDeptKey(userDept); - if (userDeptMap.containsKey(userDeptKey)) { - userDept.setId(userDeptMap.get(userDeptKey)); - } else { - userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); + if (CollectionUtil.isNotEmpty(staleUserDeptIds)) { + ComposeLogUtil.getLastLog().info("移除不再属于当前OA部门的人员关系:{}", staleUserDeptIds.size()); + for (int fromIndex = 0; fromIndex < staleUserDeptIds.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List staleIdBatch = staleUserDeptIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, staleUserDeptIds.size())); + userDeptService.removeByIds(staleIdBatch); } - }); + } List addList = userDeptList.stream() .filter(userDept -> !existsUserDeptIds.contains(userDept.getId())) .toList(); @@ -540,11 +862,11 @@ public class OaUserListSyncHelper { if (CollectionUtil.isNotEmpty(updateUserParams)) { userService.updateBatchById(updateUserParams); } - return new PersonSyncCount(syncedCount, skippedCount); + return new PersonSyncCount(syncedCount, skippedCount, removedCount); } /** - * 按 OA 登录名加载已有用户。同一登录名有多条时保留较早的一条,后续同步都更新这一条。 + * 按 OA 登录名加载已有 is_oa=2 用户。同一登录名有多条时保留较早的一条,后续同步都更新这一条。 */ private Map loadUsersByOaLoginId(String tenantId, Collection oaLoginIds) { Map existingByOaLoginId = new HashMap<>(); @@ -555,11 +877,11 @@ public class OaUserListSyncHelper { .filter(StringUtils::isNotBlank) .distinct() .toList(); - int batchSize = 500; - for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += batchSize) { - List oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + batchSize, oaLoginIdList.size())); + for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size())); userService.list(Wrappers.lambdaQuery() .eq(User::getTenantId, tenantId) + .eq(User::getIsOa, CommonConstant.YES) .in(User::getOaLoginId, oaLoginIdBatch)) .forEach(user -> { String oaLoginId = StringUtils.trimToNull(user.getOaLoginId()); @@ -584,6 +906,39 @@ public class OaUserListSyncHelper { return existingByOaLoginId; } + /** + * 按工号加载没有登录名的 is_oa=2 用户。同一工号有多条时保留较早的一条。 + */ + private Map loadUsersByBlankLoginWorkcode(String tenantId, Collection workcodes) { + Map existingByWorkcode = new HashMap<>(); + if (CollectionUtil.isEmpty(workcodes)) { + return existingByWorkcode; + } + List workcodeList = workcodes.stream() + .filter(StringUtils::isNotBlank) + .distinct() + .toList(); + for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) { + List workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size())); + this.userService.list(Wrappers.lambdaQuery() + .eq(User::getTenantId, tenantId) + .eq(User::getIsOa, CommonConstant.YES) + .in(User::getCode, workcodeBatch) + .and(wrapper -> wrapper.isNull(User::getOaLoginId).or().eq(User::getOaLoginId, ""))) + .forEach(user -> { + String workcode = StringUtils.trimToNull(user.getCode()); + if (workcode == null) { + return; + } + User previous = existingByWorkcode.get(workcode); + if (previous == null || (user.getId() != null && previous.getId() != null && user.getId() < previous.getId())) { + existingByWorkcode.put(workcode, user); + } + }); + } + return existingByWorkcode; + } + private String resolveOaLoginId(OAPersonResponse oaPerson) { if (oaPerson == null) { return null; @@ -591,6 +946,28 @@ public class OaUserListSyncHelper { return StringUtils.trimToNull(oaPerson.getLoginid()); } + private String resolveWorkcode(OAPersonResponse oaPerson) { + if (oaPerson == null) { + return null; + } + return StringUtils.trimToNull(oaPerson.getWorkcode()); + } + + /** + * 有登录名用登录名,没有登录名但有工号用工号。两者都没有则不同步。 + */ + private String resolveSyncKey(OAPersonResponse oaPerson) { + String oaLoginId = this.resolveOaLoginId(oaPerson); + if (oaLoginId != null) { + return "L:" + oaLoginId; + } + String workcode = this.resolveWorkcode(oaPerson); + if (workcode != null) { + return "W:" + workcode; + } + return null; + } + private boolean isPersonBypassCode(String deptCode, DeptCategory deptCategory) { if (StringUtils.isBlank(deptCode)) { return true; @@ -635,6 +1012,11 @@ public class OaUserListSyncHelper { return null; } UserDept userDept = userConvert.person2userDept(oaPerson, userMap); + String syncKey = this.resolveSyncKey(oaPerson); + userDept.setUserId(syncKey == null ? null : userMap.get(syncKey)); + if (StringUtils.isBlank(this.resolveOaLoginId(oaPerson))) { + userDept.setOaLoginId(null); + } if (userDept.getUserId() == null) { return null; } @@ -684,10 +1066,12 @@ public class OaUserListSyncHelper { public static class PersonSyncCount { private final int syncedCount; private final int skippedCount; + private final int removedCount; - public PersonSyncCount(int syncedCount, int skippedCount) { + public PersonSyncCount(int syncedCount, int skippedCount, int removedCount) { this.syncedCount = syncedCount; this.skippedCount = skippedCount; + this.removedCount = removedCount; } public int getSyncedCount() { @@ -697,6 +1081,10 @@ public class OaUserListSyncHelper { public int getSkippedCount() { return skippedCount; } + + public int getRemovedCount() { + return removedCount; + } } /**