Bladeren bron

完善企业微信同步器同步,修正 已同步的组织被删除后 后续同步无法恢复

link2fun 1 week geleden
bovenliggende
commit
bc54cb256c

+ 6 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/SynchroRelatedService.java

@@ -31,5 +31,11 @@ public interface SynchroRelatedService  extends IJpaService<SynchroRelated>{
     
     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) ;
 }

+ 198 - 66
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<WorkWeixinDepts> deptWxListAfterLevelSort = sortDepartments(rsp.getDepartment());
+
+            // 关键字段不能依赖映射关系,否则映射数据有问题会导致功能异常
+            // 先拿出字段映射关系
+            Map<String, String> 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<String, String> fieldMap, String expectField) {
+        for (Map.Entry<String, String> 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<String, String> fieldMap) {
         Organizations org = new Organizations();
-        //fieldMap
-        Map<String, String> fieldMap = getFieldMap(Long.parseLong(synchronizer.getId()));
 
 
         for (Map.Entry<String, String> 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<String,String> getFieldMap(Long jobId){
-        Map<String,String> filedMap = new HashMap<>();
+    public Map<String, String> getFieldMap(Long jobId) {
+        Map<String, String> filedMap = new HashMap<>();
         //根据job id查询属性映射表
         List<SyncJobConfigField> 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<WorkWeixinDepts> sortDepartments(List<WorkWeixinDepts> departments) {
+        if (departments == null || departments.isEmpty()) {
+            return departments;
+        }
+
+        // 构建部门ID到部门对象的映射
+        Map<Long, WorkWeixinDepts> deptMap = new HashMap<>();
+        // 构建父ID到子部门列表的映射
+        Map<Long, List<WorkWeixinDepts>> parentToChildrenMap = new HashMap<>();
+
+        for (WorkWeixinDepts dept : departments) {
+            deptMap.put(dept.getId(), dept);
+            parentToChildrenMap.computeIfAbsent(dept.getParentid(), k -> new ArrayList<>()).add(dept);
+        }
+
+        // 结果列表
+        List<WorkWeixinDepts> sortedList = new ArrayList<>();
 
+        // 从根节点开始遍历
+        List<Long> 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<WorkWeixinDepts> children = parentToChildrenMap.get(currentId);
+                if (children != null) {
+                    for (WorkWeixinDepts child : children) {
+                        queue.add(child.getId());
+                    }
+                }
+            }
+        }
+
+        return sortedList;
+    }
 
 
     public String getAccess_token() {

+ 10 - 0
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 +
+                '}';
+    }
 }