Przeglądaj źródła

个微sop(暂不需要)

zyy 4 dni temu
rodzic
commit
2e728605c0
21 zmienionych plików z 3740 dodań i 1 usunięć
  1. 4 1
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  2. 44 0
      fs-qw-task/src/main/java/com/fs/app/task/wxTask.java
  3. 22 0
      fs-qw-task/src/main/java/com/fs/app/taskService/SopWxLogsTaskService.java
  4. 2 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  5. 3431 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsTaskServiceImpl.java
  6. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCompanyMapper.java
  7. 2 0
      fs-service/src/main/java/com/fs/qw/service/IQwCompanyService.java
  8. 6 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCompanyServiceImpl.java
  9. 18 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  10. 3 0
      fs-service/src/main/java/com/fs/sop/mapper/QwSopMapper.java
  11. 4 0
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsInfoMapper.java
  12. 4 0
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java
  13. 51 0
      fs-service/src/main/java/com/fs/sop/vo/WxSopUserVo.java
  14. 6 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java
  15. 5 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  16. 3 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java
  17. 8 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  18. 23 0
      fs-service/src/main/resources/mapper/sop/QwSopMapper.xml
  19. 30 0
      fs-service/src/main/resources/mapper/sop/SopUserLogsInfoMapper.xml
  20. 19 0
      fs-service/src/main/resources/mapper/sop/SopUserLogsMapper.xml
  21. 52 0
      fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml

+ 4 - 1
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -162,6 +162,8 @@ public class CommonController {
 
     @Autowired
     private ISysConfigService configService;
+    @Autowired
+    private SopWxLogsTaskService sopWxLogsTaskService;
 
 
 
@@ -427,7 +429,8 @@ public class CommonController {
         if(StringUtils.isNotEmpty(sopId)){
             sopidList = Arrays.asList(sopId.split(","));
         }
-        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
+//        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
+        sopWxLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
         return R.ok();
     }
     @GetMapping("/testWx")

+ 44 - 0
fs-qw-task/src/main/java/com/fs/app/task/wxTask.java

@@ -0,0 +1,44 @@
+package com.fs.app.task;
+
+import com.fs.app.taskService.SopWxLogsTaskService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * 个微SOP定时任务管理类
+ * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
+ *
+ * @author 系统
+ * @version 1.0
+ */
+@Component
+@Slf4j
+public class wxTask {
+    @Autowired
+    private SopWxLogsTaskService sopWxLogsTaskService;
+
+    /**
+     * 定时任务:根据营期生成sopLogs待发记录
+     * 执行时间:每小时的第5分钟执行
+     * 功能:根据营期时间生成需要发送的SOP日志记录
+     *
+     * @throws Exception 执行异常
+     */
+    @Scheduled(cron = "0 5 * * * ?") // 每小时的第5分钟触发
+    @Async
+    public void selectSopUserLogsListByTime() throws Exception {
+        // 获取当前时间,精确到小时
+        LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
+        // 打印日志,确认任务执行时间
+        log.info("任务实际执行时间: {}", currentTime);
+
+        // 调用服务方法处理SOP用户日志
+        sopWxLogsTaskService.selectSopUserLogsListByTime(currentTime, null);
+    }
+
+}

+ 22 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/SopWxLogsTaskService.java

@@ -0,0 +1,22 @@
+package com.fs.app.taskService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface SopWxLogsTaskService {
+
+    public void selectSopUserLogsListByTime(LocalDateTime currentTime, List<String> sopidList) throws Exception;
+
+
+    /**
+     * 补发过期完课消息
+     */
+    void updateSopLogsByCancel();
+
+    /**
+     * 创建完课消息
+     */
+    void createCourseFinishMsg();
+
+//    void creatMessMessage(QwSopLogs logs);
+}

+ 2 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -426,6 +426,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      Map<String, QwGroupChat> groupChatMap, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                      List<Company> companies) {
         try {
+            log.info("当前线程: {}", Thread.currentThread().getName());
             processSopGroup(sopId, userLogsVos, currentTime, groupChatMap, config, miniMap, companies);
         } catch (Exception e) {
             log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
@@ -2795,6 +2796,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void enqueueQwSopLogs(QwSopLogs sopLogs) {
         try {
             boolean offered = qwSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
+            System.out.println(sopLogs.getSopId() + "插入队列结果: " + offered + "内容: " + JSON.toJSONString(sopLogs));
             if (!offered) {
                 log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
                 // 处理队列已满的情况,例如记录到失败队列或持久化存储

+ 3431 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsTaskServiceImpl.java

@@ -0,0 +1,3431 @@
+package com.fs.app.taskService.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.app.taskService.SopWxLogsTaskService;
+import com.fs.common.config.FSSysConfig;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.PubFun;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyMiniapp;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.service.ICompanyMiniappService;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.mapper.*;
+import com.fs.course.service.IFsCourseLinkService;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.mapper.LiveWatchLogMapper;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.LuckyBagCollectRecordMapper;
+import com.fs.qw.mapper.LuckyBagMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwGroupChatService;
+import com.fs.qw.service.IQwGroupChatUserService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
+import com.fs.qw.vo.*;
+import com.fs.sop.domain.*;
+import com.fs.sop.mapper.*;
+import com.fs.sop.service.IQwSopLogsService;
+import com.fs.sop.service.IQwSopTempContentService;
+import com.fs.sop.service.IQwSopTempRulesService;
+import com.fs.sop.service.IQwSopTempVoiceService;
+import com.fs.sop.vo.QwCreateLinkByAppVO;
+import com.fs.sop.vo.SopUserLogsVo;
+import com.fs.sop.vo.WxSopUserVo;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Retryable;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
+
+@Service
+@Slf4j
+public class SopWxLogsTaskServiceImpl implements SopWxLogsTaskService {
+    private static final String APP_LINK_PREFIX = "/appcourse/pages/course/learning?course=";
+    private static final String REAL_LINK_PREFIX = "/courseH5/pages/course/learning?course=";
+    private static final String SHORT_LINK_PREFIX = "/courseH5/pages/course/learning?s=";
+    private static final String miniappRealLink = "/pages_course/video.html?course=";
+    private static final String appRealLink = "/pages/courseAnswer/index?link=";
+    private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
+    // 福袋
+    private static final String appActivitlLink = "/pages_course/activity.html?link=";
+    // 注册
+    private static final String registeredRealLink = "/pages_course/register.html?link=";
+    private static final String h5LiveShortLink = "/pages_course/livingInvite.html?s=";
+    private static final String h5miniappLink = "/pages_course/shortLink.html?s=";
+
+//    private static final String miniappRealLink = "/pages/index/index?course=";
+
+    private static final String QWSOP_KEY_PREFIX = "qwsop:";
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd 07:00:00");
+
+    // Cached configurations and domain names
+    private CourseConfig cachedCourseConfig;
+    private final Object configLock = new Object();
+    private List<FsCourseDomainName> cachedDomainNames;
+    private final Object domainLock = new Object();
+    // Batch size for database inserts, configurable via application.properties
+    private final int BATCH_SIZE = 500;
+
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
+    @Autowired
+    private SopUserLogsMapper sopUserLogsMapper;
+    @Autowired
+    private QwSopTagMapper qwSopTagMapper;
+    @Autowired
+    private QwSopMapper sopMapper;
+    @Autowired
+    private QwExternalContactServiceImpl qwExternalContactService;
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+    @Autowired
+    private IWxSopLogsService wxSopLogsService;
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+    @Autowired
+    private FsCourseLinkMapper fsCourseLinkMapper;
+    @Autowired
+    private FsCourseSopAppLinkMapper fsCourseSopAppLinkMapper;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private FsCourseDomainNameMapper fsCourseDomainNameMapper;
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+    @Autowired
+    private QwUserMapper qwUserMapper;
+    @Autowired
+    private IQwSopTempRulesService qwSopTempRulesService;
+    @Autowired
+    private IQwSopTempContentService qwSopTempContentService;
+    @Autowired
+    private IQwSopTempVoiceService qwSopTempVoiceService;
+    @Autowired
+    private CloudHostProper cloudHostProper;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    private LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;
+    @Autowired
+    private LuckyBagMapper luckyBagMapper;
+    @Autowired
+    private CompanyMapper companyMapper;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private ApplicationContext applicationContext;
+
+
+    // Blocking queues with bounded capacity to implement backpressure
+    private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<WxSopLogs> wxSopLogsQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<LiveWatchLog> zmLiveWatchQueue = new LinkedBlockingQueue<>(20000);
+
+    // Executors for consumer threads
+    private ExecutorService qwSopLogsExecutor;
+    private ExecutorService watchLogsExecutor;
+    private ExecutorService courseLinkExecutor;
+    private ExecutorService courseSopAppLinkExecutor;
+    private ExecutorService zmLiveWatchLogExecutor;
+    @Autowired
+    private IQwGroupChatService qwGroupChatService;
+    @Autowired
+    private IQwGroupChatUserService qwGroupChatUserService;
+    @Autowired
+    private ICompanyMiniappService companyMiniappService;
+    // Shutdown flags
+    private volatile boolean running = true;
+    @Autowired
+    private QwSopTempMapper qwSopTempMapper;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private IQwCompanyService iQwCompanyService;
+    @Autowired
+    private AsyncCourseWatchFinishService asyncCourseWatchFinishService;
+    @Autowired
+    private IQwSopTempVoiceService sopTempVoiceService;
+    @Autowired
+    LiveWatchLogMapper liveWatchLogMapper;
+    @Autowired
+    private ConfigUtil configUtil;
+    @Autowired
+    private WxContactMapper wxContactMapper;
+    @Autowired
+    private CompanyWxAccountMapper companyWxAccountMapper;
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
+    @PostConstruct
+    public void init() {
+        loadCourseConfig();
+        startConsumers();
+    }
+
+    private void loadCourseConfig() {
+        try {
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+            if (config != null) {
+                cachedCourseConfig = config;
+                log.info("Loaded course.config successfully.");
+            } else {
+                log.error("Failed to load course.config from configService.");
+            }
+        } catch (Exception e) {
+            log.error("Exception while loading course.config: {}", e.getMessage(), e);
+        }
+    }
+
+
+    private void startConsumers() {
+        qwSopLogsExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "QwSopLogsConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+        watchLogsExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "WatchLogsConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+        courseLinkExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "courseLinkConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+
+        courseSopAppLinkExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "courseSopAppLinkConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+
+        zmLiveWatchLogExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "zmLiveWatchLogConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+
+        qwSopLogsExecutor.submit(this::consumeQwSopLogs);
+        watchLogsExecutor.submit(this::consumeWatchLogs);
+        courseLinkExecutor.submit(this::consumeCourseLink);
+        courseSopAppLinkExecutor.submit(this::consumeCourseSopAppLink);
+        zmLiveWatchLogExecutor.submit(this::consumeZmLiveWatchQueue);
+    }
+
+    // Scheduled tasks to refresh configurations and domain names periodically
+    @Scheduled(fixedDelay = 60000) // 每60秒刷新一次
+    public void refreshCourseConfig() {
+        synchronized (configLock) {
+            try {
+                String json = configService.selectConfigByKey("course.config");
+                CourseConfig newConfig = JSON.parseObject(json, CourseConfig.class);
+                if (newConfig != null) {
+                    cachedCourseConfig = newConfig;
+                    log.info("Refreshed course.config.");
+                } else {
+                    log.error("Failed to refresh course.config.");
+                }
+            } catch (Exception e) {
+                log.error("Exception while refreshing course.config: {}", e.getMessage(), e);
+            }
+        }
+    }
+
+
+    @PreDestroy
+    public void shutdownConsumers() {
+        running = false;
+        qwSopLogsExecutor.shutdown();
+        watchLogsExecutor.shutdown();
+        courseLinkExecutor.shutdown();
+        courseSopAppLinkExecutor.shutdown();
+        zmLiveWatchLogExecutor.shutdown();
+        try {
+            if (!qwSopLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                qwSopLogsExecutor.shutdownNow();
+            }
+            if (!watchLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                watchLogsExecutor.shutdownNow();
+            }
+            if (!courseLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                courseLinkExecutor.shutdownNow();
+            }
+            if (!courseSopAppLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                courseSopAppLinkExecutor.shutdownNow();
+            }
+            if (!zmLiveWatchLogExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                zmLiveWatchLogExecutor.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            qwSopLogsExecutor.shutdownNow();
+            watchLogsExecutor.shutdownNow();
+            courseLinkExecutor.shutdownNow();
+            courseSopAppLinkExecutor.shutdownNow();
+            zmLiveWatchLogExecutor.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    @Override
+    public void selectSopUserLogsListByTime(LocalDateTime currentTime, List<String> sopidList) throws Exception {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 开始选择和处理 SOP 用户日志 ======");
+
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        List<WxSopUserVo> sopUserLogsVos = sopUserLogsMapper.selectWxSopUserLogsListByTime(sopidList);
+        if (sopUserLogsVos.isEmpty()) {
+            log.info("没有需要处理的 个微SOP 用户日志。");
+            return;
+        }
+        Map<String, List<WxSopUserVo>> sopLogsGroupedById = sopUserLogsVos.stream()
+                .collect(Collectors.groupingBy(WxSopUserVo::getSopId));
+
+        // 查询公司关联小程序数据
+        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
+
+        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
+
+        List<Company> companies = companyMapper.selectCompanyAllList();
+
+        log.info("共分组 {} 个 SOP ID 进行处理。", sopLogsGroupedById.size());
+
+        CountDownLatch sopGroupLatch = new CountDownLatch(sopLogsGroupedById.size());
+
+        for (Map.Entry<String, List<WxSopUserVo>> entry : sopLogsGroupedById.entrySet()) {
+            String sopId = entry.getKey();
+            List<WxSopUserVo> userLogsVos = entry.getValue();
+            processSopGroupAsync(sopId, userLogsVos, sopGroupLatch, currentTime, config, miniMap, companies);
+
+        }
+
+        // 等待所有 SOP 分组处理完成
+        sopGroupLatch.await();
+
+        // 触发批量插入(可选,如果需要立即插入队列中的数据)
+        // batchInsertQwSopLogs();
+        // batchInsertFsCourseWatchLogs();
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+    }
+
+    @Async("sopTaskExecutor")
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void processSopGroupAsync(String sopId, List<WxSopUserVo> wxSopUserVos, CountDownLatch latch, LocalDateTime currentTime,
+                                     CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,List<Company> companies) {
+        try {
+            log.info("当前线程: {}", Thread.currentThread().getName());
+            processSopGroup(sopId, wxSopUserVos, currentTime, config, miniMap, companies);
+        } catch (Exception e) {
+            log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
+        } finally {
+            latch.countDown();
+        }
+    }
+
+
+    private void processSopGroup(String sopId, List<WxSopUserVo> wxSopUserVos, LocalDateTime currentTime, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                 List<Company> companies) throws Exception {
+        WxSopRuleTimeVO ruleTimeVO = sopMapper.selectWxSopByClickHouseId(sopId);
+
+        if (ruleTimeVO == null) {
+//            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
+            log.error("SOP ID {} 已删除或不存在,相关日志已清除。", sopId);
+            return;
+        }
+        QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
+        if (qwSopTemp == null) {
+            log.error("SOP ID {} 模板不存在,相关日志已清除。", sopId);
+            return;
+        }
+
+        ruleTimeVO.setTempStatus(qwSopTemp.getStatus());
+        ruleTimeVO.setTempGap(qwSopTemp.getGap());
+
+        if (ruleTimeVO.getStatus() == 0 || "0".equals(ruleTimeVO.getTempStatus())) {
+            log.error("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
+            return;
+        }
+
+        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId(ruleTimeVO.getTempId());
+        if (rulesList.isEmpty()) {
+            log.error("SOP ID {} 的 TempSetting 为空,跳过处理。", sopId);
+            return;
+        }
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByCompanyId(ruleTimeVO.getCompanyId());
+
+//        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(ruleTimeVO.getCorpId());
+
+//        if (qwCompany == null) {
+//            log.error("SOP ID {} 的 公司信息为空 为空,跳过处理。", sopId);
+//            return;
+//        }
+
+        CountDownLatch userLogsLatch = new CountDownLatch(wxSopUserVos.size());
+        for (WxSopUserVo logVo : wxSopUserVos) {
+            processUserLogAsync(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime, qwCompany.getMiniAppId(),
+                    config, miniMap, companies);
+        }
+        // 等待所有用户日志处理完成
+        try {
+            userLogsLatch.await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("等待用户日志处理完成时被中断: {}", e.getMessage(), e);
+        }
+        log.info("SOP ID {} 的所有用户日志已处理完毕。", sopId);
+    }
+
+    @Async("sopTaskExecutor")
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void processUserLogAsync(WxSopUserVo logVo, WxSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
+                                    CountDownLatch latch, LocalDateTime currentTime,String miniAppId, CourseConfig config,
+                                    Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                    List<Company> companies) {
+        try {
+            processUserLog(logVo, ruleTimeVO, tempSettings, currentTime, miniAppId, config, miniMap, companies);
+        } catch (Exception e) {
+            log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
+        } finally {
+            latch.countDown();
+        }
+    }
+
+
+    private void processUserLog(WxSopUserVo logVo, WxSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
+                                LocalDateTime currentTime, String miniAppId,CourseConfig config, Map<Long, Map<Integer,
+                                List<CompanyMiniapp>>> miniMap,List<Company> companies) {
+        try {
+
+            LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
+            LocalDate currentDate = currentTime.toLocalDate();
+
+            long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
+            int tempGap = ruleTimeVO.getTempGap();
+
+            if (tempGap <= 0) {
+                log.error("SOP ID {} 的 TempGap {} 无效,跳过处理。", logVo.getSopId(), tempGap);
+                return;
+            }
+
+            int intervalDay = (int) (daysBetween / tempGap);
+            if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
+                log.info("用户日志 {} 的 intervalDay {} 超出 TempSettings 范围,跳过处理。", logVo.getId(), intervalDay);
+                return;
+            }
+            long day = daysBetween;
+            if (day == 0 && ObjectUtil.isNotEmpty(ruleTimeVO.getIsAutoSop()) && ruleTimeVO.getIsAutoSop() == 1) {
+                day = 1;
+            } else {
+                day++;
+            }
+            List<QwSopTempSetting.Content> contents = getDay(tempSettings, day);
+            if (contents == null || contents.isEmpty()) {
+                log.error("SOP ID {} 的 TempSetting 内容为空,跳过处理。天数 {}", logVo.getSopId(), day);
+                return;
+            }
+
+            //获取企业微信员工的称呼//从redis里或者从库里取
+            CrmCustomer wxUserByRedis = qwExternalContactService.getWxUserByRedis(logVo.getCustomerId());
+            if (wxUserByRedis == null) {
+                log.error("无企微员工信息 {} 跳过处理。:{}", logVo.getUserId(), logVo.getAccountId());
+                return;
+            }
+
+            WxContact wxContact = wxContactMapper.selectOne(new LambdaQueryWrapper<WxContact>().eq(WxContact::getCustomerId, logVo.getCustomerId()));
+            // 获取客户信息
+            CrmCustomer crmCustomer = crmCustomerMapper.selectCrmCustomerById(wxContact.getCustomerId());
+
+            String wxUserId = String.valueOf(wxUserByRedis.getCustomerId()).trim();
+            String companyId = String.valueOf(wxUserByRedis.getCompanyId()).trim();
+            String companyUserId = String.valueOf(wxContact.getCompanyUserId()).trim();
+            Integer sendMsgType = null;
+            String welcomeText = "hello";
+
+            if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
+                log.error("员工未绑定销售账号或公司,跳过处理:" + wxUserId);
+                return;
+            }
+
+
+            CompanyUser companyUser = companyUserService.selectCompanyUserByIdForRedis(Long.valueOf(companyUserId));
+            if (Objects.nonNull(companyUser)) {
+                if (!StringUtil.strIsNullOrEmpty(companyUser.getDomain())) {
+                    logVo.setDomain(companyUser.getDomain().trim());
+                } else {
+                    logVo.setDomain(config.getRealLinkDomainName().trim());
+                }
+            } else {
+                logVo.setDomain(config.getRealLinkDomainName().trim());
+            }
+
+            // 先算好 60分钟后 ~ 再60分钟后的时间段
+            LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
+
+            // 如果发现已经跨天
+            if (!startRangeFirst.toLocalDate().equals(currentDate)) {
+                // 更新 currentDate
+                currentDate = startRangeFirst.toLocalDate();
+
+                // 重新计算 daysBetween
+                daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
+                intervalDay = (int) (daysBetween / tempGap);
+                day = daysBetween;
+                if (day == 0 && ruleTimeVO.getIsAutoSop() == 1) {
+                    day = 1;
+                } else {
+                    day++;
+                }
+
+                // 重新拿新的 “天” 的 Setting
+                contents = getDay(tempSettings, day);
+                if (contents == null || contents.isEmpty()) {
+                    log.error("跨天-SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
+                    return;
+                }
+            }
+            // 只有整倍数才做事
+            if (daysBetween % tempGap != 0) {
+                log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap, logVo.getSopId());
+                return;
+            }
+
+            for (QwSopTempSetting.Content content : contents) {
+                try {
+                    LocalTime elementLocalTime = LocalTime.parse(content.getTime());
+                    LocalDateTime elementDateTime = LocalDateTime.of(currentTime.toLocalDate(), elementLocalTime);
+                    // 动态调整 elementDateTime 的日期
+                    if (elementLocalTime.isBefore(currentTime.toLocalTime())) {
+                        elementDateTime = elementDateTime.plusDays(1);
+                    }
+                    LocalDateTime startRange = currentTime.plusMinutes(60);
+                    LocalDateTime endRange = startRange.plusMinutes(60);
+
+                    // 跨天逻辑修正:仅当 startRange 的时间晚于 endRange 的时间时调整
+                    if (startRange.toLocalTime().isAfter(endRange.toLocalTime())
+                            && startRange.toLocalDate().equals(endRange.toLocalDate())) {
+                        endRange = endRange.plusDays(1); // 将 endRange 调整为第二天
+                    }
+                    if (!elementDateTime.isBefore(startRange) && !elementDateTime.isAfter(endRange.minusMinutes(1))) {
+                        // 如果时间差在目标范围内,更新记录
+                        // 组合年月日和element的时间
+                        LocalDate targetDate = startDate.plusDays(intervalDay * tempGap);
+                        // 将 targetDate 和 elementTime 组合成 LocalDateTime
+                        LocalDateTime dateTime = LocalDateTime.of(targetDate, elementLocalTime);
+                        // 将 LocalDateTime 转换为 Date
+                        Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+                        WxSopUserInfo wxSopUserInfo = new WxSopUserInfo();
+                        wxSopUserInfo.setSopId(Long.valueOf(logVo.getSopId()));
+                        wxSopUserInfo.setSopUserId(Long.valueOf(logVo.getId()));
+                        wxSopUserInfo.setCustomerId(Long.valueOf(logVo.getCustomerId()));
+
+                        List<WxSopUserInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectWxSopUserLogsInfoList(wxSopUserInfo);
+                        insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, wxUserId,
+                                companyUserId, companyId, welcomeText, wxUserByRedis.getCustomerName(),
+                                miniAppId, config, miniMap, sendMsgType, companies,crmCustomer);
+                    }
+                } catch (Exception e) {
+                    log.error("解析模板内容 {} 失败: {}", content.getTime(), e.getMessage(), e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("解析解析模板 {} 失败: {}", logVo.getStartTime(), e.getMessage(), e);
+        }
+    }
+
+
+    private List<QwSopTempSetting.Content> getDay(List<QwSopTempRules> tempSettings, long days) {
+        List<QwSopTempRules> collect = tempSettings.stream().filter(e -> e.getDayNum() == days && e.getTime() != null).collect(Collectors.toList());
+        AtomicInteger i = new AtomicInteger();
+        return collect.stream().map(e -> {
+            QwSopTempSetting.Content content = new QwSopTempSetting.Content();
+            content.setId(e.getId());
+            content.setType(e.getType());
+            content.setContentType(e.getContentType() != null ? e.getContentType().toString() : null);
+            content.setSetting(e.getSettingList().stream().map(s -> {
+                QwSopTempSetting.Content.Setting setting = JSON.parseObject(s.getContent(), QwSopTempSetting.Content.Setting.class);
+                setting.setId(s.getId());
+                return setting;
+            }).collect(Collectors.toList()));
+            content.setAddTag(e.getAddTag());
+            content.setDelTag(e.getDelTag());
+            content.setTime(e.getTime());
+            content.setIsOfficial(e.getIsOfficial());
+            content.setCourseId(e.getCourseId());
+            content.setVideoId(e.getVideoId());
+            content.setCourseType(e.getCourseType());
+            content.setAiTouch(e.getAiTouch());
+            content.setIsAtAll(e.getIsAtAll());
+            return content;
+        }).sorted(Comparator.comparing(e -> LocalTime.parse(e.getTime() + ":00"))).peek(e -> e.setIndex(i.getAndIncrement())).collect(Collectors.toList());
+    }
+
+    //消息处理
+    private void insertSopUserLogs(List<WxSopUserInfo> sopUserLogsInfos, WxSopUserVo logVo, Date sendTime,
+                                   WxSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content,
+                                   String wxUserId, String companyUserId, String companyId, String welcomeText, String qwUserName,
+                                   String miniAppId, CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
+                                   List<Company> companies,CrmCustomer crmCustomer) {
+        String formattedSendTime = sendTime.toInstant()
+                .atZone(ZoneId.systemDefault())
+                .format(DATE_TIME_FORMATTER);
+        int type = content.getType();
+        Long courseId = content.getCourseId();
+        Long videoId = content.getVideoId();
+        Long liveId = content.getLiveId();
+        Integer isOfficial = content.getIsOfficial() != null ? Integer.valueOf(content.getIsOfficial()) : 0;
+
+
+        // 发送语音 start
+        if (content.getSetting() == null) {
+            return;
+        }
+        List<QwSopTempSetting.Content.Setting> setting = content.getSetting().stream().filter(e -> "7".equals(e.getContentType()) || "16".equals(e.getContentType())).collect(Collectors.toList());
+        if (!setting.isEmpty()) {
+            List<String> valuesList = PubFun.listToNewList(setting, QwSopTempSetting.Content.Setting::getValue);
+            if (valuesList != null && !valuesList.isEmpty()) {
+                try {
+                    List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
+                    if (voiceList != null && !voiceList.isEmpty()) {
+                        Map<String, QwSopTempVoice> collect = voiceList.stream().collect(Collectors.toMap(QwSopTempVoice::getVoiceTxt, e -> e));
+
+                        setting.parallelStream().forEach(st -> {
+                            QwSopTempVoice voice = collect.get(st.getValue());
+                            if (voice == null || voice.getVoiceUrl() == null) {
+                                return;
+                            }
+                            // 企微语音
+                            if ("7".equals(st.getContentType())) {
+                                st.setVoiceUrl(voice.getVoiceUrl());
+                                st.setVoiceDuration(voice.getDuration() + "");
+                            }
+                            // app语音
+                            else if ("16".equals(st.getContentType())) {
+                                st.setVoiceUrl(voice.getUserVoiceUrl());
+                                st.setVoiceDuration(voice.getDuration() + "");
+                            }
+                        });
+                    }
+                } catch (NumberFormatException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+//        // 发送语音 end
+        if (content.getType() == 5) {
+            sopAddTag(logVo, content, sendTime);
+        }
+
+        //当语音模板的qw_sop_temp_voice中无对应语音,就不生成qw_sop_logs记录
+        if (content.getType() == 7 && content.getSetting() != null && !content.getSetting().isEmpty()) {
+            if (content.getSetting().get(0).getVoiceUrl() == null) {
+                return;
+            }
+        }
+
+        // 处理每个 externalContactId
+        sopUserLogsInfos.forEach(contactId -> {
+            try {
+//                String externalId = contactId.getExternalId().toString();
+//                String externalUserName = contactId.getExternalUserName();
+                Long customerId = contactId.getCustomerId();
+
+                String customerName = crmCustomer.getCustomerName();
+//                String fsUserId = miniMap.get(ruleTimeVO.getCompanyId()).get(0).get(0).getAppId();
+                Long fsUserId = null;
+                Integer grade = contactId.getGrade();
+                WxSopLogs wxSopLogs = createWxBaseLog(formattedSendTime, logVo, ruleTimeVO, null, customerName, fsUserId, isOfficial, customerId, contactId.getIsDaysNotStudy());
+                handleLogBasedOnType(wxSopLogs, content, logVo, sendTime, courseId, videoId,
+                        type, wxUserId, companyUserId, companyId, String.valueOf(customerId), welcomeText, qwUserName, fsUserId, false, miniAppId,
+                        null, config, miniMap, grade, sendMsgType, companies, liveId);
+            } catch (Exception e) {
+                log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
+            }
+        });
+//        // 处理每个 externalContactId
+//        sopUserLogsInfos.forEach(contactId -> {
+//            try {
+//                String externalId = contactId.getExternalId().toString();
+//                String externalUserName = contactId.getExternalUserName();
+//                Long fsUserId = contactId.getFsUserId();
+//                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId,isOfficial,contactId.getExternalId());
+//                handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
+//                        type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName);
+//            } catch (Exception e) {
+//                log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
+//            }
+//        });
+    }
+
+    private void sopAddTag(WxSopUserVo logVo, QwSopTempSetting.Content content, Date sendTime) {
+        String id = logVo.getId();
+        String addTag = content.getAddTag();
+        String delTag = content.getDelTag();
+        String corpId = logVo.getCorpId();
+        if (addTag != null || delTag != null) {
+            QwSopTag qwSopTag = new QwSopTag();
+            qwSopTag.setAddTags(addTag);
+            qwSopTag.setDelTags(delTag);
+            qwSopTag.setCorpId(corpId);
+            qwSopTag.setSopUserLogsId(id);
+            qwSopTag.setType(1);
+            qwSopTag.setStatus(1);
+            qwSopTag.setSendTime(sendTime);
+            qwSopTag.setCreateTime(new Date());
+            qwSopTagMapper.insertQwSopTag(qwSopTag);
+        }
+    }
+
+    private WxSopLogs createWxBaseLog(String formattedSendTime, WxSopUserVo logVo,
+                                      WxSopRuleTimeVO ruleTimeVO, String externalContactId,
+                                      String customerName, Long fsUserId, Integer isOfficial,
+                                      Long customerId, Integer isDaysNotStudy) {
+        WxSopLogs wxSopLogs = new WxSopLogs();
+
+        // 基础信息
+        wxSopLogs.setSendTime(formattedSendTime);
+        wxSopLogs.setAccountId(Long.valueOf(logVo.getCustomerId())); // 个微账号ID
+        wxSopLogs.setType(logVo.getType());
+        wxSopLogs.setFsUserId(fsUserId);
+        wxSopLogs.setWxContactId(customerId);
+        wxSopLogs.setWxContactName(customerName);
+
+
+        // 发送状态设置
+        if (isOfficial != 1 && Integer.valueOf(1).equals(isDaysNotStudy)) {
+            wxSopLogs.setSendStatus(5); // 5-消息作废
+            wxSopLogs.setRemark("E级客户不发送");
+        } else {
+            wxSopLogs.setSendStatus(3); // 3-待发送
+        }
+
+        // 发送类型设置
+        if (isOfficial == 1) {
+//            if (logVo.getIsSampSend() == 1) {
+//                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+//                    wxSopLogs.setSendType(2); // 2-单链补发
+//                    wxSopLogs.setRemark("未绑定小程序用户,单链补发");
+//                    // 时间设置成固定8点
+//                    LocalDateTime dateTime = LocalDateTime.parse(formattedSendTime, DATE_TIME_FORMATTER);
+//                    wxSopLogs.setSendTime(OUTPUT_FORMATTER.format(dateTime));
+//                } else {
+//                    wxSopLogs.setSendType(1); // 1-正常发送
+//                }
+//            } else {
+                wxSopLogs.setSendType(1);
+//            }
+        } else if (isOfficial == 0) {
+//            wxSopLogs.setSendType(ruleTimeVO.getSendType() == 1 ? 2 : ruleTimeVO.getSendType());
+            wxSopLogs.setSendType(1);
+        } else {
+            wxSopLogs.setSendType(ruleTimeVO.getSendType());
+        }
+
+        // 任务相关信息
+        wxSopLogs.setSopId(Long.valueOf(logVo.getSopId()));
+        wxSopLogs.setSopUserId(Long.valueOf(logVo.getId())); // 对应wx_sop_logs的sop_user_id字段
+
+        // 发送排序(使用开始时间去除横线后的数值)
+        wxSopLogs.setSendSort(Integer.valueOf(logVo.getStartTime().replaceAll("-", "")));
+
+        // 小程序用户ID
+        wxSopLogs.setFsUserId(fsUserId);
+
+        // 生成类型(默认为0自动生成)
+        wxSopLogs.setGenerateType(0);
+
+        // 备注信息(如果有额外需要记录的信息)
+        // wxSopLogs.setSendRemark(""); // 如果有需要可以设置
+
+        return wxSopLogs;
+    }
+
+    private QwSopLogs createBaseLog(String formattedSendTime, SopUserLogsVo logVo,
+                                    WxSopRuleTimeVO ruleTimeVO, String externalContactId,
+                                    String externalUserName, Long fsUserId, Integer isOfficial,
+                                    Long externalId, Integer isDaysNotStudy) {
+        QwSopLogs sopLogs = new QwSopLogs();
+        sopLogs.setSendTime(formattedSendTime);
+        sopLogs.setQwUserid(logVo.getQwUserId());
+        sopLogs.setCorpId(logVo.getCorpId());
+        sopLogs.setLogType(ruleTimeVO.getType());
+        sopLogs.setTakeRecords(0);
+
+        if (isOfficial != 1 && Integer.valueOf(1).equals(isDaysNotStudy)) {
+            sopLogs.setSendStatus(5L);
+            sopLogs.setRemark("E级客户不发送");
+        } else {
+            sopLogs.setSendStatus(3L);
+        }
+
+        sopLogs.setReceivingStatus(0L);
+
+        if (isOfficial == 1) {
+
+            if (logVo.getIsSampSend() == 1) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    sopLogs.setSendType(2);
+                    sopLogs.setRemark("未绑定小程序用户,单链补发");
+                    //时间设置成固定8点
+                    LocalDateTime dateTime = LocalDateTime.parse(formattedSendTime, DATE_TIME_FORMATTER);
+                    sopLogs.setSendTime(OUTPUT_FORMATTER.format(dateTime));
+                } else {
+                    sopLogs.setSendType(1);
+                }
+
+            } else {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    sopLogs.setTakeRecords(1);
+                    sopLogs.setSendType(1);
+                } else {
+                    sopLogs.setSendType(1);
+                }
+            }
+
+        } else if (isOfficial == 0) {
+            sopLogs.setSendType(ruleTimeVO.getSendType() == 1 ? 2 : ruleTimeVO.getSendType());
+        } else {
+            sopLogs.setSendType(ruleTimeVO.getSendType());
+        }
+
+
+        String[] userKey = logVo.getUserId().split("\\|");
+        sopLogs.setCompanyId(Long.valueOf(userKey[2].trim()));
+        if (StringUtils.isNotEmpty(userKey[0].trim())) {
+            sopLogs.setQwUserKey(Long.valueOf(userKey[0].trim()));
+        }
+        sopLogs.setSopId(logVo.getSopId());
+        sopLogs.setSort(Integer.valueOf(logVo.getStartTime().replaceAll("-", "")));
+        sopLogs.setExternalUserId(externalContactId);
+        sopLogs.setExternalId(externalId);
+        sopLogs.setExternalUserName(externalUserName);
+        sopLogs.setFsUserId(fsUserId);
+        sopLogs.setUserLogsId(logVo.getId());
+
+        if (ObjectUtil.isNotEmpty(logVo.getActualQwId())) {
+            sopLogs.setQwUserKey(logVo.getActualQwId());
+        }
+        return sopLogs;
+    }
+
+    private void handleLogBasedOnType(WxSopLogs sopLogs, QwSopTempSetting.Content content,
+                                      WxSopUserVo logVo, Date sendTime, Long courseId, Long videoId, int type, String qwUserId,
+                                      String companyUserId, String companyId, String externalId, String welcomeText,
+                                      String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
+                                      QwGroupChat groupChat, CourseConfig config,
+                                      Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                      Integer grade, Integer sendMsgType, List<Company> companies, Long liveId) {
+        switch (type) {
+            case 1:
+                handleNormalMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo,sendTime);
+                break;
+            case 2:
+                handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
+                        qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId,
+                        isGroupChat, miniAppId, groupChat, config, miniMap, grade, sendMsgType, companies);
+                break;
+            case 3:
+                handleOrderMessage(sopLogs, content);
+                break;
+            case 4:
+//                handleAIMessage(sopLogs, content);
+                break;
+            case 5:
+//                handleTagMessage(sopLogs, content);
+                break;
+            case 7:
+                handleVoiceMessage(sopLogs, content, companyUserId);
+                break;
+            //直播间发送类型
+            case 20:
+                handleLiveMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo, liveId);
+                break;
+            case 21:
+                handleGroupNoticeMessage(sopLogs, content, isGroupChat);
+                break;
+            default:
+                log.error("未知的消息类型 {},跳过处理。", type);
+                break;
+        }
+    }
+
+    /**
+     * 处理群公告消息
+     * @param sopLogs 日志对象
+     * @param content 内容对象
+     * @param isGroupChat 是否为群聊
+     */
+    private void handleGroupNoticeMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content, boolean isGroupChat) {
+        // 群公告只能发给群聊
+        if (!isGroupChat) {
+            log.warn("群公告只能发给群聊,跳过处理");
+            return;
+        }
+
+        // 设置发送类型为21(群公告)
+        sopLogs.setSendType(21);
+        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    private void handleVoiceMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId) {
+        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    private void handleNormalMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId, String companyId,
+                                     boolean isGroupChat, String qwUserId, QwGroupChat groupChat, String externalId, WxSopUserVo logVo, Date sendTime) {
+
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        List<QwSopTempSetting.Content.Setting> settingAll = new ArrayList<>();
+        if (settings == null || settings.isEmpty()) {
+//            log.error("Cloned content settings are empty, skipping.");
+            return;
+        }
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //直播小程序单独
+                //文本
+                case "1":break;
+                //图片
+                case "2":
+                //小程序
+                case "4":
+                //文件
+                case "5":
+                case "6":
+                case "7":
+                case "8":
+                case "9":
+                //app语音
+                case "16":
+                //app文本
+                case "15":
+                //群公告
+                case "11":
+                //直播小程序
+                case "12":
+                    //直播发送类型
+                    sopLogs.setSendType(20);
+                    clonedContent.setLiveId(setting.getLiveId());
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId() + "&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 1, qwUserId, logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                case "13":
+                    try {
+                        if (sopLogs.getFsUserId() != null && !Objects.equals(0L, sopLogs.getFsUserId())) {
+                            sopLogs.setSendStatus(5);
+                            sopLogs.setReceivingStatus(0);
+                            sopLogs.setRemark("已经注册过的客户不发送");
+                        }
+                        if (ObjectUtil.isNotEmpty(setting.getValue())) {
+                            QwSopTempSetting.Content.Setting setting1 =new QwSopTempSetting.Content.Setting();
+                            setting1.setContentType("1");
+                            setting1.setValue(setting.getValue());
+                            settingAll.add(setting1);
+                        }
+
+                        String link;
+                        if (isGroupChat && groupChat != null) {
+                            link = createRegisteredGroupLinkByMiniApp(setting, logVo, sendTime,
+                                    qwUserId, Long.parseLong(companyUserId), companyId, logVo.getChatId());
+                        }else {
+                            link = createRegisteredLinkByMiniApp(setting, logVo, sendTime,
+                                    qwUserId, companyUserId, companyId, externalId, sopLogs.getFsUserId());
+                        }
+                        //算主备小程序
+                        String luckyjson1 = configService.selectConfigByKey("luckyBag.config");
+                        Map<String, Object> luckyBagConfig1 = JSON.parseObject(luckyjson1, Map.class);
+                        String finalAppId = String.valueOf(luckyBagConfig1.get("appId"));
+                        /*getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
+
+                        if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            finalAppId = miniAppId;
+                        }*/
+
+                        if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            setting.setMiniprogramAppid(finalAppId);
+                        } else {
+                            log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                        }
+
+                        setting.setMiniprogramTitle("点击注册");
+                        setting.setMiniprogramPage(link);
+//               try {
+//                   item.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+//               } catch (Exception e) {
+//                   log.error("赋值-小程序封面地址失败-" + e);
+//               }
+                    } catch (Exception e) {
+                        log.error("任务模板发送注册页面失败:" + e);
+                    }
+
+                    break;
+                //福袋
+                case "14":
+                    try {
+                        // 1. 检查必要对象是否为空
+                        if (sopLogs == null) {
+                            log.warn("sopLogs为空,跳过福袋处理");
+                        }
+                        // 查询福袋信息
+                        Long fsUserId = sopLogs.getFsUserId();
+
+                        LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(setting.getLuckyBagId());
+                        if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
+                            setSopLogsStatus(sopLogs, 5, 0, "福袋配置被禁用");
+                        }else if (ObjectUtil.isNotEmpty(fsUserId)){
+
+                            // 2. 获取系统配置
+                            SysConfig luckyBagConfig = sysConfigMapper.selectConfigByConfigKey("luckyBag.config");
+                            if (ObjectUtil.isEmpty(luckyBagConfig) || StringUtil.strIsNullOrEmpty(luckyBagConfig.getConfigValue())) {
+//                                log.warn("福袋配置为空,设置发送状态为失败");
+                                setSopLogsStatus(sopLogs, 5, 0, "福袋配置不存在");
+                            }
+
+                            // 3. 解析配置
+                            JSONObject jsonObject = null;
+                            try {
+                                jsonObject = JSON.parseObject(luckyBagConfig.getConfigValue());
+                            } catch (Exception e) {
+//                                log.error("解析福袋配置JSON失败: {}", luckyBagConfig.getConfigValue(), e);
+                                setSopLogsStatus(sopLogs, 5, 0, "福袋配置格式错误");
+                            }
+
+                            // 4. 获取周限制次数
+                            Integer count = null;
+                            try {
+                                Object weekLimitObj = jsonObject.get("weekLimit");
+                                if (weekLimitObj != null) {
+                                    count = Integer.valueOf(weekLimitObj.toString());
+                                }
+                            } catch (NumberFormatException e) {
+                                log.error("周限制次数格式错误: {}", jsonObject.get("weekLimit"), e);
+                            }
+
+                            if (count == null) {
+//                                log.warn("周限制次数配置为空");
+                                setSopLogsStatus(sopLogs, 5, 0, "周限制次数配置错误");
+                            }
+
+
+                            // 5. 查询用户福袋收集记录(带计数缓存)
+                            LuckyBagCollectRecord luckyBagCollectRecord = new LuckyBagCollectRecord();
+                            luckyBagCollectRecord.setUserId(fsUserId);
+// 动态计算时间范围
+                            LocalDate endDate = LocalDate.now();
+                            LocalDate startDate = endDate.minusDays(6); // 包含今天
+
+                            Map<String, Object> params = new HashMap<>();
+                            params.put("beginSendTime", startDate.toString());
+                            params.put("endSendTime", endDate.toString());
+                            luckyBagCollectRecord.setParams(params);
+                            luckyBagCollectRecord.setCollectType("1");
+
+                            List<LuckyBagCollectRecord> luckyBagCollectRecords;
+                            int recordCount;
+
+                            try {
+                                // 生成缓存key
+                                String dateRangeKey = startDate.toString() + "_to_" + endDate.toString();
+                                String countCacheKey = "luckyBag:user:" + fsUserId + ":recent7days:" + dateRangeKey + ":count";
+
+                                // 1. 先尝试从计数缓存获取
+                                Object cachedCount = redisCache.getCacheObject(countCacheKey);
+                                if (cachedCount != null && cachedCount instanceof Integer) {
+                                    recordCount = (Integer) cachedCount;
+//                                    log.debug("福袋计数缓存命中,userId: {},次数: {}", fsUserId, recordCount);
+
+                                    // 如果只需要判断是否超限,且已超限,直接返回
+                                    if (recordCount >= count) {
+//                                        log.info("用户福袋次数已达上限(计数缓存), userId: {}, 当前次数: {}, 限制次数: {}",
+//                                                fsUserId, recordCount, count);
+                                        setSopLogsStatus(sopLogs, 5, 0, "超过福袋发放次数");
+                                        luckyBagCollectRecords = Collections.emptyList();
+                                        // 可以直接返回,不需要查询完整记录
+                                        // return; // 根据你的流程决定是否返回
+                                    } else {
+                                        // 未超限,查询完整记录
+                                        String recordsCacheKey = "luckyBag:user:" + fsUserId + ":recent7days:" + dateRangeKey + ":records";
+                                        luckyBagCollectRecords = getCachedOrQueryRecords(fsUserId, recordsCacheKey, luckyBagCollectRecord);
+                                    }
+                                } else {
+                                    // 计数缓存未命中,查询完整记录
+                                    String recordsCacheKey = "luckyBag:user:" + fsUserId + ":recent7days:" + dateRangeKey + ":records";
+                                    luckyBagCollectRecords = getCachedOrQueryRecords(fsUserId, recordsCacheKey, luckyBagCollectRecord);
+                                    recordCount = luckyBagCollectRecords != null ? luckyBagCollectRecords.size() : 0;
+
+                                    // 更新计数缓存
+                                    cacheUserCount(fsUserId, countCacheKey, recordCount);
+                                }
+                            } catch (Exception e) {
+                                log.error("查询用户福袋记录失败, userId: {}", fsUserId, e);
+                                luckyBagCollectRecords = Collections.emptyList();
+                                recordCount = 0;
+                            }
+
+
+//                            // 5. 查询用户福袋收集记录
+//                            LuckyBagCollectRecord luckyBagCollectRecord = new LuckyBagCollectRecord();
+//                            luckyBagCollectRecord.setUserId(fsUserId);
+//                            // 动态计算时间范围
+//                            LocalDate endDate = LocalDate.now();
+//                            LocalDate startDate = endDate.minusDays(6); // 包含今天
+//
+//                            Map<String, Object> params = new HashMap<>();
+//                            params.put("beginSendTime", startDate.toString());
+//                            params.put("endSendTime", endDate.toString());
+//                            luckyBagCollectRecord.setParams(params);
+//                            luckyBagCollectRecord.setCollectType("1");
+//                            List<LuckyBagCollectRecord> luckyBagCollectRecords;
+//                            try {
+//                                luckyBagCollectRecords = luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(luckyBagCollectRecord);
+//                            } catch (Exception e) {
+//                                log.error("查询用户福袋记录失败, userId: {}", fsUserId, e);
+//                                luckyBagCollectRecords = Collections.emptyList();
+//                            }
+
+                            // 6. 检查次数限制
+                            if (recordCount >= count) {
+//                                log.info("用户福袋次数已达上限, userId: {}, 当前次数: {}, 限制次数: {}", fsUserId, recordCount, count);
+                                setSopLogsStatus(sopLogs, 5, 0, "超过福袋发放次数");
+                            }
+                        }
+
+                        // 7. 生成活动链接
+                        String link;
+
+                        String luckyjson1 = configService.selectConfigByKey("luckyBag.config");
+                        Map<String, Object> luckyBagConfig1 = JSON.parseObject(luckyjson1, Map.class);
+                        String finalAppId = String.valueOf(luckyBagConfig1.get("appId"));
+                    /*    try {
+                            finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
+                            if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                                finalAppId = miniAppId;
+                            }
+                        } catch (Exception e) {
+                            log.error("获取小程序ID失败,使用默认值", e);
+                            finalAppId = miniAppId;
+                        }
+
+                        if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            log.error("公司的小程序id为空,sopId: {}", sopLogs.getSopId());
+                            setSopLogsStatus(sopLogs, 5L, 0L, "小程序配置错误");
+                            return;
+                        }*/
+
+                        // 10. 设置小程序参数
+                        setting.setMiniprogramAppid(finalAppId);
+                        setting.setMiniprogramTitle("福袋发放");
+
+//                        log.info("福袋配置成功,userId: {}, appId: {}", fsUserId, finalAppId);
+
+                    } catch (Exception e) {
+                        log.error("任务模板福袋发放失败", e);
+                        // 确保在最终异常时也设置状态
+                        if (sopLogs != null) {
+                            setSopLogsStatus(sopLogs, 5, 0, "福袋发放系统异常");
+                        }
+                    }
+                    break;
+                //直播h5跳转卡片
+                case "18":
+                    //直播h5跳转短链
+                case "19":
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+                    shortH5Link = shortH5Link.substring(0, shortH5Link.length() - 1);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    json = configService.selectConfigByKey("his.config");
+                    sysConfig= JSON.parseObject(json,FSSysConfig.class);
+
+                    if(isGroupChat){
+                        try{
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId,companyUserId,vo.getId().toString(), setting.getLiveId(),sysConfig.getAppId(),2,qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            shortH5Link += ",\"chatId\":\"" + groupChat.getChatId() + "\"";
+                        }catch(Exception e){
+                            log.error("直播H5群聊新增报错,{}", e.getMessage(),e);
+                        }
+                    }else{
+                        try{
+                            createLiveWatchLogAndEnQueue(companyId,companyUserId,externalId, setting.getLiveId(),sysConfig.getAppId(),1,qwUserId,logVo.getCorpId());
+                            shortH5Link += ",\"externalId\":\"" + externalId + "\"";
+                        }catch(Exception e){
+                            log.error("直播H5个人新增报错,{}", e.getMessage(),e);
+                        }
+                    }
+
+                    shortH5Link += "}";
+                    miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(shortH5Link);
+
+
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+                    break;
+                    //TODO 其他消息类型继续添加case
+                default:
+                    break;
+            }
+        }
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+//        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    private String createRegisteredLinkByMiniApp(QwSopTempSetting.Content.Setting setting, WxSopUserVo logVo, Date sendTime,
+                                                  String qwUserId,
+                                                 String companyUserId, String companyId, String externalId, Long fsUserId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+//        if (StringUtils.isEmpty(config.getMiniprogramPage())){
+//            log.error("miniprogramPage is not loaded.");
+//            return "";
+//        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId!=null?Long.parseLong(qwUserId):null);
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        //link.setCorpId(logVo.getCorpId());
+        link.setQwExternalId(Long.parseLong(externalId));
+        link.setUNo(UUID.randomUUID().toString());
+        link.setLinkType(3);
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = registeredRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
+
+    public String createRegisteredGroupLinkByMiniApp(QwSopTempSetting.Content.Setting setting, WxSopUserVo logVo, Date sendTime,
+                                                      String qwUserId,
+                                                     Long companyUserId, String companyId, String chatId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+//            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId!=null?Long.parseLong(qwUserId): null);
+        link.setCompanyUserId(companyUserId);
+//        link.setVideoId(null);
+        //link.setCorpId(logVo.getCorpId());
+//        link.setCourseId(null);
+        link.setChatId(chatId);
+        link.setIsRoom(1);
+        link.setLinkType(3);
+        link.setUNo(UUID.randomUUID().toString());
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = registeredRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
+    /**
+     * 处理直播消息
+     */
+    public void handleLiveMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId, String companyId,
+                                  boolean isGroupChat, String qwUserId, QwGroupChat groupChat, String externalId, WxSopUserVo logVo, Long liveId){
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+        clonedContent.setLiveId(liveId);
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        if (settings == null || settings.isEmpty()) {
+//            log.error("Cloned content settings are empty, skipping.");
+            return;
+        }
+
+        //直播发送类型
+        sopLogs.setSendType(20);
+
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //直播小程序单独
+                case "12":
+                    clonedContent.setLiveId(setting.getLiveId());
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId() + "&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 1, qwUserId, logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                //直播h5跳转卡片
+                case "18":
+                    //直播h5跳转短链
+                case "19":
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    json = configService.selectConfigByKey("his.config");
+                    sysConfig= JSON.parseObject(json,FSSysConfig.class);
+
+                    if(isGroupChat){
+                        try{
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId,companyUserId,vo.getId().toString(), setting.getLiveId(),sysConfig.getAppId(),2,qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            shortH5Link += "&chatId=" + groupChat.getChatId();
+                        }catch(Exception e){
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(),e);
+                        }
+                    }else{
+                        try{
+                            createLiveWatchLogAndEnQueue(companyId,companyUserId,externalId, setting.getLiveId(),sysConfig.getAppId(),1,qwUserId,logVo.getCorpId());
+                            shortH5Link += "&externalId=" + externalId;
+                        }catch(Exception e){
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(),e);
+                        }
+                    }
+
+                    miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(shortH5Link);
+
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    private void handleAIMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content) {
+        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    private void handleCourseMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content,
+                                     WxSopUserVo logVo, Date sendTime, Long courseId, Long videoId, String qwUserId, String companyUserId,
+                                     String companyId, String externalId, String welcomeText, String qwUserName,
+                                     Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat, CourseConfig config, Map<Long,
+            Map<Integer, List<CompanyMiniapp>>> miniMap, Integer grade, Integer sendMsgType,
+                                     List<Company> companies) {
+        QwExternalContact contact = null;
+        if (logVo.getExternalId() != null) {
+            contact = qwExternalContactMapper.selectById(logVo.getExternalId());
+        }
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+        String isOfficial = clonedContent.getIsOfficial();
+
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        if (settings == null || settings.isEmpty()) {return;}
+        //如果是@所有人,就添加
+        if (1 == content.getIsAtAll()) {
+            QwSopTempSetting.Content.Setting atMsg = new QwSopTempSetting.Content.Setting();
+            atMsg.setContentType("99");
+            settings.add(atMsg);
+        }
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //文字和短链一起
+                case "1":
+                case "3":
+                    if ("1".equals(setting.getContentType())) {
+                        String defaultName = "同学";
+                        if (contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())) {
+                            defaultName = contact.getName();
+                        }
+                        setting.setValue(setting.getValue()
+                                .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(welcomeText) ? "" : welcomeText)
+                                .replaceAll("#客户称呼#", contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? defaultName : contact.getStageStatus()));
+                    }
+//                    }
+                    break;
+                //小程序单独
+                case "4":
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo, 2);
+                                }
+                            });
+                        } catch (Exception e) {
+                            log.error("群聊创建看课记录失败!", e);
+                        }
+                    } else {
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo, 2);
+                    }
+
+                    String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
+                            qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId(), isGroupChat ? groupChat.getChatId() : null);
+
+                    if (sopLogs.getSendType() == 1) {
+                        setting.setMiniprogramAppid(miniAppId);
+                    } else {
+                        int miniType = getLevel(grade);
+                        //算主备小程序
+                        String finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
+
+                        if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            finalAppId = miniAppId;
+                        }
+
+                        setting.setMiniType(miniType);
+                        if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            setting.setMiniprogramAppid(finalAppId);
+                        }
+
+                    }
+
+                    setting.setMiniprogramPage(sortLink.replaceAll("^[\\s\\u2005]+", ""));
+
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? config.getSidebarImageUrl() : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                //app
+                case "9":
+                    break;
+                //自定义小程序
+                case "10":
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo, 2);
+
+                    Optional<Company> matchedCompany = companies.stream()
+                            .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
+                            .findFirst();
+                    if (matchedCompany.isPresent()) {
+                        Company company = matchedCompany.get();
+
+                        String customMiniAppId = company.getCustomMiniAppId();
+
+                        if (customMiniAppId != null && !customMiniAppId.trim().isEmpty()) {
+                            setting.setMiniprogramAppid(customMiniAppId);
+                        } else {
+                            setting.setMiniprogramAppid("该公司未配置自定义小程序:" + companyId);
+                        }
+                    } else {
+                        setting.setMiniprogramAppid("未找到匹配的公司的自定义小程序:" + companyId);
+                    }
+
+                    break;
+                //直播小程序单独
+                case "12":
+                    sopLogs.setSendType(20);
+                    clonedContent.setLiveId(setting.getLiveId());
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId() + "&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                case "14":
+                case "15": //app文本
+                case "16": //app语音
+                    break;
+                case "17":
+                    try {
+                        String sroth5link;
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo,2);
+
+                        sroth5link = createH5LinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
+                                qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId());
+
+                        if(sopLogs.getSendType()==1){
+                            setting.setMiniprogramAppid(miniAppId);
+                        }else {
+                            int miniType = getLevel(grade);
+                            //算主备小程序
+                            String finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
+
+                            if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                                finalAppId = miniAppId;
+                            }
+
+                            setting.setMiniType(miniType);
+                            if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
+                                setting.setMiniprogramAppid(finalAppId);
+                            } else {
+                                log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                            }
+
+                        }
+
+                        setting.setMiniprogramTitle("邀请链接");
+                        setting.setMiniprogramPage(sroth5link);
+
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+
+                    } catch (Exception e) {
+                        log.error("浏览器看课模板解析失败:" + e);
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+
+        }
+        clonedContent.getSetting().stream().filter(e -> "1".equals(e.getIsBindUrl())).forEach(e -> {
+            e.setIsBindUrl("0");
+//            e.setLinkDescribe(null);
+            e.setLinkUrl(null);
+//            e.setLinkImageUrl(null);
+        });
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+    /**
+     * 创建福袋链接
+     *
+     * @param st
+     * @param sopLogs
+     * @param corpId
+     * @param sendTime
+     * @param courseId
+     * @param videoId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @param config
+     * @param chatId
+     * @return
+     */
+    public String createActivityLinkByMiniApp(QwSopTempSetting.Content.Setting st, QwSopLogs sopLogs, String corpId, Date sendTime, Long courseId, Long videoId, String qwUserId, String companyUserId, String companyId, CourseConfig config, String chatId) {
+        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId.toString(),
+                companyUserId, companyId, null, 3);
+        link.setChatId(chatId);
+        Date updateTime = createUpdateTime(st, sendTime, config);
+        link.setUpdateTime(updateTime);
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+        Long businessId = addLuckyBagCollectRecord(st, sopLogs, updateTime, companyUserId, companyId, chatId);
+        courseMap.setBusinessId(String.valueOf(businessId));
+        st.setBusinessId(String.valueOf(businessId));
+        String realLinkFull = appActivitlLink + JSON.toJSONString(courseMap);
+        link.setRealLink(realLinkFull);
+//        log.error("存入fs_course_link:" + registeredRealLink);
+//        log.error("QwSopCourseFinishTempSetting.Setting:{}", st);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink();
+    }
+
+    /**
+     * 增加福袋发放记录、领取记录
+     *
+     * @param content
+     * @param qwSopLogs
+     * @param sendTime
+     * @param companyUserId
+     * @param companyId
+     * @param chatId
+     */
+    private Long addLuckyBagCollectRecord(QwSopTempSetting.Content.Setting content,
+                                          QwSopLogs qwSopLogs,
+                                          Date sendTime,
+                                          String companyUserId,
+                                          String companyId,
+                                          String chatId) {
+        try {
+            // 参数校验
+            if (content == null || qwSopLogs == null || sendTime == null) {
+                log.warn("添加福袋记录失败:必要参数为空 [content:{}, qwSopLogs:{}, sendTime:{}]",
+                        content, qwSopLogs, sendTime);
+                return null;
+            }
+
+            if (StringUtils.isEmpty(companyId) || StringUtils.isEmpty(companyUserId)) {
+                log.warn("公司ID或用户ID为空 [companyId:{}, companyUserId:{}]", companyId, companyUserId);
+                return null;
+            }
+
+            // 验证福袋ID
+            if (content.getLuckyBagId() == null) {
+                log.warn("福袋ID为空");
+                return null;
+            }
+
+            // 查询福袋信息
+            LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(content.getLuckyBagId());
+            if (luckyBag == null) {
+                log.warn("未找到对应的福袋信息 [luckyBagId:{}]", content.getLuckyBagId());
+                return null;
+            }
+
+            // 检查福袋状态
+            if (luckyBag.getDataStatus() != null && luckyBag.getDataStatus().equals(0)) {
+                log.warn("福袋被禁用 [luckyBagId:{}]", content.getLuckyBagId());
+                return null;
+            }
+
+            // 查询公司信息
+            Company company = companyMapper.selectCompanyById(Long.valueOf(companyId));
+            if (company == null) {
+                log.warn("未找到对应的公司信息 [companyId:{}]", companyId);
+                return null;
+            }
+
+            // 构建福袋记录
+            LuckyBagCollectRecord luckyBagCollectRecord = buildLuckyBagRecord(content, qwSopLogs, sendTime,
+                    companyUserId, companyId, chatId, company, luckyBag);
+
+            // 插入记录并返回ID
+            int result = luckyBagCollectRecordMapper.insertLuckyBagCollectRecord(luckyBagCollectRecord);
+            if (result <= 0) {
+                log.warn("福袋记录插入失败 [luckyBagId:{}]", content.getLuckyBagId());
+                return null;
+            }
+
+            // 返回新增记录的ID
+            Long recordId = luckyBagCollectRecord.getId();
+            if (recordId == null) {
+                log.warn("福袋记录插入成功但未返回ID [luckyBagId:{}]", content.getLuckyBagId());
+                return null;
+            }
+
+//            log.info("福袋记录添加成功 [recordId:{}, luckyBagId:{}]", recordId, content.getLuckyBagId());
+            return recordId;
+
+        } catch (NumberFormatException e) {
+            log.error("ID转换失败 [companyId:{}, companyUserId:{}]", companyId, companyUserId, e);
+            return null;
+        } catch (Exception e) {
+            log.error("ID:" + (content != null ? content.getLuckyBagId() : "unknown") + "-添加福袋记录失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 构建福袋记录对象
+     */
+    private LuckyBagCollectRecord buildLuckyBagRecord(QwSopTempSetting.Content.Setting content,
+                                                      QwSopLogs qwSopLogs,
+                                                      Date sendTime,
+                                                      String companyUserId,
+                                                      String companyId,
+                                                      String chatId,
+                                                      Company company,
+                                                      LuckyBag luckyBag) {
+        LuckyBagCollectRecord record = new LuckyBagCollectRecord();
+
+        record.setLuckyBagId(content.getLuckyBagId());
+        record.setExpiryTime(sendTime);
+        record.setCollectType("3");
+        record.setCompanyId(Long.valueOf(companyId));
+        record.setUserId(qwSopLogs.getFsUserId());
+        if (ObjectUtil.isNotEmpty(qwSopLogs.getFsUserId())) {
+            FsUser fsUser = fsUserMapper.selectFsUserByUserId(qwSopLogs.getFsUserId());
+            record.setUserName(ObjectUtil.isNotEmpty(fsUser) ? fsUser.getNickName() : null);
+        }
+        record.setCompanyName(company.getCompanyName());
+        record.setCompanyUserId(Long.valueOf(companyUserId));
+        record.setSendLink(content.getMiniprogramPage());
+
+        // 设置奖励类型和聊天信息
+        if (StringUtils.isNotEmpty(chatId)) {
+            record.setRewardType(1L);
+            record.setChatId(chatId);
+            record.setExternalUserName(qwSopLogs.getExternalUserName());
+        } else {
+            record.setRewardType(2L);
+        }
+
+        // 设置币种金额
+        if (luckyBag.getRewardType() != null && luckyBag.getRewardType().equals("1")) {
+            record.setCoinAmount(luckyBag.getAmount());
+        }
+
+        return record;
+    }
+    private String createH5LinkByMiniApp(QwSopTempSetting.Content.Setting setting, WxSopUserVo logVo, Date sendTime,
+                                         Long courseId, Long videoId, String qwUserId,
+                                         String companyUserId, String companyId, String externalId, String isOfficial, Long fsUserId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+//        if (StringUtils.isEmpty(config.getMiniprogramPage())){
+//            log.error("miniprogramPage is not loaded.");
+//            return "";
+//        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(logVo.getCorpId());
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(Long.parseLong(externalId));
+        link.setUNo(UUID.randomUUID().toString());
+
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = h5miniappLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
+    private String createH5LiveShortLink(QwSopTempSetting.Content.Setting setting, String corpId, String qwUserId, String companyUserId, String companyId) {
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setLiveId(setting.getLiveId());
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        /*FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);*/
+
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = h5LiveShortLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+
+    }
+    private String getAppIdFromMiniMap(Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                       String companyId,
+                                       int sendMsgType,
+                                       Integer grade) {
+        if (miniMap.isEmpty() || sendMsgType != 1) {
+            return null;
+        }
+
+        Map<Integer, List<CompanyMiniapp>> gradeMap = miniMap.get(Long.valueOf(companyId));
+        if (gradeMap == null) {
+            return null;
+        }
+
+        int listIndex = getLevel(grade);
+        List<CompanyMiniapp> miniapps = gradeMap.get(listIndex);
+
+        if (miniapps == null || miniapps.isEmpty()) {
+            return null;
+        }
+
+        CompanyMiniapp companyMiniapp = miniapps.get(0);
+        return (companyMiniapp != null && !StringUtil.strIsNullOrEmpty(companyMiniapp.getAppId()))
+                ? companyMiniapp.getAppId()
+                : null;
+    }
+
+    private static int getLevel(Integer grade) {
+        int effectiveGrade = (grade == null) ? 5 : grade;
+        int listIndex = (effectiveGrade == 1 || effectiveGrade == 2) ? 0 : 1;
+        return listIndex;
+    }
+
+    /**
+     * 深拷贝 Content 对象,避免使用 JSON 进行序列化和反序列化
+     */
+    private QwSopTempSetting.Content deepCopyContent(QwSopTempSetting.Content content) {
+        if (content == null) {
+            return null;
+        }
+        return content.clone();
+    }
+
+    private void handleOrderMessage(WxSopLogs sopLogs, QwSopTempSetting.Content content) {
+        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
+
+    private String generateShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                     Long courseId, Long videoId, String qwUserId,
+                                     String companyUserId, String companyId, String externalId, String isOfficial, Long fsUserId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+//            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.parseLong(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(logVo.getCorpId());
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(Long.parseLong(externalId));
+
+        if (StringUtil.strIsNullOrEmpty(isOfficial)) {
+            link.setLinkType(0);
+        } else {
+            if (isOfficial.equals("1")) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    link.setLinkType(0);
+                } else {
+                    link.setLinkType(5);
+                }
+            } else if (isOfficial.equals("0")) {
+                link.setLinkType(0);
+            } else {
+                link.setLinkType(0);
+            }
+        }
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        courseMap.setCompanyId(link.getCompanyId());
+        courseMap.setQwUserId(link.getQwUserId());
+        courseMap.setCompanyUserId(link.getCompanyUserId());
+        courseMap.setVideoId(link.getVideoId());
+        courseMap.setCorpId(link.getCorpId());
+        courseMap.setCourseId(link.getCourseId());
+        courseMap.setQwExternalId(link.getQwExternalId());
+        courseMap.setFsUserId(fsUserId);
+
+        if (StringUtil.strIsNullOrEmpty(isOfficial)) {
+            courseMap.setLinkType(0);
+        } else {
+            if (isOfficial.equals("1")) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    courseMap.setLinkType(0);
+                } else {
+                    courseMap.setLinkType(5);
+                }
+            } else if (isOfficial.equals("0")) {
+                courseMap.setLinkType(0);
+            } else {
+                courseMap.setLinkType(0);
+            }
+        }
+
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = REAL_LINK_PREFIX + courseJson;
+        link.setRealLink(realLinkFull);
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //取销售绑定的二级域名
+        String sortLink = logVo.getDomain() + SHORT_LINK_PREFIX + link.getLink();
+        enqueueCourseLink(link);
+        return sortLink.replaceAll("^[\\s\\u2005]+", "");
+    }
+
+    private QwCreateLinkByAppVO createLinkByApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                                Long courseId, Long videoId, String qwUserId,
+                                                String companyUserId, String companyId, String externalId, String corpId, String qwUserName) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+//            log.error("CourseConfig is not loaded.");
+            return null;
+        }
+        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
+                companyUserId, companyId, externalId, 4);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = APP_LINK_PREFIX + courseJson;
+
+        if (CloudHostUtils.hasCloudHostName("木易华康")) {
+            realLinkFull = REAL_LINK_PREFIX + courseJson;
+        }
+        link.setRealLink(realLinkFull);
+
+        Date updateTime = createUpdateTime(setting, sendTime, config);
+
+        link.setUpdateTime(updateTime);
+
+        String sortLink = appLink + link.getLink() + "&videoId=" + videoId;
+
+        String appMsgLink = appRealLink + link.getLink();
+
+        QwCreateLinkByAppVO byAppVO = new QwCreateLinkByAppVO();
+        byAppVO.setSortLink(sortLink);
+        byAppVO.setAppMsgLink(appMsgLink);
+
+        FsCourseSopAppLink fsCourseSopAppLink = createFsCourseSopAppLink(link.getLink(), sendTime, updateTime, companyId, companyUserId, qwUserId,
+                qwUserName, corpId, courseId, setting.getLinkTitle(), setting.getLinkImageUrl(), videoId,
+                setting.getLinkDescribe(), appMsgLink, externalId);
+
+        enqueueCourseSopAppLink(fsCourseSopAppLink);
+
+        enqueueCourseLink(link);
+
+        return byAppVO;
+    }
+
+
+    public FsCourseSopAppLink createFsCourseSopAppLink(String link, Date sendTime, Date updateTime, String companyId,
+                                                       String companyUserId, String qwUserId, String qwUserName, String corpId,
+                                                       Long courseId, String linkTile, String linkImageUrl, Long videoId,
+                                                       String linkDescribe, String appMsgLink, String externalId) {
+
+        FsCourseSopAppLink sopAppLink = new FsCourseSopAppLink();
+        sopAppLink.setLink(link);
+        sopAppLink.setCreateTime(sendTime);
+        sopAppLink.setUpdateTime(updateTime);
+        sopAppLink.setCompanyId(Long.parseLong(companyId));
+        sopAppLink.setCompanyUserId(Long.parseLong(companyUserId));
+        sopAppLink.setQwUserId(Long.parseLong(qwUserId));
+        sopAppLink.setQwUserName(qwUserName);
+        sopAppLink.setCorpId(corpId);
+        sopAppLink.setCourseId(courseId);
+        sopAppLink.setCourseTitle(linkTile);
+        sopAppLink.setCourseUrl(linkImageUrl);
+        sopAppLink.setVideoId(videoId);
+        sopAppLink.setVideoTitle(linkDescribe);
+        sopAppLink.setAppRealLink(appMsgLink);
+        sopAppLink.setQwExternalId(Long.parseLong(externalId));
+
+
+        return sopAppLink;
+    }
+
+    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Long courseId, Long videoId, String qwUserId,
+                                           String companyUserId, String companyId, String externalId, Integer type) {
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.parseLong(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(corpId);
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(Long.parseLong(externalId));
+        link.setLinkType(type); //小程序
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        return link;
+    }
+
+    private Date createUpdateTime(QwSopTempSetting.Content.Setting setting, Date sendTime, CourseConfig config) {
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+//         使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        return updateTime;
+    }
+
+    private String createLinkByMiniApp(QwSopTempSetting.Content.Setting setting, WxSopUserVo logVo, Date sendTime,
+                                       Long courseId, Long videoId, String qwUserId,
+                                       String companyUserId, String companyId, String externalId, String isOfficial, Long fsUserId, String chatId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+//            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+
+
+//        if (StringUtils.isEmpty(config.getMiniprogramPage())){
+//            log.error("miniprogramPage is not loaded.");
+//            return "";
+//        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.parseLong(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId);
+        link.setCorpId(logVo.getCorpId());
+        link.setCourseId(courseId);
+        if (StringUtils.isEmpty(chatId)) {
+            link.setQwExternalId(Long.parseLong(externalId));
+        }
+        link.setProjectCode(cloudHostProper.getProjectCode());
+        link.setChatId(chatId);
+
+        if (StringUtil.strIsNullOrEmpty(isOfficial)) {
+            link.setLinkType(3);
+        } else {
+            if (isOfficial.equals("1")) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    link.setLinkType(3);
+                } else {
+                    link.setLinkType(5);
+                }
+            } else if (isOfficial.equals("0")) {
+                link.setLinkType(3);
+            } else {
+                link.setLinkType(3);
+            }
+        }
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = miniappRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
+    }
+
+    private void addWatchLogIfNeeded(WxSopLogs sopLogs, Long videoId, Long courseId,
+                                     Date sendTime, String qwUserId, String companyUserId,
+                                     String companyId, String externalId, WxSopUserVo logsVo, Integer watchType) {
+        FsCourseWatchLog watchLog = new FsCourseWatchLog();
+        watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
+        watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
+        watchLog.setSendType(2);
+        watchLog.setQwUserId(Long.parseLong(qwUserId));
+        watchLog.setSopId(String.valueOf(sopLogs.getSopId()));
+        watchLog.setDuration(0L);
+        watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
+        watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
+        watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
+        watchLog.setCreateTime(convertStringToDate(sopLogs.getSendTime(), "yyyy-MM-dd HH:mm:ss"));
+        watchLog.setUpdateTime(new Date());
+        watchLog.setLogType(3);
+        watchLog.setUserId(sopLogs.getFsUserId());
+        watchLog.setWatchType(watchType);
+        watchLog.setCampPeriodTime(convertStringToDate(logsVo.getStartTime(), "yyyy-MM-dd"));
+        enqueueWatchLog(watchLog);
+    }
+
+    /**
+     * 直播看课记录处理
+     *
+     * @param companyId
+     * @param companyUserId
+     * @param externalId
+     * @param liveId
+     * @param appId
+     * @param logSource
+     * @param qwUserId
+     * @param corpId
+     */
+    public void createLiveWatchLogAndEnQueue(String companyId, String companyUserId, String externalId, Long liveId, String appId, Integer logSource, String qwUserId, String corpId) {
+        // 写入对应数据源的记录表
+        LiveWatchLog itemLiveWatchLog = new LiveWatchLog();
+        itemLiveWatchLog.setLiveId(liveId);
+        itemLiveWatchLog.setLogType(3);
+        itemLiveWatchLog.setSopCreateTime(new Date());
+        itemLiveWatchLog.setCompanyId(Long.valueOf(companyId));
+        itemLiveWatchLog.setCompanyUserId(Long.valueOf(companyUserId));
+        itemLiveWatchLog.setSendAppId(appId);
+        itemLiveWatchLog.setLogSource(logSource);
+        itemLiveWatchLog.setQwUserId(qwUserId);
+        itemLiveWatchLog.setExternalContactId(Long.valueOf(externalId));
+        itemLiveWatchLog.setCorpId(corpId);
+        enqueueZmLiveWatchLog(itemLiveWatchLog);
+    }
+
+    private void enqueueZmLiveWatchLog(LiveWatchLog liveWatchLog) {
+        try {
+            boolean offered = zmLiveWatchQueue.offer(liveWatchLog, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("LiveWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(liveWatchLog));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 LiveWatchLog 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 时间字符串转Date时间
+     *
+     * @param dateString
+     * @return
+     */
+    public static Date convertStringToDate(String dateString, String pattern) {
+        if (dateString == null || dateString.isEmpty()) {
+            return null;
+        }
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+        LocalDateTime localDateTime;
+        LocalDate localDate;
+        // 先解析成 LocalDate(只含年月日)
+        if (pattern.equals("yyyy-MM-dd")) {
+            // 先解析成 LocalDate(只含年月日)
+            localDate = LocalDate.parse(dateString, formatter);
+            // 将 LocalDate 转为当天 00:00:00 的 LocalDateTime
+            localDateTime = localDate.atStartOfDay();
+        } else {
+            localDateTime = LocalDateTime.parse(dateString, formatter);
+        }
+        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+    }
+
+
+    /**
+     * 将 QwSopLogs 放入队列
+     */
+    private void enqueueQwSopLogs(WxSopLogs sopLogs) {
+        try {
+            boolean offered = wxSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
+            System.out.println(sopLogs.getSopId() + "插入队列结果: " + offered + "内容: " + JSON.toJSONString(sopLogs));
+            if (!offered) {
+                log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+            //wxSopLogsQueue.clear();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 QwSopLogs 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 将 FsCourseWatchLog 放入队列
+     */
+    private void enqueueWatchLog(FsCourseWatchLog watchLog) {
+        try {
+            boolean offered = watchLogsQueue.offer(watchLog, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("FsCourseWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(watchLog));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 FsCourseWatchLog 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 将 FsCourseWatchLog 放入队列
+     */
+    private void enqueueCourseLink(FsCourseLink courseLink) {
+        try {
+            boolean offered = linkQueue.offer(courseLink, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("FsCourseLink 队列已满,无法添加日志: {}", JSON.toJSONString(courseLink));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 FsCourseLink 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 将 FsCourseSopAppLing 放入队列
+     */
+    private void enqueueCourseSopAppLink(FsCourseSopAppLink sopAppLink) {
+        try {
+            boolean offered = sopAppLinks.offer(sopAppLink, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("FsCourseSopAppLink 队列已满,无法添加日志: {}", JSON.toJSONString(sopAppLink));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 FsCourseLink 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 消费 QwSopLogs 队列并进行批量插入
+     */
+    private void consumeQwSopLogs() {
+        List<WxSopLogs> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !wxSopLogsQueue.isEmpty()) {
+            try {
+                WxSopLogs log = wxSopLogsQueue.poll(1, TimeUnit.SECONDS);
+                if (log != null) {
+                    batch.add(log);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && log == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertQwSopLogs(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("QwSopLogs 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertQwSopLogs(batch);
+        }
+    }
+
+    /**
+     * 消费 FsCourseWatchLog 队列并进行批量插入
+     */
+    private void consumeCourseLink() {
+        List<FsCourseLink> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !linkQueue.isEmpty()) {
+            try {
+                FsCourseLink courseLink = linkQueue.poll(1, TimeUnit.SECONDS);
+                if (courseLink != null) {
+                    batch.add(courseLink);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && courseLink == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertFsCourseLink(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("FsCourseLink 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertFsCourseLink(batch);
+        }
+    }
+
+    /**
+     * 消费 FsCourseSopAppLink 队列并进行批量插入
+     */
+    private void consumeCourseSopAppLink() {
+        List<FsCourseSopAppLink> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !sopAppLinks.isEmpty()) {
+            try {
+                FsCourseSopAppLink courseSopAppLink = sopAppLinks.poll(1, TimeUnit.SECONDS);
+                if (courseSopAppLink != null) {
+                    batch.add(courseSopAppLink);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && courseSopAppLink == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertFsCourseSopAppLink(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("FsCourseSopAppLink 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertFsCourseSopAppLink(batch);
+        }
+    }
+
+    /**
+     * 消费 FsCourseSopAppLink 队列并进行批量插入
+     */
+    private void consumeZmLiveWatchQueue() {
+        List<LiveWatchLog> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !zmLiveWatchQueue.isEmpty()) {
+            try {
+                LiveWatchLog livewatchLog = zmLiveWatchQueue.poll(1, TimeUnit.SECONDS);
+                if (livewatchLog != null) {
+                    batch.add(livewatchLog);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && livewatchLog == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertLiveWatchLog(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("zmLiveWatchQueue 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertLiveWatchLog(batch);
+        }
+    }
+
+    /**
+     * 消费 FsCourseWatchLog 队列并进行批量插入
+     */
+    private void consumeWatchLogs() {
+        List<FsCourseWatchLog> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !watchLogsQueue.isEmpty()) {
+            try {
+                FsCourseWatchLog watchLog = watchLogsQueue.poll(1, TimeUnit.SECONDS);
+                if (watchLog != null) {
+                    batch.add(watchLog);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && watchLog == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertFsCourseWatchLogs(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("FsCourseWatchLog 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertFsCourseWatchLogs(batch);
+        }
+    }
+
+    /**
+     * 批量插入 QwSopLogs
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert) {
+        try {
+//            qwSopLogsService.batchInsertQwSopLogs(logsToInsert);
+            wxSopLogsService.batchInsertQwSopLogs(logsToInsert);
+            log.info("批量插入 QwSopLogs 完成,共插入 {} 条记录。", logsToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 QwSopLogs 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+    /**
+     * 批量插入 FsCourseWatchLog
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertFsCourseWatchLogs(List<FsCourseWatchLog> watchLogsToInsert) {
+        try {
+            fsCourseWatchLogMapper.insertFsCourseWatchLogBatch(watchLogsToInsert);
+            log.info("批量插入 FsCourseWatchLog 完成,共插入 {} 条记录。", watchLogsToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 FsCourseWatchLog 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+
+    /**
+     * 批量插入 FsCourseLink
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertFsCourseLink(List<FsCourseLink> courseLinkToInsert) {
+        try {
+            fsCourseLinkMapper.insertFsCourseLinkBatch(courseLinkToInsert);
+            log.info("批量插入 FsCourseLink 完成,共插入 {} 条记录。", courseLinkToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 FsCourseLink 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+
+    /**
+     * 批量插入 FsCourseSopAppLink
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertFsCourseSopAppLink(List<FsCourseSopAppLink> courseSopAppLinkToInsert) {
+        try {
+            fsCourseSopAppLinkMapper.insertFsCourseSopAppLinkBatch(courseSopAppLinkToInsert);
+            log.info("批量插入 FsCourseSopAppLink 完成,共插入 {} 条记录。", courseSopAppLinkToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 FsCourseSopAppLink 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+    /**
+     * 批量插入 卓美直播看课记录
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertLiveWatchLog(List<LiveWatchLog> liveWatchLogToInsert) {
+        try {
+            //更改为set 避免同一批生成的消息里面有重复数据 插入会报错
+            Set<LiveWatchLog> lastInsertSet = new HashSet<>();
+            //判断是否存在数据 liveId + his_qw_external_contact_id + qwUserId 唯一
+            for (LiveWatchLog liveWatchLog : liveWatchLogToInsert) {
+                //判断是否存在数据 存在的数据直接更新发送时间
+                if (liveWatchLogMapper.updateLiveWatchLogCondition(liveWatchLog) > 0) {
+                    continue;
+                }
+                lastInsertSet.add(liveWatchLog);
+            }
+            if (!lastInsertSet.isEmpty()) {
+                liveWatchLogMapper.insertLiveWatchLogBatch(new ArrayList<>(lastInsertSet));
+            }
+//            log.info("批量插入 LiveWatchLog 完成,共插入 {} 条记录。", liveWatchLogToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 LiveWatchLog 失败: {}", liveWatchLogToInsert, e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+
+    @Override
+    public void updateSopLogsByCancel() {
+        List<QwSopLogs> sopLogs = qwSopLogsMapper.selectQwSopLogsByCancel();
+        log.info("补发过期完课消息总条数:{}", sopLogs.size());
+        processUpdateQwSopLogs(sopLogs);
+    }
+
+
+    // 定义一个方法来批量处理插入逻辑,支持每 500 条数据一次的批量插入
+    private void processUpdateQwSopLogs(List<QwSopLogs> sopLogs) {
+        // 定义批量插入的大小
+        int batchSize = 500;
+
+        // 循环处理外部用户 ID,每次处理批量大小的子集
+        for (int i = 0; i < sopLogs.size(); i += batchSize) {
+
+            int endIndex = Math.min(i + batchSize, sopLogs.size());
+            List<QwSopLogs> batchList = sopLogs.subList(i, endIndex);  // 获取当前批次的子集
+
+            // 直接使用批次数据进行批量更新,不需要额外的 List
+            try {
+                qwSopLogsMapper.batchUpdateQwSopLogsByCancel(batchList);
+                log.info("正在补发条数:{}", batchSize);
+            } catch (Exception e) {
+                // 记录异常日志,方便后续排查问题
+                log.error("批量更新数据时发生异常,处理的批次起始索引为: " + i, e);
+            }
+        }
+    }
+
+    @Autowired
+    private FsCourseFinishTempMapper fsCourseFinishTempMapper;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+
+//    @Override
+//    @Transactional
+//    public void creatMessMessage(QwSopLogs logs) {
+//       // qwSopLogsMapper.insertQwSopLogs(logs);
+//        QwSopTempSetting.Content content = JSON.parseObject(logs.getContentJson(), QwSopTempSetting.Content.class);
+//        handleNormalMessage(logs, content,null);
+//    }
+
+    @Override
+    public void createCourseFinishMsg() {
+        long startTime = System.currentTimeMillis();
+        log.info("创建完课消息 - 定时任务开始 {}", startTime);
+
+        // 线程池配置
+        int threadPoolSize = 4;
+        ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
+
+        // 用于收集所有处理结果的队列
+        BlockingQueue<List<FsCourseWatchLog>> batchQueue = new LinkedBlockingQueue<>();
+
+        try {
+            // 查询当天日期范围
+            LocalDate today = LocalDate.now();
+            Date startDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+            Date endDate = Date.from(today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            // 启动生产者线程 - 流式分批查询数据
+            executorService.submit(() -> {
+                try {
+                    int batchSize = 1000;
+                    long maxId = 0;
+                    boolean hasMore = true;
+
+                    while (hasMore) {
+                        // 查询当前批次数据
+                        List<FsCourseWatchLog> batch = fsCourseWatchLogMapper.selectFsCourseWatchLogFinishBatchByDate(
+                                startDate, endDate, maxId, batchSize);
+
+                        if (!batch.isEmpty()) {
+                            // 将批次放入队列
+                            batchQueue.put(batch);
+                            // 更新maxId为当前批次的最后一个ID
+                            maxId = batch.get(batch.size() - 1).getLogId();
+                            log.debug("已生产批次数据,最后logId: {}, 数量: {}", maxId, batch.size());
+                        }
+
+                        if (batch.size() < batchSize) {
+                            hasMore = false;
+                            batchQueue.put(Collections.emptyList());// 结束标志
+                            log.info("数据生产完成,最后logId: {}", maxId);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("生产数据时出错", e);
+                    try {
+                        batchQueue.put(Collections.emptyList()); // 确保消费者能退出
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                    }
+                }
+            });
+
+            // 消费者线程处理数据
+            List<Future<?>> futures = new ArrayList<>();
+            for (int i = 0; i < threadPoolSize; i++) {
+                futures.add(executorService.submit(() -> {
+                    try {
+                        while (true) {
+                            List<FsCourseWatchLog> batch = batchQueue.take();
+
+                            // 空列表表示处理结束
+                            if (batch.isEmpty()) {
+                                batchQueue.put(Collections.emptyList()); // 传递给其他消费者
+                                break;
+                            }
+                            log.info("开始处理批次数据");
+                            processBatch(batch); // 处理批次数据
+                        }
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        log.error("处理数据时被中断", e);
+                    } catch (Exception e) {
+                        log.error("处理数据时出错", e);
+                    }
+                }));
+            }
+
+            // 等待所有任务完成
+            for (Future<?> future : futures) {
+                try {
+                    future.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    log.error("等待任务完成时出错", e);
+                    Thread.currentThread().interrupt();
+                }
+            }
+
+            log.info("所有批次处理完成,总耗时: {}ms", System.currentTimeMillis() - startTime);
+
+        } finally {
+            executorService.shutdown();
+            try {
+                if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
+                    executorService.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                executorService.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    // 处理单个批次的方法
+    private void processBatch(List<FsCourseWatchLog> batch) {
+        List<FsCourseWatchLog> finishLogsToUpdate = new ArrayList<>();
+        List<QwSopLogs> sopLogsToInsert = new ArrayList<>();
+        log.info("开始执行处理批次方法-数量:{}", batch.size());
+        for (FsCourseWatchLog finishLog : batch) {
+            try {
+
+                try {
+
+                    asyncCourseWatchFinishService.executeCourseWatchFinish(finishLog);
+
+                } catch (Exception e) {
+                    log.error("添加完课打备注失败", e);
+                }
+
+                // 查询外部联系人信息
+                QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(finishLog.getQwExternalContactId());
+                if (externalContact == null) {
+                    log.error("外部联系人不存在: {}", finishLog.getQwExternalContactId());
+                    continue;
+                }
+
+                // 查询完课模板信息
+                FsCourseFinishTemp finishTemp = fsCourseFinishTempMapper.selectFsCourseFinishTempByCompanyId(finishLog.getCompanyUserId(), finishLog.getCompanyId(), finishLog.getVideoId());
+
+                // 设置 finishLog 为已发送状态,并加入批量更新列表
+                finishLog.setSendFinishMsg(1);
+                finishLogsToUpdate.add(finishLog);
+
+                if (finishTemp == null) {
+//                    log.error("完课模板不存在: " + finishLog.getQwUserId() + ", " + finishLog.getVideoId());
+                    continue;
+                }
+
+                // 构建 sopLogs 对象
+                QwSopLogs sopLogs = buildSopLogs(finishLog, externalContact, finishTemp);
+                if (sopLogs == null) {
+                    log.error("生成完课发送记录为空-:{}", finishLog.getQwExternalContactId());
+                    continue;
+                }
+
+                // 如果客户状态有效,则加入批量插入列表
+                if (isValidExternalContact(externalContact)) {
+                    sopLogsToInsert.add(sopLogs);
+                } else {
+                    log.info("完课消息-客户信息有误,不生成完课消息: {}", finishLog.getQwExternalContactId());
+                }
+//                try {
+//                    fsUserCompanyBindService.finish(externalContact.getFsUserId(), externalContact.getQwUserId(), externalContact.getCompanyUserId(), finishLog);
+//                }catch (Exception e){
+//                    log.error("更新重粉看课状态失败",e);
+//                }
+            } catch (Exception e) {
+                log.error("处理完课记录失败: {}", finishLog.getLogId(), e);
+            }
+        }
+
+        // 批量更新和插入
+        if (!finishLogsToUpdate.isEmpty()) {
+            try {
+                fsCourseWatchLogMapper.batchUpdateWatchLogSendMsg(finishLogsToUpdate);
+                log.info("批量更新 finishLog 成功,数量: {}", finishLogsToUpdate.size());
+            } catch (Exception e) {
+                log.error("批量更新 finishLog 失败", e);
+            }
+        }
+
+        if (!sopLogsToInsert.isEmpty()) {
+            try {
+                qwSopLogsService.batchInsertQwSopLogs(sopLogsToInsert);
+                log.info("批量插入 sopLogs 成功,数量: {}", sopLogsToInsert.size());
+            } catch (Exception e) {
+                log.error("批量插入 sopLogs 失败", e);
+            }
+        }
+        log.info("结束处理批次方法-数量:{}", batch.size());
+    }
+
+    /**
+     * 构建 QwSopLogs 对象
+     */
+    private QwSopLogs buildSopLogs(FsCourseWatchLog finishLog, QwExternalContact externalContact, FsCourseFinishTemp finishTemp) {
+        QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime currentTime = LocalDateTime.now();
+        LocalDateTime newTime = currentTime.plusMinutes(3);
+        String newTimeString = newTime.format(formatter);
+
+        QwSopLogs sopLogs = new QwSopLogs();
+        sopLogs.setSendTime(newTimeString);
+        sopLogs.setQwUserid(externalContact.getUserId());
+        sopLogs.setCorpId(externalContact.getCorpId());
+        sopLogs.setLogType(2);
+        sopLogs.setSendType(3);
+        sopLogs.setSendStatus(3L);
+        sopLogs.setReceivingStatus(0L);
+        sopLogs.setSort(40000000);
+        sopLogs.setCompanyId(finishLog.getCompanyId());
+        sopLogs.setSopId(finishLog.getSopId());
+        sopLogs.setExternalUserId(externalContact.getExternalUserId());
+        sopLogs.setExternalUserName(externalContact.getName());
+        sopLogs.setFsUserId(finishLog.getUserId() != null ? finishLog.getUserId() : null);
+        sopLogs.setExternalId(finishLog.getQwExternalContactId());
+        sopLogs.setUserLogsId("-");
+
+        sopLogs.setQwUserKey(finishLog.getQwUserId() != null ? finishLog.getQwUserId() : null);
+
+        // 解析模板设置
+        List<QwSopCourseFinishTempSetting.Setting> settings = parseSettings(finishTemp.getSetting());
+        if (settings == null) {
+            return null;
+        }
+        //完课后若是小程序发送另外一堂课
+        saveWacthLogOfCourseLink(settings, sopLogs, newTimeString, finishLog, finishTemp);
+        // 处理音频内容
+        for (QwSopCourseFinishTempSetting.Setting st : settings) {
+            if (st.getContentType().equals("7")) {
+                Long companyUserId = finishLog.getCompanyUserId();
+                QwSopTempVoice qwSopTempVoice = sopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(companyUserId, st.getValue());
+                if (qwSopTempVoice != null && qwSopTempVoice.getVoiceUrl() != null && qwSopTempVoice.getRecordType() == 1) {
+                    st.setVoiceUrl(qwSopTempVoice.getVoiceUrl());
+                    st.setVoiceDuration(String.valueOf(qwSopTempVoice.getDuration()));
+                } else if (qwSopTempVoice == null) {
+                    if (companyUserId != null && st.getValue() != null) {
+                        qwSopTempVoice = new QwSopTempVoice();
+                        qwSopTempVoice.setCompanyUserId(companyUserId);
+                        qwSopTempVoice.setVoiceTxt(st.getValue());
+                        qwSopTempVoice.setRecordType(0);
+                        sopTempVoiceService.insertQwSopTempVoice(qwSopTempVoice);
+                    }
+                }
+            }
+        }
+//        for (QwSopCourseFinishTempSetting.Setting st : settings) {
+//            if (st.getContentType().equals("7")) {
+//                try {
+//                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), finishLog.getCompanyUserId(), false);
+//                    st.setVoiceUrl(audioVO.getUrl());
+//                    st.setVoiceDuration(audioVO.getDuration() + "");
+//                } catch (Exception e) {
+//                    log.error("音频生成失败: " + finishLog.getCompanyUserId(), e);
+//                }
+//            }
+//        }
+
+        setting.setSetting(settings);
+        sopLogs.setContentJson(JSON.toJSONString(setting));
+        return sopLogs;
+    }
+
+    /**
+     * 判定小程序的话新增创建看课记录,以及fsCourseLink
+     *
+     * @param settings
+     */
+    public void saveWacthLogOfCourseLink(List<QwSopCourseFinishTempSetting.Setting> settings, QwSopLogs sopLogs, String newTimeString, FsCourseWatchLog finishLog, FsCourseFinishTemp finishTemp) {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+        Date dataTime = new Date();
+        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
+        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(sopLogs.getCorpId());
+        QwUser qwUser = qwExternalContactService.getQwUserByRedis(sopLogs.getCorpId(), sopLogs.getQwUserid());
+        if (qwUser == null) {
+            return;
+        }
+        for (QwSopCourseFinishTempSetting.Setting st : settings) {
+            switch (st.getContentType()) {
+                //小程序单独
+                case "4":
+                    addWatchLogIfNeeded(sopLogs.getSopId(), st.getVideoId().intValue(), st.getCourseId().intValue(), sopLogs.getFsUserId(), String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),
+                            sopLogs.getExternalId(), newTimeString.substring(0, 10), dataTime);
+
+                    String linkByMiniApp = createLinkByMiniApp(st, sopLogs.getCorpId(), dataTime, finishTemp.getCourseId().intValue(), Integer.valueOf(st.getVideoId().toString()),
+                            String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), sopLogs.getExternalId(), config);
+
+
+                    String miniAppId = null;
+                    if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                        Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(qwUser.getCompanyId()));
+                        if (integerListMap != null) {
+                            int listIndex = 0;
+                            List<CompanyMiniapp> miniapps = integerListMap.get(listIndex);
+
+                            if (miniapps != null && !miniapps.isEmpty()) {
+                                CompanyMiniapp companyMiniapp = miniapps.get(0);
+                                if (companyMiniapp != null && !StringUtil.strIsNullOrEmpty(companyMiniapp.getAppId())) {
+                                    miniAppId = companyMiniapp.getAppId();
+                                }
+                            }
+                        }
+                    }
+
+                    if (StringUtil.strIsNullOrEmpty(miniAppId) && !StringUtil.strIsNullOrEmpty(qwCompany.getMiniAppId())) {
+                        miniAppId = qwCompany.getMiniAppId();
+                    }
+
+                    if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
+                        st.setMiniprogramAppid(miniAppId);
+                    } else {
+                        log.error("企业未配置小程序-" + sopLogs.getCorpId());
+                    }
+
+                    String miniprogramTitle = st.getMiniprogramTitle();
+                    int maxLength = 17;
+                    st.setMiniprogramTitle(miniprogramTitle.length() > maxLength ? miniprogramTitle.substring(0, maxLength) + "..." : miniprogramTitle);
+                    st.setMiniprogramPage(linkByMiniApp);
+                    break;
+                default:
+                    break;
+
+            }
+        }
+    }
+
+    private Date processDate(String sendTimeParam) {
+        // 1. 获取当前日期(年月日)
+        LocalDate currentDate = LocalDate.now();
+
+        // 2. 解析传入的时分(支持 "HH:mm" 或 "H:mm")
+        LocalTime sendTime = LocalTime.parse(sendTimeParam);
+
+        // 3. 合并为 LocalDateTime
+        LocalDateTime dateTime = LocalDateTime.of(currentDate, sendTime);
+
+        // 4. 转换为 Date(需通过 Instant 和系统默认时区)
+        Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        return date;
+    }
+
+    /**
+     * 新增courseLink
+     *
+     * @param setting
+     * @param corpId
+     * @param sendTime
+     * @param courseId
+     * @param videoId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @param externalId
+     * @param config
+     * @return
+     */
+    private String createLinkByMiniApp(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
+                                       Integer courseId, Integer videoId, String qwUserId,
+                                       String companyUserId, String companyId, Long externalId, CourseConfig config) {
+
+        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
+                companyUserId, companyId, externalId, 3, null);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = miniappRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        Date updateTime = createUpdateTime(setting, sendTime, config);
+
+        link.setUpdateTime(updateTime);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink();
+    }
+
+    /**
+     * 创建courselink
+     *
+     * @param corpId
+     * @param sendTime
+     * @param courseId
+     * @param videoId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @param externalId
+     * @param type
+     * @param chatId
+     * @return
+     */
+    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, String qwUserId,
+                                           String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(corpId);
+        link.setCourseId(courseId.longValue());
+        link.setChatId(chatId);
+        link.setQwExternalId(externalId);
+        link.setLinkType(type); //小程序
+        link.setUNo(UUID.randomUUID().toString());
+        link.setProjectCode(cloudHostProper.getProjectCode());
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        return link;
+    }
+
+
+    /**
+     * 计算过期时间
+     *
+     * @param setting
+     * @param sendTime
+     * @param config
+     * @return
+     */
+    private Date createUpdateTime(QwSopCourseFinishTempSetting.Setting setting, Date sendTime, CourseConfig config) {
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+//         使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        return updateTime;
+    }
+
+    /**
+     * 增加看课记录
+     *
+     * @param sopId
+     * @param videoId
+     * @param courseId
+     * @param fsUserId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @param externalId
+     * @param startTime
+     * @param createTime
+     * @return
+     */
+    private Long addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
+                                     Long fsUserId, String qwUserId, String companyUserId,
+                                     String companyId, Long externalId, String startTime, Date createTime) {
+
+        try {
+            FsCourseWatchLog watchLog = new FsCourseWatchLog();
+            watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
+            watchLog.setQwExternalContactId(externalId);
+            watchLog.setSendType(2);
+            watchLog.setQwUserId(Long.valueOf(qwUserId));
+            watchLog.setSopId(sopId);
+            watchLog.setDuration(0L);
+            watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
+            watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
+            watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
+            watchLog.setCreateTime(createTime);
+            watchLog.setUpdateTime(createTime);
+            watchLog.setLogType(3);
+            watchLog.setUserId(fsUserId);
+            watchLog.setCampPeriodTime(convertStringToDate(startTime, "yyyy-MM-dd"));
+
+            //存看课记录
+            int i = fsCourseWatchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
+            return watchLog.getLogId();
+        } catch (Exception e) {
+            log.error("插入观看记录失败:" + e.getMessage());
+            return null;
+        }
+    }
+
+
+    /**
+     * 解析模板设置
+     */
+    private List<QwSopCourseFinishTempSetting.Setting> parseSettings(String jsonData) {
+        try {
+            if (jsonData.startsWith("[") && jsonData.endsWith("]")) {
+                return JSONArray.parseArray(jsonData, QwSopCourseFinishTempSetting.Setting.class);
+            } else {
+                String fixedJson = JSON.parseObject(jsonData, String.class);
+                return JSONArray.parseArray(fixedJson, QwSopCourseFinishTempSetting.Setting.class);
+            }
+        } catch (Exception e) {
+            log.error("解析模板设置失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 检查外部联系人状态是否有效
+     */
+    private boolean isValidExternalContact(QwExternalContact externalContact) {
+        return externalContact.getStatus() == 0 || externalContact.getStatus() == 2 || externalContact.getStatus() == 3;
+    }
+
+    /**
+     * 设置SOP日志状态的辅助方法
+     */
+    private void setSopLogsStatus(WxSopLogs sopLogs, Integer sendStatus, Integer receivingStatus, String remark) {
+        if (sopLogs != null) {
+            sopLogs.setSendStatus(sendStatus);
+            sopLogs.setReceivingStatus(receivingStatus);
+            sopLogs.setRemark(remark);
+        }
+    }
+
+    /**
+     * 查询并缓存用户记录
+     *
+     * @param userId
+     * @param cacheKey
+     * @param query
+     * @return
+     */
+    // 获取缓存或查询记录
+    private List<LuckyBagCollectRecord> getCachedOrQueryRecords(Long userId, String cacheKey,
+                                                                LuckyBagCollectRecord query) {
+        Object cachedData = redisCache.getCacheObject(cacheKey);
+        if (cachedData != null && cachedData instanceof List) {
+            log.debug("福袋记录缓存命中,userId: {}", userId);
+            return (List<LuckyBagCollectRecord>) cachedData;
+        }
+
+        // 缓存未命中,查询数据库
+        log.debug("福袋记录缓存未命中,查询数据库,userId: {}", userId);
+        List<LuckyBagCollectRecord> records = luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(query);
+        cacheUserRecords(userId, cacheKey, records);
+        return records != null ? records : Collections.emptyList();
+    }
+
+    /**
+     * 缓存用户福袋记录
+     *
+     * @param userId
+     * @param cacheKey
+     * @param records
+     */
+    private void cacheUserRecords(Long userId, String cacheKey, List<LuckyBagCollectRecord> records) {
+        if (records != null && !records.isEmpty()) {
+            try {
+                // 计算到明天凌晨的剩余时间(秒)
+                LocalDateTime now = LocalDateTime.now();
+                LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
+                long secondsUntilTomorrow = Duration.between(now, tomorrowStart).getSeconds();
+
+                // 设置缓存,过期时间到明天凌晨
+                int ttlSeconds = (int) Math.max(60, secondsUntilTomorrow); // 至少缓存1分钟
+                redisCache.setCacheObject(cacheKey, records, ttlSeconds, TimeUnit.SECONDS);
+
+                log.debug("缓存用户福袋记录,userId: {},记录数: {},过期时间: {}秒",
+                        userId, records.size(), ttlSeconds);
+            } catch (Exception e) {
+                log.error("缓存用户福袋记录失败,userId: {}", userId, e);
+                // 缓存失败不影响主流程
+            }
+        }
+    }
+
+    /**
+     * 缓存用户计数
+     * @param userId
+     * @param cacheKey
+     * @param count
+     */
+    private void cacheUserCount(Long userId, String cacheKey, int count) {
+        try {
+            // 计算到明天凌晨的剩余时间
+            LocalDateTime now = LocalDateTime.now();
+            LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
+            long secondsUntilTomorrow = Duration.between(now, tomorrowStart).getSeconds();
+
+            int ttlSeconds = (int) Math.max(60, secondsUntilTomorrow);
+            redisCache.setCacheObject(cacheKey, count, ttlSeconds, TimeUnit.SECONDS);
+
+            log.debug("缓存用户福袋计数,userId: {},次数: {},过期时间: {}秒", userId, count, ttlSeconds);
+        } catch (Exception e) {
+            log.error("缓存用户计数失败,userId: {}", userId, e);
+        }
+    }
+}

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

@@ -79,4 +79,7 @@ public interface QwCompanyMapper
 
     @Select("select DISTINCT corp_id from qw_company ")
     List<String> selectQwCompanyListFormCorpId();
+
+    @Select("SELECT * FROM qw_company WHERE FIND_IN_SET(#{companyId}, company_ids) > 0")
+    QwCompany selectQwCompanyByCompanyId(Long companyId);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/qw/service/IQwCompanyService.java

@@ -24,6 +24,8 @@ public interface IQwCompanyService
     public QwCompany selectQwCompanyByCorpId(String corpId);
 
     public QwCompany getQwCompanyByRedis(String corpId);
+
+    QwCompany getQwCompanyByCompanyId(Long companyId);
     /**
      * 查询企微主体列表
      *

+ 6 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwCompanyServiceImpl.java

@@ -60,6 +60,12 @@ public class QwCompanyServiceImpl implements IQwCompanyService
         return qwCompany;
     }
 
+    @Override
+    public QwCompany getQwCompanyByCompanyId(Long companyId) {
+        QwCompany list = qwCompanyMapper.selectQwCompanyByCompanyId(companyId);
+        return list;
+    }
+
     /**
      * 查询企微主体列表
      *

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

@@ -17,6 +17,8 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.domain.FsCourseSop;
@@ -221,6 +223,8 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     private CloudHostProper cloudHostProper;
 
     Logger logger = LoggerFactory.getLogger(getClass());
+    @Autowired
+    private CompanyWxAccountMapper companyWxAccountMapper;
 
     @Override
     public void addQwCourseJob() {
@@ -2179,6 +2183,20 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         return sendWelcomeMsgParam;
     }
 
+    public CrmCustomer getWxUserByRedis(String customerId) {
+        String key = (String) redisCache.getCacheObject("wxUserRd:" + customerId);
+        if (!StringUtil.strIsNullOrEmpty(key)) {
+            return JSON.parseObject(key, CrmCustomer.class);
+        }
+        CrmCustomer crmCustomer = crmCustomerMapper.selectCrmCustomerById(Long.valueOf(customerId));
+        if (crmCustomer == null) {
+            return null;
+        }
+
+        redisCache.setCacheObject("wxUserRd:" + crmCustomer.getCustomerId(), JSON.toJSONString(crmCustomer), 1, TimeUnit.HOURS);
+        return crmCustomer;
+    }
+
     public QwUser getQwUserByRedis(String corpId, String userID) {
         String key = (String) redisCache.getCacheObject("qwUserRd:" + corpId + ":" + userID);
         if (!StringUtil.strIsNullOrEmpty(key)) {

+ 3 - 0
fs-service/src/main/java/com/fs/sop/mapper/QwSopMapper.java

@@ -371,6 +371,9 @@ public interface QwSopMapper extends BaseMapper<QwSop> {
     @DataSource(DataSourceType.SOP)
     QwSopRuleTimeVO selectQwSopByClickHouseId(@Param("id") String id);
 
+    @DataSource(DataSourceType.SOP)
+    WxSopRuleTimeVO selectWxSopByClickHouseId(@Param("id") String id);
+
     @DataSource(DataSourceType.SOP)
     void updateMinSendStatus(@Param("id") String id);
 

+ 4 - 0
fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsInfoMapper.java

@@ -10,6 +10,7 @@ import com.fs.sop.params.DeleteQwSopParam;
 import com.fs.sop.params.SopUserLogsInfoDelParam;
 import com.fs.sop.vo.ExtCourseSopWatchLogVO;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
+import com.fs.wx.sop.domain.WxSopUserInfo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -118,6 +119,9 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> selectSopUserLogsInfoList(SopUserLogsInfo info);
 
+    @DataSource(DataSourceType.SOP)
+    List<WxSopUserInfo> selectWxSopUserLogsInfoList(WxSopUserInfo info);
+
     @DataSource(DataSourceType.SOP)
     @Select("<script> SELECT li.*,ul.start_time FROM sop_user_logs_info li left join  sop_user_logs ul on li.user_logs_id=ul.id  " +
             "        <where>\n" +

+ 4 - 0
fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java

@@ -10,6 +10,7 @@ import com.fs.sop.params.*;
 import com.fs.sop.vo.ReplaceUserDto;
 import com.fs.sop.vo.SopUserLogsInfoVo;
 import com.fs.sop.vo.SopUserLogsVo;
+import com.fs.sop.vo.WxSopUserVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
@@ -56,6 +57,9 @@ public interface SopUserLogsMapper {
     @DataSource(DataSourceType.SOP)
     public List<SopUserLogsVo> selectSopUserLogsListByTime(@Param("sopIds") List<String> sopidList);
 
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopUserVo> selectWxSopUserLogsListByTime(@Param("sopIds") List<String> sopidList);
+
     @DataSource(DataSourceType.SOP)
     public List<SopUserLogs> meetsTheRatingByUserInfo();
 

+ 51 - 0
fs-service/src/main/java/com/fs/sop/vo/WxSopUserVo.java

@@ -0,0 +1,51 @@
+package com.fs.sop.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+@Data
+public class WxSopUserVo {
+    private String id;
+    private String sopId;
+    private String sopTempId;
+    private String actualQwUserId;
+    private Long actualQwId;
+    private String userId;
+    private String accountId;
+    private String customerId;
+    private Integer minSend;
+    private Long externalId;
+    private String externalUserName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String startTime;
+
+    private LocalDate startDate;
+
+    /**
+     * 状态
+     */
+    private Integer status;
+
+    /**
+     * 销售二级域名
+     */
+    private String domain;
+
+    // 是否固定营期
+    private Integer isFixed;
+
+    /**
+     * 是否按照营期 发送官方群发 1 按照【营期+插件补发】的形式发  2 按照【营期+官方单链】的形式发
+     */
+    private Integer isSampSend;
+
+    private String chatId;
+
+    private String corpId;
+    private Integer type;
+
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java

@@ -36,6 +36,12 @@ public class WxSopLogs extends BaseEntityTow {
     @Excel(name = "发送类型", readConverterExp = "字=典-wx_send_type")
     private Integer sendType;
 
+    private String sendTime;
+
+    private String contentJson;
+
+    private Integer receivingStatus;
+
     /** 生成类型(0自动1手动) */
     @Excel(name = "生成类型(0自动1手动)")
     private Integer generateType;

+ 5 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -2,6 +2,8 @@ package com.fs.wx.sop.mapper;
 
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.wx.sop.domain.WxSopLogs;
 import com.fs.wx.sop.params.WxSopLogsParam;
 import com.fs.wx.sop.vo.WxSopLogsListVO;
@@ -68,4 +70,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @return 结果
      */
     int deleteWxSopLogsByIds(Long[] ids);
+
+    @DataSource(DataSourceType.SOP)
+    void batchInsertWxSopLogs(List<WxSopLogs> logsToInsert);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java

@@ -2,6 +2,7 @@ package com.fs.wx.sop.service;
 
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.sop.domain.QwSopLogs;
 import com.fs.wx.sop.domain.WxSopLogs;
 import com.fs.wx.sop.params.WxSopLogsParam;
 import com.fs.wx.sop.vo.WxSopLogsListVO;
@@ -68,4 +69,6 @@ public interface IWxSopLogsService extends IService<WxSopLogs>{
      * @return 结果
      */
     int deleteWxSopLogsById(Long id);
+
+    void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -7,6 +7,7 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.sop.domain.QwSopLogs;
 import com.fs.wx.sop.params.WxSopLogsParam;
 import com.fs.wx.sop.vo.WxSopLogsListVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -23,6 +24,8 @@ import com.fs.wx.sop.service.IWxSopLogsService;
  */
 @Service
 public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs> implements IWxSopLogsService {
+    @Autowired
+    private WxSopLogsMapper wxSopLogsMapper;
 
     /**
      * 查询个微发送记录
@@ -116,4 +119,9 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
     {
         return baseMapper.selectWxSopLogsListBySopId(param);
     }
+
+    public void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert) {
+        if(logsToInsert == null || logsToInsert.isEmpty()) return;
+        wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
+    }
 }

+ 23 - 0
fs-service/src/main/resources/mapper/sop/QwSopMapper.xml

@@ -217,6 +217,29 @@
             ]]>
     </select>
 
+    <select id="selectWxSopByClickHouseId" parameterType="String" resultType="com.fs.qw.vo.WxSopRuleTimeVO">
+        <![CDATA[
+        SELECT
+            ws.*,
+            qst.name AS temp_name,
+            qst.setting AS temp_setting,
+            qst.status AS temp_status,
+            qst.gap AS temp_gap,
+            qst.sort AS temp_sort,
+            qst.create_time AS temp_create_time,
+            qst.create_by AS temp_create_by,
+            qst.corp_id
+        FROM
+            wx_sop ws
+                LEFT JOIN
+            qw_sop_temp qst
+            ON
+                ws.temp_id = qst.id
+        WHERE
+            ws.id = #{id}
+        ]]>
+    </select>
+
     <select id="selectQwSopList" parameterType="QwSop" resultMap="QwSopResult">
         <include refid="selectQwSopVo"/>
         <where>

+ 30 - 0
fs-service/src/main/resources/mapper/sop/SopUserLogsInfoMapper.xml

@@ -23,11 +23,28 @@
         <result property="grade" column="grade"/>
     </resultMap>
 
+    <resultMap id="WxSopUserInfoResult" type="com.fs.wx.sop.domain.WxSopUserInfo">
+        <id property="id" column="id"/>
+        <result property="sopId" column="sop_id"/>
+        <result property="sopUserId" column="sop_user_id"/>
+        <result property="customerId" column="customer_id"  jdbcType="VARCHAR"/>
+        <result property="fsUserId" column="fs_user_id"/>
+        <result property="createTime" column="create_time" jdbcType="VARCHAR" />
+        <result property="updateTime" column="update_time" jdbcType="VARCHAR" />
+        <result property="isDaysNotStudy" column="is_days_not_study"/>
+        <result property="grade" column="grade"/>
+    </resultMap>
+
     <sql id="selectSopUserLogsInfoVo">
         select *
         from sop_user_logs_info
     </sql>
 
+    <sql id="selectWxSopUserLogsInfoVo">
+        select *
+        from wx_sop_user_info
+    </sql>
+
     <select id="selectSopUserLogsInfoById" parameterType="String" resultMap="SopUserLogsInfoResult">
         <include refid="selectSopUserLogsInfoVo"/>
         where id = #{id}
@@ -212,6 +229,19 @@
         order by crt_Time desc
     </select>
 
+    <select id="selectWxSopUserLogsInfoList" parameterType="com.fs.wx.sop.domain.WxSopUserInfo" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserLogsInfoVo"/>
+        <where>
+            <if test="sopId != null">and sop_id = #{sopId}</if>
+            <if test="sopUserId != null">and sop_user_id = #{sopUserId}</if>
+            <if test="customerId != null">and customer_id = #{customerId}</if>
+            <if test="fsUserId != null">and fs_user_id = #{fsUserId}</if>
+            <if test="createTime != null">and create_time = #{createTime}</if>
+            <if test="updateTime != null">and updateTime = #{updateTime}</if>
+        </where>
+        order by create_time desc
+    </select>
+
     <select id="selectSopUserLogsInfo" parameterType="com.fs.sop.domain.SopUserLogsInfo" resultMap="SopUserLogsInfoResult">
         <include refid="selectSopUserLogsInfoVo"/>
         <where>

+ 19 - 0
fs-service/src/main/resources/mapper/sop/SopUserLogsMapper.xml

@@ -217,6 +217,25 @@
         </if>
     </select>
 
+    <select id="selectWxSopUserLogsListByTime" resultType="com.fs.sop.vo.WxSopUserVo">
+        select a.*,
+        b.is_fixed,
+        b.temp_id,
+        d.customer_id
+        from wx_sop_user a
+        inner join wx_sop_user_info d on a.id = d.sop_user_id
+        inner join wx_sop b on a.sop_id = b.id
+        inner join qw_sop_temp c on b.temp_id = c.id
+        where a.start_time &lt;= Now()
+        and a.status = 0
+        and b.status in (2,3)
+        and c.status = 1
+        <if test="sopIds != null and !sopIds.isEmpty()">
+            and a.sop_id in
+            <foreach collection="sopIds" open="(" close=")" index="index" item="item" separator=",">#{item}</foreach>
+        </if>
+    </select>
+
     <select id="meetsTheRatingByUserInfo"   resultMap="SopUserLogsResult">
         SELECT
             ul.id,

+ 52 - 0
fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml

@@ -105,6 +105,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="remark != null">#{remark},</if>
          </trim>
     </insert>
+    <insert id="batchInsertWxSopLogs" parameterType="java.util.List">
+        INSERT INTO wx_sop_logs
+        (
+        type,
+        sop_id,
+        sop_user_id,
+        send_type,
+        generate_type,
+        account_id,
+        wx_contact_id,
+        wx_contact_name,
+        wx_room_id,
+        wx_room_name,
+        fs_user_id,
+        send_status,
+        send_remark,
+        send_sort,
+        expiration_time,
+        create_time,
+        create_by,
+        update_time,
+        update_by,
+        remark,
+        content_json
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.type},
+            #{log.sopId},
+            #{log.sopUserId},
+            #{log.sendType},
+            #{log.generateType},
+            #{log.accountId},
+            #{log.wxContactId},
+            #{log.wxContactName},
+            #{log.wxRoomId},
+            #{log.wxRoomName},
+            #{log.fsUserId},
+            #{log.sendStatus},
+            #{log.sendRemark},
+            #{log.sendSort},
+            #{log.expirationTime},
+            #{log.createTime},
+            #{log.createBy},
+            #{log.updateTime},
+            #{log.updateBy},
+            #{log.remark},
+            #{log.contentJson}
+            )
+        </foreach>
+    </insert>
 
     <update id="updateWxSopLogs" parameterType="WxSopLogs">
         update wx_sop_logs