Merge pull request #260 from link2fun/fix-weixinwork-sync-issue

完善企业微信同步器同步 同步组织和用户覆盖更多场景
This commit is contained in:
MaxKey 2025-11-07 08:32:47 +08:00 committed by GitHub
commit 9f8b5b9d1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 509 additions and 131 deletions

View File

@ -31,5 +31,11 @@ public interface SynchroRelatedService extends IJpaService<SynchroRelated>{
public SynchroRelated findByOriginId(Synchronizers synchronizer,String originId,String classType) ; 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) ; public void updateSynchroRelated(Synchronizers synchronizer,SynchroRelated synchroRelated,String classType) ;
} }

View File

@ -18,11 +18,11 @@
package org.dromara.maxkey.synchronizer.workweixin; package org.dromara.maxkey.synchronizer.workweixin;
import org.dromara.maxkey.constants.ConstsStatus; import org.dromara.maxkey.constants.ConstsStatus;
import org.dromara.maxkey.entity.SyncJobConfigField;
import org.dromara.maxkey.entity.SynchroRelated; import org.dromara.maxkey.entity.SynchroRelated;
import org.dromara.maxkey.entity.idm.Organizations; import org.dromara.maxkey.entity.idm.Organizations;
import org.dromara.maxkey.synchronizer.AbstractSynchronizerService; import org.dromara.maxkey.synchronizer.AbstractSynchronizerService;
import org.dromara.maxkey.synchronizer.ISynchronizerService; import org.dromara.maxkey.synchronizer.ISynchronizerService;
import org.dromara.maxkey.entity.SyncJobConfigField;
import org.dromara.maxkey.synchronizer.service.SyncJobConfigFieldService; import org.dromara.maxkey.synchronizer.service.SyncJobConfigFieldService;
import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDepts; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDepts;
import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDeptsResponse; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinDeptsResponse;
@ -32,8 +32,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -41,14 +44,14 @@ import java.util.Map;
import static org.dromara.maxkey.synchronizer.utils.FieldUtil.*; import static org.dromara.maxkey.synchronizer.utils.FieldUtil.*;
@Service @Service
public class WorkweixinOrganizationService extends AbstractSynchronizerService implements ISynchronizerService{ public class WorkweixinOrganizationService extends AbstractSynchronizerService implements ISynchronizerService {
static final Logger _logger = LoggerFactory.getLogger(WorkweixinOrganizationService.class); static final Logger _logger = LoggerFactory.getLogger(WorkweixinOrganizationService.class);
String access_token; String access_token;
@Autowired @Autowired
private SyncJobConfigFieldService syncJobConfigFieldService; private SyncJobConfigFieldService syncJobConfigFieldService;
private static final Integer ORG_TYPE = 2; 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; static long ROOT_DEPT_ID = 1;
public void sync() { public void sync() {
@ -57,78 +60,138 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i
try { try {
WorkWeixinDeptsResponse rsp = requestDepartmentList(access_token); 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()));
// 从映射里面拿到企业微信Id映射后的本地组织的字段 用于判断本地的组织是否存在
String targetIdField = getLocalFieldMappingByWx(fieldMap, "id");
for (WorkWeixinDepts deptWxCur : deptWxListAfterLevelSort) {
_logger.debug("sync workweixin dept : {} {} {}", deptWxCur.getId(), deptWxCur.getName(), deptWxCur.getParentid());
//root //root
if(dept.getId() == ROOT_DEPT_ID) { if (deptWxCur.getId() == ROOT_DEPT_ID) {
// 当前根节点
Organizations rootOrganization = organizationsService.get(Organizations.ROOT_ORG_ID); Organizations rootOrganization = organizationsService.get(Organizations.ROOT_ORG_ID);
SynchroRelated rootSynchroRelated = buildSynchroRelated(rootOrganization,dept); if (rootOrganization == null) {
_logger.error("根组织不存在(ID: {}), 无法同步企业微信根部门", Organizations.ROOT_ORG_ID);
throw new RuntimeException("根组织不存在, 同步失败! 请先确保系统中存在根组织(ID: " + Organizations.ROOT_ORG_ID + ")");
}
// 构建同步关系
SynchroRelated rootSynchroRelated = buildSynchroRelated(rootOrganization, deptWxCur);
// 更新同步关系
synchroRelatedService.updateSynchroRelated( synchroRelatedService.updateSynchroRelated(
this.synchronizer,rootSynchroRelated,Organizations.CLASS_TYPE); this.synchronizer, rootSynchroRelated, Organizations.CLASS_TYPE);
}else { // 是否更新根节点的编码待确认, 这里先更新名称
//synchro Related rootOrganization.setOrgName(deptWxCur.getName());
organizationsService.update(rootOrganization);
} else {
// 现在不是根组织
//synchro Related 查询当前部门是否有同步记录 这里只是查有没有关系, 不是查组织
SynchroRelated synchroRelated = SynchroRelated synchroRelated =
synchroRelatedService.findByOriginId( synchroRelatedService.findByOriginId(
this.synchronizer,dept.getId() + "",Organizations.CLASS_TYPE ); this.synchronizer, deptWxCur.getId() + "", Organizations.CLASS_TYPE);
//Parent //Parent 查询当前部门父部门是否有同步记录 这里只是查有没有关系, 不是查组织
SynchroRelated synchroRelatedParent = SynchroRelated synchroRelatedParent =
synchroRelatedService.findByOriginId( synchroRelatedService.findByOriginId(
this.synchronizer,dept.getParentid() + "",Organizations.CLASS_TYPE); this.synchronizer, deptWxCur.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 { Organizations orgCurrent = buildOrgByFiledMap(deptWxCur, synchroRelatedParent, fieldMap);
organization.setId(synchroRelated.getObjectId()); // 这里需要修正一下层级关系, 防止因为映射关系错误导致的层级错乱
organizationsService.update(organization); String deptWxParentId = String.valueOf(deptWxCur.getParentid());
// 进入到这个节点的应该都是有上级的, 现在只需要根据上级Id查询上级的组织档案
Organizations parentOrg = findOrganizationByField(targetIdField, deptWxParentId);
// 这里父级不应该为 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 = findOrganizationByField(targetIdField, String.valueOf(deptWxCur.getId()));
if (currentOrg == null) {
// 当前部门已经被删除, 那就需要重新写入一次
orgCurrent.setId(synchroRelated.getObjectId());
organizationsService.insert(orgCurrent);
} else {
// 组织存在, 执行更新操作
orgCurrent.setId(synchroRelated.getObjectId());
organizationsService.update(orgCurrent);
}
} }
synchroRelatedService.updateSynchroRelated( synchroRelatedService.updateSynchroRelated(
this.synchronizer,synchroRelated,Organizations.CLASS_TYPE); this.synchronizer, synchroRelated, Organizations.CLASS_TYPE);
} }
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); _logger.error("同步企业微信组织失败", e);
throw new RuntimeException("同步企业微信组织失败: " + e.getMessage(), e);
} }
} }
public SynchroRelated buildSynchroRelated(Organizations organization,WorkWeixinDepts dept) { /**
* 构建同步关系
*
* @param organization 组织实体
* @param dept 企业微信部门实体
* @return 同步关系
*/
public SynchroRelated buildSynchroRelated(Organizations organization, WorkWeixinDepts dept) {
return new SynchroRelated( return new SynchroRelated(
organization.getId(), organization.getId(), // objectId 系统内组织ID
organization.getOrgName(), organization.getOrgName(), // objectName 系统内组织名称
organization.getOrgName(), organization.getOrgName(), // objectDisplayName 系统内组织显示名称
Organizations.CLASS_TYPE, Organizations.CLASS_TYPE, // objectType 对象类型
synchronizer.getId(), synchronizer.getId(), // syncId 同步器ID
synchronizer.getName(), synchronizer.getName(), // syncName 同步器名称
dept.getId()+"", dept.getId() + "", // originId 企业微信部门ID
dept.getName(), dept.getName(), // originName 企业微信部门名称
"", "",
dept.getParentid()+"", dept.getParentid() + "", // originId3 父部门ID
synchronizer.getInstId()); synchronizer.getInstId());
} }
public WorkWeixinDeptsResponse requestDepartmentList(String access_token) { public WorkWeixinDeptsResponse requestDepartmentList(String access_token) {
HttpRequestAdapter request =new HttpRequestAdapter(); HttpRequestAdapter request = new HttpRequestAdapter();
String responseBody = request.get(String.format(DEPTS_URL, access_token)); 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); _logger.trace("response : " + responseBody);
for(WorkWeixinDepts dept : deptsResponse.getDepartment()) { for (WorkWeixinDepts dept : deptsResponse.getDepartment()) {
_logger.debug("WorkWeixinDepts : " + dept); _logger.debug("WorkWeixinDepts : " + dept);
} }
return deptsResponse; return deptsResponse;
} }
public Organizations buildOrganization(WorkWeixinDepts dept,SynchroRelated synchroRelatedParent) { public Organizations buildOrganization(WorkWeixinDepts dept, SynchroRelated synchroRelatedParent) {
Organizations org = new Organizations(); Organizations org = new Organizations();
org.setOrgName(dept.getName()); org.setOrgName(dept.getName());
org.setOrgCode(dept.getId()+""); org.setOrgCode(dept.getId() + "");
org.setParentId(synchroRelatedParent.getObjectId()); org.setParentId(synchroRelatedParent.getObjectId());
org.setParentName(synchroRelatedParent.getObjectName()); org.setParentName(synchroRelatedParent.getObjectName());
org.setSortIndex(dept.getOrder()); org.setSortIndex(dept.getOrder());
@ -138,11 +201,35 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i
return org; 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(String.format(
"未找到企业微信字段'%s'映射后的本地字段,请检查同步器(ID: %s)的字段映射配置",
expectField, this.synchronizer.getId()));
}
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(); Organizations org = new Organizations();
//fieldMap
Map<String, String> fieldMap = getFieldMap(Long.parseLong(synchronizer.getId()));
for (Map.Entry<String, String> entry : fieldMap.entrySet()) { for (Map.Entry<String, String> entry : fieldMap.entrySet()) {
@ -153,8 +240,7 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i
if (hasField(dept.getClass(), sourceProperty)) { if (hasField(dept.getClass(), sourceProperty)) {
sourceValue = getFieldValue(dept, 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); sourceValue = getFieldValue(synchroRelatedParent, sourceProperty);
} }
if (sourceValue != null) { if (sourceValue != null) {
@ -172,21 +258,108 @@ public class WorkweixinOrganizationService extends AbstractSynchronizerService i
} }
public Map<String,String> getFieldMap(Long jobId){ public Map<String, String> getFieldMap(Long jobId) {
Map<String,String> filedMap = new HashMap<>(); Map<String, String> filedMap = new HashMap<>();
//根据job id查询属性映射表 //根据job id查询属性映射表
List<SyncJobConfigField> syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId); List<SyncJobConfigField> syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId);
//获取组织属性映射 //获取组织属性映射
for(SyncJobConfigField element:syncJobConfigFieldList){ for (SyncJobConfigField element : syncJobConfigFieldList) {
if(Integer.parseInt(element.getObjectType()) == ORG_TYPE.intValue()){ if (Integer.parseInt(element.getObjectType()) == ORG_TYPE) {
filedMap.put(element.getTargetField(), element.getSourceField()); filedMap.put(element.getTargetField(), element.getSourceField());
} }
} }
return filedMap; 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}
);
}
/**
* 对部门列表进行排序确保父节点在前子节点在后
* 使用拓扑排序算法按照层级顺序遍历部门树
*
* @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() { public String getAccess_token() {

View File

@ -17,17 +17,13 @@
package org.dromara.maxkey.synchronizer.workweixin; package org.dromara.maxkey.synchronizer.workweixin;
import java.lang.reflect.InvocationTargetException; import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dromara.maxkey.constants.ConstsStatus; import org.dromara.maxkey.constants.ConstsStatus;
import org.dromara.maxkey.entity.SyncJobConfigField;
import org.dromara.maxkey.entity.SynchroRelated; import org.dromara.maxkey.entity.SynchroRelated;
import org.dromara.maxkey.entity.idm.UserInfo; import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.synchronizer.AbstractSynchronizerService; import org.dromara.maxkey.synchronizer.AbstractSynchronizerService;
import org.dromara.maxkey.synchronizer.ISynchronizerService; import org.dromara.maxkey.synchronizer.ISynchronizerService;
import org.dromara.maxkey.entity.SyncJobConfigField;
import org.dromara.maxkey.synchronizer.service.SyncJobConfigFieldService; import org.dromara.maxkey.synchronizer.service.SyncJobConfigFieldService;
import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinUsers; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinUsers;
import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinUsersResponse; import org.dromara.maxkey.synchronizer.workweixin.entity.WorkWeixinUsersResponse;
@ -38,10 +34,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; 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.*; import static org.dromara.maxkey.synchronizer.utils.FieldUtil.*;
@Service @Service
public class WorkweixinUsersService extends AbstractSynchronizerService implements ISynchronizerService{ public class WorkweixinUsersService extends AbstractSynchronizerService implements ISynchronizerService {
final static Logger _logger = LoggerFactory.getLogger(WorkweixinUsersService.class); final static Logger _logger = LoggerFactory.getLogger(WorkweixinUsersService.class);
@Autowired @Autowired
@ -49,52 +51,154 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen
private static final Integer USER_TYPE = 1; private static final Integer USER_TYPE = 1;
String access_token; 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() { public void sync() {
_logger.info("Sync Workweixin Users..."); _logger.info("Sync Workweixin Users...");
try { try {
List<SynchroRelated> synchroRelateds = // 获取前面已经拉取下来的微信部门同步记录
synchroRelatedService.findOrgs(this.synchronizer); List<SynchroRelated> synchroRelatedOrgList = synchroRelatedService.findOrgs(this.synchronizer);
// 加载同步器的历史数据
Map<String, SynchroRelated> userRelationHistoryMap = loadUserRelationHistory();
for(SynchroRelated relatedOrg : synchroRelateds) { // 获取同步器的字段映射
HttpRequestAdapter request =new HttpRequestAdapter(); Map<String, String> fieldMap = getFieldMap(Long.parseLong(synchronizer.getId()));
String responseBody = request.get(String.format(USERS_URL, access_token,relatedOrg.getOriginId())); // 拿到微信用户Id映射后的MaxKey字段名称
WorkWeixinUsersResponse usersResponse =JsonUtils.gsonStringToObject(responseBody, WorkWeixinUsersResponse.class); String wxUserIdInMaxkeyField = getWxUserMappingField(fieldMap, "userid");
_logger.trace("response : " + responseBody);
for(WorkWeixinUsers user : usersResponse.getUserlist()) { boolean fetchFailed = false;
UserInfo userInfo = buildUserInfoByFiledMap(user); for (SynchroRelated relatedOrg : synchroRelatedOrgList) {
_logger.debug("userInfo : " + userInfo); // 根据微信部门ID拉取微信用户列表, 这里拉取的是直属员工
userInfo.setPassword(userInfo.getUsername() + UserInfo.DEFAULT_PASSWORD_SUFFIX); HttpRequestAdapter request = new HttpRequestAdapter();
userInfoService.saveOrUpdate(userInfo); String responseBody = request.get(String.format(USERS_URL, access_token, relatedOrg.getOriginId()));
WorkWeixinUsersResponse usersResponse = JsonUtils.gsonStringToObject(responseBody, WorkWeixinUsersResponse.class);
_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);
// 一个企业微信可能属于多个部门, 所以需要从 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( SynchroRelated synchroRelated = new SynchroRelated(
userInfo.getId(), maxkeyUserNew.getId(), // objectId: maxkey 里面的用户ID
userInfo.getUsername(), maxkeyUserNew.getUsername(), // objectName: maxkey 里面的用户名
userInfo.getDisplayName(), maxkeyUserNew.getDisplayName(), // displayName: maxkey 里面的显示名称
UserInfo.CLASS_TYPE, UserInfo.CLASS_TYPE, // objectType: 对象类型 是用户
synchronizer.getId(), synchronizer.getId(), // jobId: 同步器ID
synchronizer.getName(), synchronizer.getName(), // jobName: 同步器名称
user.getUserid(), wxUser.getUserid(), // originId: 企业微信用户ID
user.getName(), wxUser.getName(), // originName: 企业微信用户名
user.getUserid(), wxUser.getUserid(), // originId2: 企业微信用户ID
"", "", // originId3: 暂无
synchronizer.getInstId()); synchronizer.getInstId()); // instId: 机构ID
synchroRelatedService.updateSynchroRelated( synchroRelatedService.updateSynchroRelated(
this.synchronizer,synchroRelated,UserInfo.CLASS_TYPE); this.synchronizer, synchroRelated, UserInfo.CLASS_TYPE);
socialsAssociate(synchroRelated,"workweixin"); socialsAssociate(synchroRelated, "workweixin");
} }
} }
if (fetchFailed) {
_logger.warn("[同步微信用户] 存在部门用户拉取失败,跳过缺失用户禁用流程。");
return;
}
disableMissingUsers(userRelationHistoryMap);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); _logger.error("[同步微信用户] 同步用户失败", e);
} }
} }
private static void updateExistingUserInfo(Map<String, String> 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 void postSync(UserInfo userInfo) {
} }
@ -110,23 +214,22 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen
userInfo.setGender(Integer.parseInt(user.getGender())); userInfo.setGender(Integer.parseInt(user.getGender()));
userInfo.setWorkPhoneNumber(user.getTelephone());//工作电话 userInfo.setWorkPhoneNumber(user.getTelephone());//工作电话
userInfo.setDepartmentId(user.getMain_department()+""); userInfo.setDepartmentId(user.getMain_department() + "");
userInfo.setJobTitle(user.getPosition());//职务 userInfo.setJobTitle(user.getPosition());//职务
userInfo.setWorkAddressFormatted(user.getAddress());//工作地点 userInfo.setWorkAddressFormatted(user.getAddress());//工作地点
//激活状态: 1=已激活2=已禁用4=未激活5=退出企业 //激活状态: 1=已激活2=已禁用4=未激活5=退出企业
if(user.getStatus() == 1) { if (user.getStatus() == 1) {
userInfo.setStatus(ConstsStatus.ACTIVE); userInfo.setStatus(ConstsStatus.ACTIVE);
}else { } else {
userInfo.setStatus(ConstsStatus.INACTIVE); userInfo.setStatus(ConstsStatus.INACTIVE);
} }
userInfo.setInstId(this.synchronizer.getInstId()); userInfo.setInstId(this.synchronizer.getInstId());
return userInfo; return userInfo;
} }
public UserInfo buildUserInfoByFiledMap(WorkWeixinUsers user){ public UserInfo buildUserInfoByFiledMap(WorkWeixinUsers user, Map<String, String> fieldMap) {
UserInfo userInfo = new UserInfo(); UserInfo userInfo = new UserInfo();
Map<String, String> fieldMap = getFieldMap(Long.parseLong(synchronizer.getId()));
for (Map.Entry<String, String> entry : fieldMap.entrySet()) { for (Map.Entry<String, String> entry : fieldMap.entrySet()) {
String userInfoProperty = entry.getKey(); String userInfoProperty = entry.getKey();
@ -134,8 +237,8 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen
try { try {
Object sourceValue = null; Object sourceValue = null;
if(sourceProperty.equals("status")){ if (sourceProperty.equals("status")) {
userInfo.setStatus(user.getStatus() == 1?ConstsStatus.ACTIVE:ConstsStatus.INACTIVE); userInfo.setStatus(user.getStatus() == 1 ? ConstsStatus.ACTIVE : ConstsStatus.INACTIVE);
continue; continue;
} }
if (hasField(user.getClass(), sourceProperty)) { if (hasField(user.getClass(), sourceProperty)) {
@ -146,7 +249,7 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen
} }
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace(); _logger.error("buildUserInfoByFiledMap error: sourceProperty: {}, error: {}", sourceProperty, e.getMessage());
} }
} }
@ -156,22 +259,105 @@ public class WorkweixinUsersService extends AbstractSynchronizerService implemen
return userInfo; return userInfo;
} }
public Map<String,String> getFieldMap(Long jobId){ public Map<String, String> getFieldMap(Long jobId) {
Map<String,String> userFieldMap = new HashMap<>(); Map<String, String> userFieldMap = new HashMap<>();
//根据job id查询属性映射表 //根据job id查询属性映射表
List<SyncJobConfigField> syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId); List<SyncJobConfigField> syncJobConfigFieldList = syncJobConfigFieldService.findByJobId(jobId);
//获取用户属性映射 //获取用户属性映射
for(SyncJobConfigField element:syncJobConfigFieldList){ for (SyncJobConfigField element : syncJobConfigFieldList) {
if(Integer.parseInt(element.getObjectType()) == USER_TYPE.intValue()){ if (Integer.parseInt(element.getObjectType()) == USER_TYPE.intValue()) {
userFieldMap.put(element.getTargetField(), element.getSourceField()); userFieldMap.put(element.getTargetField(), element.getSourceField());
} }
} }
return userFieldMap; return userFieldMap;
} }
/**
* 从字段映射中获取企业微信字段映射后的MaxKey字段名称
*
* @param fieldMap 字段映射
* @param wxField 企业微信字段名称
* @return MaxKey字段名称
*/
public String getWxUserMappingField(Map<String, String> fieldMap, String wxField) {
for (Map.Entry<String, String> 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) { public void setAccess_token(String access_token) {
this.access_token = access_token; this.access_token = access_token;
} }
/**
* 加载当前同步器历史同步的企业微信用户映射用于判定本次缺失用户
*
* @return 以企业微信用户 originId 为键的同步关系映射
*/
private Map<String, SynchroRelated> loadUserRelationHistory() {
Map<String, SynchroRelated> _userRelationHistoryMap = new HashMap<>();
List<SynchroRelated> 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<String, SynchroRelated> _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() { public SyncJobConfigFieldService getSyncJobConfigFieldService() {
return syncJobConfigFieldService; return syncJobConfigFieldService;
} }

View File

@ -69,4 +69,14 @@ public class WorkWeixinDepts {
this.order = order; this.order = order;
} }
@Override
public String toString() {
return "WorkWeixinDepts{" +
"id=" + id +
", name='" + name + '\'' +
", name_en='" + name_en + '\'' +
", parentid=" + parentid +
", order=" + order +
'}';
}
} }

View File

@ -62,6 +62,9 @@ public class FieldUtil {
} }
public static Object convertValueToFieldType(Object value, Class<?> fieldType) { public static Object convertValueToFieldType(Object value, Class<?> fieldType) {
if (value == null) {
return null;
}
if (fieldType.isInstance(value)) { if (fieldType.isInstance(value)) {
return value; return value;
} }