Переглянути джерело

完课打标签/三猫配置

三七 2 тижнів тому
батько
коміт
9b9c2fb5c1
28 змінених файлів з 1072 додано та 32 видалено
  1. 1 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  2. 33 3
      fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishRtyTaskOne.java
  3. 223 0
      fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishTagRtyTask.java
  4. 5 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  5. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  6. 35 21
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java
  7. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  8. 9 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoTagVO.java
  9. 8 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastgptEventLogTotal.java
  10. 1 1
      fs-service/src/main/java/com/fs/fastGpt/vo/FastgptEventLogTotalVo.java
  11. 42 0
      fs-service/src/main/java/com/fs/qw/domain/QwCourseFinishTagRty.java
  12. 69 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCourseFinishTagRtyMapper.java
  13. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  14. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java
  15. 1 0
      fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java
  16. 70 0
      fs-service/src/main/java/com/fs/qw/service/IQwCourseFinishTagRtyService.java
  17. 1 0
      fs-service/src/main/java/com/fs/qw/service/IQwTagGroupService.java
  18. 1 0
      fs-service/src/main/java/com/fs/qw/service/IQwTagService.java
  19. 315 0
      fs-service/src/main/java/com/fs/qw/service/impl/AsyncFinishCourseTakeTag.java
  20. 105 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCourseFinishTagRtyServiceImpl.java
  21. 1 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  22. 5 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwTagGroupServiceImpl.java
  23. 5 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwTagServiceImpl.java
  24. 1 1
      fs-service/src/main/resources/application-druid-sxsm.yml
  25. 16 0
      fs-service/src/main/resources/db/changelog/table/live_group_type.sql
  26. 4 4
      fs-service/src/main/resources/mapper/fastGpt/FastgptEventLogTotalMapper.xml
  27. 106 0
      fs-service/src/main/resources/mapper/qw/QwCourseFinishTagRtyMapper.xml
  28. 4 0
      fs-service/src/main/resources/mapper/qw/QwTagMapper.xml

+ 1 - 0
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -696,6 +696,7 @@ public class IpadSendServer {
         vo.setCorpCode(parentVo.getCorpCode());
         vo.setCorpId(parentVo.getCorpId());
         vo.setQwUserId(qwUser.getId());
+
         try {
             content.setSendStatus(1);
             if(qwSopLogs.getSendType() == 21 && "11".equals(content.getContentType())){

+ 33 - 3
fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishRtyTaskOne.java

@@ -1,8 +1,11 @@
 package com.fs.app.mq;
 
 
+import com.alibaba.fastjson.JSON;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.service.IQwCourseFinishRemarkRtyService;
 import com.fs.qwApi.domain.QwExternalContactRemarkResult;
 import com.fs.qwApi.param.QwExternalContactRemarkParam;
@@ -25,12 +28,13 @@ public class courseFinishRtyTaskOne {
     @Autowired
     private IQwCourseFinishRemarkRtyService finishRemarkRtyService;
 
-    @Autowired
-    private RedisCache redisCache;
 
     @Autowired
     private QwApiService qwApiService;
 
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
     /**
      * 完课打备注失败的 重复打 一直到失败 (这个 是销售1的 启动项)
      */
@@ -54,6 +58,9 @@ public class courseFinishRtyTaskOne {
             // 使用线程安全的List来收集需要批量更新的数据
             List<Long> batchUpdateList = Collections.synchronizedList(new ArrayList<>());
 
+            // 使用线程安全的List来收集需要批量更新的数据
+            List<QwExternalContact> batchUpdateContactList = Collections.synchronizedList(new ArrayList<>());
+
             // 存储Future对象以便检查所有任务完成情况
             List<Future<?>> futures = new ArrayList<>();
 
@@ -83,6 +90,14 @@ public class courseFinishRtyTaskOne {
                         if (qwResult.getErrcode() == 0) {
                             // 添加到批量更新列表
                             batchUpdateList.add(remarkRty.getId());
+
+                            QwExternalContact qwExternal = new QwExternalContact();
+                            qwExternal.setRemark(remarkRty.getRemark());
+                            qwExternal.setId(remarkRty.getExternalId());
+
+                            // 添加到批量更新列表
+                            batchUpdateContactList.add(qwExternal);
+
                         }else {
                             log.error("客户添加备注失败task,userId: {}{}{}", remarkRty.getQwUserId(), remarkRty.getExternalUserId(), qwResult.getErrmsg());
                         }
@@ -121,6 +136,21 @@ public class courseFinishRtyTaskOne {
                 }
             }
 
+            // 批量更新数据库
+            if (!batchUpdateContactList.isEmpty()) {
+                try {
+                    // 分批处理,避免单次批量过大
+                    int batchSize = 500; // 根据数据库性能调整
+                    for (int i = 0; i < batchUpdateContactList.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, batchUpdateContactList.size());
+                        List<QwExternalContact> subList = batchUpdateContactList.subList(i, end);
+                        qwExternalContactMapper.batchUpdateQwExternalContactByTags(subList);
+                    }
+                } catch (Exception e) {
+                    log.error("批量更新失败: " + e.getMessage());
+                }
+            }
+
             // 关闭线程池
             executor.shutdown();
 
@@ -155,7 +185,7 @@ public class courseFinishRtyTaskOne {
 
 
     /**
-     * 批量删除 掉 前一天的没打上标签
+     * 批量删除 掉 前一天的没打上备注
      */
     @Scheduled(cron = "0 30 0 * * ?")
     public void deleteFinishCourseRemark() {

+ 223 - 0
fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishTagRtyTask.java

@@ -0,0 +1,223 @@
+package com.fs.app.mq;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.qw.domain.QwCourseFinishTagRty;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.service.IQwCourseFinishTagRtyService;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.param.QwEditUserTagParam;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.*;
+
+@Component
+@Slf4j
+public class courseFinishTagRtyTask {
+
+    @Autowired
+    private IQwCourseFinishTagRtyService tagRtyService;
+
+    @Autowired
+    private QwApiService qwApiService;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+
+    /**
+     * 完课打标签失败的 重复打 一直到失败
+     */
+
+    @Scheduled(cron = "0 0/12 * * * ?") // 每12分钟
+//    @Scheduled(cron = "0/1 * * * * ? ") // 每15分钟
+    public void selectSopUserLogsListByTimeOne() {
+
+        System.out.println("schedule.task-enabled-tag");
+
+        // 获取当前时间
+        LocalDateTime now = LocalDateTime.now();
+        // 计算10分钟前
+        LocalDateTime tenMinutesBefore = now.minusMinutes(10);
+
+        // 创建线程池
+        int threadCount = Math.min(8, Runtime.getRuntime().availableProcessors() * 2);
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+        try {
+            // 使用线程安全的List来收集需要批量更新的数据
+            List<Long> batchUpdateList = Collections.synchronizedList(new ArrayList<>());
+            // 使用线程安全的List来收集需要批量更新的数据
+            List<QwExternalContact> batchUpdateContactList = Collections.synchronizedList(new ArrayList<>());
+
+            // 存储Future对象以便检查所有任务完成情况
+            List<Future<?>> futures = new ArrayList<>();
+
+            // 1. 批量查询所有用户数据
+            List<QwCourseFinishTagRty> remarkRtyList=new ArrayList<>();
+            try {
+                remarkRtyList = tagRtyService.selectQwCourseFinishRemarkTagRtyListByTime(tenMinutesBefore,1);
+                if (remarkRtyList == null || remarkRtyList.isEmpty()) {
+                     return;
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+                log.info("标签-批量查询用户数据失败:" + e.getMessage());
+            }
+
+            // 直接遍历contacts而不是userIds
+            for (QwCourseFinishTagRty remarkRty : remarkRtyList) {
+                futures.add(executor.submit(() -> {
+                    try {
+
+                        //组装标签
+                        List<String> addTagId=new ArrayList<>();
+                        addTagId.add(remarkRty.getTagIds());
+
+                        QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+                        qwEditUserTagParam.setAdd_tag(addTagId);
+                        qwEditUserTagParam.setUserid(remarkRty.getQwUserId());
+                        qwEditUserTagParam.setExternal_userid(remarkRty.getExternalUserId());
+
+                        QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, remarkRty.getCorpId());
+                        if (qwResult.getErrcode() == 0) {
+                            // 添加到批量更新列表
+                            batchUpdateList.add(remarkRty.getId());
+
+                            QwExternalContact qwExternal = new QwExternalContact();
+                            qwExternal.setTagIds(JSON.toJSONString(addTagId));
+                            qwExternal.setId(remarkRty.getExternalId());
+
+                            // 添加到批量更新列表
+                            batchUpdateContactList.add(qwExternal);
+
+                        }else {
+                            log.error("客户添加标签失败task,userId: {}{}{}", remarkRty.getQwUserId(), remarkRty.getExternalUserId(), qwResult.getErrmsg());
+                        }
+                    } catch (Exception e) {
+                        log.error("客户添加标签失败task,userId: " + remarkRty.getQwUserId() +
+                                ", externalUserId: " + remarkRty.getExternalUserId() +
+                                ", 错误信息: " + e.getMessage());
+                    }
+                }));
+            }
+
+
+            // 等待所有任务完成
+            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<Long> subList = batchUpdateList.subList(i, end);
+
+                        tagRtyService.deleteQwCourseFinishTagRtyByIds(subList.toArray(new Long[0]));
+                    }
+                } catch (Exception e) {
+                    log.error("批量更新失败: " + e.getMessage());
+                }
+            }
+
+            // 批量更新数据库
+            if (!batchUpdateContactList.isEmpty()) {
+                try {
+                    // 分批处理,避免单次批量过大
+                    int batchSize = 500; // 根据数据库性能调整
+                    for (int i = 0; i < batchUpdateContactList.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, batchUpdateContactList.size());
+                        List<QwExternalContact> subList = batchUpdateContactList.subList(i, end);
+                        qwExternalContactMapper.batchUpdateQwExternalContactByTags(subList);
+                    }
+                } catch (Exception e) {
+                    log.error("批量更新失败: " + e.getMessage());
+                }
+            }
+
+            // 关闭线程池
+            executor.shutdown();
+
+        } catch (Exception e) {
+            log.error("打标签任务执行异常: " + 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();
+            }
+        }
+        try {
+            Thread.sleep(3000);
+        } catch (InterruptedException ex) {
+            log.info("线程被中断");
+        }
+
+
+
+    }
+
+
+
+
+    /**
+     * 批量删除 掉 前一天的没打上标签的
+     */
+    @Scheduled(cron = "0 30 0 * * ?")
+    public void deleteFinishCourseRemark() {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 批量删除 掉 前一天的没打上标签的 ======");
+
+        List<Long> batchUpdateList = tagRtyService.selectQwCourseFinishRemarkTagRtyListByOutTime();
+
+        // 批量更新数据库
+        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<Long> subList = batchUpdateList.subList(i, end);
+
+                    tagRtyService.deleteQwCourseFinishTagRtyByIds(subList.toArray(new Long[0]));
+                }
+            } catch (Exception e) {
+                log.error("批量更新失败: " + e.getMessage());
+            }
+        }
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== 更批量删除 掉 前一天的没打上标签的,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+    }
+
+}
+

+ 5 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -34,6 +34,11 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
      */
     public FsUserCourseVideo selectFsUserCourseVideoByVideoId(Long videoId);
 
+    @Select("select cv.description as videoDescription,uc.description as courseDescription from fs_user_course_video cv " +
+            "LEFT JOIN  fs_user_course uc on cv.course_id = uc.course_id " +
+            "where video_id = #{videoId} ")
+    public FsUserCourseVideoTagVO selectFsUserCourseVideoByCourseVideoId(@Param("videoId") Long videoId);
+
     FsUserCourseVO selectFsUserCourseVideoVoByVideoId(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
 
     /**

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -37,6 +37,8 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      */
     public FsUserCourseVideo selectFsUserCourseVideoByVideoId(Long videoId);
 
+    public FsUserCourseVideoTagVO selectFsUserCourseVideoByCourseVideoId(Long videoId);
+
     /**
      * 查询课堂视频列表
      *

+ 35 - 21
fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java

@@ -2,27 +2,42 @@ package com.fs.course.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.course.domain.FsCourseFinishTemp;
 import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseFinishTempMapper;
 import com.fs.course.service.IFsCourseFinishTempService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsCourseFinishTempListVO;
 import com.fs.course.vo.FsCourseFinishTempVO;
+import com.fs.course.vo.FsUserCourseVideoTagVO;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
-import com.fs.qw.domain.QwCourseFinishRemarkRty;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwTagGroupMapper;
+import com.fs.qw.mapper.QwTagMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwCourseFinishRemarkRtyService;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.service.IQwTagService;
+import com.fs.qw.service.impl.AsyncFinishCourseTakeTag;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.qw.vo.QwTagGroupAddParam;
+import com.fs.qw.vo.QwTagVO;
 import com.fs.qw.vo.QwUserVO;
+import com.fs.qwApi.domain.QwAddTagResult;
 import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.domain.inner.InTag;
+import com.fs.qwApi.domain.inner.TagData;
+import com.fs.qwApi.param.QwAddTagParam;
+import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwExternalContactRemarkParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.voice.utils.StringUtil;
@@ -32,6 +47,7 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
+import java.time.LocalTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -58,8 +74,6 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
     @Autowired
     private QwUserMapper qwUserMapper;
 
-    @Autowired
-    private CompanyUserMapper companyUserMapper;
 
     @Autowired
     private IQwExternalContactService iQwExternalContactService;
@@ -74,6 +88,10 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
     @Autowired
     private IQwCourseFinishRemarkRtyService finishRemarkRtyService;
 
+    @Autowired
+    private AsyncFinishCourseTakeTag asyncFinishCourseTakeTag;
+
+
     /**
      * 查询完课模板
      *
@@ -230,6 +248,13 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
             return;
         }
 
+        //先只管 今正
+        if (CloudHostUtils.hasCloudHostName("今正科技")){
+            //完课打标签-异步处理,因为调用的是 客户的另外一个api
+            asyncFinishCourseTakeTag.finishCourseTakeTag(watchLog,qwUserByRedisForId);
+        }
+
+
         //不用完课备注
         if (qwUserByRedisForId.getIsSendMsg() == 5) {
             log.error("员工不用完课备注 {} 跳过处理。", watchLog.getQwUserId());
@@ -302,7 +327,6 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
             remarkParam.setUserid(externalContact.getUserId());
             remarkParam.setExternal_userid(externalContact.getExternalUserId());
 
-//            for (int attempt = 1; attempt <= 2; attempt++) {
             for (int attempt = 1; attempt <= 3; attempt++) { // 增加到3次重试
                 try {
                     QwExternalContactRemarkResult qwResult = qwApiService.externalcontactRemark(remarkParam, externalContact.getCorpId());
@@ -317,19 +341,6 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
 
                         break;
                     } else {
-//                        if (attempt==2 && (qwResult.getErrcode() == 45033 || qwResult.getErrcode()== -1 || qwResult.getErrcode()== 60020 )) {
-//                            QwCourseFinishRemarkRty remarkRty=new QwCourseFinishRemarkRty();
-//                            remarkRty.setQwUserId(externalContact.getUserId());
-//                            remarkRty.setCorpId(externalContact.getCorpId());
-//                            remarkRty.setExternalUserId(externalContact.getExternalUserId());
-//                            remarkRty.setExternalId(externalContact.getId());
-//                            remarkRty.setRemark(newRemark);
-//                            remarkRty.setCreateTime(new Date());
-//                            finishRemarkRtyService.insertOrUpdateQwCourseFinishRemarkRty(remarkRty);
-//
-//                        }
-//
-//                        log.error("完课加备注失败:" + externalContact.getName() + "|" + externalContact.getExternalUserId() + "|" + externalContact.getCorpId() + "|" + externalContact.getUserId() + "|" + newRemark + "|原因" + qwResult.getErrmsg());
 
                         // 根据错误码智能处理
                         if (isRateLimitError(qwResult.getErrcode())) {
@@ -348,7 +359,6 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
                                 newRemark, qwResult.getErrmsg());
                     }
                 } catch (Exception e) {
-//                    log.error("添加备注异常 [尝试第 " + attempt + " 次]:" + externalContact.getName() + "|" + externalContact.getExternalUserId() + "|" + externalContact.getCorpId() + "|" + externalContact.getUserId() + "|" + newRemark + "|" + e.getMessage());
                     log.error("添加备注异常 [尝试第 {} 次]:{}|{}|{}|{}|{}|{}",
                             attempt, externalContact.getName(), externalContact.getExternalUserId(),
                             externalContact.getCorpId(), externalContact.getUserId(),
@@ -481,7 +491,7 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
     }
 
     /**
-     * 保存到重试表
+     * 保存到重试表-备注
      */
     private void saveToRetryTable(QwExternalContact externalContact, String remark) {
         try {
@@ -498,6 +508,8 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
         }
     }
 
+
+
     /**
      * 批量更新完课模板状态
      * @param fsCourseFinishTemp
@@ -506,4 +518,6 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
     public int updateFsCourseFinishTempBatch(FsCourseFinishTemp fsCourseFinishTemp){
         return fsCourseFinishTempMapper.updateFsCourseFinishTempBatch(fsCourseFinishTemp);
     }
+
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -290,6 +290,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
     }
 
+    @Override
+    public FsUserCourseVideoTagVO selectFsUserCourseVideoByCourseVideoId(Long videoId) {
+        return fsUserCourseVideoMapper.selectFsUserCourseVideoByCourseVideoId(videoId);
+    }
+
     /**
      * 查询课堂视频列表
      *

+ 9 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoTagVO.java

@@ -0,0 +1,9 @@
+package com.fs.course.vo;
+
+import lombok.Data;
+
+@Data
+public class FsUserCourseVideoTagVO {
+    private String videoDescription;
+    private String courseDescription;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastgptEventLogTotal.java

@@ -1,5 +1,6 @@
 package com.fs.fastGpt.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
@@ -53,4 +54,11 @@ public class FastgptEventLogTotal extends BaseEntity{
 
     private List<String> userIds;
 
+
+    @TableField(exist = false)
+    private String beginTime;
+
+    @TableField(exist = false)
+    private String endTime;
+
 }

+ 1 - 1
fs-service/src/main/java/com/fs/fastGpt/vo/FastgptEventLogTotalVo.java

@@ -25,7 +25,7 @@ public class FastgptEventLogTotalVo extends BaseEntity{
     //@Excel(name = "角色id")
     private Long roleId;
 
-    //@Excel(name = "角色名称")
+    @Excel(name = "角色名称")
     private String roleName;
 
     @Excel(name = "日志统计时间")

+ 42 - 0
fs-service/src/main/java/com/fs/qw/domain/QwCourseFinishTagRty.java

@@ -0,0 +1,42 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 企业微信完课打标签-失败重试对象 qw_course_finish_tag_rty
+ *
+ * @author fs
+ * @date 2026-07-07
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class QwCourseFinishTagRty extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 企业微信账号 */
+    @Excel(name = "企业微信账号")
+    private String qwUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private String corpId;
+
+    /** 外部联系人长字符串编号 */
+    @Excel(name = "外部联系人长字符串编号")
+    private String externalUserId;
+
+    /** 外部联系人主键 */
+    @Excel(name = "外部联系人主键")
+    private Long externalId;
+
+    /** 完课打的标签 */
+    @Excel(name = "完课打的标签")
+    private String tagIds;
+
+
+}

+ 69 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwCourseFinishTagRtyMapper.java

@@ -0,0 +1,69 @@
+package com.fs.qw.mapper;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.domain.QwCourseFinishTagRty;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 企业微信完课打标签-失败重试Mapper接口
+ *
+ * @author fs
+ * @date 2026-07-07
+ */
+public interface QwCourseFinishTagRtyMapper extends BaseMapper<QwCourseFinishTagRty>{
+    /**
+     * 查询企业微信完课打标签-失败重试
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 企业微信完课打标签-失败重试
+     */
+    QwCourseFinishTagRty selectQwCourseFinishTagRtyById(Long id);
+
+    List<QwCourseFinishTagRty> selectQwCourseFinishRemarkTagRtyListByTime(@Param("time") LocalDateTime tenMinutesBefore,@Param("num") Integer num);
+
+
+    /**
+     * 查询企业微信完课打标签-失败重试列表
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 企业微信完课打标签-失败重试集合
+     */
+    List<QwCourseFinishTagRty> selectQwCourseFinishTagRtyList(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 新增企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    int insertQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 修改企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    int updateQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 删除企业微信完课打标签-失败重试
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 结果
+     */
+    int deleteQwCourseFinishTagRtyById(Long id);
+
+    /**
+     * 批量删除企业微信完课打标签-失败重试
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwCourseFinishTagRtyByIds(Long[] ids);
+
+    List<Long>  selectQwCourseFinishRemarkTagRtyListByOutTime();
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -484,7 +484,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     @Select("select fs_user_id from qw_external_contact where  id=#{id} ")
     Long selectQwExternalContactTimeById(@Param("id") Long id);
 
-    @Select("select id,user_id,corp_id,external_user_id,name,remark,status from qw_external_contact where  id=#{id} ")
+    @Select("select id,user_id,corp_id,external_user_id,name,remark,status,tag_ids from qw_external_contact where  id=#{id} ")
     QwExternalContact selectQwExternalContactByRemark(Long id);
 
     List<QwExternalContactVOTime> selectQwExternalContactListVOByUserIds(@Param("ids") List<String> ids);

+ 3 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java

@@ -24,6 +24,9 @@ public interface QwTagGroupMapper
      */
     public QwTagGroup selectQwTagGroupById(Long id);
 
+    @Select("select * from  qw_tag_group where name = #{name} limit 1")
+    public QwTagGroup selectQwTagGroupByCourseName(@Param("name") String name);
+
     public List<QwTagGroupListVO> selectQwTagGroupListByIds(@Param("groupIds") List<String> groupIds);
 
     /**

+ 1 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java

@@ -34,6 +34,7 @@ public interface QwTagMapper
      * @return 企微客户标签集合
      */
     public List<QwTag> selectQwTagList(QwTag qwTag);
+    public int countQwTagNumByCorpId(@Param("corpId") String corpId);
 
     /**
      * 新增企微客户标签

+ 70 - 0
fs-service/src/main/java/com/fs/qw/service/IQwCourseFinishTagRtyService.java

@@ -0,0 +1,70 @@
+package com.fs.qw.service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.domain.QwCourseFinishTagRty;
+
+/**
+ * 企业微信完课打标签-失败重试Service接口
+ *
+ * @author fs
+ * @date 2026-07-07
+ */
+public interface IQwCourseFinishTagRtyService extends IService<QwCourseFinishTagRty>{
+    /**
+     * 查询企业微信完课打标签-失败重试
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 企业微信完课打标签-失败重试
+     */
+    QwCourseFinishTagRty selectQwCourseFinishTagRtyById(Long id);
+
+    /**
+     * 获取10分钟以前的所有打标签失败的
+     */
+    List<QwCourseFinishTagRty> selectQwCourseFinishRemarkTagRtyListByTime(LocalDateTime tenMinutesBefore, Integer num);
+
+    /**
+     * 查询企业微信完课打标签-失败重试列表
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 企业微信完课打标签-失败重试集合
+     */
+    List<QwCourseFinishTagRty> selectQwCourseFinishTagRtyList(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 新增企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    int insertQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 修改企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    int updateQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty);
+
+    /**
+     * 批量删除企业微信完课打标签-失败重试
+     *
+     * @param ids 需要删除的企业微信完课打标签-失败重试主键集合
+     * @return 结果
+     */
+    int deleteQwCourseFinishTagRtyByIds(Long[] ids);
+
+    /**
+     * 删除企业微信完课打标签-失败重试信息
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 结果
+     */
+    int deleteQwCourseFinishTagRtyById(Long id);
+
+    List<Long> selectQwCourseFinishRemarkTagRtyListByOutTime();
+}

+ 1 - 0
fs-service/src/main/java/com/fs/qw/service/IQwTagGroupService.java

@@ -23,6 +23,7 @@ public interface IQwTagGroupService
      * @return 企微客户标签组
      */
     public QwTagGroup selectQwTagGroupById(Long id);
+    public QwTagGroup selectQwTagGroupByCourseName(String name);
 
     /**
      * 查询企微客户标签组列表

+ 1 - 0
fs-service/src/main/java/com/fs/qw/service/IQwTagService.java

@@ -33,6 +33,7 @@ public interface IQwTagService
      * @return 企微客户标签集合
      */
     public List<QwTag> selectQwTagList(QwTag qwTag);
+    public int countQwTagNumByCorpId(String corpId);
     public List<QwTagGroupListVO> searchTags(QwTagParam tagParam);
 
     /**

+ 315 - 0
fs-service/src/main/java/com/fs/qw/service/impl/AsyncFinishCourseTakeTag.java

@@ -0,0 +1,315 @@
+package com.fs.qw.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.vo.FsUserCourseVideoTagVO;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwTagMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwCourseFinishTagRtyService;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.service.IQwTagService;
+import com.fs.qw.vo.QwTagGroupAddParam;
+import com.fs.qw.vo.QwTagVO;
+import com.fs.qwApi.domain.QwAddTagResult;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.domain.inner.InTag;
+import com.fs.qwApi.domain.inner.TagData;
+import com.fs.qwApi.param.QwAddTagParam;
+import com.fs.qwApi.param.QwEditUserTagParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.voice.utils.StringUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.*;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class AsyncFinishCourseTakeTag {
+
+
+    @Autowired
+    private IQwExternalContactService iQwExternalContactService;
+
+    @Autowired
+    private QwApiService qwApiService;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+    @Autowired
+    private IFsUserCourseVideoService videoService;
+
+    @Autowired
+    private IQwTagService tagService;
+
+    @Autowired
+    private QwTagMapper qwTagMapper;
+
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+
+    @Autowired
+    private QwExternalContactServiceImpl qwExternalContactService;
+
+    @Autowired
+    private IQwCourseFinishTagRtyService tagRtyService;
+
+    /**
+     * 完课打把标签统一方法
+     */
+    @Async("scheduledExecutorService")
+    public void finishCourseTakeTag(FsCourseWatchLog watchLog, QwUser qwUserByRedisForId) {
+
+        try {
+            FsUserCourseVideoTagVO tagVO = videoService.selectFsUserCourseVideoByCourseVideoId(watchLog.getVideoId());
+
+            // 主体的标签是否超过1万个
+            int tagNum = tagService.countQwTagNumByCorpId(qwUserByRedisForId.getCorpId());
+            //将要打的标签
+            String videoTagId="";
+            //视频的 课程和课节的 标签描述 不为空,总标签数小于等于10000
+            if (tagVO!=null && !StringUtil.strIsNullOrEmpty(tagVO.getVideoDescription())
+                    && !StringUtil.strIsNullOrEmpty(tagVO.getCourseDescription())
+                    && tagNum <= 10000) {
+                log.info("满足打标签条件-,{},{}",tagVO,tagNum);
+                //根据 描述去查 主体里的标签库 是否有这个标签,没有就新建(不能超过1W),有就直接用
+                QwTagVO qwTagVO = qwTagMapper.selectQwTagByName(tagVO.getVideoDescription(), qwUserByRedisForId.getCorpId());
+                QwTagGroup tagGroup = qwTagGroupService.selectQwTagGroupByCourseName(tagVO.getCourseDescription());
+                //如果标签组和标签都为空  则新建标签
+                if (tagGroup==null && qwTagVO == null) {
+                    QwTagGroupAddParam qwTagGroup=new QwTagGroupAddParam();
+
+                    qwTagGroup.setName(tagVO.getCourseDescription());
+                    qwTagGroup.setCorpId(qwUserByRedisForId.getCorpId());
+
+                    List<QwTag> QwTags = new ArrayList<>();
+                    QwTag tag = new QwTag();
+                    tag.setName(tagVO.getVideoDescription());
+                    QwTags.add(tag);
+                    qwTagGroup.setTag(QwTags);
+
+                    //添加 标签/标签组
+                    int i = qwTagGroupService.insertQwTagGroupParam(qwTagGroup);
+
+                    // 加好了之后再去查 标签id
+                    QwTagVO qwTagNew = qwTagMapper.selectQwTagByName(tagVO.getVideoDescription(), qwUserByRedisForId.getCorpId());
+
+                    //获取到的 标签id
+                    videoTagId= qwTagNew.getTagId();
+
+                }else
+                    //如果标签组不为空,这个标签没查到,则只在这个标签下 在这个标签组里新增一个标签
+                    if (tagGroup!=null && qwTagVO == null) {
+
+                        QwAddTagParam qwAddTagParam = new QwAddTagParam();
+                        qwAddTagParam.setGroup_id(tagGroup.getGroupId());
+                        TagData tagData = new TagData();
+                        tagData.setName(tagVO.getVideoDescription());
+                        List<TagData> list = Arrays.asList(tagData);
+                        qwAddTagParam.setTag(list);
+                        QwAddTagResult result = qwApiService.addTag(qwAddTagParam, qwUserByRedisForId.getCorpId());
+                        if (result.getErrcode() == 0) {
+                            List<InTag> addTag = result.getTag_group().getTag();
+                            if (addTag != null && addTag.size() > 0) {
+                                QwTag addQwTag = new QwTag();
+                                addQwTag.setGroupId(tagGroup.getGroupId());
+                                addQwTag.setTagId(addTag.get(0).getId());
+                                addQwTag.setOrder(addTag.get(0).getOrder());
+                                addQwTag.setName(addTag.get(0).getName());
+                                addQwTag.setCorpId(qwUserByRedisForId.getCorpId());
+                                addQwTag.setCreateTime(new Date(addTag.get(0).getCreate_time()));
+                                addQwTag.setTagFrom(1);
+                                qwTagMapper.insertQwTag(addQwTag);
+
+                                //获取到的 标签id
+                                videoTagId= addQwTag.getTagId();
+                            }
+                        }else {
+                            log.error("完课打标签-添加标签失败");
+                        }
+
+                    }
+                //都不为空,拿到标签id
+                if (tagGroup != null && qwTagVO!=null) {
+                    videoTagId=qwTagVO.getTagId();
+                }
+
+            }else {
+                log.error("完课打标签-获取标签组失败:-"+tagVO+"数量:-"+tagNum+"视频:"+videoTagId);
+            }
+
+
+            if (!StringUtil.strIsNullOrEmpty(videoTagId)){
+
+                Long qwExternalContactId = watchLog.getQwExternalContactId();
+                QwExternalContact externalContact = iQwExternalContactService.selectQwExternalContactByRemark(qwExternalContactId);
+
+                if (externalContact != null) {
+
+                    //组装标签
+                    List<String> tagIds11111=new ArrayList<>();
+                    tagIds11111.add(videoTagId);
+
+                    QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+                    qwEditUserTagParam.setAdd_tag(tagIds11111);
+                    qwEditUserTagParam.setUserid(externalContact.getUserId());
+                    qwEditUserTagParam.setExternal_userid(externalContact.getExternalUserId());
+
+                    for (int attempt = 1; attempt <= 3; attempt++) { // 增加到3次重试
+                        try {
+                            QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, qwUserByRedisForId.getCorpId());
+                            if (qwResult.getErrcode() == 0) {
+
+                                // 处理标签
+                                String oldtagIds = externalContact.getTagIds();
+                                Set<String> uniqueIds = new HashSet<>();
+
+                                if (oldtagIds != null && !oldtagIds.isEmpty()) {
+                                    List<String> parsedTags = JSON.parseArray(oldtagIds, String.class);
+                                    if (parsedTags != null && !parsedTags.isEmpty()) {
+                                        uniqueIds.addAll(parsedTags);
+                                    }
+                                }
+
+                                if (qwEditUserTagParam.getAdd_tag() != null && !qwEditUserTagParam.getAdd_tag().isEmpty()) {
+                                    uniqueIds.addAll(qwEditUserTagParam.getAdd_tag());
+                                }
+
+                                QwExternalContact qwExternal = new QwExternalContact();
+                                qwExternal.setTagIds(JSON.toJSONString(uniqueIds));
+                                qwExternal.setId(externalContact.getId());
+//先不走打标签走sop逻辑
+//                                List<String> tagIdsList = new ArrayList<>();
+//                                if (qwExternal.getTagIds() != null && !qwExternal.getTagIds().isEmpty()) {
+//                                    tagIdsList = JSON.parseArray(qwExternal.getTagIds(), String.class);
+//                                }
+//
+//                                LocalDate currentDate = LocalDate.now();
+//                                // 获取当前系统时间 (HH:mm)
+//                                LocalTime localTime = LocalTime.now();
+//                                // 插件sop处理
+//                                qwExternalContactService.processTagsAll(externalContact, externalContact.getCorpId(),
+//                                        tagIdsList, currentDate, localTime);
+
+                                qwExternalContactMapper.updateQwExternalContact(externalContact);
+
+                                log.info("客户完课添加标签addUserTag:" + externalContact.getName() +"|"+externalContact.getId()+
+                                        "|公司" + externalContact.getCorpId() +
+                                        "|员工" + externalContact.getUserId() +
+                                        "|添加的标签" + JSON.toJSONString(tagIds11111));
+                                break;
+                            } else {
+
+                                // 根据错误码智能处理
+                                if (isRateLimitError(qwResult.getErrcode())) {
+                                    // 保存到重试表
+                                    saveToRetryTableTag(externalContact, videoTagId);
+
+                                    // 智能延迟
+                                    if (attempt < 3) {
+                                        smartDelayByErrorCode(qwResult.getErrcode(), attempt);
+                                        continue; // 继续重试
+                                    }
+                                }
+                                log.error("完课加标签失败:{}|{}|{}|{}|{}|原因{}",
+                                        externalContact.getName(), externalContact.getExternalUserId(),
+                                        externalContact.getCorpId(), externalContact.getUserId(),
+                                        videoTagId, qwResult.getErrmsg());
+                            }
+                        } catch (Exception e) {
+                            log.error("添加标签异常 [尝试第 {} 次]:{}|{}|{}|{}|{}|{}",
+                                    attempt, externalContact.getName(), externalContact.getExternalUserId(),
+                                    externalContact.getCorpId(), externalContact.getUserId(),
+                                    videoTagId, e.getMessage());
+
+                            if (attempt < 3) {
+                                smartDelayByErrorCode(-1, attempt);
+                            }
+                        }
+
+                        // 若不是最后一次尝试,则等待3秒再试
+                        if (attempt < 2) {
+                            try {
+                                Thread.sleep(3000);
+                            } catch (InterruptedException e) {
+                                log.warn("线程等待被中断", e);
+                                break;
+                            }
+                        }
+                    }
+
+                }else {
+                    log.error("完课加标签失败无客户信息:" + watchLog);
+                }
+
+            }else {
+                log.error("完课打标签-获取标签id失败,{},{}",videoTagId,watchLog);
+            }
+
+        }catch (Exception e){
+            log.error("完课打标签失败,{}",watchLog,e);
+        }
+
+
+
+    }
+
+    /**
+     * 保存到重试表-标签
+     */
+    private void saveToRetryTableTag(QwExternalContact externalContact, String tagId) {
+        try {
+            QwCourseFinishTagRty tagRty=new QwCourseFinishTagRty();
+            tagRty.setQwUserId(externalContact.getUserId());
+            tagRty.setCorpId(externalContact.getCorpId());
+            tagRty.setExternalUserId(externalContact.getExternalUserId());
+            tagRty.setExternalId(externalContact.getId());
+            tagRty.setTagIds(tagId);
+            tagRty.setCreateTime(new Date());
+            tagRtyService.insertQwCourseFinishTagRty(tagRty);
+        } catch (Exception e) {
+            log.error("保存重试记录失败", e);
+        }
+    }
+
+    /**
+     * 根据错误码智能延迟
+     */
+    private void smartDelayByErrorCode(Integer errcode, int attempt) {
+        try {
+            long delayMs;
+            if (errcode == 45033) { // 并发限制
+                delayMs = 5000 + attempt * 2000L; // 递增延迟
+            } else if (errcode == 60020) { // 频率限制
+                delayMs = 3000 + attempt * 1000L;
+            } else if (errcode == -1) { // 系统繁忙
+                delayMs = 2000;
+            } else {
+                delayMs = 1000; // 默认延迟
+            }
+            log.info("因错误码 {} 延迟 {}ms", errcode, delayMs);
+            Thread.sleep(delayMs);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * 判断是否为频率限制类错误
+     */
+    private boolean isRateLimitError(Integer errcode) {
+        return errcode == 45033 || errcode == 60020 || errcode == -1;
+    }
+
+}

+ 105 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwCourseFinishTagRtyServiceImpl.java

@@ -0,0 +1,105 @@
+package com.fs.qw.service.impl;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.qw.mapper.QwCourseFinishTagRtyMapper;
+import com.fs.qw.domain.QwCourseFinishTagRty;
+import com.fs.qw.service.IQwCourseFinishTagRtyService;
+
+/**
+ * 企业微信完课打标签-失败重试Service业务层处理
+ *
+ * @author fs
+ * @date 2026-07-07
+ */
+@Service
+public class QwCourseFinishTagRtyServiceImpl extends ServiceImpl<QwCourseFinishTagRtyMapper, QwCourseFinishTagRty> implements IQwCourseFinishTagRtyService {
+
+    /**
+     * 查询企业微信完课打标签-失败重试
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 企业微信完课打标签-失败重试
+     */
+    @Override
+    public QwCourseFinishTagRty selectQwCourseFinishTagRtyById(Long id)
+    {
+        return baseMapper.selectQwCourseFinishTagRtyById(id);
+    }
+
+    @Override
+    public List<QwCourseFinishTagRty> selectQwCourseFinishRemarkTagRtyListByTime(LocalDateTime tenMinutesBefore, Integer num) {
+        return baseMapper.selectQwCourseFinishRemarkTagRtyListByTime(tenMinutesBefore,num);
+    }
+
+    /**
+     * 查询企业微信完课打标签-失败重试列表
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 企业微信完课打标签-失败重试
+     */
+    @Override
+    public List<QwCourseFinishTagRty> selectQwCourseFinishTagRtyList(QwCourseFinishTagRty qwCourseFinishTagRty)
+    {
+        return baseMapper.selectQwCourseFinishTagRtyList(qwCourseFinishTagRty);
+    }
+
+    /**
+     * 新增企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    @Override
+    public int insertQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty)
+    {
+        qwCourseFinishTagRty.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertQwCourseFinishTagRty(qwCourseFinishTagRty);
+    }
+
+    /**
+     * 修改企业微信完课打标签-失败重试
+     *
+     * @param qwCourseFinishTagRty 企业微信完课打标签-失败重试
+     * @return 结果
+     */
+    @Override
+    public int updateQwCourseFinishTagRty(QwCourseFinishTagRty qwCourseFinishTagRty)
+    {
+        return baseMapper.updateQwCourseFinishTagRty(qwCourseFinishTagRty);
+    }
+
+    /**
+     * 批量删除企业微信完课打标签-失败重试
+     *
+     * @param ids 需要删除的企业微信完课打标签-失败重试主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwCourseFinishTagRtyByIds(Long[] ids)
+    {
+        return baseMapper.deleteQwCourseFinishTagRtyByIds(ids);
+    }
+
+    /**
+     * 删除企业微信完课打标签-失败重试信息
+     *
+     * @param id 企业微信完课打标签-失败重试主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwCourseFinishTagRtyById(Long id)
+    {
+        return baseMapper.deleteQwCourseFinishTagRtyById(id);
+    }
+
+    @Override
+    public List<Long> selectQwCourseFinishRemarkTagRtyListByOutTime() {
+        return baseMapper.selectQwCourseFinishRemarkTagRtyListByOutTime();
+    }
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -4337,7 +4337,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     /**
      * 整合客户的标签的逻辑 ,新增和移除 都要移除和添加到营期里
      */
-    private void processTagsAll(QwExternalContact qwExternalContact, String corpId, List<String> tagArr, LocalDate currentDate, LocalTime localTime) {
+    public void processTagsAll(QwExternalContact qwExternalContact, String corpId, List<String> tagArr, LocalDate currentDate, LocalTime localTime) {
 
         List<String> cleanedTagList = tagArr.stream()
                 .map(String::trim)   // 去除前后空格

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwTagGroupServiceImpl.java

@@ -114,6 +114,11 @@ public class QwTagGroupServiceImpl implements IQwTagGroupService {
         return qwTagGroupMapper.selectQwTagGroupById(id);
     }
 
+    @Override
+    public QwTagGroup selectQwTagGroupByCourseName(String name) {
+        return qwTagGroupMapper.selectQwTagGroupByCourseName(name);
+    }
+
     /**
      * 查询企微客户标签组列表
      *

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwTagServiceImpl.java

@@ -66,6 +66,11 @@ public class QwTagServiceImpl implements IQwTagService
         return qwTagMapper.selectQwTagList(qwTag);
     }
 
+    @Override
+    public int countQwTagNumByCorpId(String corpId) {
+        return qwTagMapper.countQwTagNumByCorpId(corpId);
+    }
+
     @Override
     public List<QwTagGroupListVO> searchTags(QwTagParam tagParam) {
 

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

@@ -210,7 +210,7 @@ token:
 openIM:
     secret: openIM123
     userID: imAdmin
-    url: https://
+    url: https://webim.mxzjkwbvk.cn/api
     pushUrl: https://fc-mp-d0421385-3496-42e7-b67f-59161a36a0e4.next.bspapp.com/push
 #是否使用新im
 im:

+ 16 - 0
fs-service/src/main/resources/db/changelog/table/live_group_type.sql

@@ -59,3 +59,19 @@ CREATE TABLE `live_completion_answer_record` (
                                                  KEY `idx_user_id` (`user_id`) USING BTREE,
                                                  KEY `idx_create_time` (`create_time`) USING BTREE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='直播完课答题记录';
+
+
+--changeset sgw:20260707-qw_course_finish_tag_rty
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'qw_course_finish_tag_rty'
+CREATE TABLE `qw_course_finish_tag_rty` (
+                                            `id` bigint NOT NULL AUTO_INCREMENT,
+                                            `qw_user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '企业微信账号',
+                                            `corp_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '公司id',
+                                            `external_user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '外部联系人长字符串编号',
+                                            `external_id` bigint DEFAULT NULL COMMENT '外部联系人主键',
+                                            `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                                            `tag_ids` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '完课打的标签',
+                                            PRIMARY KEY (`id`) USING BTREE,
+                                            UNIQUE KEY `index1` (`qw_user_id`,`corp_id`,`external_user_id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='企业微信完课打标签-失败重试表';

+ 4 - 4
fs-service/src/main/resources/mapper/fastGpt/FastgptEventLogTotalMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.fastGpt.mapper.FastgptEventLogTotalMapper">
-    
+
     <resultMap type="FastgptEventLogTotal" id="FastgptEventLogTotalResult">
         <result property="id"    column="id"    />
         <result property="roleId"    column="role_id"    />
@@ -32,7 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                                                                  and DATE_FORMAT(create_time, '%Y-%m-%d') <= #{endTime} ]]></if>
         </where>
     </select>
-    
+
     <select id="selectFastgptEventLogTotalById" parameterType="Long" resultMap="FastgptEventLogTotalResult">
         <include refid="selectFastgptEventLogTotalVo"/>
         where id = #{id}
@@ -212,9 +212,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteFastgptEventLogTotalByIds" parameterType="String">
-        delete from fastgpt_event_log_total where id in 
+        delete from fastgpt_event_log_total where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+</mapper>

+ 106 - 0
fs-service/src/main/resources/mapper/qw/QwCourseFinishTagRtyMapper.xml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwCourseFinishTagRtyMapper">
+
+    <resultMap type="QwCourseFinishTagRty" id="QwCourseFinishTagRtyResult">
+        <result property="id"    column="id"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="externalUserId"    column="external_user_id"    />
+        <result property="externalId"    column="external_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="tagIds"    column="tag_ids"    />
+    </resultMap>
+
+    <sql id="selectQwCourseFinishTagRtyVo">
+        select id, qw_user_id, corp_id, external_user_id, external_id, create_time, tag_ids from qw_course_finish_tag_rty
+    </sql>
+
+    <select id="selectQwCourseFinishTagRtyList" parameterType="QwCourseFinishTagRty" resultMap="QwCourseFinishTagRtyResult">
+        <include refid="selectQwCourseFinishTagRtyVo"/>
+        <where>
+            <if test="qwUserId != null  and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="externalUserId != null  and externalUserId != ''"> and external_user_id = #{externalUserId}</if>
+            <if test="externalId != null "> and external_id = #{externalId}</if>
+            <if test="tagIds != null  and tagIds != ''"> and tag_ids = #{tagIds}</if>
+        </where>
+    </select>
+
+    <select id="selectQwCourseFinishTagRtyById" parameterType="Long" resultMap="QwCourseFinishTagRtyResult">
+        <include refid="selectQwCourseFinishTagRtyVo"/>
+        where id = #{id}
+    </select>
+
+
+    <insert id="insertQwCourseFinishTagRty" parameterType="QwCourseFinishTagRty" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_course_finish_tag_rty
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="corpId != null">corp_id,</if>
+            <if test="externalUserId != null">external_user_id,</if>
+            <if test="externalId != null">external_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="tagIds != null">tag_ids,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="corpId != null">#{corpId},</if>
+            <if test="externalUserId != null">#{externalUserId},</if>
+            <if test="externalId != null">#{externalId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="tagIds != null">#{tagIds},</if>
+         </trim>
+    </insert>
+
+    <update id="updateQwCourseFinishTagRty" parameterType="QwCourseFinishTagRty">
+        update qw_course_finish_tag_rty
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+            <if test="externalUserId != null">external_user_id = #{externalUserId},</if>
+            <if test="externalId != null">external_id = #{externalId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="tagIds != null">tag_ids = #{tagIds},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwCourseFinishTagRtyById" parameterType="Long">
+        delete from qw_course_finish_tag_rty where id = #{id}
+    </delete>
+
+    <delete id="deleteQwCourseFinishTagRtyByIds" parameterType="String">
+        delete from qw_course_finish_tag_rty where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectQwCourseFinishRemarkTagRtyListByTime" resultType="com.fs.qw.domain.QwCourseFinishTagRty">
+        SELECT
+            frr.id,
+            frr.qw_user_id,
+            frr.external_id,
+            frr.external_user_id,
+            frr.corp_id,
+            frr.tag_ids,
+            frr.create_time
+        FROM
+            qw_course_finish_tag_rty  frr
+                left join qw_company qc on frr.corp_id =qc.corp_id
+        WHERE
+            DATE(frr.create_time) = CURRENT_DATE()
+          AND frr.create_time &lt;= #{time}
+          and qc.company_server_num=#{num}
+
+    </select>
+
+    <select id="selectQwCourseFinishRemarkTagRtyListByOutTime" resultType="java.lang.Long">
+        SELECT id
+        FROM qw_course_finish_tag_rty
+        WHERE create_time  &lt; CURRENT_DATE()
+    </select>
+</mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/qw/QwTagMapper.xml

@@ -71,6 +71,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </select>
 
+    <select id="countQwTagNumByCorpId" resultType="java.lang.Integer" >
+        select count(1) from qw_tag where corp_id = #{corpId}
+    </select>
+
 
     <insert id="insertQwTag" parameterType="QwTag">
         insert into qw_tag