From bc54cb256c0576a795bdb5ac7bdda70d9e52ef6d Mon Sep 17 00:00:00 2001 From: link2fun <19241982+link2fun@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:01:49 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=90=8C=E6=AD=A5=E5=99=A8=E5=90=8C=E6=AD=A5?= =?UTF-8?q?,=E4=BF=AE=E6=AD=A3=20=E5=B7=B2=E5=90=8C=E6=AD=A5=E7=9A=84?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E8=A2=AB=E5=88=A0=E9=99=A4=E5=90=8E=20?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=90=8C=E6=AD=A5=E6=97=A0=E6=B3=95=E6=81=A2?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/SynchroRelatedService.java | 6 + .../WorkweixinOrganizationService.java | 264 +++++++++++++----- .../workweixin/entity/WorkWeixinDepts.java | 10 + 3 files changed, 214 insertions(+), 66 deletions(-) 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 + + '}'; + } } From 38ea7b7b60bff77a952b4f1308c497820e7fb9d3 Mon Sep 17 00:00:00 2001 From: link2fun <19241982+link2fun@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:23:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WorkweixinOrganizationService.java | 63 +++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) 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 742fc95ce..78b0d76ca 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 @@ -66,14 +66,19 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i // 关键字段不能依赖映射关系,否则映射数据有问题会导致功能异常 // 先拿出字段映射关系 Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); + // 从映射里面拿到企业微信Id映射后的本地组织的字段 用于判断本地的组织是否存在 + String targetIdField = getLocalFieldMappingByWx(fieldMap, "id"); for (WorkWeixinDepts deptWxCur : deptWxListAfterLevelSort) { _logger.debug("sync workweixin dept : {} {} {}", deptWxCur.getId(), deptWxCur.getName(), deptWxCur.getParentid()); //root if (deptWxCur.getId() == ROOT_DEPT_ID) { // 当前根节点 - // 先查询本地根节点, 这里可能有问题, ROOT_ORG_ID的组织可能不存在(原本的被删除了), 这里先假设存在 Organizations rootOrganization = organizationsService.get(Organizations.ROOT_ORG_ID); + if (rootOrganization == null) { + _logger.error("根组织不存在(ID: {}), 无法同步企业微信根部门", Organizations.ROOT_ORG_ID); + throw new RuntimeException("根组织不存在, 同步失败! 请先确保系统中存在根组织(ID: " + Organizations.ROOT_ORG_ID + ")"); + } // 构建同步关系 SynchroRelated rootSynchroRelated = buildSynchroRelated(rootOrganization, deptWxCur); // 更新同步关系 @@ -98,9 +103,8 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i // 这里需要修正一下层级关系, 防止因为映射关系错误导致的层级错乱 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}); + + Organizations parentOrg = findOrganizationByField(targetIdField, deptWxParentId); // 这里父级不应该为 null if (parentOrg == null) { throw new RuntimeException("无法找到上级组织, 同步失败! 企业微信父部门Id: " + deptWxParentId); @@ -125,16 +129,16 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i 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}); + Organizations currentOrg = findOrganizationByField(targetIdField, String.valueOf(deptWxCur.getId())); if (currentOrg == null) { // 当前部门已经被删除, 那就需要重新写入一次 orgCurrent.setId(synchroRelated.getObjectId()); organizationsService.insert(orgCurrent); + } else { + // 组织存在, 执行更新操作 + orgCurrent.setId(synchroRelated.getObjectId()); + organizationsService.update(orgCurrent); } - - orgCurrent.setId(synchroRelated.getObjectId()); - organizationsService.update(orgCurrent); } synchroRelatedService.updateSynchroRelated( @@ -143,7 +147,8 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i } } catch (Exception e) { - e.printStackTrace(); + _logger.error("同步企业微信组织失败", e); + throw new RuntimeException("同步企业微信组织失败: " + e.getMessage(), e); } } @@ -210,7 +215,9 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i return orgProperty; } } - throw new RuntimeException("未找到企业微信字段映射后的本地字段"); + throw new RuntimeException(String.format( + "未找到企业微信字段'%s'映射后的本地字段,请检查同步器(ID: %s)的字段映射配置", + expectField, this.synchronizer.getId())); } /** @@ -264,6 +271,40 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i return filedMap; } + /** + * 验证字段名是否合法,防止SQL注入 + * + * @param fieldName 字段名 + * @throws IllegalArgumentException 如果字段名不合法 + */ + private void validateFieldName(String fieldName) { + if (fieldName == null || fieldName.trim().isEmpty()) { + throw new IllegalArgumentException("字段名不能为空"); + } + // 只允许字母、数字、下划线,且必须以字母或下划线开头 + if (!fieldName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException("非法的字段名: " + fieldName + ", 字段名只能包含字母、数字和下划线,且必须以字母或下划线开头"); + } + } + + /** + * 根据指定字段查询组织 + * + * @param fieldName 字段名 + * @param fieldValue 字段值 + * @return 查询到的组织,如果不存在返回null + */ + private Organizations findOrganizationByField(String fieldName, String fieldValue) { + // 验证字段名防止SQL注入 + validateFieldName(fieldName); + + return organizationsService.findOne( + fieldName + " = ? AND instId = ?", + new Object[]{fieldValue, this.synchronizer.getInstId()}, + new int[]{Types.VARCHAR, Types.VARCHAR} + ); + } + /** * 对部门列表进行排序,确保父节点在前,子节点在后 * 使用拓扑排序算法,按照层级顺序遍历部门树 From f92c0032a7642258c3b9f8db8a8c31455d1ccac3 Mon Sep 17 00:00:00 2001 From: link2fun <19241982+link2fun@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:54:22 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=AF=B9=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E7=94=A8=E6=88=B7=E5=90=8C=E6=AD=A5=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=9C=BA=E6=99=AF=E5=88=A4=E6=96=AD=20,=20?= =?UTF-8?q?=E5=A4=A7=E8=87=B4=E5=BA=94=E8=AF=A5=E6=98=AF=E5=88=86=E6=88=90?= =?UTF-8?q?=E4=BA=864=E7=A7=8D=E6=83=85=E5=86=B5=201.=20=E6=9C=89=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=AE=B0=E5=BD=95,=20=E7=94=A8=E6=88=B7=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=20=20->=20=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=202.=20=E6=9C=89=E5=90=8C=E6=AD=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95,=20=E7=94=A8=E6=88=B7=E4=B8=8D=E5=AD=98=E5=9C=A8=20?= =?UTF-8?q?=20->=20=E9=87=8D=E6=96=B0=E5=88=9B=E5=BB=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=20=E4=BD=BF=E7=94=A8=E5=8E=9F=E6=9C=AC=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7Id=203.=20=E6=97=A0=E5=90=8C=E6=AD=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95,=20=E7=94=A8=E6=88=B7=E5=AD=98=E5=9C=A8=20=20->=20?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=98=AF=E6=89=8B=E5=B7=A5=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E7=9A=84=E7=94=A8=E6=88=B7,=20=E6=89=8B=E5=8A=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E7=9A=84=E7=94=A8=E6=88=B7=E5=BA=94=E5=BD=93=E6=98=AF?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E7=9B=B4=E6=8E=A5=E5=85=B3=E8=81=94,=20?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E6=97=A0=E6=B3=95=E7=A1=AE=E8=AE=A4=20?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E7=94=A8=E6=88=B7=E5=92=8C=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=90=8C=E6=AD=A5=E8=BF=87=E6=9D=A5=E7=9A=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=98=AF=E5=90=8C=E4=B8=80=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=204.=20=E6=97=A0=E5=90=8C=E6=AD=A5=E8=AE=B0=E5=BD=95,=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=8D=E5=AD=98=E5=9C=A8=20=20->=20?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E5=88=9B=E5=BB=BA=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workweixin/WorkweixinUsersService.java | 245 +++++++++++++----- .../maxkey/synchronizer/utils/FieldUtil.java | 3 + 2 files changed, 185 insertions(+), 63 deletions(-) diff --git a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java index 3c86ecedf..8a64d41e7 100644 --- a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java +++ b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java @@ -1,33 +1,29 @@ /* * 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 java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import org.apache.commons.lang3.StringUtils; import org.dromara.maxkey.constants.ConstsStatus; +import org.dromara.maxkey.entity.SyncJobConfigField; import org.dromara.maxkey.entity.SynchroRelated; import org.dromara.maxkey.entity.idm.UserInfo; 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.WorkWeixinUsers; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinUsersResponse; @@ -38,95 +34,183 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.lang.reflect.InvocationTargetException; +import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import static org.dromara.maxkey.synchronizer.utils.FieldUtil.*; @Service -public class WorkweixinUsersService extends AbstractSynchronizerService implements ISynchronizerService{ +public class WorkweixinUsersService extends AbstractSynchronizerService implements ISynchronizerService { final static Logger _logger = LoggerFactory.getLogger(WorkweixinUsersService.class); @Autowired public SyncJobConfigFieldService syncJobConfigFieldService; private static final Integer USER_TYPE = 1; String access_token; - - static String USERS_URL="https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%s&fetch_child=0"; - + + static String USERS_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%s&fetch_child=0"; + public void sync() { _logger.info("Sync Workweixin Users..."); try { - List synchroRelateds = - synchroRelatedService.findOrgs(this.synchronizer); - - for(SynchroRelated relatedOrg : synchroRelateds) { - HttpRequestAdapter request =new HttpRequestAdapter(); - String responseBody = request.get(String.format(USERS_URL, access_token,relatedOrg.getOriginId())); - WorkWeixinUsersResponse usersResponse =JsonUtils.gsonStringToObject(responseBody, WorkWeixinUsersResponse.class); + // 获取前面已经拉取下来的微信部门同步记录 + List synchroRelatedOrgList = synchroRelatedService.findOrgs(this.synchronizer); + Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); + + // 拿到微信用户Id映射后的MaxKey字段名称 + String wxUserIdInMaxkeyField = getWxUserMappingField(fieldMap, "userid"); + + for (SynchroRelated relatedOrg : synchroRelatedOrgList) { + // 根据微信部门ID,拉取微信用户列表, 这里拉取的是直属员工 + HttpRequestAdapter request = new HttpRequestAdapter(); + String responseBody = request.get(String.format(USERS_URL, access_token, relatedOrg.getOriginId())); + WorkWeixinUsersResponse usersResponse = JsonUtils.gsonStringToObject(responseBody, WorkWeixinUsersResponse.class); _logger.trace("response : " + responseBody); - - for(WorkWeixinUsers user : usersResponse.getUserlist()) { - UserInfo userInfo = buildUserInfoByFiledMap(user); - _logger.debug("userInfo : " + userInfo); - userInfo.setPassword(userInfo.getUsername() + UserInfo.DEFAULT_PASSWORD_SUFFIX); - userInfoService.saveOrUpdate(userInfo); - + + + for (WorkWeixinUsers wxUser : usersResponse.getUserlist()) { + // 依次处理每个员工 + // 根据员工信息,构建MaxKey用户信息 + UserInfo maxkeyUserNew = buildUserInfoByFiledMap(wxUser, fieldMap); + + // 一个企业微信可能属于多个部门, 所以需要从 wxUser 中获取主部门 + String mainDepartment = String.valueOf(wxUser.getMain_department()); + // 从 synchroRelatedOrgList 中找到对应的 MaxKey 部门ID + synchroRelatedOrgList.stream().filter(syncInfo -> StringUtils.equals(syncInfo.getOriginId(), mainDepartment)) + .findFirst() + .ifPresent(orgRelInfo -> { + maxkeyUserNew.setDepartmentId(orgRelInfo.getObjectId()); + maxkeyUserNew.setDepartment(orgRelInfo.getObjectName()); + }); + + + // 根据企业微信用户Id映射字段获取用户信息 + UserInfo existingUser = getUserByWxUserIdMappingField(wxUserIdInMaxkeyField, wxUser.getUserid()); + // 加载一下历史的同步记录, 查看这个用户是否历史同步过 + SynchroRelated userSyncRecord = synchroRelatedService.findByOriginId(synchronizer, wxUser.getUserid(), UserInfo.CLASS_TYPE); + + // 现在需要进行一些场景判断 , 大致应该是分成了4种情况 + // 1. 有同步记录, 用户存在 -> 更新用户信息 + // 2. 有同步记录, 用户不存在 -> 重新创建用户 + // 3. 无同步记录, 用户存在 -> 说明是手工创建的用户, 手动创建的用户应当是不能直接关联, 因为无法确认 这个用户和企业微信同步过来的用户是同一个人 + // 4. 无同步记录, 用户不存在 -> 正常创建用户 + + // + + if (userSyncRecord != null) { + // 说明之前 已经同步过, 但是同步过也不代表用户一定存在, 可能被删除了 + if (existingUser != null) { + _logger.info("[同步微信用户] 用户 {} {} 已存在, 进行信息更新", wxUser.getUserid(), wxUser.getName()); + // 用户存在, 那么就是这个用户, 不需要重新创建了, 但是用户的信息需要更新一下, fieldMap key 中的字段是需要更新的, 但是以防配置有问题 部分字段还是需要排除的 + updateExistingUserInfo(fieldMap, maxkeyUserNew, existingUser); + // 直接更新到数据库就完事了 + userInfoService.update(existingUser); + continue; + + + } else { + _logger.info("[同步微信用户] 用户 {} {} 被删除了, 重新创建用户", wxUser.getUserid(), wxUser.getName()); + // 用户不存在, 说明被删除了, 那么就重新创建一个用户 仍然使用原用户的Id objectId maxkey 里面的用户ID + maxkeyUserNew.setId(userSyncRecord.getObjectId()); + } + } else { + // 没有同步记录不代表用户不存在, 可能是以前手工创建的用户,需要判断一下 + if (existingUser != null) { + _logger.warn("[同步微信用户] 用户 {} {} 无法确认和本地的用户是否是同一人 跳过同步", wxUser.getUserid(), wxUser.getName()); + // 手工创建的用户应当是不能直接关联, 因为无法确认 这个用户和企业微信同步过来的用户是同一个人 + continue; + } + _logger.info("[同步微信用户] 用户 {} {} 不存在, 正常创建用户", wxUser.getUserid(), wxUser.getName()); + // 到这里应该是正常创建用户的流程 + maxkeyUserNew.setId(maxkeyUserNew.generateId()); // 使用一个新的Id + + } + + + _logger.debug("userInfo : {}", maxkeyUserNew); + // 设置密码 + maxkeyUserNew.setPassword(maxkeyUserNew.getUsername() + UserInfo.DEFAULT_PASSWORD_SUFFIX); + // 按照 username + instId 保持唯一 , username 是判重字段 + userInfoService.saveOrUpdate(maxkeyUserNew); + SynchroRelated synchroRelated = new SynchroRelated( - userInfo.getId(), - userInfo.getUsername(), - userInfo.getDisplayName(), - UserInfo.CLASS_TYPE, - synchronizer.getId(), - synchronizer.getName(), - user.getUserid(), - user.getName(), - user.getUserid(), - "", - synchronizer.getInstId()); - + maxkeyUserNew.getId(), // objectId: maxkey 里面的用户ID + maxkeyUserNew.getUsername(), // objectName: maxkey 里面的用户名 + maxkeyUserNew.getDisplayName(), // displayName: maxkey 里面的显示名称 + UserInfo.CLASS_TYPE, // objectType: 对象类型 是用户 + synchronizer.getId(), // jobId: 同步器ID + synchronizer.getName(), // jobName: 同步器名称 + wxUser.getUserid(), // originId: 企业微信用户ID + wxUser.getName(), // originName: 企业微信用户名 + wxUser.getUserid(), // originId2: 企业微信用户ID + "", // originId3: 暂无 + synchronizer.getInstId()); // instId: 机构ID + synchroRelatedService.updateSynchroRelated( - this.synchronizer,synchroRelated,UserInfo.CLASS_TYPE); - - socialsAssociate(synchroRelated,"workweixin"); + this.synchronizer, synchroRelated, UserInfo.CLASS_TYPE); + + socialsAssociate(synchroRelated, "workweixin"); } } - + } catch (Exception e) { - e.printStackTrace(); + _logger.error("[同步微信用户] 同步用户失败", e); } - + } - + + private static void updateExistingUserInfo(Map fieldMap, UserInfo maxkeyUserNew, UserInfo existingUser) { + fieldMap.keySet().forEach(fieldName -> { + // 排除这些字段不更新 + if (!StringUtils.equalsAny(fieldName, "id", "password", "instId", "userType")) { + try { + Object newValue = getFieldValue(maxkeyUserNew, fieldName); + setFieldValue(existingUser, fieldName, newValue); + } catch (NoSuchMethodException | InvocationTargetException | + IllegalAccessException e) { + _logger.error("update existingUser error: fieldName: {}, error: {}", fieldName, e.getMessage()); + } + } + }); + // 额外设置一下 部门 + existingUser.setDepartmentId(maxkeyUserNew.getDepartmentId()); + existingUser.setDepartment(maxkeyUserNew.getDepartment()); + } + public void postSync(UserInfo userInfo) { - + } public UserInfo buildUserInfo(WorkWeixinUsers user) { - UserInfo userInfo = new UserInfo(); + UserInfo userInfo = new UserInfo(); userInfo.setUsername(user.getUserid());//账号 userInfo.setNickName(user.getAlias());//名字 userInfo.setDisplayName(user.getName());//名字 - + userInfo.setMobile(user.getMobile());//手机 userInfo.setEmail(user.getEmail()); userInfo.setGender(Integer.parseInt(user.getGender())); - + userInfo.setWorkPhoneNumber(user.getTelephone());//工作电话 - userInfo.setDepartmentId(user.getMain_department()+""); + userInfo.setDepartmentId(user.getMain_department() + ""); userInfo.setJobTitle(user.getPosition());//职务 userInfo.setWorkAddressFormatted(user.getAddress());//工作地点 //激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。 - if(user.getStatus() == 1) { + if (user.getStatus() == 1) { userInfo.setStatus(ConstsStatus.ACTIVE); - }else { + } else { userInfo.setStatus(ConstsStatus.INACTIVE); } userInfo.setInstId(this.synchronizer.getInstId()); return userInfo; } - public UserInfo buildUserInfoByFiledMap(WorkWeixinUsers user){ + public UserInfo buildUserInfoByFiledMap(WorkWeixinUsers user, Map fieldMap) { UserInfo userInfo = new UserInfo(); - Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); for (Map.Entry entry : fieldMap.entrySet()) { String userInfoProperty = entry.getKey(); @@ -134,8 +218,8 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen try { Object sourceValue = null; - if(sourceProperty.equals("status")){ - userInfo.setStatus(user.getStatus() == 1?ConstsStatus.ACTIVE:ConstsStatus.INACTIVE); + if (sourceProperty.equals("status")) { + userInfo.setStatus(user.getStatus() == 1 ? ConstsStatus.ACTIVE : ConstsStatus.INACTIVE); continue; } if (hasField(user.getClass(), sourceProperty)) { @@ -146,7 +230,7 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen } } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - e.printStackTrace(); + _logger.error("buildUserInfoByFiledMap error: sourceProperty: {}, error: {}", sourceProperty, e.getMessage()); } } @@ -156,22 +240,57 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen return userInfo; } - public Map getFieldMap(Long jobId){ - Map userFieldMap = new HashMap<>(); + public Map getFieldMap(Long jobId) { + Map userFieldMap = new HashMap<>(); //根据job id查询属性映射表 List syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId); //获取用户属性映射 - for(SyncJobConfigField element:syncJobConfigFieldList){ - if(Integer.parseInt(element.getObjectType()) == USER_TYPE.intValue()){ + for (SyncJobConfigField element : syncJobConfigFieldList) { + if (Integer.parseInt(element.getObjectType()) == USER_TYPE.intValue()) { userFieldMap.put(element.getTargetField(), element.getSourceField()); } } return userFieldMap; } + /** + * 从字段映射中获取企业微信字段映射后的MaxKey字段名称 + * + * @param fieldMap 字段映射 + * @param wxField 企业微信字段名称 + * @return MaxKey字段名称 + */ + public String getWxUserMappingField(Map fieldMap, String wxField) { + for (Map.Entry entry : fieldMap.entrySet()) { + String userInfoProperty = entry.getKey(); + String sourceProperty = entry.getValue(); + if (sourceProperty.equals(wxField)) { + return userInfoProperty; + } + } + throw new RuntimeException(String.format("未找到企业微信字段'%s'映射后的本地字段,请检查同步器(ID: %s)的字段映射配置", wxField, this.synchronizer.getId())); + } + + + /** + * 根据企业微信用户Id映射字段获取用户信息 + * + * @param fieldName 字段映射中和企业微信用户Id对应的MaxKey的字段名称 + * @param fieldValue 企业微信用户Id + * @return 用户信息 + */ + public UserInfo getUserByWxUserIdMappingField(String fieldName, String fieldValue) { + return userInfoService.findOne(fieldName + " = ? and instId = ?", + new Object[]{fieldValue, this.synchronizer.getInstId()}, + new int[]{Types.VARCHAR, Types.VARCHAR} + ); + } + + public void setAccess_token(String access_token) { this.access_token = access_token; } + public SyncJobConfigFieldService getSyncJobConfigFieldService() { return syncJobConfigFieldService; } diff --git a/maxkey-synchronizers/maxkey-synchronizer/src/main/java/org/dromara/maxkey/synchronizer/utils/FieldUtil.java b/maxkey-synchronizers/maxkey-synchronizer/src/main/java/org/dromara/maxkey/synchronizer/utils/FieldUtil.java index 80b2cdf07..a008c48dc 100644 --- a/maxkey-synchronizers/maxkey-synchronizer/src/main/java/org/dromara/maxkey/synchronizer/utils/FieldUtil.java +++ b/maxkey-synchronizers/maxkey-synchronizer/src/main/java/org/dromara/maxkey/synchronizer/utils/FieldUtil.java @@ -62,6 +62,9 @@ public class FieldUtil { } public static Object convertValueToFieldType(Object value, Class fieldType) { + if (value == null) { + return null; + } if (fieldType.isInstance(value)) { return value; } From ee4d52e65a255c02744bfb30c259d1b8f61d441f Mon Sep 17 00:00:00 2001 From: link2fun <19241982+link2fun@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:30:58 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8C=E6=AD=A5=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=A6=81=E7=94=A8=E7=A6=BB=E8=81=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=B4=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workweixin/WorkweixinUsersService.java | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java index 8a64d41e7..ab098143c 100644 --- a/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java +++ b/maxkey-synchronizers/maxkey-synchronizer-workweixin/src/main/java/org/dromara/maxkey/synchronizer/workweixin/WorkweixinUsersService.java @@ -58,20 +58,33 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen try { // 获取前面已经拉取下来的微信部门同步记录 List synchroRelatedOrgList = synchroRelatedService.findOrgs(this.synchronizer); - Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); + // 加载同步器的历史数据 + Map userRelationHistoryMap = loadUserRelationHistory(); + // 获取同步器的字段映射 + Map fieldMap = getFieldMap(Long.parseLong(synchronizer.getId())); // 拿到微信用户Id映射后的MaxKey字段名称 String wxUserIdInMaxkeyField = getWxUserMappingField(fieldMap, "userid"); + boolean fetchFailed = false; for (SynchroRelated relatedOrg : synchroRelatedOrgList) { // 根据微信部门ID,拉取微信用户列表, 这里拉取的是直属员工 HttpRequestAdapter request = new HttpRequestAdapter(); String responseBody = request.get(String.format(USERS_URL, access_token, relatedOrg.getOriginId())); WorkWeixinUsersResponse usersResponse = JsonUtils.gsonStringToObject(responseBody, WorkWeixinUsersResponse.class); - _logger.trace("response : " + responseBody); + _logger.trace("response : {}", responseBody); + if (usersResponse == null || usersResponse.getErrcode() != 0 || usersResponse.getUserlist() == null) { + fetchFailed = true; + _logger.warn("[同步微信用户] 拉取部门 {} (originId={}) 用户失败, errcode={}, errmsg={}", + relatedOrg.getObjectName(), relatedOrg.getOriginId(), + usersResponse == null ? "null" : usersResponse.getErrcode(), + usersResponse == null ? "null" : usersResponse.getErrmsg()); + continue; + } for (WorkWeixinUsers wxUser : usersResponse.getUserlist()) { + userRelationHistoryMap.remove(wxUser.getUserid()); // 依次处理每个员工 // 根据员工信息,构建MaxKey用户信息 UserInfo maxkeyUserNew = buildUserInfoByFiledMap(wxUser, fieldMap); @@ -156,6 +169,12 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen } } + if (fetchFailed) { + _logger.warn("[同步微信用户] 存在部门用户拉取失败,跳过缺失用户禁用流程。"); + return; + } + + disableMissingUsers(userRelationHistoryMap); } catch (Exception e) { _logger.error("[同步微信用户] 同步用户失败", e); } @@ -291,6 +310,54 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen this.access_token = access_token; } + /** + * 加载当前同步器历史同步的企业微信用户映射,用于判定本次缺失用户。 + * + * @return 以企业微信用户 originId 为键的同步关系映射 + */ + private Map loadUserRelationHistory() { + Map _userRelationHistoryMap = new HashMap<>(); + List historyRecords = synchroRelatedService.find( + "instid = ? and syncId = ? and objecttype = ?", + new Object[]{synchronizer.getInstId(), synchronizer.getId(), UserInfo.CLASS_TYPE}, + new int[]{Types.VARCHAR, Types.VARCHAR, Types.VARCHAR} + ); + if (historyRecords != null) { + for (SynchroRelated related : historyRecords) { + _userRelationHistoryMap.put(related.getOriginId(), related); + } + } + return _userRelationHistoryMap; + } + + /** + * 禁用本次企业微信未返回的历史同步用户,并将 userState 标记为 WITHDRAWN。 + * + * @param _userRelationHistoryMap 尚未在本次同步中匹配到的历史同步关系 + */ + private void disableMissingUsers(Map _userRelationHistoryMap) { + if (_userRelationHistoryMap.isEmpty()) { + _logger.info("[同步微信用户] 本次同步未发现需要禁用的历史用户。"); + return; + } + _logger.info("[同步微信用户] 发现 {} 个历史同步用户未出现在本次企业微信返回,开始禁用。", _userRelationHistoryMap.size()); + for (SynchroRelated orphanRelation : _userRelationHistoryMap.values()) { + UserInfo localUser = userInfoService.get(orphanRelation.getObjectId()); + if (localUser == null) { + _logger.warn("[同步微信用户] 历史同步用户 originId={} (objectId={}) 在本地不存在,跳过禁用。", orphanRelation.getOriginId(), orphanRelation.getObjectId()); + continue; + } + if (localUser.getStatus() == ConstsStatus.DISABLED && "WITHDRAWN".equals(localUser.getUserState())) { + _logger.debug("[同步微信用户] 用户 {} (originId={}) 已处于禁用且离职状态,跳过更新。", localUser.getUsername(), orphanRelation.getOriginId()); + continue; + } + localUser.setStatus(ConstsStatus.DISABLED); + localUser.setUserState("WITHDRAWN"); + userInfoService.update(localUser); + _logger.info("[同步微信用户] 用户 {} (originId={}) 已标记为禁用,userState=WITHDRAWN。", localUser.getUsername(), orphanRelation.getOriginId()); + } + } + public SyncJobConfigFieldService getSyncJobConfigFieldService() { return syncJobConfigFieldService; }