feat(feishu): 增加飞书用户同步功能及相关API和定时任务
- 新增飞书用户同步控制器,提供手动全量及按部门同步接口 - 新增飞书员工信息相关DTO,支持飞书API响应数据映射 - 新增飞书员工列表查询请求和响应DTO,支持分页查询功能 - 实现飞书SDK客户端服务,封装调用飞书官方API逻辑 - 实现飞书用户同步服务,支持全量及按部门同步,处理分页与数据持久化 - 增加飞书用户同步定时任务,每天0点自动同步飞书员工信息 - 在主应用类启用计划任务支持(@EnableScheduling) - 优化全局异常处理中Token无效提示信息 - 在BaseResponse增加success和error静态方法便捷创建响应对象 - 支持BusinessException新增仅消息构造方法,简化异常创建 - pom.xml中更新sa-token-redis注释,强调分布式会话持久化用途
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -138,7 +138,7 @@
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token Redis 集成(可选,用于分布式环境) -->
|
||||
<!-- Sa-Token Redis 集成(用于分布式环境会话持久化) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
|
||||
@@ -2,7 +2,12 @@ package cn.yinlihupo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* AI项目进度与风险管控平台启动类
|
||||
*/
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
public class YlhpAiProjectManagerApplication {
|
||||
|
||||
|
||||
@@ -32,4 +32,27 @@ public class BaseResponse<T> implements Serializable {
|
||||
public BaseResponse(ErrorCode errorCode) {
|
||||
this(errorCode.getCode(), null, errorCode.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param data 数据
|
||||
* @param <T> 数据类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public static <T> BaseResponse<T> success(T data) {
|
||||
return new BaseResponse<>(0, data, "success");
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*
|
||||
* @param message 错误消息
|
||||
* @param data 数据
|
||||
* @param <T> 数据类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public static <T> BaseResponse<T> error(String message, T data) {
|
||||
return new BaseResponse<>(500, data, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ public class BusinessException extends RuntimeException {
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = ErrorCode.SYSTEM_ERROR.getCode();
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,6 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public BaseResponse<?> notLoginExceptionHandler(NotLoginException e) {
|
||||
log.error("NotLoginException", e);
|
||||
return ResultUtils.error(ErrorCode.TOKEN_INVALID, "没有传递token");
|
||||
return ResultUtils.error(ErrorCode.TOKEN_INVALID, "没有传递token或登录已失效");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.yinlihupo.controller.system;
|
||||
|
||||
import cn.yinlihupo.common.core.BaseResponse;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuUserSyncResult;
|
||||
import cn.yinlihupo.service.system.FeishuUserSyncService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 飞书用户同步控制器
|
||||
* 提供手动触发用户同步的接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/feishu/sync")
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuSyncController {
|
||||
|
||||
private final FeishuUserSyncService feishuUserSyncService;
|
||||
|
||||
/**
|
||||
* 手动同步所有飞书员工
|
||||
* 同步所有在职员工信息到系统用户表
|
||||
*
|
||||
* @return 同步结果
|
||||
*/
|
||||
@PostMapping("/users")
|
||||
public BaseResponse<FeishuUserSyncResult> syncAllUsers() {
|
||||
log.info("手动触发飞书用户全量同步");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
FeishuUserSyncResult result = feishuUserSyncService.syncAllEmployees();
|
||||
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
log.info("手动同步完成,耗时: {}ms, 结果: {}", costTime, result);
|
||||
|
||||
if (result.getSuccess()) {
|
||||
return BaseResponse.success(result);
|
||||
} else {
|
||||
return BaseResponse.error(result.getMessage(), result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID同步员工
|
||||
*
|
||||
* @param departmentId 飞书部门ID
|
||||
* @return 同步结果
|
||||
*/
|
||||
@PostMapping("/users/department/{departmentId}")
|
||||
public BaseResponse<FeishuUserSyncResult> syncUsersByDepartment(
|
||||
@PathVariable("departmentId") String departmentId) {
|
||||
log.info("手动触发部门用户同步, departmentId: {}", departmentId);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
FeishuUserSyncResult result = feishuUserSyncService.syncEmployeesByDepartment(departmentId);
|
||||
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
log.info("部门同步完成,耗时: {}ms, 结果: {}", costTime, result);
|
||||
|
||||
if (result.getSuccess()) {
|
||||
return BaseResponse.success(result);
|
||||
} else {
|
||||
return BaseResponse.error(result.getMessage(), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package cn.yinlihupo.domain.dto.feishu;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 飞书员工信息DTO
|
||||
* 对应飞书 directory/v1/employees/filter 接口返回的员工数据
|
||||
*/
|
||||
@Data
|
||||
public class FeishuEmployeeDTO {
|
||||
|
||||
/**
|
||||
* 基础信息
|
||||
*/
|
||||
@JsonProperty("base_info")
|
||||
private BaseInfo baseInfo;
|
||||
|
||||
@Data
|
||||
public static class BaseInfo {
|
||||
|
||||
/**
|
||||
* 飞书用户唯一标识 (employee_id/open_id)
|
||||
*/
|
||||
@JsonProperty("employee_id")
|
||||
private String employeeId;
|
||||
|
||||
/**
|
||||
* 用户姓名
|
||||
*/
|
||||
private I18nField name;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 企业邮箱
|
||||
*/
|
||||
@JsonProperty("enterprise_email")
|
||||
private String enterpriseEmail;
|
||||
|
||||
/**
|
||||
* 性别: 1-男, 2-女
|
||||
*/
|
||||
private Integer gender;
|
||||
|
||||
/**
|
||||
* 头像信息
|
||||
*/
|
||||
private AvatarInfo avatar;
|
||||
|
||||
/**
|
||||
* 部门列表
|
||||
*/
|
||||
private List<DepartmentInfo> departments;
|
||||
|
||||
/**
|
||||
* 部门路径信息
|
||||
*/
|
||||
@JsonProperty("department_path_infos")
|
||||
private List<List<DepartmentPathInfo>> departmentPathInfos;
|
||||
|
||||
/**
|
||||
* 职位信息
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否离职
|
||||
*/
|
||||
@JsonProperty("is_resigned")
|
||||
private Boolean isResigned;
|
||||
|
||||
/**
|
||||
* 激活状态
|
||||
*/
|
||||
@JsonProperty("active_status")
|
||||
private Integer activeStatus;
|
||||
|
||||
/**
|
||||
* 是否管理员
|
||||
*/
|
||||
@JsonProperty("is_admin")
|
||||
private Boolean isAdmin;
|
||||
|
||||
/**
|
||||
* 是否超级管理员
|
||||
*/
|
||||
@JsonProperty("is_primary_admin")
|
||||
private Boolean isPrimaryAdmin;
|
||||
|
||||
/**
|
||||
* 入职时间 (时间戳,毫秒)
|
||||
*/
|
||||
@JsonProperty("entry_time")
|
||||
private String entryTime;
|
||||
|
||||
/**
|
||||
* 离职时间 (时间戳,毫秒)
|
||||
*/
|
||||
@JsonProperty("resign_time")
|
||||
private String resignTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class I18nField {
|
||||
private FieldValue name;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FieldValue {
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
@JsonProperty("default_value")
|
||||
private String defaultValue;
|
||||
|
||||
/**
|
||||
* 多语言值
|
||||
*/
|
||||
@JsonProperty("i18n_value")
|
||||
private Map<String, String> i18nValue;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AvatarInfo {
|
||||
/**
|
||||
* 72x72 头像
|
||||
*/
|
||||
@JsonProperty("avatar_72")
|
||||
private String avatar72;
|
||||
|
||||
/**
|
||||
* 240x240 头像
|
||||
*/
|
||||
@JsonProperty("avatar_240")
|
||||
private String avatar240;
|
||||
|
||||
/**
|
||||
* 640x640 头像
|
||||
*/
|
||||
@JsonProperty("avatar_640")
|
||||
private String avatar640;
|
||||
|
||||
/**
|
||||
* 原图头像
|
||||
*/
|
||||
@JsonProperty("avatar_origin")
|
||||
private String avatarOrigin;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DepartmentInfo {
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
@JsonProperty("department_id")
|
||||
private String departmentId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private Map<String, String> name;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DepartmentPathInfo {
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
@JsonProperty("department_id")
|
||||
private String departmentId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
@JsonProperty("department_name")
|
||||
private I18nField departmentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户真实姓名
|
||||
*
|
||||
* @return 真实姓名
|
||||
*/
|
||||
public String getRealName() {
|
||||
if (baseInfo != null && baseInfo.getName() != null && baseInfo.getName().getName() != null) {
|
||||
return baseInfo.getName().getName().getDefaultValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手机号(去除+86前缀)
|
||||
*
|
||||
* @return 手机号
|
||||
*/
|
||||
public String getPhoneNumber() {
|
||||
if (baseInfo != null && baseInfo.getMobile() != null) {
|
||||
String mobile = baseInfo.getMobile();
|
||||
// 去除 +86 前缀
|
||||
if (mobile.startsWith("+86")) {
|
||||
return mobile.substring(3);
|
||||
}
|
||||
return mobile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业邮箱
|
||||
*
|
||||
* @return 企业邮箱
|
||||
*/
|
||||
public String getEmail() {
|
||||
if (baseInfo != null) {
|
||||
// 优先返回企业邮箱
|
||||
if (baseInfo.getEnterpriseEmail() != null && !baseInfo.getEnterpriseEmail().isEmpty()) {
|
||||
return baseInfo.getEnterpriseEmail();
|
||||
}
|
||||
return baseInfo.getEmail();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像URL(优先使用240x240尺寸)
|
||||
*
|
||||
* @return 头像URL
|
||||
*/
|
||||
public String getAvatarUrl() {
|
||||
if (baseInfo != null && baseInfo.getAvatar() != null) {
|
||||
AvatarInfo avatar = baseInfo.getAvatar();
|
||||
if (avatar.getAvatar240() != null) {
|
||||
return avatar.getAvatar240();
|
||||
}
|
||||
if (avatar.getAvatar72() != null) {
|
||||
return avatar.getAvatar72();
|
||||
}
|
||||
return avatar.getAvatarOrigin();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书OpenID
|
||||
*
|
||||
* @return OpenID
|
||||
*/
|
||||
public String getOpenId() {
|
||||
if (baseInfo != null) {
|
||||
return baseInfo.getEmployeeId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性别
|
||||
*
|
||||
* @return 性别: 0-未知, 1-男, 2-女
|
||||
*/
|
||||
public Integer getGender() {
|
||||
if (baseInfo != null && baseInfo.getGender() != null) {
|
||||
return baseInfo.getGender();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主部门名称
|
||||
*
|
||||
* @return 部门名称
|
||||
*/
|
||||
public String getPrimaryDepartmentName() {
|
||||
if (baseInfo != null && baseInfo.getDepartmentPathInfos() != null
|
||||
&& !baseInfo.getDepartmentPathInfos().isEmpty()) {
|
||||
List<List<DepartmentPathInfo>> pathInfos = baseInfo.getDepartmentPathInfos();
|
||||
// 取第一个部门路径的最后一个节点作为主部门
|
||||
for (List<DepartmentPathInfo> path : pathInfos) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
DepartmentPathInfo lastDept = path.get(path.size() - 1);
|
||||
if (lastDept.getDepartmentName() != null && lastDept.getDepartmentName().getName() != null) {
|
||||
return lastDept.getDepartmentName().getName().getDefaultValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主部门ID
|
||||
*
|
||||
* @return 部门ID
|
||||
*/
|
||||
public String getPrimaryDepartmentId() {
|
||||
if (baseInfo != null && baseInfo.getDepartments() != null && !baseInfo.getDepartments().isEmpty()) {
|
||||
// 取第一个部门作为主部门
|
||||
return baseInfo.getDepartments().get(0).getDepartmentId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在职
|
||||
*
|
||||
* @return true-在职, false-离职
|
||||
*/
|
||||
public Boolean isActive() {
|
||||
if (baseInfo != null && baseInfo.getIsResigned() != null) {
|
||||
return !baseInfo.getIsResigned();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cn.yinlihupo.domain.dto.feishu;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 飞书员工列表查询请求DTO
|
||||
* 对应飞书 directory/v1/employees/filter 接口请求体
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class FeishuEmployeeListRequest {
|
||||
|
||||
/**
|
||||
* 查询筛选条件
|
||||
*/
|
||||
private Filter filter;
|
||||
|
||||
/**
|
||||
* 需要返回的字段列表
|
||||
*/
|
||||
@JsonProperty("required_fields")
|
||||
private List<String> requiredFields;
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
@JsonProperty("page_request")
|
||||
private PageRequest pageRequest;
|
||||
|
||||
/**
|
||||
* 构建查询在职员工的默认请求
|
||||
*
|
||||
* @param pageSize 每页大小
|
||||
* @param pageToken 分页标记
|
||||
* @return 请求对象
|
||||
*/
|
||||
public static FeishuEmployeeListRequest buildActiveEmployeeRequest(int pageSize, String pageToken) {
|
||||
return FeishuEmployeeListRequest.builder()
|
||||
.filter(Filter.builder()
|
||||
.conditions(List.of(
|
||||
Condition.builder()
|
||||
.field("work_info.staff_status")
|
||||
.operator("eq")
|
||||
.value("1") // 1 表示在职
|
||||
.build()
|
||||
))
|
||||
.build())
|
||||
.requiredFields(List.of("base_info.*"))
|
||||
.pageRequest(PageRequest.builder()
|
||||
.pageSize(pageSize)
|
||||
.pageToken(pageToken)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询指定部门员工的请求
|
||||
*
|
||||
* @param departmentId 部门ID
|
||||
* @param pageSize 每页大小
|
||||
* @param pageToken 分页标记
|
||||
* @return 请求对象
|
||||
*/
|
||||
public static FeishuEmployeeListRequest buildDepartmentEmployeeRequest(String departmentId, int pageSize, String pageToken) {
|
||||
return FeishuEmployeeListRequest.builder()
|
||||
.filter(Filter.builder()
|
||||
.conditions(List.of(
|
||||
Condition.builder()
|
||||
.field("work_info.staff_status")
|
||||
.operator("eq")
|
||||
.value("1")
|
||||
.build(),
|
||||
Condition.builder()
|
||||
.field("department_id")
|
||||
.operator("eq")
|
||||
.value(departmentId)
|
||||
.build()
|
||||
))
|
||||
.build())
|
||||
.requiredFields(List.of("base_info.*"))
|
||||
.pageRequest(PageRequest.builder()
|
||||
.pageSize(pageSize)
|
||||
.pageToken(pageToken)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class Filter {
|
||||
/**
|
||||
* 筛选条件列表
|
||||
*/
|
||||
private List<Condition> conditions;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class Condition {
|
||||
/**
|
||||
* 字段名
|
||||
*/
|
||||
private String field;
|
||||
|
||||
/**
|
||||
* 操作符: eq(等于), ne(不等于), gt(大于), lt(小于), contains(包含)等
|
||||
*/
|
||||
private String operator;
|
||||
|
||||
/**
|
||||
* 字段值
|
||||
*/
|
||||
private String value;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class PageRequest {
|
||||
/**
|
||||
* 每页大小,最大100
|
||||
*/
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
/**
|
||||
* 分页标记,首次请求为空
|
||||
*/
|
||||
@JsonProperty("page_token")
|
||||
private String pageToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cn.yinlihupo.domain.dto.feishu;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞书员工列表查询响应DTO
|
||||
* 对应飞书 directory/v1/employees/filter 接口响应体
|
||||
*/
|
||||
@Data
|
||||
public class FeishuEmployeeListResponse {
|
||||
|
||||
/**
|
||||
* 响应码,0表示成功
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private Data data;
|
||||
|
||||
@lombok.Data
|
||||
public static class Data {
|
||||
|
||||
/**
|
||||
* 员工列表
|
||||
*/
|
||||
private List<FeishuEmployeeDTO> employees;
|
||||
|
||||
/**
|
||||
* 分页信息
|
||||
*/
|
||||
@JsonProperty("page_response")
|
||||
private PageResponse pageResponse;
|
||||
}
|
||||
|
||||
@lombok.Data
|
||||
public static class PageResponse {
|
||||
|
||||
/**
|
||||
* 是否有更多数据
|
||||
*/
|
||||
@JsonProperty("has_more")
|
||||
private Boolean hasMore;
|
||||
|
||||
/**
|
||||
* 下一页的分页标记
|
||||
*/
|
||||
@JsonProperty("page_token")
|
||||
private String pageToken;
|
||||
|
||||
/**
|
||||
* 总数量(可能为空)
|
||||
*/
|
||||
@JsonProperty("total_count")
|
||||
private Integer totalCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否请求成功
|
||||
*
|
||||
* @return true-成功, false-失败
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return code != null && code == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取员工列表
|
||||
*
|
||||
* @return 员工列表,失败返回空列表
|
||||
*/
|
||||
public List<FeishuEmployeeDTO> getEmployees() {
|
||||
if (data != null && data.getEmployees() != null) {
|
||||
return data.getEmployees();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有更多数据
|
||||
*
|
||||
* @return true-有下一页, false-无下一页
|
||||
*/
|
||||
public boolean hasMore() {
|
||||
if (data != null && data.getPageResponse() != null && data.getPageResponse().getHasMore() != null) {
|
||||
return data.getPageResponse().getHasMore();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一页的分页标记
|
||||
*
|
||||
* @return 分页标记,无下一页返回null
|
||||
*/
|
||||
public String getNextPageToken() {
|
||||
if (data != null && data.getPageResponse() != null) {
|
||||
return data.getPageResponse().getPageToken();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package cn.yinlihupo.domain.dto.feishu;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞书用户同步结果DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class FeishuUserSyncResult {
|
||||
|
||||
/**
|
||||
* 同步是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 同步消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 从飞书获取的员工总数
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 新增用户数
|
||||
*/
|
||||
private Integer createdCount;
|
||||
|
||||
/**
|
||||
* 更新用户数
|
||||
*/
|
||||
private Integer updatedCount;
|
||||
|
||||
/**
|
||||
* 失败数量
|
||||
*/
|
||||
private Integer failedCount;
|
||||
|
||||
/**
|
||||
* 跳过的用户数量(如手机号为空等无效数据)
|
||||
*/
|
||||
private Integer skippedCount;
|
||||
|
||||
/**
|
||||
* 同步开始时间
|
||||
*/
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 同步结束时间
|
||||
*/
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 失败的员工列表
|
||||
*/
|
||||
@Builder.Default
|
||||
private List<FailedEmployee> failedList = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 失败员工信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public static class FailedEmployee {
|
||||
/**
|
||||
* 飞书员工ID
|
||||
*/
|
||||
private String employeeId;
|
||||
|
||||
/**
|
||||
* 员工姓名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 失败原因
|
||||
*/
|
||||
private String reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*
|
||||
* @param message 消息
|
||||
* @return 结果对象
|
||||
*/
|
||||
public static FeishuUserSyncResult success(String message) {
|
||||
return FeishuUserSyncResult.builder()
|
||||
.success(true)
|
||||
.message(message)
|
||||
.startTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败结果
|
||||
*
|
||||
* @param message 错误消息
|
||||
* @return 结果对象
|
||||
*/
|
||||
public static FeishuUserSyncResult fail(String message) {
|
||||
return FeishuUserSyncResult.builder()
|
||||
.success(false)
|
||||
.message(message)
|
||||
.startTime(LocalDateTime.now())
|
||||
.endTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成同步,设置结束时间
|
||||
*/
|
||||
public void complete() {
|
||||
this.endTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加失败记录
|
||||
*
|
||||
* @param employeeId 员工ID
|
||||
* @param name 姓名
|
||||
* @param reason 失败原因
|
||||
*/
|
||||
public void addFailed(String employeeId, String name, String reason) {
|
||||
if (this.failedList == null) {
|
||||
this.failedList = new ArrayList<>();
|
||||
}
|
||||
this.failedList.add(FailedEmployee.builder()
|
||||
.employeeId(employeeId)
|
||||
.name(name)
|
||||
.reason(reason)
|
||||
.build());
|
||||
this.failedCount = this.failedList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加创建计数
|
||||
*/
|
||||
public void incrementCreated() {
|
||||
if (this.createdCount == null) {
|
||||
this.createdCount = 0;
|
||||
}
|
||||
this.createdCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加更新计数
|
||||
*/
|
||||
public void incrementUpdated() {
|
||||
if (this.updatedCount == null) {
|
||||
this.updatedCount = 0;
|
||||
}
|
||||
this.updatedCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加跳过计数
|
||||
*/
|
||||
public void incrementSkipped() {
|
||||
if (this.skippedCount == null) {
|
||||
this.skippedCount = 0;
|
||||
}
|
||||
this.skippedCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置总数
|
||||
*
|
||||
* @param count 总数
|
||||
*/
|
||||
public void setTotal(Integer count) {
|
||||
this.totalCount = count;
|
||||
}
|
||||
}
|
||||
60
src/main/java/cn/yinlihupo/job/FeishuUserSyncJob.java
Normal file
60
src/main/java/cn/yinlihupo/job/FeishuUserSyncJob.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package cn.yinlihupo.job;
|
||||
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuUserSyncResult;
|
||||
import cn.yinlihupo.service.system.FeishuUserSyncService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 飞书用户同步定时任务
|
||||
* 每天0点自动同步飞书员工信息到系统
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuUserSyncJob {
|
||||
|
||||
private final FeishuUserSyncService feishuUserSyncService;
|
||||
|
||||
/**
|
||||
* 每天0点执行用户同步
|
||||
* cron表达式: 秒 分 时 日 月 周
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void syncUsersDaily() {
|
||||
log.info("========== 开始执行每日飞书用户同步任务 ==========");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
FeishuUserSyncResult result = feishuUserSyncService.syncAllEmployees();
|
||||
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
if (result.getSuccess()) {
|
||||
log.info("========== 每日飞书用户同步任务完成 ==========");
|
||||
log.info("同步结果: 总计={}, 新增={}, 更新={}, 跳过={}, 失败={}",
|
||||
result.getTotalCount(),
|
||||
result.getCreatedCount(),
|
||||
result.getUpdatedCount(),
|
||||
result.getSkippedCount(),
|
||||
result.getFailedCount());
|
||||
log.info("耗时: {}ms", costTime);
|
||||
|
||||
// 如果有失败记录,输出失败详情
|
||||
if (result.getFailedCount() != null && result.getFailedCount() > 0) {
|
||||
log.warn("存在同步失败的员工,共 {} 条", result.getFailedCount());
|
||||
result.getFailedList().forEach(failed ->
|
||||
log.warn(" - {}({}): {}", failed.getName(), failed.getEmployeeId(), failed.getReason())
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.error("========== 每日飞书用户同步任务失败 ==========");
|
||||
log.error("失败原因: {}", result.getMessage());
|
||||
log.error("耗时: {}ms", costTime);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("========== 每日飞书用户同步任务异常 ==========", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.yinlihupo.service.system;
|
||||
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeDTO;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeListRequest;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeListResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞书SDK客户端服务接口
|
||||
* 封装飞书开放平台API调用
|
||||
*/
|
||||
public interface FeishuClientService {
|
||||
|
||||
/**
|
||||
* 查询企业员工列表
|
||||
* 调用飞书 directory/v1/employees/filter 接口
|
||||
*
|
||||
* @param request 查询请求参数
|
||||
* @return 员工列表响应
|
||||
*/
|
||||
FeishuEmployeeListResponse listEmployees(FeishuEmployeeListRequest request);
|
||||
|
||||
/**
|
||||
* 查询所有在职员工
|
||||
* 自动处理分页,返回全部员工列表
|
||||
*
|
||||
* @return 所有在职员工列表
|
||||
*/
|
||||
List<FeishuEmployeeDTO> listAllActiveEmployees();
|
||||
|
||||
/**
|
||||
* 根据部门ID查询员工列表
|
||||
*
|
||||
* @param departmentId 部门ID
|
||||
* @return 员工列表
|
||||
*/
|
||||
List<FeishuEmployeeDTO> listEmployeesByDepartment(String departmentId);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.yinlihupo.service.system;
|
||||
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuUserSyncResult;
|
||||
|
||||
/**
|
||||
* 飞书用户同步服务接口
|
||||
* 用于将飞书企业员工信息同步到系统用户表
|
||||
*/
|
||||
public interface FeishuUserSyncService {
|
||||
|
||||
/**
|
||||
* 同步所有飞书在职员工到系统
|
||||
* 自动处理分页,获取全部员工数据
|
||||
*
|
||||
* @return 同步结果
|
||||
*/
|
||||
FeishuUserSyncResult syncAllEmployees();
|
||||
|
||||
/**
|
||||
* 根据部门ID同步员工
|
||||
*
|
||||
* @param departmentId 飞书部门ID
|
||||
* @return 同步结果
|
||||
*/
|
||||
FeishuUserSyncResult syncEmployeesByDepartment(String departmentId);
|
||||
|
||||
/**
|
||||
* 同步指定员工
|
||||
*
|
||||
* @param employeeId 飞书员工ID
|
||||
* @return 同步结果
|
||||
*/
|
||||
FeishuUserSyncResult syncEmployeeById(String employeeId);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package cn.yinlihupo.service.system.impl;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.yinlihupo.common.config.FeishuConfig;
|
||||
import cn.yinlihupo.common.exception.BusinessException;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeDTO;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeListRequest;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeListResponse;
|
||||
import cn.yinlihupo.service.system.FeishuClientService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 飞书SDK客户端服务实现类
|
||||
* 使用 Hutool HTTP 直接调用飞书 API,避免 Java 17 模块化反射问题
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuClientServiceImpl implements FeishuClientService {
|
||||
|
||||
private final FeishuConfig feishuConfig;
|
||||
|
||||
// 每页最大查询数量
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
// 员工列表查询接口路径
|
||||
private static final String EMPLOYEES_FILTER_API = "https://open.feishu.cn/open-apis/directory/v1/employees/filter";
|
||||
// 应用访问令牌获取接口
|
||||
private static final String APP_ACCESS_TOKEN_API = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal";
|
||||
|
||||
/**
|
||||
* 获取应用访问令牌 (app_access_token)
|
||||
*
|
||||
* @return app_access_token
|
||||
*/
|
||||
private String getAppAccessToken() {
|
||||
try {
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.set("app_id", feishuConfig.getAppId());
|
||||
requestBody.set("app_secret", feishuConfig.getAppSecret());
|
||||
|
||||
try (HttpResponse response = HttpRequest.post(APP_ACCESS_TOKEN_API)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(requestBody.toString())
|
||||
.execute()) {
|
||||
|
||||
String body = response.body();
|
||||
log.debug("获取应用访问令牌响应: {}", body);
|
||||
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
if (jsonObject.getInt("code") != 0) {
|
||||
throw new BusinessException("获取应用访问令牌失败: " + jsonObject.getStr("msg"));
|
||||
}
|
||||
|
||||
return jsonObject.getStr("app_access_token");
|
||||
}
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("获取应用访问令牌异常", e);
|
||||
throw new BusinessException("获取应用访问令牌异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeishuEmployeeListResponse listEmployees(FeishuEmployeeListRequest request) {
|
||||
try {
|
||||
// 1. 获取应用访问令牌
|
||||
String appAccessToken = getAppAccessToken();
|
||||
|
||||
// 2. 将请求对象转换为Map
|
||||
Map<String, Object> body = convertRequestToMap(request);
|
||||
|
||||
// 3. 构建查询参数
|
||||
String url = EMPLOYEES_FILTER_API + "?department_id_type=open_department_id&employee_id_type=open_id";
|
||||
|
||||
// 4. 发起请求
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + appAccessToken)
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.execute()) {
|
||||
|
||||
String responseBody = response.body();
|
||||
log.debug("飞书员工列表查询响应: {}", responseBody);
|
||||
|
||||
// 5. 使用Hutool解析JSON
|
||||
FeishuEmployeeListResponse resp = JSONUtil.toBean(responseBody, FeishuEmployeeListResponse.class);
|
||||
|
||||
if (!resp.isSuccess()) {
|
||||
log.error("飞书员工列表查询失败, code: {}, msg: {}", resp.getCode(), resp.getMsg());
|
||||
throw new BusinessException("飞书员工列表查询失败: " + resp.getMsg());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("飞书员工列表查询异常", e);
|
||||
throw new BusinessException("飞书员工列表查询异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求对象转换为Map
|
||||
*/
|
||||
private Map<String, Object> convertRequestToMap(FeishuEmployeeListRequest request) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
|
||||
// 构建filter
|
||||
if (request.getFilter() != null && request.getFilter().getConditions() != null) {
|
||||
Map<String, Object> filter = new HashMap<>();
|
||||
List<Map<String, Object>> conditions = new ArrayList<>();
|
||||
|
||||
for (FeishuEmployeeListRequest.Condition condition : request.getFilter().getConditions()) {
|
||||
Map<String, Object> cond = new HashMap<>();
|
||||
cond.put("field", condition.getField());
|
||||
cond.put("operator", condition.getOperator());
|
||||
cond.put("value", condition.getValue());
|
||||
conditions.add(cond);
|
||||
}
|
||||
|
||||
filter.put("conditions", conditions);
|
||||
body.put("filter", filter);
|
||||
}
|
||||
|
||||
// 构建required_fields
|
||||
if (request.getRequiredFields() != null) {
|
||||
body.put("required_fields", request.getRequiredFields());
|
||||
}
|
||||
|
||||
// 构建page_request
|
||||
if (request.getPageRequest() != null) {
|
||||
Map<String, Object> pageRequest = new HashMap<>();
|
||||
pageRequest.put("page_size", request.getPageRequest().getPageSize());
|
||||
if (request.getPageRequest().getPageToken() != null && !request.getPageRequest().getPageToken().isEmpty()) {
|
||||
pageRequest.put("page_token", request.getPageRequest().getPageToken());
|
||||
}
|
||||
body.put("page_request", pageRequest);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FeishuEmployeeDTO> listAllActiveEmployees() {
|
||||
List<FeishuEmployeeDTO> allEmployees = new ArrayList<>();
|
||||
String pageToken = "";
|
||||
int pageCount = 0;
|
||||
final int maxPages = 100; // 最大分页数,防止死循环
|
||||
boolean hasMore = true;
|
||||
|
||||
while (hasMore && pageCount < maxPages) {
|
||||
FeishuEmployeeListRequest request = FeishuEmployeeListRequest.buildActiveEmployeeRequest(
|
||||
MAX_PAGE_SIZE,
|
||||
pageToken
|
||||
);
|
||||
|
||||
FeishuEmployeeListResponse response = listEmployees(request);
|
||||
|
||||
if (response.isSuccess() && response.getData() != null) {
|
||||
List<FeishuEmployeeDTO> employees = response.getEmployees();
|
||||
if (employees != null && !employees.isEmpty()) {
|
||||
allEmployees.addAll(employees);
|
||||
log.debug("获取到 {} 条员工数据", employees.size());
|
||||
}
|
||||
|
||||
// 更新分页标记和hasMore标志
|
||||
hasMore = response.hasMore();
|
||||
pageToken = response.getNextPageToken();
|
||||
pageCount++;
|
||||
|
||||
log.debug("第 {} 页查询完成, hasMore: {}, nextPageToken: {}",
|
||||
pageCount, hasMore, pageToken);
|
||||
} else {
|
||||
log.error("查询员工列表失败: {}", response.getMsg());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("共查询到 {} 条在职员工数据", allEmployees.size());
|
||||
return allEmployees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FeishuEmployeeDTO> listEmployeesByDepartment(String departmentId) {
|
||||
List<FeishuEmployeeDTO> allEmployees = new ArrayList<>();
|
||||
String pageToken = "";
|
||||
int pageCount = 0;
|
||||
final int maxPages = 100;
|
||||
boolean hasMore = true;
|
||||
|
||||
while (hasMore && pageCount < maxPages) {
|
||||
FeishuEmployeeListRequest request = FeishuEmployeeListRequest.buildDepartmentEmployeeRequest(
|
||||
departmentId,
|
||||
MAX_PAGE_SIZE,
|
||||
pageToken
|
||||
);
|
||||
|
||||
FeishuEmployeeListResponse response = listEmployees(request);
|
||||
|
||||
if (response.isSuccess() && response.getData() != null) {
|
||||
List<FeishuEmployeeDTO> employees = response.getEmployees();
|
||||
if (employees != null && !employees.isEmpty()) {
|
||||
allEmployees.addAll(employees);
|
||||
}
|
||||
|
||||
hasMore = response.hasMore();
|
||||
pageToken = response.getNextPageToken();
|
||||
pageCount++;
|
||||
} else {
|
||||
log.error("查询部门员工列表失败: {}", response.getMsg());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("部门 {} 共查询到 {} 条员工数据", departmentId, allEmployees.size());
|
||||
return allEmployees;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package cn.yinlihupo.service.system.impl;
|
||||
|
||||
import cn.yinlihupo.common.exception.BusinessException;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuEmployeeDTO;
|
||||
import cn.yinlihupo.domain.dto.feishu.FeishuUserSyncResult;
|
||||
import cn.yinlihupo.domain.entity.SysUser;
|
||||
import cn.yinlihupo.mapper.SysUserMapper;
|
||||
import cn.yinlihupo.service.system.FeishuClientService;
|
||||
import cn.yinlihupo.service.system.FeishuUserSyncService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞书用户同步服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuUserSyncServiceImpl implements FeishuUserSyncService {
|
||||
|
||||
private final FeishuClientService feishuClientService;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
// 飞书用户默认密码
|
||||
private static final String DEFAULT_PASSWORD = "FEISHU_SYNC_USER";
|
||||
// 默认状态:正常
|
||||
private static final Integer DEFAULT_STATUS = 1;
|
||||
|
||||
@Override
|
||||
public FeishuUserSyncResult syncAllEmployees() {
|
||||
log.info("开始同步所有飞书在职员工...");
|
||||
FeishuUserSyncResult result = FeishuUserSyncResult.success("同步完成");
|
||||
|
||||
try {
|
||||
// 1. 获取所有在职员工
|
||||
List<FeishuEmployeeDTO> employees = feishuClientService.listAllActiveEmployees();
|
||||
result.setTotal(employees.size());
|
||||
log.info("从飞书获取到 {} 条在职员工数据", employees.size());
|
||||
|
||||
// 2. 逐个同步员工(每个员工独立事务)
|
||||
for (FeishuEmployeeDTO employee : employees) {
|
||||
syncSingleEmployee(employee, result);
|
||||
}
|
||||
|
||||
result.complete();
|
||||
log.info("员工同步完成: 总计={}, 新增={}, 更新={}, 跳过={}, 失败={}",
|
||||
result.getTotalCount(),
|
||||
result.getCreatedCount(),
|
||||
result.getUpdatedCount(),
|
||||
result.getSkippedCount(),
|
||||
result.getFailedCount());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步员工列表异常", e);
|
||||
result.setSuccess(false);
|
||||
result.setMessage("同步异常: " + e.getMessage());
|
||||
result.complete();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeishuUserSyncResult syncEmployeesByDepartment(String departmentId) {
|
||||
log.info("开始同步部门 {} 的飞书员工...", departmentId);
|
||||
FeishuUserSyncResult result = FeishuUserSyncResult.success("同步完成");
|
||||
|
||||
try {
|
||||
// 1. 获取部门员工
|
||||
List<FeishuEmployeeDTO> employees = feishuClientService.listEmployeesByDepartment(departmentId);
|
||||
result.setTotal(employees.size());
|
||||
log.info("从飞书获取到部门 {} 的 {} 条员工数据", departmentId, employees.size());
|
||||
|
||||
// 2. 逐个同步员工(每个员工独立事务)
|
||||
for (FeishuEmployeeDTO employee : employees) {
|
||||
syncSingleEmployee(employee, result);
|
||||
}
|
||||
|
||||
result.complete();
|
||||
log.info("部门 {} 员工同步完成: 总计={}, 新增={}, 更新={}, 跳过={}, 失败={}",
|
||||
departmentId,
|
||||
result.getTotalCount(),
|
||||
result.getCreatedCount(),
|
||||
result.getUpdatedCount(),
|
||||
result.getSkippedCount(),
|
||||
result.getFailedCount());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步部门员工列表异常, departmentId: {}", departmentId, e);
|
||||
result.setSuccess(false);
|
||||
result.setMessage("同步异常: " + e.getMessage());
|
||||
result.complete();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeishuUserSyncResult syncEmployeeById(String employeeId) {
|
||||
log.info("同步指定员工: {}", employeeId);
|
||||
// 由于飞书API不支持直接查询单个员工,这里先获取全部员工再筛选
|
||||
// 实际使用时可以根据需求调整
|
||||
throw new BusinessException("暂不支持单个员工同步,请使用全部同步或部门同步");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步单个员工
|
||||
* 使用独立事务,失败不影响其他员工
|
||||
*
|
||||
* @param employee 飞书员工数据
|
||||
* @param result 同步结果统计
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
|
||||
public void syncSingleEmployee(FeishuEmployeeDTO employee, FeishuUserSyncResult result) {
|
||||
try {
|
||||
// 1. 获取员工基本信息
|
||||
String openId = employee.getOpenId();
|
||||
String phone = employee.getPhoneNumber();
|
||||
String realName = employee.getRealName();
|
||||
String email = employee.getEmail();
|
||||
String avatar = employee.getAvatarUrl();
|
||||
Integer gender = employee.getGender();
|
||||
|
||||
// 2. 数据校验
|
||||
if (!StringUtils.hasText(openId)) {
|
||||
log.warn("员工OpenID为空,跳过同步");
|
||||
result.incrementSkipped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(phone)) {
|
||||
log.warn("员工 {} 手机号为空,跳过同步", realName);
|
||||
result.addFailed(openId, realName, "手机号为空");
|
||||
result.incrementSkipped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(realName)) {
|
||||
log.warn("员工 {} 姓名为空,使用默认值", openId);
|
||||
realName = "未知用户";
|
||||
}
|
||||
|
||||
// 3. 根据手机号查询用户
|
||||
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysUser::getPhone, phone);
|
||||
SysUser existingUser = sysUserMapper.selectOne(queryWrapper);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (existingUser != null) {
|
||||
// 4. 用户已存在,更新信息
|
||||
log.debug("用户已存在,更新用户信息, phone: {}, name: {}", phone, realName);
|
||||
existingUser.setRealName(realName);
|
||||
existingUser.setNickname(realName);
|
||||
if (StringUtils.hasText(avatar)) {
|
||||
existingUser.setAvatar(avatar);
|
||||
}
|
||||
if (StringUtils.hasText(email)) {
|
||||
existingUser.setEmail(email);
|
||||
}
|
||||
if (gender != null && gender > 0) {
|
||||
existingUser.setGender(gender);
|
||||
}
|
||||
existingUser.setUpdateTime(now);
|
||||
sysUserMapper.updateById(existingUser);
|
||||
result.incrementUpdated();
|
||||
} else {
|
||||
// 5. 用户不存在,创建新用户
|
||||
log.debug("创建新用户, phone: {}, name: {}", phone, realName);
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUsername(openId);
|
||||
newUser.setRealName(realName);
|
||||
newUser.setNickname(realName);
|
||||
newUser.setPhone(phone);
|
||||
newUser.setAvatar(avatar);
|
||||
newUser.setEmail(email);
|
||||
newUser.setGender(gender != null ? gender : 0);
|
||||
newUser.setPassword(DEFAULT_PASSWORD);
|
||||
newUser.setStatus(DEFAULT_STATUS);
|
||||
newUser.setCreateTime(now);
|
||||
newUser.setUpdateTime(now);
|
||||
newUser.setDeleted(0);
|
||||
sysUserMapper.insert(newUser);
|
||||
result.incrementCreated();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步员工失败: {}", employee.getOpenId(), e);
|
||||
result.addFailed(
|
||||
employee.getOpenId(),
|
||||
employee.getRealName(),
|
||||
"同步异常: " + e.getMessage()
|
||||
);
|
||||
// 抛出异常触发事务回滚
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user