浏览代码

Merge remote-tracking branch 'origin/master'

lk 2 周之前
父节点
当前提交
e822bfb1b8

+ 40 - 2
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -17,6 +17,7 @@ import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.user.UserPasswordNotMatchException;
 import com.fs.common.utils.MessageUtils;
 import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
@@ -26,6 +27,7 @@ import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.mapper.FastGptRoleMapper;
 import com.fs.framework.manager.AsyncManager;
 import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.framework.web.service.TokenService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -82,7 +84,8 @@ public class QwUserController extends BaseController {
 
     @Autowired
     private IQwDeptService qwDeptService;
-
+    @Autowired
+    private TokenService tokenService;
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
 
@@ -649,7 +652,8 @@ public class QwUserController extends BaseController {
     @PostMapping("sync/{corpId}")
     public R sync(@PathVariable String corpId)
     {
-        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(loginUser.getUser().getCompanyId());
         Long tenantId = SecurityUtils.getTenantId();
         for (String string : strings) {
             if (string.equals(corpId)){
@@ -693,6 +697,40 @@ public class QwUserController extends BaseController {
         }
         return R.ok();
     }
+    /**
+     * 同步企微用户
+     */
+    @RepeatSubmit
+    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
+    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+    @PostMapping("/syncUser/{corpId}")
+    public R syncUser(@PathVariable String corpId)
+    {
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        for (String string : strings) {
+            if (string.equals(corpId)){
+                // 远程调用 fs-qw-api 同步企微用户
+                String syncUserUrl = OpenQwConfig.taskApi + "/app/common/syncQwUserAsync?corpId=" + corpId;
+                try {
+                    HttpResponse response = HttpRequest.post(syncUserUrl)
+                            .timeout(apiTimeout * 1000)
+                            .execute();
+                    if (response.getStatus() != 200) {
+                        log.error("同步企微用户失败,HTTP状态码: {}", response.getStatus());
+                        return R.error("同步企微用户失败,服务返回状态码: " + response.getStatus());
+                    }
+                } catch (Exception e) {
+                    log.error("同步企微用户异常, url={}", syncUserUrl, e);
+                    if (e.getCause() instanceof SocketTimeoutException) {
+                        return R.error("同步企微用户超时,请稍后重试");
+                    }
+                    return R.error("同步企微用户失败: " + e.getMessage());
+                }
+
+            }
+        }
+        return R.ok();
+    }
     @RepeatSubmit
     @PreAuthorize("@ss.hasPermi('qw:user:sync')")
     @Log(title = "同步企微用户名称", businessType = BusinessType.INSERT)

+ 35 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -935,4 +935,39 @@ public class QwUserController extends BaseController
         List<QwUser> list = qwUserService.selectQwUserList(qwUser);
         return getDataTable(list);
     }
+
+    /**
+     * 同步企微用户
+     */
+    @RepeatSubmit
+    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+    @PostMapping("/syncUser/{corpId}")
+    public R syncUser(@PathVariable String corpId)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(loginUser.getCompany().getCompanyId());
+        for (String string : strings) {
+            if (string.equals(corpId)){
+                // 远程调用 fs-qw-api 同步企微用户
+                String syncUserUrl = OpenQwConfig.taskApi + "/app/common/syncQwUserAsync?corpId=" + corpId;
+                try {
+                    HttpResponse response = HttpRequest.post(syncUserUrl)
+                            .timeout(apiTimeout * 1000)
+                            .execute();
+                    if (response.getStatus() != 200) {
+                        log.error("同步企微用户失败,HTTP状态码: {}", response.getStatus());
+                        return R.error("同步企微用户失败,服务返回状态码: " + response.getStatus());
+                    }
+                } catch (Exception e) {
+                    log.error("同步企微用户异常, url={}", syncUserUrl, e);
+                    if (e.getCause() instanceof SocketTimeoutException) {
+                        return R.error("同步企微用户超时,请稍后重试");
+                    }
+                    return R.error("同步企微用户失败: " + e.getMessage());
+                }
+
+            }
+        }
+        return R.ok();
+    }
 }

+ 222 - 29
fs-qw-api/src/main/java/com/fs/app/service/impl/OpenQwApiServiceImpl.java

@@ -19,7 +19,9 @@ import com.fs.qw.param.QwExternalContactAddTagParam;
 import com.fs.qw.param.QwExternalContactUpdateNoteParam;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.qwApi.Result.DeptUserResult;
 import com.fs.qwApi.Result.QwOpenidResult;
+import com.fs.qwApi.Result.UserResult;
 import com.fs.qwApi.domain.QwDeptResult;
 import com.fs.qwApi.domain.QwExternalContactRemarkResult;
 import com.fs.qwApi.domain.QwResult;
@@ -39,11 +41,13 @@ import com.fs.sop.params.SopUserLogsInfoDelParam;
 import com.fs.sop.params.SopUserLogsParamByDate;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.voice.utils.StringUtil;
+import com.google.common.collect.Lists;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.MDC;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
@@ -54,7 +58,9 @@ import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 @Slf4j
 @Service
@@ -81,42 +87,229 @@ public class OpenQwApiServiceImpl implements OpenQwApiService {
     @Autowired
     private TenantDataSourceUtil tenantDataSourceUtil;
 
+    /** 同步用户线程池 */
+    private static final ExecutorService SYNC_USER_EXECUTOR = new ThreadPoolExecutor(
+            8, 16, 60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(500),
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
     @Override
     public R getSyncQwUser(Long tenantId, String corpId) {
-        return tenantDataSourceUtil.executeWithResult(tenantId, () -> {
-            QwUserIdResult userList = qwApiService.getUserList(corpId);
-            List<DeptUser> deptUser = userList.getDept_user();
-            log.info("返回数据:{}", JSON.toJSONString(userList));
-            log.info("同步用户数量:{}", deptUser.size());
-            QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
-            String accessToken = qwApiService.getToken(corpId, qwCompany.getPermanentCode());
-            for (DeptUser user : deptUser) {
-                QwUser qw = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, user.getUserid());
-                String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), user.getUserid(), qwCompany.getPermanentCode());
-                log.info("同步用户名称:{}", serverQwUserName);
-                QwUser qwUser = new QwUser();
-                qwUser.setQwUserId(user.getUserid());
-                qwUser.setDepartment(user.getDepartment().toString());
-                qwUser.setQwUserName(serverQwUserName);
-                qwUser.setCorpId(corpId);
-                QwOpenidByUserParams param = new QwOpenidByUserParams();
-                param.setUserid(user.getUserid());
-                QwOpenidResult qwOpenidResult = qwApiService.useridToOpenid(param, corpId);
-                qwUser.setOpenid(qwOpenidResult.getOpenid());
-
-                qwUser.setQwOpenUserId(qwApiService.getOpenUserid(accessToken, user.getUserid(), corpId));
-                if (qw == null) {
-                    qwUserMapper.insertQwUser(qwUser);
-                } else {
-                    qwUser.setId(qw.getId());
-                    qwUser.setIsDel(0);
-                    qwUserMapper.updateQwUser(qwUser);
+        String key = "qw:sync:" + corpId;
+
+        if (redisCache.hasKey(key)) {
+            return R.error("同步任务正在执行中,请稍后再试");
+        }
+
+        boolean locked = redisCache.setIfAbsent(key, String.valueOf(System.currentTimeMillis()), 300, TimeUnit.SECONDS);
+        if (!locked) {
+            return R.error("同步任务正在执行中,请稍后再试");
+        }
+
+        // 异步执行同步任务(使用线程池代替@Async自调用失效问题)
+        SYNC_USER_EXECUTOR.submit(() -> {
+            try {
+                executeSync(tenantId, corpId);
+            } finally {
+                redisCache.deleteObject(key);
+            }
+        });
+
+        return R.ok("同步任务已启动,请稍后查看结果");
+    }
+
+    /**
+     * 实际的同步逻辑
+     */
+    private void executeSync(Long tenantId, String corpId) {
+        tenantDataSourceUtil.executeWithResult(tenantId, () -> {
+            long startTime = System.currentTimeMillis();
+            log.info("========== 开始同步用户数据 ========== 租户ID: {}, 企业ID: {}", tenantId, corpId);
+
+            try {
+                // 1. 获取部门列表
+                List<Department> departmentList = fetchDepartments(corpId);
+                if (departmentList == null) {
+                    return R.ok("未获取到部门列表");
                 }
+
+                // 2. 获取access_token
+                QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
+                if (qwCompany == null) {
+                    log.error("未找到企业信息, corpId: {}", corpId);
+                    return R.error("未找到企业信息");
+                }
+                String accessToken = qwApiService.getToken(corpId, qwCompany.getPermanentCode());
+
+                // 3. 并发获取所有部门用户(去重)
+                Map<String, DeptUserResult> userMap = fetchDeptUsersConcurrently(corpId, accessToken, departmentList);
+                if (userMap.isEmpty()) {
+                    return R.ok("无用户需要同步");
+                }
+
+                // 4. 查询数据库中已存在的用户
+                Map<String, QwUser> existingUserMap = queryExistingUsers(corpId, userMap.keySet());
+
+                // 5. 并发转换openid并构建用户对象
+                List<QwUser> usersToProcess = convertUsersConcurrently(corpId, userMap, existingUserMap);
+
+                // 6. 批量数据库操作
+                int errorCount = (int) (userMap.size() - usersToProcess.size());
+                saveUsersToDatabase(usersToProcess, errorCount, startTime);
+
+                return R.ok("同步完成");
+
+            } catch (Exception e) {
+                log.error("同步用户过程发生异常", e);
+                return R.error("同步失败:" + e.getMessage());
             }
-            return R.ok();
         });
     }
 
+    /**
+     * 获取部门列表
+     */
+    private List<Department> fetchDepartments(String corpId) {
+        QwDeptResult deptResult = qwApiService.getDepartmentList(corpId);
+        List<Department> departmentList = deptResult.getDepartment();
+        if (departmentList == null || departmentList.isEmpty()) {
+            log.warn("未获取到任何部门,同步结束");
+            return null;
+        }
+        log.info("获取到部门数量: {}", departmentList.size());
+        return departmentList;
+    }
+
+    /**
+     * 并发获取所有部门用户并去重
+     */
+    private Map<String, DeptUserResult> fetchDeptUsersConcurrently(String corpId, String accessToken, List<Department> departmentList) {
+        Map<String, DeptUserResult> userMap = new ConcurrentHashMap<>();
+
+        List<CompletableFuture<Void>> futures = departmentList.stream()
+                .map(dept -> CompletableFuture.runAsync(() -> {
+                    try {
+                        UserResult userResult = qwApiService.getUserSimpleList(corpId, accessToken, dept.getId());
+                        List<DeptUserResult> deptUsers = userResult.getUserlist();
+                        if (deptUsers != null && !deptUsers.isEmpty()) {
+                            deptUsers.forEach(user -> userMap.putIfAbsent(user.getUserid(), user));
+                        }
+                    } catch (Exception e) {
+                        log.error("获取部门 [{}] {} 的用户列表失败", dept.getId(), dept.getName(), e);
+                    }
+                }, SYNC_USER_EXECUTOR))
+                .collect(Collectors.toList());
+
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+        log.info("部门用户去重后数量: {}", userMap.size());
+        return userMap;
+    }
+
+    /**
+     * 查询数据库中已存在的用户
+     */
+    private Map<String, QwUser> queryExistingUsers(String corpId, Set<String> userIds) {
+        List<QwUser> existingUsers = qwUserMapper.selectQwUsersByCorpIdAndUserIds(corpId, new ArrayList<>(userIds));
+        Map<String, QwUser> existingUserMap = existingUsers.stream()
+                .collect(Collectors.toMap(QwUser::getQwOpenUserId, Function.identity()));
+        log.info("数据库已存在用户数: {}, 新增用户数: {}", existingUserMap.size(), userIds.size() - existingUserMap.size());
+        return existingUserMap;
+    }
+
+    /**
+     * 并发转换openid并构建QwUser对象
+     */
+    private List<QwUser> convertUsersConcurrently(String corpId, Map<String, DeptUserResult> userMap, Map<String, QwUser> existingUserMap) {
+        AtomicInteger errorCount = new AtomicInteger(0);
+
+        List<CompletableFuture<QwUser>> futures = userMap.entrySet().stream()
+                .map(entry -> CompletableFuture.supplyAsync(() -> {
+                    String userId = entry.getKey();
+                    DeptUserResult apiUser = entry.getValue();
+                    try {
+                        // 调用API转换openid
+                        QwOpenidByUserParams params = new QwOpenidByUserParams();
+                        params.setUserid(userId);
+                        QwOpenidResult openidResult = qwApiService.useridToOpenid(params, corpId);
+
+                        return buildQwUser(apiUser, existingUserMap.get(userId), corpId, openidResult.getOpenid());
+                    } catch (Exception e) {
+                        log.error("处理用户失败,userId: {}", userId, e);
+                        errorCount.incrementAndGet();
+                        return null;
+                    }
+                }, SYNC_USER_EXECUTOR))
+                .collect(Collectors.toList());
+
+        List<QwUser> result = futures.stream()
+                .map(CompletableFuture::join)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        log.info("用户处理统计: 成功={}, 失败={}", result.size(), errorCount.get());
+        return result;
+    }
+
+    /**
+     * 构建QwUser对象
+     */
+    private QwUser buildQwUser(DeptUserResult apiUser, QwUser existingUser, String corpId, String openid) {
+        QwUser qwUser = new QwUser();
+        qwUser.setQwUserName(apiUser.getName());
+        qwUser.setCorpId(corpId);
+        qwUser.setIsDel(0);
+        qwUser.setOpenid(openid);
+        qwUser.setQwOpenUserId(apiUser.getUserid());
+
+        // 设置部门(取第一个部门)
+        List<Integer> depts = apiUser.getDepartment();
+        qwUser.setDepartment(depts != null && !depts.isEmpty() ? String.valueOf(depts.get(0)) : "");
+
+        // 存在则设置id(更新),不存在则为null(新增)
+        if (existingUser != null) {
+            qwUser.setId(existingUser.getId());
+        }
+        return qwUser;
+    }
+
+    /**
+     * 批量保存用户到数据库
+     */
+    private void saveUsersToDatabase(List<QwUser> users, int errorCount, long startTime) {
+        if (users.isEmpty()) {
+            log.info("没有需要新增或更新的用户");
+            return;
+        }
+
+        List<QwUser> toInsert = users.stream().filter(u -> u.getId() == null).collect(Collectors.toList());
+        List<QwUser> toUpdate = users.stream().filter(u -> u.getId() != null).collect(Collectors.toList());
+
+        log.info("数据库操作: 待新增{}条, 待更新{}条", toInsert.size(), toUpdate.size());
+
+        int insertSuccess = 0, updateSuccess = 0;
+        if (!toInsert.isEmpty()) {
+            insertSuccess = qwUserMapper.batchUpdateQwUser(toInsert);
+        }
+        if (!toUpdate.isEmpty()) {
+            updateSuccess = qwUserMapper.batchUpdateQwUser(toUpdate);
+        }
+
+        long elapsed = System.currentTimeMillis() - startTime;
+        String resultMsg = String.format("同步完成!总耗时: %d ms | 新增: %d/%d | 更新: %d/%d | 失败: %d",
+                elapsed, insertSuccess, toInsert.size(), updateSuccess, toUpdate.size(), errorCount);
+        log.info(resultMsg);
+        log.info("========== 用户同步结束 ==========");
+    }
+
+    /**
+     * 判断是否需要更新用户信息
+     */
+    private boolean needUpdateUserInfo(QwUser existingUser) {
+        return existingUser.getUpdateTime() == null ||
+                System.currentTimeMillis() - existingUser.getUpdateTime().getTime() > 24 * 60 * 60 * 1000;
+    }
     @Override
     public R getSyncQwDept(Long tenantId, String corpId) {
         return tenantDataSourceUtil.executeWithResult(tenantId, () -> {

+ 58 - 0
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -2,9 +2,11 @@ package com.fs.app.controller;
 
 
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.task.qwTask;
 import com.fs.app.taskService.*;
+import com.fs.common.config.RedisTenantContext;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.redis.RedisCache;
@@ -16,6 +18,7 @@ import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.service.*;
 import com.fs.course.vo.FsUserCourseVideoQVO;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsInquiryOrderService;
 import com.fs.his.utils.qrcode.QRCodeUtils;
@@ -34,11 +37,14 @@ import com.fs.sop.mapper.SopUserLogsMapper;
 import com.fs.sop.service.*;
 import com.fs.sop.vo.QwSopLogsDoSendListTVO;
 import com.fs.store.service.IFsUserCourseCountService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
 import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
 import com.fs.wxwork.service.WxWorkService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -153,7 +159,59 @@ public class CommonController {
     IQwExternalContactService externalContactService;
     @Autowired
     WxWorkService wxWorkService;
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    private IQwDeptService qwDeptService;
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @RequestMapping("/syncQwUserAsync")
+    public void syncQwUserAsync(String corpId) {
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (ObjectUtil.isEmpty(corpId)) {
+            return; // 跳过无效租户,继续下一个
+        }
+
+        TenantInfo tenantInfo = null;
+        try {
+            tenantInfo = tenantInfoService.getById(qwCompany.getTenantId());
+            if (ObjectUtil.isEmpty(tenantInfo)) {
+                log.warn("租户信息不存在,tenantId={}", qwCompany.getTenantId());
+                return;
+            }
 
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+
+            log.info("开始同步企微用户,租户={}, corpId={}", tenantInfo.getId(), qwCompany.getCorpId());
+
+            // 执行同步操作
+            qwUserService.syncQwUser(qwCompany.getCorpId());
+
+            log.info("同步完成,租户={}", tenantInfo.getId());
+
+        } catch (Exception e) {
+            log.error("同步企微员工和部门失败,租户={}, corpId={}",
+                    qwCompany.getTenantId(), qwCompany.getCorpId(), e);
+        } finally {
+            // 清理租户上下文(数据源和Redis)
+            try {
+                tenantDataSourceManager.clear(); // 假设有此方法,请根据实际API调整
+            } catch (Exception ignored) {}
+
+            try {
+                RedisTenantContext.clear(); // 或 RedisTenantContext.removeTenantId()
+            } catch (Exception ignored) {}
+        }
+    }
     /**
      *
      */

+ 83 - 0
fs-qw-task/src/main/java/com/fs/app/task/QwUserAsyncTask.java

@@ -0,0 +1,83 @@
+package com.fs.app.task;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.config.RedisTenantContext;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwDeptService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+@Slf4j
+@Component
+public class QwUserAsyncTask {
+
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    private IQwDeptService qwDeptService;
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Scheduled(cron = "0 0 0/3 * * ?")
+    public void syncQwUserAsync() {
+        List<QwCompany> qwCompanies = qwCompanyService.selectQwCompanyList(new QwCompany());
+        if (CollectionUtils.isEmpty(qwCompanies)) {
+            return;
+        }
+
+        for (QwCompany qwCompany : qwCompanies) {
+            if (ObjectUtil.isEmpty(qwCompany.getTenantId())) {
+                continue; // 跳过无效租户,继续下一个
+            }
+
+            TenantInfo tenantInfo = null;
+            try {
+                tenantInfo = tenantInfoService.getById(qwCompany.getTenantId());
+                if (ObjectUtil.isEmpty(tenantInfo)) {
+                    log.warn("租户信息不存在,tenantId={}", qwCompany.getTenantId());
+                    continue;
+                }
+
+                // 切换到租户数据源
+                tenantDataSourceManager.switchTenant(tenantInfo);
+                // 切换Redis租户上下文
+                RedisTenantContext.setTenantId(tenantInfo.getId());
+
+                log.info("开始同步企微用户,租户={}, corpId={}", tenantInfo.getId(), qwCompany.getCorpId());
+
+                // 执行同步操作
+                qwUserService.syncQwUser(qwCompany.getCorpId());
+//                qwDeptService.insertOrUpdateQwDept(qwCompany.getCorpId());
+
+                log.info("同步完成,租户={}", tenantInfo.getId());
+
+            } catch (Exception e) {
+                log.error("同步企微员工和部门失败,租户={}, corpId={}",
+                        qwCompany.getTenantId(), qwCompany.getCorpId(), e);
+            } finally {
+                // 清理租户上下文(数据源和Redis)
+                try {
+                    tenantDataSourceManager.clear(); // 假设有此方法,请根据实际API调整
+                } catch (Exception ignored) {}
+
+                try {
+                    RedisTenantContext.clear(); // 或 RedisTenantContext.removeTenantId()
+                } catch (Exception ignored) {}
+            }
+        }
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -510,4 +510,15 @@ public interface QwUserMapper extends BaseMapper<QwUser>
 
     @Select("select * from qw_user where ipad_status = 1 and corp_id=#{corpId} and qw_user_id=#{qwUserId} limit 1 ")
     QwUser selectQwUserAppKeyAndIdByCorpIdAndUserIdAndIpad(@Param("corpId")String corpId,@Param("qwUserId") String qwUserId);
+
+    // 批量查询
+    List<QwUser> selectQwUsersByCorpIdAndUserIds(@Param("corpId") String corpId,
+                                                 @Param("userIds") List<String> userIds);
+
+    // 批量插入
+    int batchInsertQwUser(@Param("list") List<QwUser> users);
+
+    // 批量更新
+    int batchUpdateQwUser(@Param("list") List<QwUser> users);
+
 }

+ 5 - 7
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -942,21 +942,19 @@ public class QwUserServiceImpl implements IQwUserService
         log.info("同步用户数量:{}", deptUser.size());
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
         for (DeptUser user : deptUser) {
-            QwUser qw=qwUserMapper.selectQwUserByCorpIdAndUserId(corpId,user.getUserid());
-            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), user.getUserid(),qwCompany.getPermanentCode());
-            log.info("同步用户名称:{}", serverQwUserName);
+            QwUser qw=qwUserMapper.selectQwUserByCorpIdAndUserId(corpId,qwApiService.getOpenUserid(qwApiService.getToken(corpId,qwCompany.getPermanentCode()),user.getUserid(),corpId));
+//            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), user.getUserid(),qwCompany.getPermanentCode());
+//            log.info("同步用户名称:{}", serverQwUserName);
             QwUser qwUser = new QwUser();
             qwUser.setQwUserId(user.getUserid());
             qwUser.setDepartment(user.getDepartment().toString());
-            qwUser.setQwUserName(serverQwUserName);
+//            qwUser.setQwUserName(serverQwUserName);
             qwUser.setCorpId(corpId);
             QwOpenidByUserParams param=new QwOpenidByUserParams();
             param.setUserid(user.getUserid());
             QwOpenidResult qwOpenidResult = qwApiService.useridToOpenid(param, corpId);
             qwUser.setOpenid(qwOpenidResult.getOpenid());
-             if (qw==null){
-                 qwUserMapper.insertQwUser(qwUser);
-             }else {
+             if (qw!=null){
                  qwUser.setId(qw.getId());
                  qwUser.setIsDel(0);
                  qwUserMapper.updateQwUser(qwUser);

+ 16 - 0
fs-service/src/main/java/com/fs/qwApi/Result/DeptUserResult.java

@@ -0,0 +1,16 @@
+package com.fs.qwApi.Result;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeptUserResult {
+    private String userid;
+    private String name;
+    private List<Integer> department;
+
+    @JsonProperty("open_userid")
+    private String openUserid;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/qwApi/Result/UserResult.java

@@ -0,0 +1,13 @@
+package com.fs.qwApi.Result;
+
+import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+@Data
+public class UserResult {
+    private Integer errcode;
+    private String errmsg;
+    private List<DeptUserResult> userlist;
+}
+

+ 1 - 0
fs-service/src/main/java/com/fs/qwApi/config/OpenQwConfig.java

@@ -3,4 +3,5 @@ package com.fs.qwApi.config;
 public interface OpenQwConfig {
     String baseApi ="http://saasqwapi.ylrzcloud.com/open/qwapi";
     String api ="http://saasqwapi.ylrzcloud.com";
+    String taskApi ="192.168.0.64:7006";
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java

@@ -341,4 +341,9 @@ public interface QwApiConfig {
     String useridToOpenUserid="https://qyapi.weixin.qq.com/cgi-bin/batch/userid_to_openuserid";
 
     String getNewExternalUserid="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_new_external_userid";
+
+    /**
+     * 获取部门成员
+     */
+    String getUserSimpleList="https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
 }

+ 9 - 4
fs-service/src/main/java/com/fs/qwApi/service/QwApiService.java

@@ -358,8 +358,13 @@ public interface QwApiService {
 
     QwLinkCustomerResult linkCustomer(QwLinkCustomerParam param,String corpId);
 
-
-
-
-
+    /**
+     * 获取部门成员
+     *
+     * @param corpId
+     * @param accessToken
+     * @param id
+     * @return
+     */
+    UserResult getUserSimpleList(String corpId, String accessToken, Integer id);
 }

+ 43 - 1
fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java

@@ -1590,7 +1590,7 @@ public class QwApiServiceImpl implements QwApiService {
 
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
 
-        String bookSecret = qwCompany.getServerBookSecret();
+        String bookSecret = qwCompany.getPermanentCode();
 
         HttpClient httpClient = HttpClients.createDefault();
         try {
@@ -1679,6 +1679,48 @@ public class QwApiServiceImpl implements QwApiService {
         return null;
     }
 
+
+    /**
+     * 获取部门成员
+     * @param corpId
+     * @return
+     */
+    @Override
+    public UserResult getUserSimpleList(String corpId, String accessToken, Integer departmentId) {
+        HttpClient httpClient = HttpClients.createDefault();
+        try {
+            // 企业微信API:获取部门成员
+            URIBuilder builder = new URIBuilder(QwApiConfig.getUserSimpleList);
+            builder.setParameter("access_token", accessToken);
+            builder.setParameter("department_id", departmentId.toString());
+            builder.setParameter("fetch_child", "0"); // 0-不获取子部门,1-获取
+
+            URI uri = builder.build();
+            HttpGet httpGet = new HttpGet(uri);
+
+            // 设置请求头
+            httpGet.setHeader("Content-Type", "application/json");
+
+            HttpResponse response = httpClient.execute(httpGet);
+            String reJson = EntityUtils.toString(response.getEntity());
+            log.info("获取部门{}成员返回:{}", departmentId, reJson);
+
+            UserResult qwResult = JSON.parseObject(reJson, UserResult.class);
+
+            // 检查返回码
+            if (qwResult.getErrcode() != 0) {
+                log.error("获取部门成员失败,errcode: {}, errmsg: {}",
+                        qwResult.getErrcode(), qwResult.getErrmsg());
+            }
+
+            return qwResult;
+        } catch (Exception e) {
+            log.error("获取部门成员异常,departmentId: {}", departmentId, e);
+        }
+        return null;
+    }
+
+
     @Override
     public QwDeptResult getDepartmentList(String corpId) {
 ////        CompanyConfig companyConfig = companyConfigService.selectCompanyConfigByKey(corpId, "sys:qw:config");

+ 1 - 1
fs-service/src/main/resources/application-dev.yml

@@ -124,4 +124,4 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: true

+ 53 - 0
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -332,5 +332,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
              #{serverId}
          </foreach>
     </select>
+    <!-- QwUserMapper.xml -->
+    <!-- 批量查询 -->
+    <select id="selectQwUsersByCorpIdAndUserIds" resultType="QwUser">
+        SELECT * FROM qw_user
+        WHERE corp_id = #{corpId}
+        AND qw_open_user_id IN
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </select>
+
+    <!-- 批量插入 - 补充完整字段 -->
+    <insert id="batchInsertQwUser">
+        INSERT INTO qw_user (
+        department, qw_user_name, corp_id, openid, qw_open_user_id,
+        qw_user_id, company_id, company_user_id, status, welcome_text,
+        welcome_image, is_send_msg, app_key, contact_way, config_id,
+        qw_hook_id, fastGpt_role_id, login_status, tool_status,
+        login_code_url, version, vid, uid, ipad_status, server_id,
+        server_status, is_auto, video_get_status, ai_status,
+        is_del, create_time
+        ) VALUES
+        <foreach collection="list" item="user" separator=",">
+            (
+            #{user.department}, #{user.qwUserName}, #{user.corpId}, #{user.openid}, #{user.qwOpenUserId},
+            #{user.qwUserId}, #{user.companyId}, #{user.companyUserId}, #{user.status}, #{user.welcomeText},
+            #{user.welcomeImage}, #{user.isSendMsg}, #{user.appKey}, #{user.contactWay}, #{user.configId},
+            #{user.qwHookId}, #{user.fastGptRoleId}, #{user.loginStatus}, #{user.toolStatus},
+            #{user.loginCodeUrl}, #{user.version}, #{user.vid}, #{user.uid}, #{user.ipadStatus}, #{user.serverId},
+            #{user.serverStatus}, #{user.isAuto}, #{user.videoGetStatus}, #{user.aiStatus},
+            0, NOW()
+            )
+        </foreach>
+    </insert>
 
+    <!-- 批量更新 - 使用 CASE WHEN 方式,更高效 -->
+    <insert id="batchUpdateQwUser">
+        INSERT INTO qw_user (
+        id, corp_id, qw_open_user_id, qw_user_name,
+        department, openid, is_del, create_time, update_time
+        ) VALUES
+        <foreach collection="list" item="user" separator=",">
+            (
+            #{user.id}, #{user.corpId}, #{user.qwOpenUserId}, #{user.qwUserName},
+            #{user.department}, #{user.openid}, #{user.isDel},
+            NOW(), NOW()
+            )
+        </foreach>
+        ON DUPLICATE KEY UPDATE
+        qw_user_name = VALUES(qw_user_name),
+        department = VALUES(department),
+        openid = VALUES(openid),
+        update_time = NOW()
+    </insert>
 </mapper>