diff --git a/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/SynchroRelatedService.java b/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/SynchroRelatedService.java index 4cf11736a..0133e643e 100644 --- a/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/SynchroRelatedService.java +++ b/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/SynchroRelatedService.java @@ -31,5 +31,11 @@ public interface SynchroRelatedService extends IJpaService{ public SynchroRelated findByOriginId(Synchronizers synchronizer,String originId,String classType) ; + /** + * 根据 同步器 + originId + classType 查询同步关系, 如果存在则更新, 不存在则插入 + * @param synchronizer 同步器 + * @param synchroRelated 同步关系 + * @param classType 对象类型 + */ public void updateSynchroRelated(Synchronizers synchronizer,SynchroRelated synchroRelated,String classType) ; } diff --git a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinOrganizationService.java b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinOrganizationService.java index 7564722f1..742fc95ce 100644 --- a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinOrganizationService.java +++ b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinOrganizationService.java @@ -1,28 +1,28 @@ /* * Copyright [2021] [MaxKey of copyright http://www.maxkey.top] - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ - + package org.dromara.maxkey.synchronizer.workweixin; import org.dromara.maxkey.constants.ConstsStatus; +import org.dromara.maxkey.entity.SyncJobConfigField; import org.dromara.maxkey.entity.SynchroRelated; import org.dromara.maxkey.entity.idm.Organizations; import org.dromara.maxkey.synchronizer.AbstractSynchronizerService; import org.dromara.maxkey.synchronizer.ISynchronizerService; -import org.dromara.maxkey.entity.SyncJobConfigField; import org.dromara.maxkey.synchronizer.service.SyncJobConfigFieldService; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDepts; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDeptsResponse; @@ -32,8 +32,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; import java.lang.reflect.InvocationTargetException; +import java.sql.Types; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,94 +44,149 @@ import java.util.Map; import static org.dromara.maxkey.synchronizer.utils.FieldUtil.*; @Service -public class WorkweixinOrganizationService extends AbstractSynchronizerService implements ISynchronizerService{ - static final Logger _logger = LoggerFactory.getLogger(WorkweixinOrganizationService.class); - +public class WorkweixinOrganizationService extends AbstractSynchronizerService implements ISynchronizerService { + static final Logger _logger = LoggerFactory.getLogger(WorkweixinOrganizationService.class); + String access_token; @Autowired private SyncJobConfigFieldService syncJobConfigFieldService; private static final Integer ORG_TYPE = 2; - static String DEPTS_URL="https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s"; + static String DEPTS_URL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s"; static long ROOT_DEPT_ID = 1; - + public void sync() { _logger.info("Sync Workweixin Organizations ..."); - try { + try { WorkWeixinDeptsResponse rsp = requestDepartmentList(access_token); - - for(WorkWeixinDepts dept : rsp.getDepartment()) { - _logger.debug("dept : " + dept.getId()+" "+ dept.getName()+" "+ dept.getParentid()); + + // 需要对企业微信部门列表进行一次重排,保证父节点在前,子节点在后 + List deptWxListAfterLevelSort = sortDepartments(rsp.getDepartment()); + + // 关键字段不能依赖映射关系,否则映射数据有问题会导致功能异常 + // 先拿出字段映射关系 + Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); + + for (WorkWeixinDepts deptWxCur : deptWxListAfterLevelSort) { + _logger.debug("sync workweixin dept : {} {} {}", deptWxCur.getId(), deptWxCur.getName(), deptWxCur.getParentid()); //root - if(dept.getId() == ROOT_DEPT_ID) { + if (deptWxCur.getId() == ROOT_DEPT_ID) { + // 当前根节点 + // 先查询本地根节点, 这里可能有问题, ROOT_ORG_ID的组织可能不存在(原本的被删除了), 这里先假设存在 Organizations rootOrganization = organizationsService.get(Organizations.ROOT_ORG_ID); - SynchroRelated rootSynchroRelated = buildSynchroRelated(rootOrganization,dept); + // 构建同步关系 + SynchroRelated rootSynchroRelated = buildSynchroRelated(rootOrganization, deptWxCur); + // 更新同步关系 synchroRelatedService.updateSynchroRelated( - this.synchronizer,rootSynchroRelated,Organizations.CLASS_TYPE); - }else { - //synchro Related - SynchroRelated synchroRelated = + this.synchronizer, rootSynchroRelated, Organizations.CLASS_TYPE); + // 是否更新根节点的编码待确认, 这里先更新名称 + rootOrganization.setOrgName(deptWxCur.getName()); + organizationsService.update(rootOrganization); + } else { + // 现在不是根组织 + //synchro Related 查询当前部门是否有同步记录 这里只是查有没有关系, 不是查组织 + SynchroRelated synchroRelated = synchroRelatedService.findByOriginId( - this.synchronizer,dept.getId() + "",Organizations.CLASS_TYPE ); - //Parent + this.synchronizer, deptWxCur.getId() + "", Organizations.CLASS_TYPE); + //Parent 查询当前部门父部门是否有同步记录 这里只是查有没有关系, 不是查组织 SynchroRelated synchroRelatedParent = synchroRelatedService.findByOriginId( - this.synchronizer,dept.getParentid() + "",Organizations.CLASS_TYPE); - Organizations organization = buildOrgByFiledMap(dept,synchroRelatedParent); - if(synchroRelated == null) { - organization.setId(organization.generateId()); - organizationsService.insert(organization); - _logger.debug("Organizations : " + organization); - - synchroRelated = buildSynchroRelated(organization,dept); - }else { - organization.setId(synchroRelated.getObjectId()); - organizationsService.update(organization); + this.synchronizer, deptWxCur.getParentid() + "", Organizations.CLASS_TYPE); + + // 根据字段映射构建当前组织的实体 + Organizations orgCurrent = buildOrgByFiledMap(deptWxCur, synchroRelatedParent, fieldMap); + // 这里需要修正一下层级关系, 防止因为映射关系错误导致的层级错乱 + String deptWxParentId = String.valueOf(deptWxCur.getParentid()); + // 进入到这个节点的应该都是有上级的, 现在只需要根据上级Id查询上级的组织档案 + String targetIdField = getLocalFieldMappingByWx(fieldMap, "id"); // 从映射里面拿到企业微信Id映射后的本地组织的字段 + Organizations parentOrg = organizationsService.findOne(targetIdField + " = ? and instId = ? ", + new Object[]{deptWxParentId, this.synchronizer.getInstId()}, new int[]{Types.VARCHAR, Types.VARCHAR}); + // 这里父级不应该为 null + if (parentOrg == null) { + throw new RuntimeException("无法找到上级组织, 同步失败! 企业微信父部门Id: " + deptWxParentId); } - + + orgCurrent.setParentId(parentOrg.getId()); + orgCurrent.setParentCode(parentOrg.getOrgCode()); + orgCurrent.setParentName(parentOrg.getOrgName()); + + if (ObjectUtils.isEmpty(orgCurrent.getFullName())) { + // 兜底设置一下组织全称 + orgCurrent.setFullName(orgCurrent.getOrgName()); + } + + + if (synchroRelated == null) { + // 当前部门还没有同步过 + orgCurrent.setId(orgCurrent.generateId()); + organizationsService.insert(orgCurrent); + _logger.debug("Organizations : " + orgCurrent); + + synchroRelated = buildSynchroRelated(orgCurrent, deptWxCur); + } else { + // 部门曾经同步过, 但是不能保证没被删除过, 所以还需要判定一次 + Organizations currentOrg = organizationsService.findOne(targetIdField + " = ? and instId = ? ", + new Object[]{deptWxCur.getId(), this.synchronizer.getInstId()}, new int[]{Types.VARCHAR, Types.VARCHAR}); + if (currentOrg == null) { + // 当前部门已经被删除, 那就需要重新写入一次 + orgCurrent.setId(synchroRelated.getObjectId()); + organizationsService.insert(orgCurrent); + } + + orgCurrent.setId(synchroRelated.getObjectId()); + organizationsService.update(orgCurrent); + } + synchroRelatedService.updateSynchroRelated( - this.synchronizer,synchroRelated,Organizations.CLASS_TYPE); + this.synchronizer, synchroRelated, Organizations.CLASS_TYPE); } } } catch (Exception e) { e.printStackTrace(); } - + } - - public SynchroRelated buildSynchroRelated(Organizations organization,WorkWeixinDepts dept) { + + /** + * 构建同步关系 + * + * @param organization 组织实体 + * @param dept 企业微信部门实体 + * @return 同步关系 + */ + public SynchroRelated buildSynchroRelated(Organizations organization, WorkWeixinDepts dept) { return new SynchroRelated( - organization.getId(), - organization.getOrgName(), - organization.getOrgName(), - Organizations.CLASS_TYPE, - synchronizer.getId(), - synchronizer.getName(), - dept.getId()+"", - dept.getName(), - "", - dept.getParentid()+"", - synchronizer.getInstId()); + organization.getId(), // objectId 系统内组织ID + organization.getOrgName(), // objectName 系统内组织名称 + organization.getOrgName(), // objectDisplayName 系统内组织显示名称 + Organizations.CLASS_TYPE, // objectType 对象类型 + synchronizer.getId(), // syncId 同步器ID + synchronizer.getName(), // syncName 同步器名称 + dept.getId() + "", // originId 企业微信部门ID + dept.getName(), // originName 企业微信部门名称 + "", + dept.getParentid() + "", // originId3 父部门ID + synchronizer.getInstId()); } - + public WorkWeixinDeptsResponse requestDepartmentList(String access_token) { - HttpRequestAdapter request =new HttpRequestAdapter(); + HttpRequestAdapter request = new HttpRequestAdapter(); String responseBody = request.get(String.format(DEPTS_URL, access_token)); - WorkWeixinDeptsResponse deptsResponse =JsonUtils.gsonStringToObject(responseBody, WorkWeixinDeptsResponse.class); - + WorkWeixinDeptsResponse deptsResponse = JsonUtils.gsonStringToObject(responseBody, WorkWeixinDeptsResponse.class); + _logger.trace("response : " + responseBody); - for(WorkWeixinDepts dept : deptsResponse.getDepartment()) { + for (WorkWeixinDepts dept : deptsResponse.getDepartment()) { _logger.debug("WorkWeixinDepts : " + dept); } return deptsResponse; } - - public Organizations buildOrganization(WorkWeixinDepts dept,SynchroRelated synchroRelatedParent) { + + public Organizations buildOrganization(WorkWeixinDepts dept, SynchroRelated synchroRelatedParent) { Organizations org = new Organizations(); org.setOrgName(dept.getName()); - org.setOrgCode(dept.getId()+""); + org.setOrgCode(dept.getId() + ""); org.setParentId(synchroRelatedParent.getObjectId()); org.setParentName(synchroRelatedParent.getObjectName()); org.setSortIndex(dept.getOrder()); @@ -138,11 +196,33 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i return org; } + /** + * 从字段映射中获取企业微信字段映射后的本地字段 + * @param fieldMap 字段映射 + * @param expectField 企业微信字段 + * @return 本地字段 + */ + public String getLocalFieldMappingByWx(Map fieldMap, String expectField) { + for (Map.Entry entry : fieldMap.entrySet()) { + String orgProperty = entry.getKey(); + String sourceProperty = entry.getValue(); + if (sourceProperty.equals(expectField)) { + return orgProperty; + } + } + throw new RuntimeException("未找到企业微信字段映射后的本地字段"); + } - public Organizations buildOrgByFiledMap(WorkWeixinDepts dept, SynchroRelated synchroRelatedParent){ + /** + * 根据字段映射构建组织实体 + * + * @param dept 企业微信部门实体 + * @param synchroRelatedParent 父部门同步关系 + * @param fieldMap 同步器配置的字段映射 + * @return 组织实体 + */ + public Organizations buildOrgByFiledMap(WorkWeixinDepts dept, SynchroRelated synchroRelatedParent, Map fieldMap) { Organizations org = new Organizations(); - //fieldMap - Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); for (Map.Entry entry : fieldMap.entrySet()) { @@ -153,8 +233,7 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i if (hasField(dept.getClass(), sourceProperty)) { sourceValue = getFieldValue(dept, sourceProperty); - } - else if (synchroRelatedParent != null && hasField(SynchroRelated.class, sourceProperty)) { + } else if (synchroRelatedParent != null && hasField(SynchroRelated.class, sourceProperty)) { sourceValue = getFieldValue(synchroRelatedParent, sourceProperty); } if (sourceValue != null) { @@ -172,21 +251,74 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i } - public Map getFieldMap(Long jobId){ - Map filedMap = new HashMap<>(); + public Map getFieldMap(Long jobId) { + Map filedMap = new HashMap<>(); //根据job id查询属性映射表 List syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId); //获取组织属性映射 - for(SyncJobConfigField element:syncJobConfigFieldList){ - if(Integer.parseInt(element.getObjectType()) == ORG_TYPE.intValue()){ + for (SyncJobConfigField element : syncJobConfigFieldList) { + if (Integer.parseInt(element.getObjectType()) == ORG_TYPE) { filedMap.put(element.getTargetField(), element.getSourceField()); } } return filedMap; } + /** + * 对部门列表进行排序,确保父节点在前,子节点在后 + * 使用拓扑排序算法,按照层级顺序遍历部门树 + * + * @param departments 原始部门列表 + * @return 排序后的部门列表 + */ + private List sortDepartments(List departments) { + if (departments == null || departments.isEmpty()) { + return departments; + } + // 构建部门ID到部门对象的映射 + Map deptMap = new HashMap<>(); + // 构建父ID到子部门列表的映射 + Map> parentToChildrenMap = new HashMap<>(); + for (WorkWeixinDepts dept : departments) { + deptMap.put(dept.getId(), dept); + parentToChildrenMap.computeIfAbsent(dept.getParentid(), k -> new ArrayList<>()).add(dept); + } + + // 结果列表 + List sortedList = new ArrayList<>(); + + // 从根节点开始遍历 + List queue = new ArrayList<>(); + + // 找到所有根节点(没有父节点的部门,或者父节点不在列表中的部门) + for (WorkWeixinDepts dept : departments) { + if (!deptMap.containsKey(dept.getParentid())) { + queue.add(dept.getId()); + } + } + + // 遍历 + while (!queue.isEmpty()) { + Long currentId = queue.remove(0); + WorkWeixinDepts currentDept = deptMap.get(currentId); + + if (currentDept != null) { + sortedList.add(currentDept); + + // 将当前部门的所有子部门加入队列 + List children = parentToChildrenMap.get(currentId); + if (children != null) { + for (WorkWeixinDepts child : children) { + queue.add(child.getId()); + } + } + } + } + + return sortedList; + } public String getAccess_token() { diff --git a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/entity/WorkWeixinDepts.java b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/entity/WorkWeixinDepts.java index a5377018d..572234871 100644 --- a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/entity/WorkWeixinDepts.java +++ b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/entity/WorkWeixinDepts.java @@ -69,4 +69,14 @@ public class WorkWeixinDepts { this.order = order; } + @Override + public String toString() { + return "WorkWeixinDepts{" + + "id=" + id + + ", name='" + name + '\'' + + ", name_en='" + name_en + '\'' + + ", parentid=" + parentid + + ", order=" + order + + '}'; + } }