Преглед изворни кода

Merge remote-tracking branch 'origin/master'

yys пре 3 месеци
родитељ
комит
4a16a13dfe

+ 32 - 3
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -1,6 +1,9 @@
 package com.fs.company.controller.qw;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -36,6 +39,7 @@ import com.fs.qw.service.*;
 import com.fs.qw.vo.QwExternalContactVO;
 import com.fs.qw.vo.QwFsUserVO;
 import com.fs.qw.vo.QwUserDelLossLogVO;
+import com.fs.qwApi.config.OpenQwConfig;
 import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageHelper;
 import com.google.gson.Gson;
@@ -468,7 +472,16 @@ public class QwExternalContactController extends BaseController
         if(Param.getUserIds() == null || Param.getUserIds().isEmpty()){
             return R.error("修改用户为空");
         }
-        return qwExternalContactService.addUserTag(Param);
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        String url = OpenQwConfig.baseApi + "/addTag?tenantId=" + loginUser.getTenantId();
+        String result = HttpUtil.createPost(url)
+                .body(JSON.toJSONString(Param))
+                .execute()
+                .body();
+        return JSONObject.parseObject(result, R.class);
+//        return qwExternalContactService.addUserTag(Param);
     }
 
     @PreAuthorize("@ss.hasPermi('qw:externalContact:addTag')")
@@ -565,8 +578,16 @@ public class QwExternalContactController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody QwExternalContact qwExternalContact)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        String url = OpenQwConfig.baseApi + "/edit?tenantId=" + loginUser.getTenantId();
+        String result = HttpUtil.createPost(url)
+                .body(JSON.toJSONString(qwExternalContact))
+                .execute()
+                .body();
 
-        return toAjax(qwExternalContactService.updateQwExternalContact(qwExternalContact));
+        return toAjax(Boolean.getBoolean(result));
+//        return toAjax(qwExternalContactService.updateQwExternalContact(qwExternalContact));
     }
 
     @PreAuthorize("@ss.hasPermi('qw:externalContact:edit')")
@@ -733,7 +754,15 @@ public class QwExternalContactController extends BaseController
             return R.error("修改用户为空");
         }
 //        qwExternalContactService.batchUpdateExternalContactNotes(Param);
-        qwUserServiceAsyncHelper.batchUpdateExternalContactNotes(param);
+//        qwUserServiceAsyncHelper.batchUpdateExternalContactNotes(param);
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        String url = OpenQwConfig.baseApi + "/batchUpdateExternalContactNotes?tenantId=" + loginUser.getTenantId();
+        String result = HttpUtil.createPost(url)
+                .body(JSON.toJSONString(param))
+                .execute()
+                .body();
         return R.ok("正在批量修改备注中");
     }
 

+ 6 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -651,7 +651,12 @@ public class QwUserController extends BaseController
         List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(company.getCompanyId());
         for (String string : strings) {
             if (string.equals(corpId)){
-                qwUserService.syncQwUserName(string);
+//                qwUserService.syncQwUserName(string);
+
+                Map<String, Object> paramMap = new HashMap<>();
+                paramMap.put("tenantId", loginUser.getTenantId());
+                paramMap.put("corpId", string);
+                HttpUtil.post(OpenQwConfig.baseApi + "/getSyncName", paramMap);
             }
         }
         return R.ok();

+ 65 - 1
fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java

@@ -1,14 +1,19 @@
 package com.fs.app.controller;
 
 import com.fs.app.service.OpenQwApiService;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.param.QwExternalContactAddTagParam;
+import com.fs.qw.param.QwExternalContactUpdateNoteParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/open/qwapi")
-public class OpenQwApiController {
+public class OpenQwApiController extends BaseController {
 
     @Autowired
     private OpenQwApiService openQwApiService;
@@ -48,5 +53,64 @@ public class OpenQwApiController {
             tenantDataSourceManager.clear();
         }
     }
+
+    /**
+     * 同步企微员工名称
+     * @param tenantId
+     * @param corpId
+     * @return
+     */
+    @PostMapping("/getSyncName")
+    public R getSyncName(@RequestParam Long tenantId, @RequestParam String corpId){
+        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+        try {
+            return openQwApiService.getSyncName(corpId);
+        } finally {
+            tenantDataSourceManager.clear();
+        }
+    }
+
+    /**
+     * 批量修改备注
+     */
+    @PostMapping("/batchUpdateExternalContactNotes")
+    public R batchUpdateExternalContactNotes(@RequestBody QwExternalContactUpdateNoteParam param, @RequestParam(value = "tenantId", required = true) Long tenantId){
+        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+
+        try {
+            return openQwApiService.batchUpdateExternalContactNotes(param);
+        } finally {
+            tenantDataSourceManager.clear();
+        }
+    }
+
+    /**
+     * 批量修改备注
+     */
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody QwExternalContact qwExternalContact, @RequestParam(value = "tenantId", required = true) Long tenantId){
+        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+
+        try {
+            return toAjax(openQwApiService.edit(qwExternalContact));
+        } finally {
+            tenantDataSourceManager.clear();
+        }
+    }
+
+
+    /**
+     * 添加标签
+     */
+    @PostMapping("/addTag")
+    public R edit(@RequestBody QwExternalContactAddTagParam param, @RequestParam(value = "tenantId", required = true) Long tenantId){
+        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+
+        try {
+            return openQwApiService.addTag(param);
+        } finally {
+            tenantDataSourceManager.clear();
+        }
+    }
 }
 

+ 11 - 0
fs-qw-api/src/main/java/com/fs/app/service/OpenQwApiService.java

@@ -1,9 +1,20 @@
 package com.fs.app.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.param.QwExternalContactAddTagParam;
+import com.fs.qw.param.QwExternalContactUpdateNoteParam;
 
 public interface OpenQwApiService {
     R getSyncQwUser(String corpId);
 
     R getSyncQwDept(String corpId);
+
+    R getSyncName(String corpId);
+
+    R batchUpdateExternalContactNotes(QwExternalContactUpdateNoteParam param);
+
+    int edit(QwExternalContact qwExternalContact);
+
+    R addTag(QwExternalContactAddTagParam param);
 }

+ 849 - 1
fs-qw-api/src/main/java/com/fs/app/service/impl/OpenQwApiServiceImpl.java

@@ -3,24 +3,54 @@ package com.fs.app.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.service.OpenQwApiService;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwDept;
+import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwDeptMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwAutoSopTimeParam;
+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.QwOpenidResult;
 import com.fs.qwApi.domain.QwDeptResult;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.domain.QwUserIdResult;
 import com.fs.qwApi.domain.inner.Department;
 import com.fs.qwApi.domain.inner.DeptUser;
+import com.fs.qwApi.param.QwEditUserTagParam;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
 import com.fs.qwApi.param.QwOpenidByUserParams;
 import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+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 lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Service
@@ -37,6 +67,9 @@ public class OpenQwApiServiceImpl implements OpenQwApiService {
     @Autowired
     private QwDeptMapper qwDeptMapper;
 
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
     @Override
     public R getSyncQwUser(String corpId) {
         QwUserIdResult userList = qwApiService.getUserList(corpId);
@@ -88,4 +121,819 @@ public class OpenQwApiServiceImpl implements OpenQwApiService {
 
         return null;
     }
+
+    @Override
+    public R getSyncName(String corpId) {
+        QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
+        List<QwUser> qwUsers=qwUserMapper.selectQwUserByCorpId(corpId);
+        if (qwCompany == null) {
+            log.warn("syncQwUserName skipped: corpId={} 对应企业为空", corpId);
+            return R.error("企业信息不存在");
+        }
+        if (qwUsers == null || qwUsers.isEmpty()) {
+            log.info("syncQwUserName no users: corpId={}", corpId);
+            return R.ok();
+        }
+        String currentDataSource = getCurrentDataSourceType();
+        Long tenantId = SecurityUtils.getTenantId();
+        String currentTraceId = MDC.get("traceId");
+        log.info("syncQwUserName start: corpId={}, tenantId={}, dataSource={}, userCount={}",
+                corpId, tenantId, currentDataSource, qwUsers.size());
+        ExecutorService executorService = Executors.newFixedThreadPool(50);
+        for (QwUser user : qwUsers) {
+            executorService.submit(() -> {
+                try {
+                    if (currentDataSource != null && !currentDataSource.isEmpty()) {
+                        setCurrentDataSourceType(currentDataSource);
+                        MDC.put("dataSource", currentDataSource);
+                    }
+                    if (tenantId != null) {
+                        MDC.put("tenantId", String.valueOf(tenantId));
+                    }
+                    if (currentTraceId != null && !currentTraceId.isEmpty()) {
+                        MDC.put("traceId", currentTraceId);
+                    }
+                    updateQwUserName(user, qwCompany);
+                } finally {
+                    clearCurrentDataSourceType();
+                    MDC.remove("dataSource");
+                    MDC.remove("tenantId");
+                    MDC.remove("traceId");
+                }
+            });
+        }
+        executorService.shutdown(); // 关闭线程池,等待所有任务完成
+        try {
+            if (!executorService.awaitTermination(5, TimeUnit.MINUTES)) {
+                log.warn("syncQwUserName timeout: corpId={}, tenantId={}, dataSource={}",
+                        corpId, tenantId, currentDataSource);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("syncQwUserName interrupted: corpId={}, tenantId={}, dataSource={}",
+                    corpId, tenantId, currentDataSource, e);
+        }
+        log.info("syncQwUserName finish: corpId={}, tenantId={}, dataSource={}",
+                corpId, tenantId, currentDataSource);
+        return R.ok();
+    }
+
+    @Override
+    public R batchUpdateExternalContactNotes(QwExternalContactUpdateNoteParam param) {
+        try {
+            log.info("批量修改备注Async");
+            List<String> failList = new CopyOnWriteArrayList<>(); // 记录失败客户的名字
+
+            AtomicInteger suc = new AtomicInteger();
+
+
+            List<QwExternalContact> contactList = qwExternalContactMapper.selectQwExternalContactByIds(param.getUserIds());
+
+            ExecutorService executorService = Executors.newFixedThreadPool(10); // 限制最大线程数
+            CountDownLatch latch = new CountDownLatch(contactList.size()); // 同步等待所有任务完成
+
+            AtomicReference<Integer> failReason = new AtomicReference<>(0);
+
+            contactList.forEach(item -> {
+                executorService.submit(() -> {
+                    try {
+                        String originalRemark = item.getRemark();
+                        String newRemark = "";
+
+                        if (StringUtil.strIsNullOrEmpty(originalRemark)) {
+                            originalRemark = "";
+                        }
+
+                        if (StringUtil.strIsNullOrEmpty(param.getNotes())) {
+                            newRemark = originalRemark;
+                        } else {
+
+                            String newNotes = "";
+
+                            switch (param.getNameType()) {
+                                case 1:
+                                    newNotes = item.getName() + "-" + param.getNotes();
+                                    break;
+                                case 2:
+                                    newNotes = param.getNotes() + "-" + item.getName();
+                                    break;
+                                case 3:
+                                    newNotes = param.getNotes();
+                                    break;
+                                default:
+                                    break;
+                            }
+
+                            switch (param.getType()) {
+                                case 1:
+                                    newRemark = (newNotes + originalRemark);
+                                    if (newRemark.length() > 20) {
+                                        newRemark = newRemark.substring(0, 20);
+                                    }
+                                    break;
+                                case 2:
+                                    int keepLength = 20 - newNotes.length();
+                                    if (originalRemark.length() > keepLength) {
+                                        originalRemark = originalRemark.substring(0, keepLength);
+                                    }
+                                    newRemark = originalRemark + param.getNotes();
+                                    break;
+                                case 3:
+                                    newRemark = newNotes;
+                                    break;
+                                default:
+                                    break;
+                            }
+
+                        }
+
+
+                        QwExternalContactRemarkParam remarkParam = new QwExternalContactRemarkParam();
+                        remarkParam.setRemark(newRemark);
+                        remarkParam.setUserid(item.getUserId());
+                        remarkParam.setExternal_userid(item.getExternalUserId());
+
+                        boolean success = false;
+                        for (int attempt = 1; attempt <= 3; attempt++) {
+                            try {
+                                QwExternalContactRemarkResult qwResult = qwApiService.externalcontactRemark(remarkParam, item.getCorpId());
+                                if (qwResult.getErrcode() == 0) {
+                                    item.setRemark(newRemark);
+                                    log.info("成功添加备注:" + item.getName() + "|公司" + item.getCorpId() + "|员工" + item.getUserId() + "|备注" + param.getNotes() + "|类型" + param.getType());
+                                    suc.getAndIncrement();
+                                    qwExternalContactMapper.updateQwExternalContact(item);
+                                    success = true;
+                                    break;
+                                } else {
+                                    failReason.set(qwResult.getErrcode()); // 记录失败原因
+                                }
+                            } catch (Exception e) {
+                                log.error("添加备注异常 [尝试第 " + attempt + " 次]:" + item.getName() + ",异常:" + e.getMessage(), e);
+                            }
+
+                            // 若不是最后一次尝试,则等待3秒再试
+                            if (attempt < 3) {
+                                try {
+                                    Thread.sleep(3000);
+                                } catch (InterruptedException e) {
+                                    log.warn("线程等待被中断", e);
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (!success) {
+                            log.error(" 加备注失败:" + item.getName() + "|" + item.getExternalUserId() + "|" + item.getUserId());
+
+                            failList.add("【" + item.getName() + "】" + "原因:(" + getErrorMsg(failReason.get()) + ")\n");
+                        }
+
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            });
+
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                log.error("等待线程执行完成时被中断", e);
+            } finally {
+                executorService.shutdown();
+            }
+
+            String failNames = null;
+            if (!failList.isEmpty()) {
+                failNames = String.join(",", failList);
+            }
+            log.info("批量修改备注 成功:" + suc + ",失败:" + failNames);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.info("批量修改备注失败");
+        }
+
+        return R.ok();
+    }
+
+    @Override
+    public int edit(QwExternalContact qwExternalContact) {
+        QwExternalContactRemarkParam param = new QwExternalContactRemarkParam();
+        param.setRemark(qwExternalContact.getRemark());
+        param.setUserid(qwExternalContact.getUserId());
+        param.setExternal_userid(qwExternalContact.getExternalUserId());
+        if (qwExternalContact.getRemarkMobiles() != null && qwExternalContact.getRemarkMobiles() != "") {
+            List<String> mobiles = JSON.parseArray(qwExternalContact.getRemarkMobiles(), String.class);
+            if (mobiles != null && mobiles.size() > 0) {
+                param.setRemark_mobiles(mobiles);
+            } else {
+                param.setRemark_mobiles(Arrays.asList(""));
+            }
+        } else {
+            param.setRemark_mobiles(Arrays.asList(""));
+        }
+
+        param.setDescription(qwExternalContact.getDescription());
+        param.setRemark_company(qwExternalContact.getRemarkCorpName());
+        QwExternalContactRemarkResult qwExternalContactRemarkResult = qwApiService.externalcontactRemark(param, qwExternalContact.getCorpId());
+        log.info("QwExternalContactRemarkResult206:" + qwExternalContactRemarkResult);
+        if (qwExternalContactRemarkResult.getErrcode() == 0) {
+            return qwExternalContactMapper.updateQwExternalContact(qwExternalContact);
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public R addTag(QwExternalContactAddTagParam param) {
+        // 获取当前日期和时间
+        LocalDate currentDate = LocalDate.now();
+        LocalTime localTime = LocalTime.now();
+
+        // 使用线程安全的计数器
+        AtomicInteger suc = new AtomicInteger(0);
+
+        List<String> failList = new CopyOnWriteArrayList<>(); // 记录失败客户的名字
+
+        // 创建线程池
+        int threadCount = Math.min(8, Runtime.getRuntime().availableProcessors() * 2);
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+
+        try {
+            // 使用线程安全的List来收集需要批量更新的数据
+            List<QwExternalContact> batchUpdateList = Collections.synchronizedList(new ArrayList<>());
+
+            // 存储Future对象以便检查所有任务完成情况
+            List<Future<?>> futures = new ArrayList<>();
+
+
+            // 1. 批量查询所有用户数据(流失客户不打了)
+            List<QwExternalContact> contacts;
+            try {
+//                contacts = qwExternalContactMapper.selectQwExternalContactByIds(param.getUserIds());
+                contacts = qwExternalContactMapper.selectQwExternalContactByIdsStatus(param.getUserIds());
+                if (contacts == null || contacts.isEmpty()) {
+                    return R.error("成功:0,失败:" + param.getUserIds().size());
+                }
+            } catch (Exception e) {
+                return R.error("批量查询用户数据失败:"+e.getMessage());
+            }
+
+            // 直接遍历contacts而不是userIds
+            for (QwExternalContact qwExternalContact : contacts) {
+                futures.add(executor.submit(() -> {
+                    try {
+                        QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+                        qwEditUserTagParam.setAdd_tag(param.getTagIds());
+                        qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
+                        qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+
+                        QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, param.getCorpId());
+                        if (qwResult.getErrcode() == 0) {
+                            // 处理标签
+                            String tagIds = qwExternalContact.getTagIds();
+                            Set<String> uniqueIds = new HashSet<>();
+
+                            if (tagIds != null && !tagIds.isEmpty()) {
+                                List<String> parsedTags = JSON.parseArray(tagIds, String.class);
+                                if (parsedTags != null && !parsedTags.isEmpty()) {
+                                    uniqueIds.addAll(parsedTags);
+                                }
+                            }
+
+                            if (param.getTagIds() != null && !param.getTagIds().isEmpty()) {
+                                uniqueIds.addAll(param.getTagIds());
+                            }
+
+                            QwExternalContact qwExternal = new QwExternalContact();
+                            qwExternal.setTagIds(JSON.toJSONString(uniqueIds));
+                            qwExternal.setId(qwExternalContact.getId());
+
+                            List<String> tagIdsList = new ArrayList<>();
+                            if (qwExternal.getTagIds() != null && !qwExternal.getTagIds().isEmpty()) {
+                                tagIdsList = JSON.parseArray(qwExternal.getTagIds(), String.class);
+                            }
+
+                            log.info("客户添加标签addUserTag:" + qwExternalContact.getName() +
+                                    "|公司" + qwExternalContact.getCorpId() +
+                                    "|员工" + qwExternalContact.getUserId() +
+                                    "|总标签" + tagIdsList);
+
+                            // 插件sop处理
+                            processTagsAll(qwExternalContact, qwExternalContact.getCorpId(),
+                                    tagIdsList, currentDate, localTime);
+
+                            // 添加到批量更新列表
+                            batchUpdateList.add(qwExternal);
+                            suc.incrementAndGet();
+                        } else {
+                            failList.add("【"+qwExternalContact.getName()+"】" + "原因:(" + getErrorMsg(qwResult.getErrcode()) + ")\n");
+                        }
+                    } catch (Exception e) {
+                        log.error("客户添加标签失败,userId: " + qwExternalContact.getId() +
+                                ", externalUserId: " + qwExternalContact.getExternalUserId() +
+                                ", 错误信息: " + e.getMessage());
+                        failList.add("【"+qwExternalContact.getName()+"】" + "原因:(" + e.getMessage() + ")\n");
+                    }
+                }));
+            }
+
+
+            // 等待所有任务完成
+            for (Future<?> future : futures) {
+                try {
+                    future.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    log.error("任务执行异常: " + e.getMessage());
+                    Thread.currentThread().interrupt();
+                }
+            }
+
+            // 批量更新数据库
+            if (!batchUpdateList.isEmpty()) {
+                try {
+                    // 分批处理,避免单次批量过大
+                    int batchSize = 500; // 根据数据库性能调整
+                    for (int i = 0; i < batchUpdateList.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, batchUpdateList.size());
+                        List<QwExternalContact> subList = batchUpdateList.subList(i, end);
+                        qwExternalContactMapper.batchUpdateQwExternalContactByTags(subList);
+                    }
+                } catch (Exception e) {
+                    log.error("批量更新失败: " + e.getMessage());
+                }
+            }
+
+            // 关闭线程池
+            executor.shutdown();
+
+        }catch (Exception e){
+            failList.add("处理过程中发生异常:"+e.getMessage());
+        }finally {
+            // 7. 确保线程池关闭
+            try {
+                // 先尝试正常关闭
+                executor.shutdown();
+                if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
+                    // 超时后强制关闭
+                    executor.shutdownNow();
+                    log.warn("线程池强制关闭");
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                executor.shutdownNow();
+            }
+        }
+
+        return R.ok("成功:" + suc.get() + ",失败:" + failList);
+    }
+
+    @Autowired
+    private QwSopMapper qwSopMapper;
+
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+
+    @Autowired
+    private ISopUserLogsService sopUserLogsService;
+    /**
+     * 整合客户的标签的逻辑 ,新增和移除 都要移除和添加到营期里
+     */
+    private void processTagsAll(QwExternalContact qwExternalContact, String corpId, List<String> tagArr,LocalDate currentDate,LocalTime localTime){
+
+        List<String> cleanedTagList = tagArr.stream()
+                .map(String::trim)   // 去除前后空格
+                .filter(s -> !s.isEmpty()) // 过滤掉空字符串(如果有)
+                .distinct()           // 去重
+                .collect(Collectors.toList());
+
+        QwSopAutoByTags qwSopAutoByTags = new QwSopAutoByTags();
+        qwSopAutoByTags.setQwUserId(String.valueOf(qwExternalContact.getQwUserId()));
+        qwSopAutoByTags.setCorpId(corpId);
+        qwSopAutoByTags.setTagsIdsSelectList(cleanedTagList);
+        qwSopAutoByTags.setSendType(2);
+
+        // 定义日期和时间格式化器
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
+
+
+        try {
+            //查询匹配这个销售所有正在执行的 SOP任务 ps:仅仅是AI插件的
+            List<QwSopRuleTimeVO> runRuleTime = qwSopMapper.selectQwSopByQwUserId(qwSopAutoByTags);
+
+            log.info("查出来的销售 任务长度:"+runRuleTime.size());
+            //如果剩下的总标签 啥也没有
+            // 查询匹配这些Tag的SOP任务
+            if (cleanedTagList.isEmpty()){
+
+                //这个人标签空了,以防万一 将这个销售下的这个人的所有sop里的他(可能没有) 全刷一遍
+                if (!runRuleTime.isEmpty()){
+                    runRuleTime.forEach(runSop->{
+
+                        log.info("cleanedTagList-空了,将这个销售下的这个人的所有sop里的他(可能没有) 全刷一遍 sopId:{}",runSop.getId());
+                        deleteBySopIdToContactIdTools(runSop.getId(),qwExternalContact.getUserId(),qwExternalContact.getCorpId(),qwExternalContact.getId());
+
+                    });
+                }
+            }
+            else {
+                List<QwSopRuleTimeVO> qwSopRuleTimeVOS = qwSopMapper.selectQwSopAutoByTagsByForeachNotAuto(qwSopAutoByTags);
+
+                if (!qwSopRuleTimeVOS.isEmpty()){
+
+                    //匹配上剩下的标签看看符合不符合
+                    qwSopRuleTimeVOS.forEach(ruleTimeVO -> {
+
+                        // 将排除的字符串转成列表
+                        List<String> excludedTagsList=new ArrayList<>();
+                        if (ruleTimeVO.getExcludeTags() != null && !ruleTimeVO.getExcludeTags().isEmpty()){
+                            excludedTagsList= Arrays.asList( ruleTimeVO.getExcludeTags().split(","));
+                        }
+
+
+                        // 检查 combinedTagsList 是否包含排除列表中的任意一个标签
+                        boolean containsExcludedTag = cleanedTagList.stream()
+                                .anyMatch(excludedTagsList::contains);
+
+                        //含任意一个排除标签
+                        if (containsExcludedTag) {
+                            log.info("该客户已匹配到规则,但是被排除,规则id:"+ruleTimeVO.getId()+"|cleanedTagList"+cleanedTagList+"|excludedTagsList"+excludedTagsList);
+                            deleteBySopIdToContactIdTools(ruleTimeVO.getId(),qwExternalContact.getUserId(),qwExternalContact.getCorpId(),qwExternalContact.getId());
+
+                        }
+                        else {
+                            //检查 有可能在营期里 有可能不在。在不动,不在
+                            // 从数据库中取到的开始时间(Date类型),转换为 LocalDate
+                            Date sopStartTime = ruleTimeVO.getStartTime();
+                            //开始时间
+                            LocalDate sopStartLocalDate = sopStartTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+                            // 自动sop的规则
+                            QwAutoSopTimeParam qwAutoSopTimeParam = JSON.parseObject(ruleTimeVO.getAutoSopTime(), QwAutoSopTimeParam.class);
+
+                            // 用于查询/或新增
+                            SopUserLogs userLogs = new SopUserLogs();
+                            userLogs.setSopId(ruleTimeVO.getId());
+                            userLogs.setSopTempId(ruleTimeVO.getTempId());
+                            userLogs.setCorpId(ruleTimeVO.getCorpId());
+                            userLogs.setStatus(1);
+
+                            // 用于今天的新增
+                            SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
+                            userLogsParamByDate.setSopId(ruleTimeVO.getId());
+                            userLogsParamByDate.setSopTempId(ruleTimeVO.getTempId());
+                            userLogsParamByDate.setCorpId(ruleTimeVO.getCorpId());
+                            userLogsParamByDate.setStatus(1);
+
+                            // 设定用户信息
+                            userLogs.setQwUserId(qwExternalContact.getUserId());
+                            userLogs.setUserId(qwExternalContact.getQwUserId() + "|" + qwExternalContact.getCompanyUserId() + "|" + qwExternalContact.getCompanyId());
+                            userLogsParamByDate.setQwUserId(qwExternalContact.getUserId());
+                            userLogsParamByDate.setUserId(qwExternalContact.getQwUserId() + "|" + qwExternalContact.getCompanyUserId() + "|" + qwExternalContact.getCompanyId());
+
+                            // 创建 SopUserLogsInfo
+                            SopUserLogsInfo logsInfo = new SopUserLogsInfo();
+                            logsInfo.setQwUserId(qwExternalContact.getUserId());
+                            logsInfo.setCorpId(corpId);
+                            logsInfo.setExternalContactId(qwExternalContact.getExternalUserId());
+                            logsInfo.setExternalId(qwExternalContact.getId());
+                            logsInfo.setFsUserId(qwExternalContact.getFsUserId());
+                            logsInfo.setExternalUserName(qwExternalContact.getName());
+                            logsInfo.setSopId(ruleTimeVO.getId());
+
+
+                            //如果是固定营期
+                            if (Integer.valueOf(1).equals(ruleTimeVO.getIsFixed())){
+
+                                qwSopRuleTimeToolsCheckByIsFixed(sopStartLocalDate,dateFormatter,userLogs,logsInfo,userLogsParamByDate);
+
+                            }else {
+                                //检查营期
+                                qwSopRuleTimeToolsCheck(sopStartLocalDate,currentDate,dateFormatter,userLogs,logsInfo,userLogsParamByDate,qwAutoSopTimeParam,timeFormatter,localTime);
+                            }
+
+
+                        }
+
+                    });
+
+                    List<QwSopRuleTimeVO> qwSopRuleTimeVOSRemove = qwSopMapper.selectQwSopAutoByTagsByForeachRemoveNotAuto(qwSopAutoByTags);
+                    if (!qwSopRuleTimeVOSRemove.isEmpty()){
+                        qwSopRuleTimeVOSRemove.forEach(vosRemove->{
+                            log.info("删除这个客户在这个销售的其他无关的sop任务"+vosRemove.getId()+"标签:"+tagArr+"id:"+qwExternalContact.getQwUserId());
+                            deleteBySopIdToContactIdTools(vosRemove.getId(),qwExternalContact.getUserId(),qwExternalContact.getCorpId(),qwExternalContact.getId());
+
+                        });
+                    }
+
+                }else {
+                    //没匹配上任意一个(即这个剩下的标签匹配不上任意的sop),但是销售有任务-以防万一-删除这个客户在这个销售的sop
+                    if (!runRuleTime.isEmpty()){
+                        runRuleTime.forEach(runSop->{
+                            log.info("没匹配上任意一个(即这个剩下的标签匹配不上任意的sop),但是销售有任务-以防万一-删除这个客户在这个销售的sop"+runSop.getId()+"标签:"+tagArr+"id:"+qwExternalContact.getQwUserId());
+                            deleteBySopIdToContactIdTools(runSop.getId(),qwExternalContact.getUserId(),qwExternalContact.getCorpId(),qwExternalContact.getId());
+
+                        });
+                    }
+                }
+
+            }
+
+        }catch (Exception e) {
+            log.error("改动了客户的标签的异常:qwExternalContact"+qwExternalContact+"|corpId"+corpId+"|cleanedTagList"+cleanedTagList);
+        }
+
+    };
+
+    public void qwSopRuleTimeToolsCheck(LocalDate sopStartLocalDate,LocalDate currentDate,DateTimeFormatter dateFormatter,
+                                        SopUserLogs userLogs,SopUserLogsInfo logsInfo,SopUserLogsParamByDate userLogsParamByDate,
+                                        QwAutoSopTimeParam qwAutoSopTimeParam,DateTimeFormatter timeFormatter,LocalTime localTime) {
+
+        try {
+            // 还没开始 (未来,不包括今天)
+            if (sopStartLocalDate.isAfter(currentDate)) {
+                // 大于当前日期,说明还没有开始:
+                // -> 以 SOP 任务的开始时间为营期
+                String sopStartLocalDateStr = sopStartLocalDate.format(dateFormatter);
+
+                userLogs.setStartTime(sopStartLocalDateStr);
+                // 查询开始时间营期表
+                String unionSopStartId = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+                // 如果营期表里有,则加入;否则创建新营期
+                if (!StringUtil.strIsNullOrEmpty(unionSopStartId)) {
+                    logsInfo.setUserLogsId(unionSopStartId);
+                    // 查询这个人是否已在该营期里
+                    handleInsertSopUserLogsInfo(logsInfo);
+                }
+                else {
+                    // 没有营期记录就先插入
+                    userLogsParamByDate.setStartTime(sopStartLocalDateStr);
+
+                    sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+
+                    // 再次查询 拿营期主键
+                    String unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+                    logsInfo.setUserLogsId(unionSopStartIdNew);
+
+                    // 查询是否在营期里,如果没有再插入
+                    handleInsertSopUserLogsInfo(logsInfo);
+                }
+            }
+            else {
+                // sopStartLocalDate <= currentDate -> 说明已经开始
+                // -> 以“今天 / 明天”作为营期
+                // 获取 AutoSopType
+                Integer autoSopType = qwAutoSopTimeParam.getAutoSopType();
+
+                // 今天的日期字符串
+                String todayStr = currentDate.format(dateFormatter);
+                userLogs.setStartTime(todayStr);
+
+                // 查询今天的营期表
+                String unionSopStartId2 = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+                // 明天日期字符串
+                LocalDate tomorrowDate = currentDate.plusDays(1);
+                String tomorrowStr = tomorrowDate.format(dateFormatter);
+
+                // 复制一份 userLogs,设为明天
+                SopUserLogs userLogsTomorrow = new SopUserLogs();
+                BeanUtils.copyProperties(userLogs, userLogsTomorrow);
+                userLogsTomorrow.setStartTime(tomorrowStr);
+
+                // 复制一份 SopUserLogsParamByDate,设为明天
+                SopUserLogsParamByDate paramByDateTomorrow=new SopUserLogsParamByDate();
+                BeanUtils.copyProperties(userLogsParamByDate, paramByDateTomorrow);
+                paramByDateTomorrow.setStartTime(tomorrowStr);
+
+                // 查询明天的营期表
+                String unionTomorrowSopId2 = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogsTomorrow);
+
+                // 根据 autoSopType 分支处理
+                switch (autoSopType) {
+                    // 立即执行
+                    case 1: {
+                        String autoStartTime = qwAutoSopTimeParam.getAutoStartTime();
+                        String autoEndTime = qwAutoSopTimeParam.getAutoEndTime();
+
+                        // 如果结束时间为 24:00,则替换成 23:59
+                        if ("24:00".equals(autoEndTime)) {
+                            autoEndTime = "23:59";
+                        }
+
+                        // 转为 LocalTime
+                        LocalTime startTime = LocalTime.parse(autoStartTime, timeFormatter);
+                        LocalTime endTime = LocalTime.parse(autoEndTime, timeFormatter);
+
+                        // 判断当前时间是否在 [startTime, endTime] 范围内
+                        if (!localTime.isBefore(startTime) && !localTime.isAfter(endTime)) {
+                            // 在范围内 => 加入到今天的营期
+                            if (!StringUtil.strIsNullOrEmpty(unionSopStartId2)) {
+
+                                logsInfo.setUserLogsId(unionSopStartId2);
+
+                                // 查询客户是否已存在
+                                handleInsertSopUserLogsInfo(logsInfo);
+
+                            } else {
+                                // 没有今天天的营期就先建
+                                userLogsParamByDate.setStartTime(todayStr);
+                                sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+                                // 查询客户是否已存在
+                                SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(logsInfo);
+                                if (userLogsInfo == null) {
+
+                                    // 再查今天的营期的主键
+                                    userLogs.setStartTime(todayStr);
+                                    String unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+                                    logsInfo.setUserLogsId(unionSopStartIdNew);
+                                    //客户入营期
+                                    sopUserLogsInfoMapper.insertSopUserLogsInfo(logsInfo);
+                                }
+                            }
+                        } else {
+                            // 不在范围内 => 加入到明天的营期
+                            if (!StringUtil.strIsNullOrEmpty(unionTomorrowSopId2)) {
+                                logsInfo.setUserLogsId(unionTomorrowSopId2);
+                                // 查询是否已存在客户入营期
+                                handleInsertSopUserLogsInfo(logsInfo);
+                            } else {
+                                // 没有明天的营期就先建
+                                handleNextDayExecution( paramByDateTomorrow, logsInfo, userLogsTomorrow);
+                            }
+                        }
+                        break;
+                    }
+                    // 次日执行
+                    case 2: {
+                        // 直接加入到明天的营期没有明天的营期就先建
+                        if (!StringUtil.strIsNullOrEmpty(unionTomorrowSopId2)) {
+                            logsInfo.setUserLogsId(unionTomorrowSopId2);
+                            //客户入营期
+                            handleInsertSopUserLogsInfo(logsInfo);
+                        } else {
+                            // 没有明天的营期就先建
+                            handleNextDayExecution( paramByDateTomorrow, logsInfo, userLogsTomorrow);
+                        }
+                        break;
+                    }
+                    default:
+                        // 其他类型可根据需要自行扩展
+                        break;
+                }
+            }
+        }catch (Exception e){
+            log.error("qwSopRuleTimeToolsCheckError:"+e);
+        }
+
+    }
+
+    public void handleNextDayExecution(SopUserLogsParamByDate paramByDateTomorrow,SopUserLogsInfo sopUserLogsInfo,SopUserLogs userLogsTomorrow) {
+
+        sopUserLogsService.insertSopUserLogsByDate(paramByDateTomorrow);
+
+        SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(sopUserLogsInfo);
+        if (userLogsInfo == null) {
+
+            String unionTomorrowSopId2New = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogsTomorrow);
+            sopUserLogsInfo.setUserLogsId(unionTomorrowSopId2New);
+
+            sopUserLogsInfoMapper.insertSopUserLogsInfo(sopUserLogsInfo);
+        }
+    }
+
+    //固定营期  以 SOP 任务的开始时间为营期
+    private void qwSopRuleTimeToolsCheckByIsFixed(LocalDate sopStartLocalDate,DateTimeFormatter dateFormatter,
+                                                  SopUserLogs userLogs,SopUserLogsInfo logsInfo,
+                                                  SopUserLogsParamByDate userLogsParamByDate){
+
+        String sopStartLocalDateStr = sopStartLocalDate.format(dateFormatter);
+
+        userLogs.setStartTime(sopStartLocalDateStr);
+        // 查询开始时间营期表
+        String unionSopStartId = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+        // 如果营期表里有,则加入;否则创建新营期
+        if (!StringUtil.strIsNullOrEmpty(unionSopStartId)) {
+            logsInfo.setUserLogsId(unionSopStartId);
+            // 查询这个人是否已在该营期里
+            handleInsertSopUserLogsInfo(logsInfo);
+        }
+        else {
+            // 没有营期记录就先插入
+            userLogsParamByDate.setStartTime(sopStartLocalDateStr);
+
+            sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+
+            // 再次查询 拿营期主键
+            String unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+            logsInfo.setUserLogsId(unionSopStartIdNew);
+
+            // 查询是否在营期里,如果没有再插入
+            handleInsertSopUserLogsInfo(logsInfo);
+        }
+    }
+
+    public void handleInsertSopUserLogsInfo(SopUserLogsInfo sopUserLogsInfo){
+        SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(sopUserLogsInfo);
+        if (userLogsInfo == null) {
+            sopUserLogsInfoMapper.insertSopUserLogsInfo(sopUserLogsInfo);
+        }
+    }
+
+    public void deleteBySopIdToContactIdTools(String sopId,String qwUserId,String corpId,Long externalId){
+
+        // 删除Sop任务营期里的该客户信息
+        SopUserLogsInfoDelParam logsInfoDelParam = new SopUserLogsInfoDelParam();
+        try {
+            logsInfoDelParam.setSopId(sopId);
+            logsInfoDelParam.setQwUserId(qwUserId);
+            logsInfoDelParam.setCorpId(corpId);
+            logsInfoDelParam.setExternalId(externalId);
+            sopUserLogsInfoMapper.deleteBySopIdToContactId(logsInfoDelParam);
+
+            log.info("移除sop营期:"+logsInfoDelParam);
+
+        }catch (Exception e){
+            log.error("删除Sop任务营期里的该客户信息异常:"+logsInfoDelParam+"|"+e.getMessage());
+        }
+    }
+
+    private String getErrorMsg(Integer code) {
+
+        String msg = "";
+        switch (code) {
+            case 40003:
+                msg = "无效的UserID(员工账号)";
+                break;
+            case 40096:
+                msg = "不合法的外部联系人userid";
+                break;
+            case 60020:
+                msg = "不安全的访问IP";
+                break;
+            case 84061:
+                msg = "不存在外部联系人的关系(客户【不存在】于员工的好友列表中)";
+                break;
+            default:
+                msg = code.toString();
+                break;
+
+        }
+        return msg;
+    }
+
+    private String getCurrentDataSourceType() {
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            Object value = holderClass.getMethod("getDataSourceType").invoke(null);
+            return value == null ? null : String.valueOf(value);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private void setCurrentDataSourceType(String dataSourceType) {
+        if (dataSourceType == null || dataSourceType.isEmpty()) {
+            return;
+        }
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            holderClass.getMethod("setDataSourceType", String.class).invoke(null, dataSourceType);
+        } catch (Exception e) {
+            log.debug("setCurrentDataSourceType skipped: {}", dataSourceType);
+        }
+    }
+
+    void updateQwUserName(QwUser user,QwCompany qwCompany){
+        String serverQwUserName = qwApiService.getServerQwUserName(qwCompany.getCorpId(), qwCompany.getOpenSecret(), user.getQwUserId(),qwCompany.getPermanentCode());
+        if (serverQwUserName==null|| serverQwUserName.isEmpty()){
+            log.debug("syncQwUserName skip empty: corpId={}, userId={}", qwCompany.getCorpId(), user.getQwUserId());
+            return;
+        }
+        if (serverQwUserName.equals(user.getQwUserName())){
+            log.debug("syncQwUserName skip same: corpId={}, userId={}, name={}",
+                    qwCompany.getCorpId(), user.getQwUserId(), serverQwUserName);
+            return;
+        }
+        QwUser qwUser = new QwUser();
+        qwUser.setId(user.getId());
+        qwUser.setQwUserName (serverQwUserName);
+        int updateRows = qwUserMapper.updateQwUser(qwUser);
+        log.info("syncQwUserName update result: corpId={}, userId={}, oldName={}, newName={}, rows={}",
+                qwCompany.getCorpId(), user.getQwUserId(), user.getQwUserName(), serverQwUserName, updateRows);
+    }
+
+    private void clearCurrentDataSourceType() {
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            holderClass.getMethod("clearDataSourceType").invoke(null);
+        } catch (Exception e) {
+            // ignore
+        }
+    }
 }