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] =?UTF-8?q?=E5=AF=B9=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=E8=BF=9B=E8=A1=8C=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=9C=BA=E6=99=AF=E5=88=A4=E6=96=AD=20,=20=E5=A4=A7?= =?UTF-8?q?=E8=87=B4=E5=BA=94=E8=AF=A5=E6=98=AF=E5=88=86=E6=88=90=E4=BA=86?= =?UTF-8?q?4=E7=A7=8D=E6=83=85=E5=86=B5=201.=20=E6=9C=89=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E8=AE=B0=E5=BD=95,=20=E7=94=A8=E6=88=B7=E5=AD=98=E5=9C=A8=20?= =?UTF-8?q?=20->=20=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=202.=20=E6=9C=89=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?=E9=87=8D=E6=96=B0=E5=88=9B=E5=BB=BA=E7=94=A8=E6=88=B7=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=8E=9F=E6=9C=AC=E7=9A=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?Id=203.=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=E5=AD=98=E5=9C=A8=20=20->=20=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E6=98=AF=E6=89=8B=E5=B7=A5=E5=88=9B=E5=BB=BA=E7=9A=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7,=20=E6=89=8B=E5=8A=A8=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E7=9A=84=E7=94=A8=E6=88=B7=E5=BA=94=E5=BD=93=E6=98=AF=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E7=9B=B4=E6=8E=A5=E5=85=B3=E8=81=94,=20=E5=9B=A0?= =?UTF-8?q?=E4=B8=BA=E6=97=A0=E6=B3=95=E7=A1=AE=E8=AE=A4=20=E8=BF=99?= =?UTF-8?q?=E4=B8=AA=E7=94=A8=E6=88=B7=E5=92=8C=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=90=8C=E6=AD=A5=E8=BF=87=E6=9D=A5=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=98=AF=E5=90=8C=E4=B8=80=E4=B8=AA=E4=BA=BA=204.=20?= =?UTF-8?q?=E6=97=A0=E5=90=8C=E6=AD=A5=E8=AE=B0=E5=BD=95,=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=8D=E5=AD=98=E5=9C=A8=20=20->=20=E6=AD=A3?= =?UTF-8?q?=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; }