diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/enums/DeptCategory.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/enums/DeptCategory.java index cf21c31b..86e522e3 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/enums/DeptCategory.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/enums/DeptCategory.java @@ -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; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java index f7c1829b..ba5bdf43 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java @@ -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 companyDeptMap) { + default Dept dept2dept(OADepartmentResponse oaDept, Map 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; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java b/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java index ba893bfd..07a0ca8f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java @@ -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); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java index cee1baf1..b2afe5f0 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java @@ -149,12 +149,19 @@ public interface IDeptService extends IService { boolean removeDept(String ids); /** - * 删除非顶级组织,顶级组织(父级为 0)保留 + * 清除非保留顶级外的组织;保留 id=135 集团锚点,以及名称为「外部组织」的顶级 * * @return 删除数量 */ int clearNonTopDepts(); + /** + * 确保双顶级:id=135 集团(编码 GWWL)+ 独立「外部组织」顶级 + * + * @return 集团顶级组织(id=135) + */ + Dept ensureOaSyncRootDept(); + /** * 新增或修改部门 *

diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index 24f461de..c0a7c966 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -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 implements ID @Override @Transactional(rollbackFor = Exception.class) public int clearNonTopDepts() { - Long nonTopCount = this.count(Wrappers.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 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.lambdaQuery() - .isNotNull(Dept::getParentId) - .ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID)); - return nonTopCount.intValue(); + Long clearCount = this.count(Wrappers.lambdaQuery().notIn(Dept::getId, keepIds)); + if (clearCount == null || clearCount == 0L) { + return 0; + } + this.remove(Wrappers.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 externalTops = this.list(Wrappers.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 duplicateIds = externalTops.stream() + .skip(1) + .map(Dept::getId) + .filter(Objects::nonNull) + .toList(); + if (CollectionUtil.isNotEmpty(duplicateIds)) { + this.update(Wrappers.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 legacyRoots = this.list(Wrappers.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 legacyIds = legacyRoots.stream().map(Dept::getId).filter(Objects::nonNull).collect(Collectors.toSet()); + this.update(Wrappers.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.lambdaUpdate() + .set(Dept::getDeptCategory, DeptCategory.DEPT.getCode()) + .likeRight(Dept::getDeptCode, OAConvertConstant.DEPARTMENT_OA_PREFIX)); + this.update(Wrappers.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 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 implements ID log.info("没有记录OA主键的组织,跳过层级重建"); return; } - Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode()); - Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode()); + Map companyByOaId = this.indexOaCompanies(oaDeptList); + Map departmentByOaId = this.indexOaDepartments(oaDeptList); Map desiredParentIdMap = new LinkedHashMap<>(); Map desiredParentCodeMap = new HashMap<>(); for (Dept dept : oaDeptList) { @@ -778,21 +963,20 @@ public class DeptServiceImpl extends ServiceImpl implements ID if (CollectionUtil.isEmpty(oaDeptList)) { return 0; } - Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode()); - Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode()); + Map companyByOaId = this.indexOaCompanies(oaDeptList); + Map departmentByOaId = this.indexOaDepartments(oaDeptList); Set 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 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 implements ID return deleteIds.size(); } + /** + * 索引 OA 公司:同时兼容类型迁移前后的公司记录 + */ + private Map indexOaCompanies(List oaDeptList) { + Map 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 indexOaDepartments(List oaDeptList) { + Map 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 implements ID private OaParentLink resolveOaParentLink(Dept dept, Map companyByOaId, Map 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) { 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 3829e7a6..2156b11f 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 @@ -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.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 companyNames = syncAll ? Collections.emptySet() : getCompanyNames(); - // 转换数据 List 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 existingDeptMap = loadExistingDeptMap(DeptCategory.COMPANY, null); - Map deptIdByCode = toDeptIdMap(existingDeptMap); - return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount); + Map existingByOaId = loadExistingByOaId(DeptCategory.COMPANY); + Map codeOwnerMap = loadCompanyCodeOwnerMap(); + Set usedCodes = new HashSet<>(codeOwnerMap.keySet()); + Map 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 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 companyDeptMap = getAllCompanyDeptMap(); - // 根公司id + Map companyByOaId = loadCompanyByOaId(); String rootCompanyId = getRootCompanyId(); if (!syncAll && rootCompanyId == null) { return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList()); } - // 根公司下要同步的部门名称 Set rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames(); - // 转换数据 List 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 deptCodes = new HashSet<>(); - allParam.forEach(dept -> { - if (StringUtils.isNotEmpty(dept.getDeptCode())) { - deptCodes.add(dept.getDeptCode()); + + Map existingByOaId = loadExistingByOaId(DeptCategory.DEPT); + Map nextSerialByCompany = new HashMap<>(); + Map> 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 batchByOaId = new LinkedHashMap<>(); + for (Dept dept : allParam) { + this.assignDepartmentCode(dept, existingByOaId, companyByOaId, usedCodesByCompany, nextSerialByCompany); + batchByOaId.put(dept.getOaId(), dept); + } + // 解析上级:根部门挂公司;子部门挂上级部门(本批或已存在) + Map deptIdByCode = new HashMap<>(); + companyByOaId.values().forEach(company -> { + if (StringUtils.isNotBlank(company.getDeptCode()) && company.getId() != null) { + deptIdByCode.put(company.getDeptCode(), company.getId()); } }); - // 查询数据库已有部门(含 oaModified) - Map existingDeptMap = loadExistingDeptMap(DeptCategory.DEPT, deptCodes); - Map 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 loadExistingDeptMap(DeptCategory deptCategory, Set deptCodes) { - if (deptCodes != null && deptCodes.isEmpty()) { - return new HashMap<>(); + private void assignCompanyCode(Dept dept, Map existingByOaId, Set 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 existingByOaId, Map companyByOaId, + Map> usedCodesByCompany, Map 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 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 loadExistingByOaId(DeptCategory deptCategory) { List deptList = deptService.list(Wrappers.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 toDeptIdMap(Map existingDeptMap) { - Map 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 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 allParam, Map existingDeptMap, + private Map loadCompanyCodeOwnerMap() { + List companyList = deptService.list(Wrappers.lambdaQuery() + .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode()) + .likeRight(Dept::getDeptCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE) + .select(Dept::getId, Dept::getDeptCode, Dept::getOaId)); + Map codeMap = new HashMap<>(); + for (Dept dept : companyList) { + if (DeptCodeUtil.isCompanyCode(dept.getDeptCode())) { + codeMap.put(dept.getDeptCode(), dept); + } + } + return codeMap; + } + + /** + * 已同步公司:oaId → 公司 + */ + private Map loadCompanyByOaId() { + List companyList = deptService.list(Wrappers.lambdaQuery() + .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode()) + .isNotNull(Dept::getOaId) + .ne(Dept::getOaId, "")); + Map 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 allParam, Map existingByOaId, Map deptIdByCode, DeptCategory deptCategory, int filteredSkipCount) { - Set existsDeptIds = existingDeptMap.values().stream() + Set 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 list = allParam.stream() + .filter(dept -> StringUtils.isNotBlank(dept.getDeptCode())) .filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId())) .toList(); List addList = list.stream() @@ -761,10 +936,9 @@ public class OASyncServiceImpl implements IOASyncService { ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size()); deptService.saveBatch(addList); } - // 已存在且 oaModified 不一致(或不存在可比对值)才修改 List 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 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.lambdaQuery() .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode()) + .isNotNull(Dept::getOaId) + .ne(Dept::getOaId, "") ); } else { Set companyNames = this.getCompanyNames(); @@ -851,6 +1033,8 @@ public class OASyncServiceImpl implements IOASyncService { companyList = deptService.list(Wrappers.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.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.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, ""); } } 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 eba6b8cd..a67f4ff8 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 @@ -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.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, diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java b/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java new file mode 100644 index 00000000..687a3a46 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * 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 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; + } +} diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java index 4f6027c0..e157641b 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java @@ -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; }