From b7dc9659f3924ab7809711d4abd7bf27f4862c0c Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Mon, 26 Jan 2026 10:35:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=98=E5=B7=A5=E8=A1=A8=E9=92=89=E9=92=89?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BB=BB=E5=8A=A1=E5=8A=A0=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 ++ .../com/project/base/domain/dto/BaseDTO.java | 10 ++ .../ding/config/SyncThreadPoolConfig.java | 24 ++++ .../ding/domain/entity/SyncLogEntity.java | 37 ++++++ .../ding/domain/entity/UserEntity.java | 3 +- .../ding/domain/service/UserBaseService.java | 7 ++ .../service/impl/UserBaseServiceImpl.java | 11 ++ .../project/ding/mapper/DepartmentMapper.java | 9 ++ .../project/ding/mapper/SyncLogMapper.java | 9 ++ .../com/project/ding/mapper/UserMapper.java | 7 ++ .../project/ding/utils/DingUserSyncUtil.java | 108 ++++++++++++++++++ .../java/com/project/ding/utils/DingUtil.java | 18 ++- 12 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/project/ding/config/SyncThreadPoolConfig.java create mode 100644 src/main/java/com/project/ding/domain/entity/SyncLogEntity.java create mode 100644 src/main/java/com/project/ding/domain/service/UserBaseService.java create mode 100644 src/main/java/com/project/ding/domain/service/impl/UserBaseServiceImpl.java create mode 100644 src/main/java/com/project/ding/mapper/DepartmentMapper.java create mode 100644 src/main/java/com/project/ding/mapper/SyncLogMapper.java create mode 100644 src/main/java/com/project/ding/mapper/UserMapper.java create mode 100644 src/main/java/com/project/ding/utils/DingUserSyncUtil.java diff --git a/pom.xml b/pom.xml index 9015450..35350c4 100644 --- a/pom.xml +++ b/pom.xml @@ -135,5 +135,16 @@ hibernate-core 6.4.4.Final + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + diff --git a/src/main/java/com/project/base/domain/dto/BaseDTO.java b/src/main/java/com/project/base/domain/dto/BaseDTO.java index a4364a7..68ce355 100644 --- a/src/main/java/com/project/base/domain/dto/BaseDTO.java +++ b/src/main/java/com/project/base/domain/dto/BaseDTO.java @@ -1,9 +1,11 @@ package com.project.base.domain.dto; +import cn.hutool.core.bean.BeanUtil; import lombok.Data; import java.util.Date; +import java.util.function.Supplier; @Data public class BaseDTO { @@ -20,4 +22,12 @@ public class BaseDTO { private Boolean deleted; + + public T toEntity(Supplier supplier) { + // 这里的 supplier.get() 等价于执行了 new UserEntity() + T entity = supplier.get(); + BeanUtil.copyProperties(this, entity); + return entity; + } + } diff --git a/src/main/java/com/project/ding/config/SyncThreadPoolConfig.java b/src/main/java/com/project/ding/config/SyncThreadPoolConfig.java new file mode 100644 index 0000000..b10c9ba --- /dev/null +++ b/src/main/java/com/project/ding/config/SyncThreadPoolConfig.java @@ -0,0 +1,24 @@ +package com.project.ding.config; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Configuration +public class SyncThreadPoolConfig { + @Bean(name = "dingUserSyncExecutor") + public ExecutorService dingUserSyncExecutor() { + return new ThreadPoolExecutor( + 1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(1), + new ThreadFactoryBuilder().setNameFormat("ding-user-sync-%d").build(), + new ThreadPoolExecutor.DiscardPolicy() + ); + } +} diff --git a/src/main/java/com/project/ding/domain/entity/SyncLogEntity.java b/src/main/java/com/project/ding/domain/entity/SyncLogEntity.java new file mode 100644 index 0000000..6f13bee --- /dev/null +++ b/src/main/java/com/project/ding/domain/entity/SyncLogEntity.java @@ -0,0 +1,37 @@ +package com.project.ding.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.project.base.domain.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; + +import java.util.Date; + + +@Data +@Table(name = "evaluator_sync_log" ) +@Entity +@TableName("evaluator_sync_log") +@EqualsAndHashCode(callSuper = true) +public class SyncLogEntity extends BaseEntity { + + @Comment("同步开始时间") + @Column(name = "start_time") + private Date startTime; + + @Comment("同步结束时间") + @Column(name = "end_time") + private Date endTime; + + @Column(name = "status") + @Comment("任务状态") + private Integer status; // 0-进行中, 1-成功, 2-失败 + + @Column(name = "error_msg" , columnDefinition="TEXT comment '错误信息'") + private String errorMsg; + +} diff --git a/src/main/java/com/project/ding/domain/entity/UserEntity.java b/src/main/java/com/project/ding/domain/entity/UserEntity.java index cf531fd..7579615 100644 --- a/src/main/java/com/project/ding/domain/entity/UserEntity.java +++ b/src/main/java/com/project/ding/domain/entity/UserEntity.java @@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.annotation.TableName; import com.project.base.domain.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.Index; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,7 +12,7 @@ import org.hibernate.annotations.Comment; import java.util.Date; @Data -@Table(name = "evaluator_user" , indexes = {@Index(name = "Idx_parentId", columnList = "parent_id")}) +@Table(name = "evaluator_user" ) @Entity @TableName("evaluator_user") @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/project/ding/domain/service/UserBaseService.java b/src/main/java/com/project/ding/domain/service/UserBaseService.java new file mode 100644 index 0000000..3d6e9b6 --- /dev/null +++ b/src/main/java/com/project/ding/domain/service/UserBaseService.java @@ -0,0 +1,7 @@ +package com.project.ding.domain.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.project.ding.domain.entity.UserEntity; + +public interface UserBaseService extends IService { +} diff --git a/src/main/java/com/project/ding/domain/service/impl/UserBaseServiceImpl.java b/src/main/java/com/project/ding/domain/service/impl/UserBaseServiceImpl.java new file mode 100644 index 0000000..4b52fb6 --- /dev/null +++ b/src/main/java/com/project/ding/domain/service/impl/UserBaseServiceImpl.java @@ -0,0 +1,11 @@ +package com.project.ding.domain.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.project.ding.domain.entity.UserEntity; +import com.project.ding.domain.service.UserBaseService; +import com.project.ding.mapper.UserMapper; +import org.springframework.stereotype.Service; + +@Service +public class UserBaseServiceImpl extends ServiceImpl implements UserBaseService { +} diff --git a/src/main/java/com/project/ding/mapper/DepartmentMapper.java b/src/main/java/com/project/ding/mapper/DepartmentMapper.java new file mode 100644 index 0000000..4dffc03 --- /dev/null +++ b/src/main/java/com/project/ding/mapper/DepartmentMapper.java @@ -0,0 +1,9 @@ +package com.project.ding.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.ding.domain.entity.DepartmentEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DepartmentMapper extends BaseMapper { +} diff --git a/src/main/java/com/project/ding/mapper/SyncLogMapper.java b/src/main/java/com/project/ding/mapper/SyncLogMapper.java new file mode 100644 index 0000000..432f646 --- /dev/null +++ b/src/main/java/com/project/ding/mapper/SyncLogMapper.java @@ -0,0 +1,9 @@ +package com.project.ding.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.ding.domain.entity.SyncLogEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SyncLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/project/ding/mapper/UserMapper.java b/src/main/java/com/project/ding/mapper/UserMapper.java new file mode 100644 index 0000000..b7757c1 --- /dev/null +++ b/src/main/java/com/project/ding/mapper/UserMapper.java @@ -0,0 +1,7 @@ +package com.project.ding.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.ding.domain.entity.UserEntity; + +public interface UserMapper extends BaseMapper { +} diff --git a/src/main/java/com/project/ding/utils/DingUserSyncUtil.java b/src/main/java/com/project/ding/utils/DingUserSyncUtil.java new file mode 100644 index 0000000..34b46e1 --- /dev/null +++ b/src/main/java/com/project/ding/utils/DingUserSyncUtil.java @@ -0,0 +1,108 @@ +package com.project.ding.utils; + + +import com.project.ding.domain.dto.UserDTO; +import com.project.ding.domain.entity.SyncLogEntity; +import com.project.ding.domain.entity.UserEntity; +import com.project.ding.domain.service.UserBaseService; +import com.project.ding.mapper.SyncLogMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class DingUserSyncUtil { + @Resource(name = "dingUserSyncExecutor") + private ExecutorService executor; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private UserBaseService userBaseService; + + @Autowired + private DingUtil dingUtil; + + @Autowired + private SyncLogMapper syncLogMapper; + + private static final String LOCK_KEY = "lock:ding:user_sync"; + private static final String LAST_SYNC_KEY = "cache:ding:last_sync_time"; + + /** + * 触发同步任务 + * @param force 是否强制触发(跳过120分钟冷却检查) + */ + public String triggerSync(boolean force) { + // 1. 抢占 Redis 锁(防止多个实例同时跑全量) + Boolean acquired = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "true", 1, TimeUnit.HOURS); + if (Boolean.FALSE.equals(acquired)) { + return "同步任务已在运行中,请勿重复触发"; + } + + // 2. 提交异步任务(不使用 @Async) + CompletableFuture.runAsync(() -> { + try { + // 3. 冷却检查 + if (!force) { + String lastSync = redisTemplate.opsForValue().get(LAST_SYNC_KEY); + if (lastSync != null && (System.currentTimeMillis() - Long.parseLong(lastSync) < 120 * 60 * 1000)) { + log.info("处于冷却期,跳过全量同步"); + return; + } + } + + log.info(">>> 开始钉钉用户全量同步 (force={})", force); + runActualSyncTask(); + + } catch (Exception e) { + log.error("全量同步发生严重异常", e); + } finally { + // 4. 释放锁 + redisTemplate.delete(LOCK_KEY); + } + }, executor); + + return "同步任务已启动,请查看系统日志或更新时间"; + } + private void runActualSyncTask() { + // 插入一条同步日志 + SyncLogEntity syncLogEntity = new SyncLogEntity(); + syncLogEntity.setStartTime(new Date()); + syncLogEntity.setStatus(0); + syncLogMapper.insert(syncLogEntity); + + // 开始同步 + try { + List allUserDTOList = dingUtil.getAllUserDTO(); + userBaseService.saveOrUpdateBatch(allUserDTOList.stream() + .map(dto -> dto.toEntity(UserEntity::new)) + .collect(Collectors.toList()) , 300); + syncLogEntity.setStatus(1); + syncLogEntity.setEndTime(new Date()); + syncLogMapper.updateById(syncLogEntity); + + redisTemplate.opsForValue().set(LAST_SYNC_KEY, String.valueOf(System.currentTimeMillis())); + log.info(">>> 全量同步完成"); + } catch (Exception e) { + // 同步失败更新 + syncLogEntity.setEndTime(new Date()); + syncLogEntity.setStatus(2); // 失败 + syncLogEntity.setErrorMsg(e.getMessage()); + syncLogMapper.updateById(syncLogEntity); + log.error(">>> 全量同步过程报错", e); + } + } + +} diff --git a/src/main/java/com/project/ding/utils/DingUtil.java b/src/main/java/com/project/ding/utils/DingUtil.java index a65d421..f4326ad 100644 --- a/src/main/java/com/project/ding/utils/DingUtil.java +++ b/src/main/java/com/project/ding/utils/DingUtil.java @@ -8,12 +8,14 @@ import com.github.tingyugetc520.ali.dingtalk.bean.department.DtDepart; import com.google.gson.JsonObject; import com.jayway.jsonpath.JsonPath; import com.project.ding.domain.dto.DingUserDTO; +import com.project.ding.domain.dto.UserDTO; import io.vavr.control.Try; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Component public class DingUtil { @@ -22,12 +24,11 @@ public class DingUtil { - public List getAllDepartment() throws Exception { return dtService.getDepartmentService().list(null , true); } - public List getAllUser() throws Exception { + public List getAllDingUserDTO() throws Exception { List list = getAllDepartment(); List userList = new ArrayList<>(); @@ -38,6 +39,19 @@ public class DingUtil { return userList; } + public List getAllUserDTO() throws Exception { + List list = getAllDepartment(); + + List userList = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + List userInDepartment = getUserIdInDepartment(list.get(i).getId()); + userList.addAll(userInDepartment.stream() + .map(UserDTO::fromDingUserDTO) + .collect(Collectors.toList())); + } + return userList; + } + public List getUserIdInDepartment(Long id) throws Exception { JsonObject jsonObject = new JsonObject();