✨ 机构OA同步支持双顶级与GWWL分段编码规则

固定集团锚点 id=135/GWWL 与独立外部组织顶级并存;公司/部门编码按上级直拼三位数字生成,并修正类型与孤儿清理逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 10:44:38 +08:00
parent fe78152515
commit 5bd7c8d755
9 changed files with 733 additions and 192 deletions
@@ -4,29 +4,34 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 部门类型枚举
* 组织类型枚举,与业务字典 organization_type 对齐
*
* @author bfhuange
* @date 2024/8/26
*/
@AllArgsConstructor
@Getter
public enum DeptCategory {
/**
* 公司
*/
COMPANY(1, "公司"),
/**
* 部门
*/
DEPT(2, "部门"),
;
/**
* 集团
*/
GROUP(1, "集团"),
/**
* 公司
*/
COMPANY(2, "公司"),
/**
* 部门
*/
DEPT(3, "部门"),
;
/**
* 部门类型编码
*/
private final Integer code;
/**
* 部门类型名称
*/
private final String name;
/**
* 组织类型编码
*/
private final Integer code;
/**
* 组织类型名称
*/
private final String name;
}
@@ -44,94 +44,79 @@ public interface DeptConvert {
@Mapping(source = "subcompanyname", target = "deptName")
@Mapping(source = "subcompanydesc", target = "fullName")
// 父公司id转为父部门编码,要拼接前缀
@Mapping(source = "supsubcomid", target = "parentCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OACompanyResponse company);
@Mapping(source = "departmentname", target = "deptName")
@Mapping(source = "departmentmark", target = "fullName")
// 父部门id转为父部门编码,要拼接前缀
@Mapping(source = "supdepid", target = "parentCode")
// 所属子公司id转为所属公司编码,要拼接前缀
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OADepartmentResponse department);
/**
* oa公司转本系统部门
* @param company
* @return
* oa公司转本系统部门。组织编码由同步服务按 GWWL+三位数字 统一分配。
*/
default Dept company2dept(OACompanyResponse company) {
Dept dept = this.baseConvert(company);
// 部门编码添加前缀
dept.setDeptCode(OAConvertConstant.COMPANY_OA_PREFIX + company.getId());
// 编码稍后按规则生成;此处仅落 OA 主键,供按 oaId 匹配与重建层级
dept.setDeptCode(null);
String parentCode = dept.getParentCode();
if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
// 父部门编码不是0,拼接前缀
dept.setParentCode(OAConvertConstant.COMPANY_OA_PREFIX + parentCode);
// 父部门id后续处理
// 非顶级:parentCode 暂存上级 OA 主键,分配编码后再替换为正式组织编码
dept.setParentCode(parentCode);
dept.setParentId(null);
} else {
// 父部门编码是0,父id也设置为0
dept.setParentId(OAConvertConstant.ROOT_PARENT_ID);
dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
}
// 公司所属公司编码就是他自己的部门编码
dept.setBelongCompanyCode(dept.getDeptCode());
// OA主键与上级OA主键,分批同步结束后用于重建层级
dept.setBelongCompanyCode(null);
dept.setOaId(company.getId());
dept.setOaSupSubComId(company.getSupsubcomid());
// 显示顺序处理
dept.setSort(OAUtils.parseInt(company.getShoworder()));
// 部门类型,公司
dept.setDeptCategory(DeptCategory.COMPANY.getCode());
dept.setSyncTime(new Date());
// 是oa的
dept.setIsOa(CommonConstant.YES);
return dept;
}
/**
* oa部门转本系统部门
* @param oaDept
* @param companyDeptMap 所有公司的部门编码与id map
* @return
* oa部门转本系统部门。组织编码由同步服务按「所属公司编码+三位数字」统一分配。
*
* @param oaDept OA部门
* @param companyByOaId 已同步公司(oaId → 本地公司)
*/
default Dept dept2dept(OADepartmentResponse oaDept, Map<String, Long> companyDeptMap) {
default Dept dept2dept(OADepartmentResponse oaDept, Map<String, Dept> companyByOaId) {
Dept dept = this.baseConvert(oaDept);
// 部门编码添加前缀
dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
// OA主键;上级为根部门时记录所属公司OA主键,否则记录上级部门OA主键
dept.setDeptCode(null);
dept.setOaId(oaDept.getId());
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
dept.setOaSupSubComId(oaDept.getSubcompanyid1());
} else {
dept.setOaSupSubComId(oaDept.getSupdepid());
}
// 部门所属公司编码就是他所属公司编码加前缀
dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
String parentCode = dept.getParentCode();
if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
// 父部门编码不是0,拼接前缀
dept.setParentCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + parentCode);
// 父部门id后续处理
Dept belongCompany = companyByOaId == null ? null : companyByOaId.get(oaDept.getSubcompanyid1());
if (belongCompany != null) {
dept.setBelongCompanyCode(belongCompany.getDeptCode());
dept.setParentId(null);
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
dept.setParentCode(belongCompany.getDeptCode());
dept.setParentId(belongCompany.getId());
} else {
// 上级为部门:parentCode 暂存上级部门 OA 主键,重建层级时再解析
dept.setParentCode(oaDept.getSupdepid());
}
} else {
// 父部门编码是0,设置为公司编码
dept.setParentCode(dept.getBelongCompanyCode());
// 根据父部门编码获取对应的id
dept.setParentId(companyDeptMap.get(dept.getParentCode()));
// 所属公司尚未解析时,暂存 OA 公司主键,后续补齐正式编码
dept.setBelongCompanyCode(oaDept.getSubcompanyid1());
dept.setParentCode(oaDept.getSupdepid());
}
// 显示顺序处理
dept.setSort(OAUtils.parseInt(oaDept.getShoworder()));
// 部门类型,部门
dept.setDeptCategory(DeptCategory.DEPT.getCode());
dept.setSyncTime(new Date());
// 是oa的
dept.setIsOa(CommonConstant.YES);
return dept;
}
@@ -30,6 +30,7 @@ import org.springblade.core.literule.core.RuleComponent;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.Tenant;
import org.springblade.system.pojo.enums.DeptCategory;
import org.springblade.system.rule.context.TenantContext;
import static org.springblade.system.rule.constant.TenantRuleConstant.TENANT_DEPT_RULE;
@@ -54,7 +55,7 @@ public class TenantDeptRule extends RuleComponent {
dept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
dept.setDeptName(tenant.getTenantName());
dept.setFullName(tenant.getTenantName());
dept.setDeptCategory(1);
dept.setDeptCategory(DeptCategory.GROUP.getCode());
dept.setSort(2);
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
@@ -149,12 +149,19 @@ public interface IDeptService extends IService<Dept> {
boolean removeDept(String ids);
/**
* 删除非顶级组织,顶级组织(父级为 0)保留
* 清除非保留顶级外的组织;保留 id=135 集团锚点,以及名称为「外部组织」的顶级
*
* @return 删除数量
*/
int clearNonTopDepts();
/**
* 确保双顶级:id=135 集团(编码 GWWL)+ 独立「外部组织」顶级
*
* @return 集团顶级组织(id=135)
*/
Dept ensureOaSyncRootDept();
/**
* 新增或修改部门
* <p>
@@ -27,6 +27,7 @@ package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.JsonNode;
@@ -389,16 +390,193 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
@Override
@Transactional(rollbackFor = Exception.class)
public int clearNonTopDepts() {
Long nonTopCount = this.count(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getParentId)
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
if (nonTopCount == null || nonTopCount == 0L) {
// 同步前清理:保留集团锚点 id=135,以及「外部组织」顶级;其余删除
Dept groupRoot = this.ensureOaSyncRootDept();
Dept externalRoot = this.ensureExternalOrgTopDept();
Set<Long> keepIds = new HashSet<>();
if (groupRoot != null && groupRoot.getId() != null) {
keepIds.add(groupRoot.getId());
}
if (externalRoot != null && externalRoot.getId() != null) {
keepIds.add(externalRoot.getId());
}
if (keepIds.isEmpty()) {
return 0;
}
this.remove(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getParentId)
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
return nonTopCount.intValue();
Long clearCount = this.count(Wrappers.<Dept>lambdaQuery().notIn(Dept::getId, keepIds));
if (clearCount == null || clearCount == 0L) {
return 0;
}
this.remove(Wrappers.<Dept>lambdaQuery().notIn(Dept::getId, keepIds));
return clearCount.intValue();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Dept ensureOaSyncRootDept() {
this.migrateLegacyOaDeptCategory();
this.removeLegacyOaSyncRoots();
Dept rootDept = this.getById(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
String tenantId = AuthUtil.getTenantId();
if (StringUtil.isBlank(tenantId)) {
tenantId = BladeConstant.ADMIN_TENANT_ID;
}
if (rootDept == null) {
rootDept = new Dept();
rootDept.setId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
rootDept.setTenantId(tenantId);
rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
rootDept.setSort(0);
rootDept.setStatus(DataStatusEnum.ENABLE.getCode());
rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
rootDept.setIsOa(0);
rootDept.setIsPlatformCompany(0);
rootDept.setSyncTime(new Date());
this.save(rootDept);
log.info("已创建 OA 同步集团顶级 id={} name={} code={}",
OAConvertConstant.OA_SYNC_ROOT_DEPT_ID,
OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME,
OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
} else {
boolean wronglyNamedAsExternal = OAConvertConstant.EXTERNAL_ORG_NAME.equals(rootDept.getDeptName());
boolean needUpdate = wronglyNamedAsExternal
|| !OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE.equals(rootDept.getDeptCode())
|| !DeptCategory.GROUP.getCode().equals(rootDept.getDeptCategory())
|| !Objects.equals(BladeConstant.TOP_PARENT_ID, rootDept.getParentId());
if (needUpdate) {
Dept updateParam = new Dept();
updateParam.setId(rootDept.getId());
if (wronglyNamedAsExternal || StringUtil.isBlank(rootDept.getDeptName())) {
updateParam.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
updateParam.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
updateParam.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
}
updateParam.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
updateParam.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
updateParam.setDeptCategory(DeptCategory.GROUP.getCode());
updateParam.setParentId(BladeConstant.TOP_PARENT_ID);
updateParam.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
updateParam.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
this.updateById(updateParam);
rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
log.info("已校正 OA 同步集团顶级 id={} name={} code={} category={}",
rootDept.getId(), rootDept.getDeptName(), rootDept.getDeptCode(), rootDept.getDeptCategory());
}
}
this.ensureExternalOrgTopDept();
return rootDept;
}
/**
* 确保存在独立的「外部组织」顶级(与 id=135 集团并存,仅保留一个同名顶级)
*/
private Dept ensureExternalOrgTopDept() {
List<Dept> externalTops = this.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptName, OAConvertConstant.EXTERNAL_ORG_NAME)
.and(wrapper -> wrapper
.isNull(Dept::getParentId)
.or()
.eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID))
.ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
.orderByAsc(Dept::getId));
if (CollectionUtil.isNotEmpty(externalTops)) {
Dept keep = externalTops.get(0);
if (externalTops.size() > 1) {
List<Long> duplicateIds = externalTops.stream()
.skip(1)
.map(Dept::getId)
.filter(Objects::nonNull)
.toList();
if (CollectionUtil.isNotEmpty(duplicateIds)) {
this.update(Wrappers.<Dept>lambdaUpdate()
.set(Dept::getParentId, keep.getId())
.in(Dept::getParentId, duplicateIds));
this.removeByIds(duplicateIds);
log.info("已合并重复的外部组织顶级,保留 id={},删除{}", keep.getId(), duplicateIds);
}
}
return keep;
}
String tenantId = AuthUtil.getTenantId();
if (StringUtil.isBlank(tenantId)) {
tenantId = BladeConstant.ADMIN_TENANT_ID;
}
Dept externalDept = new Dept();
externalDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
externalDept.setTenantId(tenantId);
externalDept.setParentId(BladeConstant.TOP_PARENT_ID);
externalDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
externalDept.setDeptName(OAConvertConstant.EXTERNAL_ORG_NAME);
externalDept.setFullName(OAConvertConstant.EXTERNAL_ORG_NAME);
externalDept.setShortName(OAConvertConstant.EXTERNAL_ORG_NAME);
externalDept.setDeptCode(null);
externalDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
externalDept.setDeptCategory(DeptCategory.GROUP.getCode());
externalDept.setSort(0);
externalDept.setStatus(DataStatusEnum.ENABLE.getCode());
externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
externalDept.setIsOa(0);
externalDept.setIsPlatformCompany(0);
externalDept.setSyncTime(new Date());
this.save(externalDept);
log.info("已创建外部组织顶级 id={}", externalDept.getId());
return externalDept;
}
/**
* 清理历史伪顶级(OACROOT);同名「桂物物流集团」仅清理非 id=135 的顶级
*/
private void removeLegacyOaSyncRoots() {
List<Dept> legacyRoots = this.list(Wrappers.<Dept>lambdaQuery()
.ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
.and(wrapper -> wrapper
.eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
.or(nameWrapper -> nameWrapper
.eq(Dept::getDeptName, OAConvertConstant.LEGACY_ROOT_DEPT_NAME)
.eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
.and(parentWrapper -> parentWrapper
.isNull(Dept::getParentId)
.or()
.eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID)))));
if (CollectionUtil.isEmpty(legacyRoots)) {
return;
}
Set<Long> legacyIds = legacyRoots.stream().map(Dept::getId).filter(Objects::nonNull).collect(Collectors.toSet());
this.update(Wrappers.<Dept>lambdaUpdate()
.set(Dept::getParentId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
.set(Dept::getParentCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
.in(Dept::getParentId, legacyIds));
this.removeByIds(legacyIds);
log.info("已清理历史伪顶级组织{}条:{}", legacyIds.size(), legacyIds);
}
/**
* 按编码前缀对齐组织类型:OAC→公司、OAD→部门(覆盖历史 1/2 映射)
*/
private void migrateLegacyOaDeptCategory() {
this.update(Wrappers.<Dept>lambdaUpdate()
.set(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
.likeRight(Dept::getDeptCode, OAConvertConstant.DEPARTMENT_OA_PREFIX));
this.update(Wrappers.<Dept>lambdaUpdate()
.set(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.likeRight(Dept::getDeptCode, OAConvertConstant.COMPANY_OA_PREFIX)
.ne(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE));
}
@Override
@@ -650,9 +828,16 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (StringUtil.isBlank(parent.getDeptCode())) {
throw new ServiceException("请选择已配置组织编码的上级组织");
}
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
if (!deptCode.matches(pattern)) {
throw new ServiceException("组织编码格式应为:上级编码-分段数字");
// 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字
String expectedParentCode = parent.getDeptCode();
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
expectedParentCode = OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE;
} else if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
&& StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
expectedParentCode = dept.getBelongCompanyCode();
}
if (!org.springblade.system.util.DeptCodeUtil.matchesChildCode(expectedParentCode, deptCode)) {
throw new ServiceException("组织编码格式应为:" + expectedParentCode + "三位数字(不足补零,中间不加-)");
}
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
@@ -724,8 +909,8 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
log.info("没有记录OA主键的组织,跳过层级重建");
return;
}
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
Map<String, Dept> companyByOaId = this.indexOaCompanies(oaDeptList);
Map<String, Dept> departmentByOaId = this.indexOaDepartments(oaDeptList);
Map<Long, Long> desiredParentIdMap = new LinkedHashMap<>();
Map<Long, String> desiredParentCodeMap = new HashMap<>();
for (Dept dept : oaDeptList) {
@@ -778,21 +963,20 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (CollectionUtil.isEmpty(oaDeptList)) {
return 0;
}
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
Map<String, Dept> companyByOaId = this.indexOaCompanies(oaDeptList);
Map<String, Dept> departmentByOaId = this.indexOaDepartments(oaDeptList);
Set<Long> deleteIds = new HashSet<>();
for (Dept dept : oaDeptList) {
String parentOaId = dept.getOaSupSubComId();
// 上级为空或为 0:视为本地锚点/根级,不作为孤儿删除
// 上级为空或为 0:OA 顶级公司挂本地锚点,不作为孤儿
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
boolean parentExists;
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
parentExists = companyByOaId.containsKey(parentOaId);
} else {
parentExists = departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
}
boolean isCompany = this.isOaCompany(dept);
boolean parentExists = isCompany
? companyByOaId.containsKey(parentOaId)
: departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
// 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留
if (!parentExists) {
deleteIds.add(dept.getId());
}
@@ -813,7 +997,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
Dept parent = DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())
Dept parent = this.isOaCompany(dept)
? companyByOaId.get(parentOaId)
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
if (parent != null && deleteIds.contains(parent.getId())) {
@@ -842,6 +1026,72 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return deleteIds.size();
}
/**
* 索引 OA 公司:同时兼容类型迁移前后的公司记录
*/
private Map<String, Dept> indexOaCompanies(List<Dept> oaDeptList) {
Map<String, Dept> oaIdMap = new HashMap<>();
for (Dept dept : oaDeptList) {
if (!this.isOaCompany(dept) || StringUtil.isBlank(dept.getOaId())) {
continue;
}
Dept previous = oaIdMap.put(dept.getOaId(), dept);
if (previous != null) {
log.warn("OA公司主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
dept.getOaId(), dept.getId(), previous.getId());
}
}
return oaIdMap;
}
private boolean isOaCompany(Dept dept) {
if (dept == null) {
return false;
}
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
return true;
}
if (org.springblade.system.util.DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
return true;
}
return StringUtil.isNotBlank(dept.getDeptCode())
&& dept.getDeptCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX)
&& !OAConvertConstant.LEGACY_ROOT_DEPT_CODE.equals(dept.getDeptCode());
}
private boolean isOaDepartment(Dept dept) {
if (dept == null) {
return false;
}
if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
return true;
}
if (StringUtil.isNotBlank(dept.getBelongCompanyCode())
&& org.springblade.system.util.DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())) {
return true;
}
return StringUtil.isNotBlank(dept.getDeptCode())
&& dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
}
/**
* 索引 OA 部门:兼容类型迁移前后
*/
private Map<String, Dept> indexOaDepartments(List<Dept> oaDeptList) {
Map<String, Dept> oaIdMap = new HashMap<>();
for (Dept dept : oaDeptList) {
if (!this.isOaDepartment(dept) || StringUtil.isBlank(dept.getOaId())) {
continue;
}
Dept previous = oaIdMap.put(dept.getOaId(), dept);
if (previous != null) {
log.warn("OA部门主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
dept.getOaId(), dept.getId(), previous.getId());
}
}
return oaIdMap;
}
/**
* 按机构类型建立 OA 主键索引
*/
@@ -866,19 +1116,22 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
String parentOaId = dept.getOaSupSubComId();
boolean rootParent = StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId);
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
if (this.isOaCompany(dept)) {
if (rootParent) {
return new OaParentLink(OAConvertConstant.ROOT_PARENT_ID, OAConstant.ROOT_COMPANY_ID);
// OA 顶级公司统一挂到本地顶级锚点下
return new OaParentLink(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
}
Dept parent = companyByOaId.get(parentOaId);
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
// 全量整理后仍无上级:跳过重建,交由孤儿清理直接删除
log.warn("未找到上级公司,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
dept.getOaId(), parentOaId, dept.getId());
return null;
}
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, OAConvertConstant.COMPANY_OA_PREFIX, parentOaId));
}
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
&& !(StringUtil.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX))) {
return null;
}
if (rootParent) {
@@ -22,6 +22,7 @@ import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
import org.springblade.system.service.*;
import org.springblade.system.util.DataSyncRecordUtils;
import org.springblade.system.util.DeptCodeUtil;
import org.springblade.thirdparty.oa.constant.OAConstant;
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
import org.springblade.thirdparty.oa.feign.IOAClient;
@@ -73,6 +74,8 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.addLog(log);
// 1. 同步公司和部门并记录
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
// 0. 确保双顶级:id=135 集团(GWWL) + 独立「外部组织」
deptService.ensureOaSyncRootDept();
// 1. 同步公司
this.syncCompany(queryStartTime);
// 公司分批入库后,重建层级并清理无法挂载的公司
@@ -338,6 +341,10 @@ public class OASyncServiceImpl implements IOASyncService {
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
// 首屏确保双顶级:id=135 集团(GWWL) + 独立「外部组织」
if (pageNo == 1) {
deptService.ensureOaSyncRootDept();
}
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.COMPANY);
OACompanySearch companySearch = buildCompanySearch(incrementalStart.getStartTime());
companySearch.setCurPage(pageNo);
@@ -437,10 +444,16 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("解析回传 modifiedSince 失败:{},重新计算", modifiedSince);
}
}
String codePrefix = DeptCategory.COMPANY.equals(deptCategory)
? OAConvertConstant.COMPANY_OA_PREFIX
: OAConvertConstant.DEPARTMENT_OA_PREFIX;
Dept latest = deptService.getOne(Wrappers.<Dept>lambdaQuery()
.select(Dept::getOaModified)
.eq(Dept::getIsOa, YES)
.eq(Dept::getDeptCategory, deptCategory.getCode())
.and(wrapper -> wrapper
.eq(Dept::getDeptCategory, deptCategory.getCode())
.or()
.likeRight(Dept::getDeptCode, codePrefix))
.isNotNull(Dept::getOaModified)
.ne(Dept::getOaModified, "")
.orderByDesc(Dept::getOaModified)
@@ -608,13 +621,8 @@ public class OASyncServiceImpl implements IOASyncService {
if (CollectionUtil.isEmpty(oaCompanies)) {
return OrgSyncCount.empty();
}
// 获取需要的公司名称
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
// 转换数据
List<Dept> allParam = oaCompanies.stream()
// 强制跳过上级公司为 0 的数据
.filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
// 非全量同步时,筛选只要配置的公司
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
.map(deptConvert::company2dept)
.toList();
@@ -623,10 +631,41 @@ public class OASyncServiceImpl implements IOASyncService {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
// 查询已存在公司(含 oaModified),按编码索引
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.COMPANY, null);
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
Map<String, Dept> existingByOaId = loadExistingByOaId(DeptCategory.COMPANY);
Map<String, Dept> codeOwnerMap = loadCompanyCodeOwnerMap();
Set<String> usedCodes = new HashSet<>(codeOwnerMap.keySet());
Map<String, Dept> batchByOaId = new LinkedHashMap<>();
for (Dept dept : allParam) {
this.assignCompanyCode(dept, existingByOaId, usedCodes);
batchByOaId.put(dept.getOaId(), dept);
}
// 上级公司编码:OA 上级为 0 → GWWL;否则取上级公司已分配编码
for (Dept dept : allParam) {
if (OAConstant.ROOT_COMPANY_ID.equals(dept.getOaSupSubComId()) || StringUtils.isBlank(dept.getOaSupSubComId())) {
dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
continue;
}
Dept parent = batchByOaId.get(dept.getOaSupSubComId());
if (parent == null) {
parent = existingByOaId.get(dept.getOaSupSubComId());
}
if (parent != null && StringUtils.isNotBlank(parent.getDeptCode())) {
dept.setParentCode(parent.getDeptCode());
dept.setParentId(parent.getId());
} else {
dept.setParentCode(null);
dept.setParentId(null);
}
}
Map<String, Long> deptIdByCode = new HashMap<>();
existingByOaId.values().forEach(dept -> {
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
});
deptIdByCode.put(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
}
/**
@@ -639,103 +678,239 @@ public class OASyncServiceImpl implements IOASyncService {
if (CollectionUtil.isEmpty(oaDepts)) {
return OrgSyncCount.empty();
}
// 查询所有公司的编码和id的map
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
// 根公司id
Map<String, Dept> companyByOaId = loadCompanyByOaId();
String rootCompanyId = getRootCompanyId();
if (!syncAll && rootCompanyId == null) {
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
}
// 根公司下要同步的部门名称
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
// 转换数据
List<Dept> allParam = oaDepts.stream()
// 全量同步时不过滤;否则:部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
.filter(oaDept -> syncAll
|| rootCompanyId == null
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
.map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
.map(dept -> deptConvert.dept2dept(dept, companyByOaId))
.toList();
int filteredSkipCount = oaDepts.size() - allParam.size();
if (CollectionUtil.isEmpty(allParam)) {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
Set<String> deptCodes = new HashSet<>();
allParam.forEach(dept -> {
if (StringUtils.isNotEmpty(dept.getDeptCode())) {
deptCodes.add(dept.getDeptCode());
Map<String, Dept> existingByOaId = loadExistingByOaId(DeptCategory.DEPT);
Map<String, Integer> nextSerialByCompany = new HashMap<>();
Map<String, Set<String>> usedCodesByCompany = new HashMap<>();
// 预载各公司下已有部门编码,保证分段连续
for (Dept existing : existingByOaId.values()) {
if (StringUtils.isBlank(existing.getBelongCompanyCode()) || StringUtils.isBlank(existing.getDeptCode())) {
continue;
}
if (StringUtils.isNotEmpty(dept.getParentCode())) {
deptCodes.add(dept.getParentCode());
usedCodesByCompany
.computeIfAbsent(existing.getBelongCompanyCode(), key -> new HashSet<>())
.add(existing.getDeptCode());
}
Map<String, Dept> batchByOaId = new LinkedHashMap<>();
for (Dept dept : allParam) {
this.assignDepartmentCode(dept, existingByOaId, companyByOaId, usedCodesByCompany, nextSerialByCompany);
batchByOaId.put(dept.getOaId(), dept);
}
// 解析上级:根部门挂公司;子部门挂上级部门(本批或已存在)
Map<String, Long> deptIdByCode = new HashMap<>();
companyByOaId.values().forEach(company -> {
if (StringUtils.isNotBlank(company.getDeptCode()) && company.getId() != null) {
deptIdByCode.put(company.getDeptCode(), company.getId());
}
});
// 查询数据库已有部门(含 oaModified)
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.DEPT, deptCodes);
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
// 公司编码也放入 map,根部门挂到公司时才能解析父级
companyDeptMap.forEach(deptIdByCode::putIfAbsent);
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
existingByOaId.values().forEach(dept -> {
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
});
for (Dept dept : allParam) {
if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getParentCode())
&& DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())
&& dept.getBelongCompanyCode().equals(dept.getParentCode())) {
continue;
}
String parentOaId = dept.getOaSupSubComId();
Dept belongCompany = companyByOaId.values().stream()
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()))
.findFirst()
.orElse(null);
if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId)
|| StringUtils.equals(parentOaId, belongCompany.getOaId()))) {
dept.setParentCode(belongCompany.getDeptCode());
dept.setParentId(belongCompany.getId());
continue;
}
Dept parentDept = batchByOaId.get(parentOaId);
if (parentDept == null) {
parentDept = existingByOaId.get(parentOaId);
}
if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) {
dept.setParentCode(parentDept.getDeptCode());
dept.setParentId(parentDept.getId());
} else if (belongCompany != null) {
// 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建
dept.setParentCode(belongCompany.getDeptCode());
dept.setParentId(belongCompany.getId());
} else {
dept.setParentCode(null);
dept.setParentId(null);
}
}
return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
}
/**
* 加载已存在组织,按部门编码索引
*
* @param deptCategory 组织类型
* @param deptCodes 限定编码;为空则按类型全量加载
* 公司编码:GWWL+三位数字;已有合规编码则复用
*/
private Map<String, Dept> loadExistingDeptMap(DeptCategory deptCategory, Set<String> deptCodes) {
if (deptCodes != null && deptCodes.isEmpty()) {
return new HashMap<>();
private void assignCompanyCode(Dept dept, Map<String, Dept> existingByOaId, Set<String> usedCodes) {
Dept existing = existingByOaId.get(dept.getOaId());
if (existing != null && DeptCodeUtil.isCompanyCode(existing.getDeptCode())) {
dept.setDeptCode(existing.getDeptCode());
dept.setBelongCompanyCode(existing.getDeptCode());
usedCodes.add(existing.getDeptCode());
return;
}
int serial = DeptCodeUtil.nextSerial(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, usedCodes);
String deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
while (usedCodes.contains(deptCode)) {
serial++;
deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
}
usedCodes.add(deptCode);
dept.setDeptCode(deptCode);
dept.setBelongCompanyCode(deptCode);
}
/**
* 部门编码:所属公司编码+三位数字;已有合规编码则复用
*/
private void assignDepartmentCode(Dept dept, Map<String, Dept> existingByOaId, Map<String, Dept> companyByOaId,
Map<String, Set<String>> usedCodesByCompany, Map<String, Integer> nextSerialByCompany) {
Dept existing = existingByOaId.get(dept.getOaId());
Dept belongCompany = null;
if (StringUtils.isNotBlank(dept.getBelongCompanyCode())) {
belongCompany = companyByOaId.values().stream()
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())
|| StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode()))
.findFirst()
.orElse(null);
}
if (belongCompany == null && existing != null && StringUtils.isNotBlank(existing.getBelongCompanyCode())) {
String belongCode = existing.getBelongCompanyCode();
belongCompany = companyByOaId.values().stream()
.filter(company -> StringUtils.equals(company.getDeptCode(), belongCode))
.findFirst()
.orElse(null);
}
if (belongCompany == null) {
return;
}
final Dept resolvedCompany = belongCompany;
final String companyCode = resolvedCompany.getDeptCode();
dept.setBelongCompanyCode(companyCode);
if (existing != null && DeptCodeUtil.matchesChildCode(companyCode, existing.getDeptCode())) {
dept.setDeptCode(existing.getDeptCode());
usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>()).add(existing.getDeptCode());
return;
}
Set<String> usedCodes = usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>());
int serial = nextSerialByCompany.computeIfAbsent(companyCode,
key -> DeptCodeUtil.nextSerial(companyCode, usedCodes));
String deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
while (usedCodes.contains(deptCode)) {
serial++;
deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
}
nextSerialByCompany.put(companyCode, serial + 1);
usedCodes.add(deptCode);
dept.setDeptCode(deptCode);
}
/**
* 按 oaId 加载已存在组织
*/
private Map<String, Dept> loadExistingByOaId(DeptCategory deptCategory) {
List<Dept> deptList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, deptCategory.getCode())
.in(deptCodes != null, Dept::getDeptCode, deptCodes)
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId));
return deptList.stream()
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
.collect(Collectors.toMap(Dept::getDeptCode, dept -> dept, (a, b) -> b));
}
private Map<String, Long> toDeptIdMap(Map<String, Dept> existingDeptMap) {
Map<String, Long> deptIdByCode = new HashMap<>();
existingDeptMap.forEach((code, dept) -> deptIdByCode.put(code, dept.getId()));
return deptIdByCode;
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, "")
.select(Dept::getId, Dept::getDeptCode, 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())) {
byOaId.put(dept.getOaId(), dept);
}
}
return byOaId;
}
/**
* 持久化同步数据:不存在则新增;存在且 oaModified 不一致才修改
*
* @param allParam 待处理数据
* @param existingDeptMap 已存在组织(同类型)
* @param deptIdByCode 编码→id(含上级公司等)
* @param deptCategory 组织类型
* @param filteredSkipCount 上游过滤跳过数
* @return 同步统计
* 已占用的公司编码(GWWLxxx)
*/
private OrgSyncCount persistSyncedDepts(List<Dept> allParam, Map<String, Dept> existingDeptMap,
private Map<String, Dept> loadCompanyCodeOwnerMap() {
List<Dept> companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.likeRight(Dept::getDeptCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
.select(Dept::getId, Dept::getDeptCode, Dept::getOaId));
Map<String, Dept> codeMap = new HashMap<>();
for (Dept dept : companyList) {
if (DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
codeMap.put(dept.getDeptCode(), dept);
}
}
return codeMap;
}
/**
* 已同步公司:oaId → 公司
*/
private Map<String, Dept> loadCompanyByOaId() {
List<Dept> companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, ""));
Map<String, Dept> byOaId = new HashMap<>();
for (Dept dept : companyList) {
if (StringUtils.isNotBlank(dept.getOaId())) {
byOaId.put(dept.getOaId(), dept);
}
}
return byOaId;
}
/**
* 持久化同步数据:按 oaId 匹配;不存在则新增;存在且 oaModified 不一致才修改
*/
private OrgSyncCount persistSyncedDepts(List<Dept> allParam, Map<String, Dept> existingByOaId,
Map<String, Long> deptIdByCode, DeptCategory deptCategory,
int filteredSkipCount) {
Set<Long> existsDeptIds = existingDeptMap.values().stream()
Set<Long> existsDeptIds = existingByOaId.values().stream()
.map(Dept::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
allParam.forEach(dept -> {
if (deptIdByCode.containsKey(dept.getDeptCode())) {
Dept existing = existingByOaId.get(dept.getOaId());
if (existing != null && existing.getId() != null) {
dept.setId(existing.getId());
} else if (StringUtils.isNotBlank(dept.getDeptCode()) && deptIdByCode.containsKey(dept.getDeptCode())) {
dept.setId(deptIdByCode.get(dept.getDeptCode()));
} else {
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
}
if (StringUtils.isNotBlank(dept.getDeptCode())) {
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
});
// 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
int unresolvedParentCount = 0;
for (Dept dept : allParam) {
if (dept.getParentId() != null) {
continue;
}
Long parentId = deptIdByCode.get(dept.getParentCode());
Long parentId = StringUtils.isNotBlank(dept.getParentCode()) ? deptIdByCode.get(dept.getParentCode()) : null;
if (parentId != null) {
dept.setParentId(parentId);
continue;
@@ -750,8 +925,8 @@ public class OASyncServiceImpl implements IOASyncService {
deptCategory.getName(), unresolvedParentCount);
}
// 父id不为空才新增;已存在的记录即使父id未解析也允许更新(空父id不会覆盖原值)
List<Dept> list = allParam.stream()
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
.toList();
List<Dept> addList = list.stream()
@@ -761,10 +936,9 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
deptService.saveBatch(addList);
}
// 已存在且 oaModified 不一致(或不存在可比对值)才修改
List<Dept> updateList = list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
.filter(dept -> !shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
.filter(dept -> !shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
.toList();
if (CollectionUtil.isNotEmpty(updateList)) {
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
@@ -772,10 +946,11 @@ public class OASyncServiceImpl implements IOASyncService {
}
int unchangedCount = (int) list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
.filter(dept -> shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
.filter(dept -> shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
.count();
List<Dept> notHandledList = allParam.stream()
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
.filter(dept -> StringUtils.isBlank(dept.getDeptCode())
|| (dept.getParentId() == null && !existsDeptIds.contains(dept.getId())))
.toList();
int syncedCount = addList.size() + updateList.size();
int skippedCount = filteredSkipCount + unchangedCount + notHandledList.size();
@@ -786,12 +961,17 @@ public class OASyncServiceImpl implements IOASyncService {
}
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
* 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。
*/
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
if (existing == null) {
return false;
}
if (incoming != null && StringUtils.isNotBlank(incoming.getDeptCode())
&& !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) {
return false;
}
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
@@ -842,6 +1022,8 @@ public class OASyncServiceImpl implements IOASyncService {
if (syncAll) {
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, "")
);
} else {
Set<String> companyNames = this.getCompanyNames();
@@ -851,6 +1033,8 @@ public class OASyncServiceImpl implements IOASyncService {
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.in(Dept::getDeptName, companyNames)
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, "")
);
}
return companyList.stream()
@@ -951,13 +1135,20 @@ public class OASyncServiceImpl implements IOASyncService {
return Collections.emptySet();
}
// 查询父部门编码是根公司的,部门类型的,根公司下部门名称的部门
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getOaId, rootCompanyId)
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
.last("limit 1"), false);
if (rootCompany == null || StringUtils.isBlank(rootCompany.getDeptCode())) {
return Collections.emptySet();
}
return deptService.list(Wrappers.<Dept>lambdaQuery()
.in(Dept::getDeptName, rootCompanyDeptNames)
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
.eq(Dept::getParentCode, OAConvertConstant.COMPANY_OA_PREFIX + rootCompanyId)
.eq(Dept::getParentCode, rootCompany.getDeptCode())
).stream()
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
.map(dept -> dept.getDeptCode().replace(OAConvertConstant.DEPARTMENT_OA_PREFIX, ""))
.map(Dept::getOaId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
}
@@ -980,11 +1171,11 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("未查询到根公司 名称:{}", companyName);
return null;
}
if (StringUtils.isBlank(dept.getDeptCode())) {
if (StringUtils.isBlank(dept.getOaId()) && StringUtils.isBlank(dept.getDeptCode())) {
ComposeLogUtil.getLastLog().warn("根公司编码为空:{}", companyName);
return null;
}
// 部门编码去掉前缀,就是oa的id
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
return StringUtils.isNotBlank(dept.getOaId()) ? dept.getOaId()
: dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
}
}
@@ -86,7 +86,7 @@ public class OaUserListSyncHelper {
private final IRoleService roleService;
/**
* 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下
* 从人员数据提取二级公司、三级部门,并挂到集团顶级(id=135/GWWL) 下
*
* @param oaPersons OA人员
* @return 组织索引
@@ -383,33 +383,7 @@ public class OaUserListSyncHelper {
}
private Dept getOrCreateRootCompany() {
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME)
.last("limit 1"), false);
if (rootCompany != null) {
return rootCompany;
}
rootCompany = new Dept();
rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
rootCompany.setTenantId(resolveTenantId());
rootCompany.setParentId(BladeConstant.TOP_PARENT_ID);
rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME);
rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME);
rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME);
rootCompany.setDeptCode("OACROOT");
rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
rootCompany.setBelongCompanyCode("OACROOT");
rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode());
rootCompany.setSort(0);
rootCompany.setStatus(DataStatusEnum.ENABLE.getCode());
rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED);
rootCompany.setIsOa(YES);
rootCompany.setIsPlatformCompany(0);
rootCompany.setSyncTime(new Date());
deptService.save(rootCompany);
ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME);
return rootCompany;
return deptService.ensureOaSyncRootDept();
}
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
@@ -0,0 +1,97 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.util;
import org.apache.commons.lang3.StringUtils;
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 组织编码工具:上级编码直接拼接固定 3 位分段数字(不足补零,中间不加 "-")
*
* @author Chill
*/
public final class DeptCodeUtil {
/**
* 分段数字位数
*/
public static final int SEGMENT_WIDTH = 3;
private DeptCodeUtil() {
}
/**
* 生成子级编码:上级编码 + 三位数字
*/
public static String formatChildCode(String parentCode, int serial) {
return parentCode + String.format("%0" + SEGMENT_WIDTH + "d", serial);
}
/**
* 是否符合「上级编码 + 三位数字」格式(恰好多出 3 位数字)
*/
public static boolean matchesChildCode(String parentCode, String deptCode) {
if (StringUtils.isBlank(parentCode) || StringUtils.isBlank(deptCode)) {
return false;
}
return deptCode.matches("^" + Pattern.quote(parentCode) + "\\d{" + SEGMENT_WIDTH + "}$");
}
/**
* 是否为公司编码(GWWL + 三位数字)
*/
public static boolean isCompanyCode(String deptCode) {
return matchesChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, deptCode);
}
/**
* 在已有编码中取下一分段序号(从 1 起)
*/
public static int nextSerial(String parentCode, Collection<String> existingCodes) {
if (StringUtils.isBlank(parentCode)) {
return 1;
}
Pattern pattern = Pattern.compile("^" + Pattern.quote(parentCode) + "(\\d{" + SEGMENT_WIDTH + "})$");
int maxSerial = 0;
if (existingCodes != null) {
for (String code : existingCodes) {
if (StringUtils.isBlank(code)) {
continue;
}
Matcher matcher = pattern.matcher(code.trim());
if (!matcher.matches()) {
continue;
}
maxSerial = Math.max(maxSerial, Integer.parseInt(matcher.group(1)));
}
}
return maxSerial + 1;
}
}
@@ -31,7 +31,35 @@ public class OAConvertConstant {
*/
public static final Long ROOT_PARENT_ID = 0L;
/**
* 同步人员时挂载的顶级组织名称
* OA 同步挂载的本地顶级组织主键(集团)
*/
public static final String ROOT_COMPANY_NAME = "桂物物流集团";
public static final Long OA_SYNC_ROOT_DEPT_ID = 135L;
/**
* OA 同步挂载的本地顶级组织默认名称(集团;已存在时不覆盖原名称,除非误写成外部组织)
*/
public static final String OA_SYNC_ROOT_DEPT_NAME = "桂物物流集团";
/**
* OA 同步挂载的本地顶级组织编码(集团)
*/
public static final String OA_SYNC_ROOT_DEPT_CODE = "GWWL";
/**
* 承运商等业务使用的外部组织顶级名称(与集团顶级并存)
*/
public static final String EXTERNAL_ORG_NAME = "外部组织";
/**
* 组织编码分段数字位数(不足补零)
*/
public static final int DEPT_CODE_SEGMENT_WIDTH = 3;
/**
* 历史人员同步创建的伪顶级编码,同步时需清理
*/
public static final String LEGACY_ROOT_DEPT_CODE = "OACROOT";
/**
* 历史人员同步创建的伪顶级名称(与集团同名时仅清理非 id=135 的记录)
*/
public static final String LEGACY_ROOT_DEPT_NAME = "桂物物流集团";
/**
* 同步人员时挂载的顶级组织名称(兼容旧引用)
*/
public static final String ROOT_COMPANY_NAME = OA_SYNC_ROOT_DEPT_NAME;
}