This commit is contained in:
2026-10-22 12:10:38 +08:00
32 changed files with 1288 additions and 351 deletions
@@ -49,8 +49,8 @@ public interface DeptConvert {
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OACompanyResponse company);
@Mapping(source = "departmentname", target = "deptName")
@Mapping(source = "departmentmark", target = "fullName")
@Mapping(source = "departmentmark", target = "deptName")
@Mapping(source = "departmentname", target = "fullName")
@Mapping(source = "supdepid", target = "parentCode")
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@@ -148,7 +148,7 @@ public interface UserConvert {
/**
* oa人员转本系统用户部门
* @param person
* @param userMap 所有用户手机号与id map
* @param userMap OA登录名与用户id
* @return
*/
default UserDept person2userDept(OAPersonResponse person, Map<String, Long> userMap) {
@@ -162,8 +162,9 @@ public interface UserConvert {
userDept.setStatus(status);
// 排序
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
// 用户id
userDept.setUserId(userMap.get(resolveAccount(person)));
// 用户id,按 OA 登录名关联本批已新增或更新的用户
String oaLoginId = person == null ? null : StringUtils.trimToNull(person.getLoginid());
userDept.setUserId(oaLoginId == null ? null : userMap.get(oaLoginId));
userDept.setSyncTime(new Date());
return userDept;
}
@@ -14,6 +14,7 @@
<result column="dept_code" property="deptCode"/>
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
<result column="contract_code_prefix" property="contractCodePrefix"/>
<result column="contract_initial_serial" property="contractInitialSerial"/>
<result column="short_name" property="shortName"/>
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
<result column="mnemonic_code" property="mnemonicCode"/>
@@ -36,6 +37,7 @@
<result column="dept_code" property="deptCode"/>
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
<result column="contract_code_prefix" property="contractCodePrefix"/>
<result column="contract_initial_serial" property="contractInitialSerial"/>
<result column="short_name" property="shortName"/>
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
<result column="mnemonic_code" property="mnemonicCode"/>
@@ -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
@@ -687,6 +687,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
}
validateDeptCategory(dept);
validateDeptCode(dept, parent);
validateContractInitialSerial(dept);
return saveOrUpdate(dept);
}
@@ -874,6 +875,16 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
}
}
private void validateContractInitialSerial(Dept dept) {
Integer serial = dept.getContractInitialSerial();
if (serial == null) {
return;
}
if (serial < 1 || serial > 999) {
throw new ServiceException("合同初始流水号范围为1到999");
}
}
private void validateDeptCode(Dept dept, Dept parent) {
String deptCode = dept.getDeptCode();
// 组织编码非必填,为空时存 null,避免唯一索引冲突
@@ -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;
}
@@ -761,7 +773,7 @@ public class OASyncServiceImpl implements IOASyncService {
.filter(oaDept -> syncAll
|| rootCompanyId == null
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentmark())))
.map(dept -> deptConvert.dept2dept(dept, companyByOaId))
.toList();
int filteredSkipCount = oaDepts.size() - allParam.size();
@@ -921,8 +933,9 @@ public class OASyncServiceImpl implements IOASyncService {
.eq(Dept::getDeptCategory, deptCategory.getCode())
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, "")
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId,
Dept::getOaId, Dept::getBelongCompanyCode, Dept::getParentCode, Dept::getOaSupSubComId));
.select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName,
Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode,
Dept::getParentCode, Dept::getOaSupSubComId));
Map<String, Dept> byOaId = new HashMap<>();
for (Dept dept : deptList) {
if (StringUtils.isNotBlank(dept.getOaId())) {
@@ -1047,7 +1060,7 @@ public class OASyncServiceImpl implements IOASyncService {
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
* 组织编码已按新规则重算、或父子挂载与库中不一致时,即使 modified 相同也要更新。
* 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。
*/
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
if (existing == null) {
@@ -1066,6 +1079,14 @@ public class OASyncServiceImpl implements IOASyncService {
StringUtils.trimToEmpty(incoming.getParentCode()))) {
return false;
}
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getDeptName()),
StringUtils.trimToEmpty(incoming.getDeptName()))) {
return false;
}
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getFullName()),
StringUtils.trimToEmpty(incoming.getFullName()))) {
return false;
}
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
@@ -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<String> fullSyncLoginIds = ConcurrentHashMap.newKeySet();
private final Set<String> fullSyncWorkcodes = ConcurrentHashMap.newKeySet();
private volatile boolean fullSyncCollecting;
private final IDeptService deptService;
private final IUserService userService;
private final UserConvert userConvert;
@@ -390,83 +409,327 @@ 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<OAPersonResponse> 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<String> oaLoginIds = new LinkedHashSet<>(this.fullSyncLoginIds);
Set<String> workcodes = new LinkedHashSet<>(this.fullSyncWorkcodes);
this.fullSyncLoginIds.clear();
this.fullSyncWorkcodes.clear();
return this.removeOaUsersAbsentFrom(oaLoginIds, workcodes);
}
/**
* 提取仍在职的 OA 人员登录名。离职人员不计入,全量同步结束时会删掉对应本地账号。
*
* @param oaPersons OA人员
* @return 去重后的登录名
*/
public Set<String> extractOaLoginIds(List<OAPersonResponse> oaPersons) {
Set<String> 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<String> extractWorkcodes(List<OAPersonResponse> oaPersons) {
Set<String> 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<String> oaLoginIds, Set<String> workcodes) {
boolean noLoginIds = CollectionUtil.isEmpty(oaLoginIds);
boolean noWorkcodes = CollectionUtil.isEmpty(workcodes);
if (noLoginIds && noWorkcodes) {
ComposeLogUtil.getLastLog().warn("全量同步未收集到OA登录名和工号,跳过删除本地 is_oa=2 人员");
return 0;
}
Set<String> loginIdSet = noLoginIds ? Set.of() : oaLoginIds;
Set<String> workcodeSet = noWorkcodes ? Set.of() : workcodes;
String tenantId = this.resolveTenantId();
List<User> oaUsers = this.userService.list(Wrappers.<User>lambdaQuery()
.select(User::getId, User::getOaLoginId, User::getCode)
.eq(User::getTenantId, tenantId)
.eq(User::getIsOa, CommonConstant.YES));
Long currentUserId = AuthUtil.getUserId();
List<Long> 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<String> oaLoginIds) {
if (CollectionUtil.isEmpty(oaLoginIds)) {
return 0;
}
List<String> oaLoginIdList = oaLoginIds.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
if (oaLoginIdList.isEmpty()) {
return 0;
}
Long currentUserId = AuthUtil.getUserId();
List<Long> removeIds = new ArrayList<>();
for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
List<String> oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size()));
this.userService.list(Wrappers.<User>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<String> workcodes) {
if (CollectionUtil.isEmpty(workcodes)) {
return 0;
}
List<String> workcodeList = workcodes.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
if (workcodeList.isEmpty()) {
return 0;
}
Long currentUserId = AuthUtil.getUserId();
List<Long> removeIds = new ArrayList<>();
for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
List<String> workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size()));
this.userService.list(Wrappers.<User>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<Long> removeIds, String logMessage) {
if (CollectionUtil.isEmpty(removeIds)) {
return 0;
}
for (int fromIndex = 0; fromIndex < removeIds.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
List<Long> removeIdBatch = removeIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, removeIds.size()));
this.userService.deleteLogic(removeIdBatch);
this.userDeptService.remove(Wrappers.<UserDept>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<OAPersonResponse> oaPersons, OaOrgIndex orgIndex) {
if (CollectionUtil.isEmpty(oaPersons)) {
return new PersonSyncCount(0, 0);
return new PersonSyncCount(0, 0, 0);
}
Map<String, OAPersonResponse> uniquePersonMap = new LinkedHashMap<>();
Set<String> resignedLoginIds = new LinkedHashSet<>();
Set<String> resignedWorkcodes = new LinkedHashSet<>();
int skippedCount = 0;
for (OAPersonResponse oaPerson : oaPersons) {
String account = userConvert.resolveAccount(oaPerson);
if (StringUtils.isBlank(account)) {
String syncKey = this.resolveSyncKey(oaPerson);
if (syncKey == null) {
skippedCount++;
continue;
}
uniquePersonMap.putIfAbsent(account, oaPerson);
}
if (uniquePersonMap.isEmpty()) {
ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步");
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<User> users = uniquePersonMap.values().stream()
.map(person -> {
User user = userConvert.person2user(person, defaultPassword);
user.setTenantId(tenantId);
return user;
})
.toList();
List<String> accounts = users.stream()
.map(User::getAccount)
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
List<String> phones = users.stream()
.map(User::getPhone)
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
Map<String, User> existingByAccount = new HashMap<>();
Map<String, User> existingByPhone = new HashMap<>();
userService.list(Wrappers.<User>lambdaQuery()
.and(wrapper -> {
wrapper.in(User::getAccount, accounts);
if (CollectionUtil.isNotEmpty(phones)) {
wrapper.or().in(User::getPhone, phones);
}
})
).stream()
.peek(userService::decryptPhone)
.forEach(user -> {
if (StringUtils.isNotBlank(user.getAccount())) {
existingByAccount.put(user.getAccount(), user);
}
if (StringUtils.isNotBlank(user.getPhone())) {
existingByPhone.put(user.getPhone(), user);
}
});
Set<String> oaLoginIds = new LinkedHashSet<>();
Set<String> 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<String, User> existingByOaLoginId = this.loadUsersByOaLoginId(tenantId, oaLoginIds);
Map<String, User> existingByWorkcode = this.loadUsersByBlankLoginWorkcode(tenantId, workcodes);
List<User> users = new ArrayList<>();
Map<String, Long> userMap = new HashMap<>();
Set<Long> existsUserIds = new HashSet<>();
Set<Long> unchangedUserIds = new HashSet<>();
users.forEach(user -> {
User existingUser = existingByAccount.get(user.getAccount());
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
existingUser = existingByPhone.get(user.getPhone());
for (Map.Entry<String, OAPersonResponse> 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());
if (shouldSkipOaUpdate(existingUser, user)) {
unchangedUserIds.add(existingUser.getId());
}
} else {
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
user.setPostId("-1");
@@ -474,10 +737,11 @@ public class OaUserListSyncHelper {
user.setDataScopeRange(1);
user.setDataLevelRange(1);
user.setIncludeNewCustomer(0);
userService.encryptPhone(user);
}
userMap.put(user.getAccount(), user.getId());
});
this.userService.encryptPhone(user);
users.add(user);
userMap.put(entry.getKey(), user.getId());
}
List<User> addUsers = users.stream()
.filter(user -> !existsUserIds.contains(user.getId()))
@@ -488,46 +752,63 @@ public class OaUserListSyncHelper {
}
List<User> updateUsers = users.stream()
.filter(user -> existsUserIds.contains(user.getId()))
.filter(user -> !unchangedUserIds.contains(user.getId()))
.toList();
if (CollectionUtil.isNotEmpty(updateUsers)) {
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
userService.updateBatchById(updateUsers);
}
if (!unchangedUserIds.isEmpty()) {
ComposeLogUtil.getLastLog().info("跳过未变更用户:{}", unchangedUserIds.size());
}
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
ComposeLogUtil.getLastLog().info("缺少登录名和工号已跳过的人员:{}", skippedCount);
int syncedCount = addUsers.size() + updateUsers.size();
skippedCount += unchangedUserIds.size();
if (userMap.isEmpty()) {
return new PersonSyncCount(0, skippedCount);
return new PersonSyncCount(0, skippedCount, removedCount);
}
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
.in(UserDept::getUserId, userMap.values())
).stream()
List<UserDept> existingUserDeptList = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
.in(UserDept::getUserId, userMap.values()));
Map<String, Long> userDeptMap = existingUserDeptList.stream()
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
List<UserDept> userDeptList = oaPersons.stream()
Map<String, UserDept> currentUserDeptMap = new LinkedHashMap<>();
oaPersons.stream()
.filter(oaPerson -> {
String account = userConvert.resolveAccount(oaPerson);
if (!userMap.containsKey(account)) {
return false;
}
return !unchangedUserIds.contains(userMap.get(account));
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<UserDept> userDeptList = new ArrayList<>(currentUserDeptMap.values());
Set<Long> currentUserDeptIds = userDeptList.stream()
.map(UserDept::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> reboundUserIds = userDeptList.stream()
.map(UserDept::getUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
List<Long> 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<Long> staleIdBatch = staleUserDeptIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, staleUserDeptIds.size()));
userDeptService.removeByIds(staleIdBatch);
}
});
}
List<UserDept> addList = userDeptList.stream()
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
.toList();
@@ -554,7 +835,6 @@ public class OaUserListSyncHelper {
.collect(Collectors.toSet());
String defaultRoleId = getDefaultRoleId();
List<User> updateUserParams = list.stream()
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
.filter(userDeptIds -> {
if (StringUtils.isBlank(userDeptIds.getDeptIds())) {
return false;
@@ -582,22 +862,110 @@ public class OaUserListSyncHelper {
if (CollectionUtil.isNotEmpty(updateUserParams)) {
userService.updateBatchById(updateUserParams);
}
return new PersonSyncCount(syncedCount, skippedCount);
return new PersonSyncCount(syncedCount, skippedCount, removedCount);
}
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
* 按 OA 登录名加载已有 is_oa=2 用户。同一登录名有多条时保留较早的一条,后续同步都更新这一条。
*/
private boolean shouldSkipOaUpdate(User existing, User incoming) {
if (existing == null) {
return false;
private Map<String, User> loadUsersByOaLoginId(String tenantId, Collection<String> oaLoginIds) {
Map<String, User> existingByOaLoginId = new HashMap<>();
if (CollectionUtil.isEmpty(oaLoginIds)) {
return existingByOaLoginId;
}
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
return false;
List<String> oaLoginIdList = oaLoginIds.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
List<String> oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size()));
userService.list(Wrappers.<User>lambdaQuery()
.eq(User::getTenantId, tenantId)
.eq(User::getIsOa, CommonConstant.YES)
.in(User::getOaLoginId, oaLoginIdBatch))
.forEach(user -> {
String oaLoginId = StringUtils.trimToNull(user.getOaLoginId());
if (oaLoginId == null) {
return;
}
User previous = existingByOaLoginId.get(oaLoginId);
if (previous == null) {
existingByOaLoginId.put(oaLoginId, user);
return;
}
if (user.getId() != null && previous.getId() != null && user.getId() < previous.getId()) {
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
oaLoginId, user.getId(), previous.getId());
existingByOaLoginId.put(oaLoginId, user);
return;
}
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
oaLoginId, previous.getId(), user.getId());
});
}
return localModified.equals(remoteModified);
return existingByOaLoginId;
}
/**
* 按工号加载没有登录名的 is_oa=2 用户。同一工号有多条时保留较早的一条。
*/
private Map<String, User> loadUsersByBlankLoginWorkcode(String tenantId, Collection<String> workcodes) {
Map<String, User> existingByWorkcode = new HashMap<>();
if (CollectionUtil.isEmpty(workcodes)) {
return existingByWorkcode;
}
List<String> workcodeList = workcodes.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
List<String> workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size()));
this.userService.list(Wrappers.<User>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;
}
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) {
@@ -638,12 +1006,17 @@ public class OaUserListSyncHelper {
Dept department = orgIndex.findDept(oaPerson);
if (department == null || department.getId() == null) {
ComposeLogUtil.getLastLog().warn(
"人员未匹配到已有部门,请先同步组织架构 account={} departmentid={} departmentname={} subcompanyid1={}",
userConvert.resolveAccount(oaPerson), oaPerson.getDepartmentid(), oaPerson.getDepartmentname(),
"人员未匹配到已有部门,请先同步组织架构 oaLoginId={} departmentid={} departmentname={} subcompanyid1={}",
resolveOaLoginId(oaPerson), oaPerson.getDepartmentid(), oaPerson.getDepartmentname(),
oaPerson.getSubcompanyid1());
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;
}
@@ -693,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() {
@@ -706,6 +1081,10 @@ public class OaUserListSyncHelper {
public int getSkippedCount() {
return skippedCount;
}
public int getRemovedCount() {
return removedCount;
}
}
/**