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();