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

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	fs-user-app/src/main/resources/application-druid.yml
zx 2 місяців тому
батько
коміт
0497219861

+ 48 - 0
fs-company/src/main/resources/application-druid.yml

@@ -77,3 +77,51 @@ spring:
                     wall:
                     wall:
                         config:
                         config:
                             multi-statement-allow: true
                             multi-statement-allow: true
+        sop:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://139.186.77.83:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true

+ 18 - 7
fs-qw-task/src/main/java/com/fs/app/task/qwTask.java

@@ -19,7 +19,9 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.List;
 
 
 @Component
 @Component
@@ -98,18 +100,27 @@ public class qwTask {
         sopLogsTaskChatService.createAiChatSopLogs(today);
         sopLogsTaskChatService.createAiChatSopLogs(today);
     }
     }
 
 
+
+
+
+
     /**
     /**
-    * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息
-    */
-    @Scheduled(cron = "0 0/15 * * * ?")
+     * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息
+     */
+    @Scheduled(cron = "0 15 0 * * ?")
     public void SendQwApiSopLogTimer(){
     public void SendQwApiSopLogTimer(){
-        qwSopLogsService.checkQwSopLogs();
+        log.info("zyp \n【企微官方接口群发开始】");
+//        qwSopLogsService.checkQwSopLogs();
+        LocalDate localDate = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0).toLocalDate();
+        String date = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+        qwSopLogsService.createCorpMassSending(date);
     }
     }
 
 
     /**
     /**
-    * 定时获取 通过调用 企业微信接口 发送的 SOP 客户群发消息 的反馈结果
-    */
-    @Scheduled(cron = "0 0/30 * * * ?")
+     * 定时获取 通过调用 企业微信接口 发送的 SOP 客户群发消息 的反馈结果
+     */
+    @Scheduled(cron = "0 0 7 * * ?")
     public void GetQwApiSopLogResultTimer(){
     public void GetQwApiSopLogResultTimer(){
         qwSopLogsService.qwSopLogsResult();
         qwSopLogsService.qwSopLogsResult();
     }
     }

+ 425 - 93
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -12,28 +12,31 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.*;
 import com.fs.course.domain.*;
-import com.fs.course.mapper.FsCourseDomainNameMapper;
-import com.fs.course.mapper.FsCourseFinishTempMapper;
-import com.fs.course.mapper.FsCourseLinkMapper;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.*;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopRuleTimeVO;
 import com.fs.qw.vo.QwSopRuleTimeVO;
 import com.fs.qw.vo.QwSopTempSetting;
 import com.fs.qw.vo.QwSopTempSetting;
 import com.fs.sop.domain.*;
 import com.fs.sop.domain.*;
+import com.fs.sop.dto.QwCreateLinkByAppDTO;
 import com.fs.sop.mapper.*;
 import com.fs.sop.mapper.*;
+import com.fs.sop.params.SopUserLogsInfoParam;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempRulesService;
 import com.fs.sop.service.IQwSopTempRulesService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.retry.annotation.Backoff;
 import org.springframework.retry.annotation.Backoff;
 import org.springframework.retry.annotation.Retryable;
 import org.springframework.retry.annotation.Retryable;
@@ -65,6 +68,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private static final String QWSOP_KEY_PREFIX = "qwsop:";
     private static final String QWSOP_KEY_PREFIX = "qwsop:";
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     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 DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    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=";
 
 
     // Cached configurations and domain names
     // Cached configurations and domain names
     private CourseConfig cachedCourseConfig;
     private CourseConfig cachedCourseConfig;
@@ -83,8 +89,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private QwSopTagMapper qwSopTagMapper ;
     private QwSopTagMapper qwSopTagMapper ;
     @Autowired
     @Autowired
     private QwSopMapper sopMapper;
     private QwSopMapper sopMapper;
+
+    @Autowired
+    private IQwExternalContactService iQwExternalContactService;
+
     @Autowired
     @Autowired
-    private IQwExternalContactService qwExternalContactService;
+    private QwExternalContactServiceImpl qwExternalContactService;
 
 
     @Autowired
     @Autowired
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
@@ -97,6 +107,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
     @Autowired
     @Autowired
     private FsCourseLinkMapper fsCourseLinkMapper;
     private FsCourseLinkMapper fsCourseLinkMapper;
+
+    @Autowired
+    private FsCourseSopAppLinkMapper fsCourseSopAppLinkMapper;
+
     @Autowired
     @Autowired
     private ISysConfigService configService;
     private ISysConfigService configService;
 
 
@@ -118,11 +132,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
 
 
     // Executors for consumer threads
     // Executors for consumer threads
     private ExecutorService qwSopLogsExecutor;
     private ExecutorService qwSopLogsExecutor;
     private ExecutorService watchLogsExecutor;
     private ExecutorService watchLogsExecutor;
     private ExecutorService courseLinkExecutor;
     private ExecutorService courseLinkExecutor;
+    private ExecutorService courseSopAppLinkExecutor;
 
 
     // Shutdown flags
     // Shutdown flags
     private volatile boolean running = true;
     private volatile boolean running = true;
@@ -181,9 +197,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return t;
             return t;
         });
         });
 
 
+        courseSopAppLinkExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "courseSopAppLinkConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+
+
         qwSopLogsExecutor.submit(this::consumeQwSopLogs);
         qwSopLogsExecutor.submit(this::consumeQwSopLogs);
         watchLogsExecutor.submit(this::consumeWatchLogs);
         watchLogsExecutor.submit(this::consumeWatchLogs);
         courseLinkExecutor.submit(this::consumeCourseLink);
         courseLinkExecutor.submit(this::consumeCourseLink);
+        courseSopAppLinkExecutor.submit(this::consumeCourseSopAppLink);
     }
     }
 
 
     // Scheduled tasks to refresh configurations and domain names periodically
     // Scheduled tasks to refresh configurations and domain names periodically
@@ -332,7 +356,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
         QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
         QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
         if (qwSopTemp == null) {
         if (qwSopTemp == null) {
-            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
+//            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
             log.info("SOP ID {} 模板不存在,相关日志已清除。", sopId);
             log.info("SOP ID {} 模板不存在,相关日志已清除。", sopId);
             return;
             return;
         }
         }
@@ -340,10 +364,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         ruleTimeVO.setTempGap(qwSopTemp.getGap());
         ruleTimeVO.setTempGap(qwSopTemp.getGap());
 
 
         if (ruleTimeVO.getStatus() == 0 || "0".equals(ruleTimeVO.getTempStatus())) {
         if (ruleTimeVO.getStatus() == 0 || "0".equals(ruleTimeVO.getTempStatus())) {
-            SopUserLogs sopUserLogs = new SopUserLogs();
-            sopUserLogs.setSopId(sopId);
-            sopUserLogs.setStatus(2);
-            sopUserLogsMapper.updateSopUserLogsByStatus(sopUserLogs);
+//            SopUserLogs sopUserLogs = new SopUserLogs();
+//            sopUserLogs.setSopId(sopId);
+//            sopUserLogs.setStatus(2);
+//            sopUserLogsMapper.updateSopUserLogsByStatus(sopUserLogs);
             log.info("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
             log.info("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
             return;
             return;
         }
         }
@@ -417,9 +441,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 return;
                 return;
             }
             }
 
 
+            String[] userKey = logVo.getUserId().split("\\|");
+            if (userKey.length < 3) {
+                log.error("用户 ID {} 格式不正确,跳过处理。", logVo.getUserId());
+                return;
+            }
+            String qwUserId = userKey[0].trim();
+            String companyUserId = userKey[1].trim();
+            String companyId = userKey[2].trim();
+
 
 
-            //寻找时间
-//            LocalDateTime currentTime = LocalDateTime.of(2024, 12, 25,23 , 40);
+            //获取企业微信员工的称呼//从redis里或者从库里取
+            QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedis(logVo.getCorpId(),logVo.getQwUserId());
+            if (qwUserByRedis==null){
+                log.error("无企微员工信息 {} 跳过处理。:{}", logVo.getUserId(),logVo.getCorpId());
+                return;
+            }
 
 
             // 先算好 60分钟后 ~ 再60分钟后的时间段
             // 先算好 60分钟后 ~ 再60分钟后的时间段
             LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
             LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
@@ -438,17 +475,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 }else{
                 }else{
                     day++;
                     day++;
                 }
                 }
-//
-//                // 再次验证 intervalDay 是否在范围内
-//                if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
-//                    log.info("跨天后,intervalDay={} 超出 TempSettings 范围,跳过。", intervalDay);
-//                    return;
-//                }
-//
-//                if (daysBetween % tempGap != 0) {
-//                    log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap,logVo.getSopId());
-//                    return;
-//                }
 
 
                 // 重新拿新的 “天” 的 Setting
                 // 重新拿新的 “天” 的 Setting
                 contents = getDay(tempSettings, day);
                 contents = getDay(tempSettings, day);
@@ -497,32 +523,40 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         // 将 LocalDateTime 转换为 Date
                         // 将 LocalDateTime 转换为 Date
                         Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
                         Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
 
 
-                        SopUserLogsInfo userLogsInfo=new SopUserLogsInfo();
-                        userLogsInfo.setSopId(logVo.getSopId());
-                        userLogsInfo.setUserLogsId(logVo.getId());
+//                        SopUserLogsInfo userLogsInfo=new SopUserLogsInfo();
+//                        userLogsInfo.setSopId(logVo.getSopId());
+//                        userLogsInfo.setUserLogsId(logVo.getId());
 
 
-                        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoList(userLogsInfo);
-                        if(logVo.getIsRegister() == 1){
-                            List<Long> externalContactIdList = PubFun.listToNewList(sopUserLogsInfos, SopUserLogsInfo::getExternalId);
-                            List<QwExternalContact> list = qwExternalContactService.list(new QueryWrapper<QwExternalContact>().isNotNull("fs_user_id").in("id", externalContactIdList));
-                            Map<Long, QwExternalContact> map = PubFun.listToMapByGroupObject(list, QwExternalContact::getId);
-                            sopUserLogsInfos = sopUserLogsInfos.stream().filter(e -> map.containsKey(e.getExternalId())).collect(Collectors.toList());
-                        }
+                        SopUserLogsInfoParam logsInfoParam=new SopUserLogsInfoParam();
+                        logsInfoParam.setSopId(logVo.getSopId());
+                        logsInfoParam.setUserLogsId(logVo.getId());
+                        logsInfoParam.setIsRegister(logVo.getIsRegister());
+
+                        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoListByIsRegister(logsInfoParam);
+
+//                        if(logVo.getIsRegister() == 1){
+//                            List<Long> externalContactIdList = PubFun.listToNewList(sopUserLogsInfos, SopUserLogsInfo::getExternalId);
+//                            List<QwExternalContact> list = qwExternalContactService.list(new QueryWrapper<QwExternalContact>().isNotNull("fs_user_id").in("id", externalContactIdList));
+//                            Map<Long, QwExternalContact> map = PubFun.listToMapByGroupObject(list, QwExternalContact::getId);
+//                            sopUserLogsInfos = sopUserLogsInfos.stream().filter(e -> map.containsKey(e.getExternalId())).collect(Collectors.toList());
+//                        }
 
 
 
 
                         // 获取fsUserId
                         // 获取fsUserId
-                        Set<Long> externalIds = sopUserLogsInfos.stream().map(SopUserLogsInfo::getExternalId).collect(Collectors.toSet());
-                        if (!externalIds.isEmpty()) {
-                            List<QwExternalContact> externalContactList = qwExternalContactService.list(Wrappers.<QwExternalContact>lambdaQuery().in(QwExternalContact::getId, externalIds));
-                            sopUserLogsInfos.forEach(s -> {
-                                QwExternalContact qwExternalContact = externalContactList.stream().filter(e -> Objects.equals(s.getExternalId(), e.getId())).findFirst().orElse(null);
-                                if (Objects.nonNull(qwExternalContact)) {
-                                    s.setFsUserId(qwExternalContact.getFsUserId());
-                                }
-                            });
-                        }
+//                        Set<Long> externalIds = sopUserLogsInfos.stream().map(SopUserLogsInfo::getExternalId).collect(Collectors.toSet());
+//                        if (!externalIds.isEmpty()) {
+//                            List<QwExternalContact> externalContactList = qwExternalContactService.list(Wrappers.<QwExternalContact>lambdaQuery().in(QwExternalContact::getId, externalIds));
+//                            sopUserLogsInfos.forEach(s -> {
+//                                QwExternalContact qwExternalContact = externalContactList.stream().filter(e -> Objects.equals(s.getExternalId(), e.getId())).findFirst().orElse(null);
+//                                if (Objects.nonNull(qwExternalContact)) {
+//                                    s.setFsUserId(qwExternalContact.getFsUserId());
+//                                }
+//                            });
+//                        }
+
+//                        insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content);
+                        insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId, companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName());
 
 
-                        insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content);
 
 
                     }
                     }
                 } catch (Exception e) {
                 } catch (Exception e) {
@@ -561,7 +595,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
     //消息处理
     //消息处理
     private void insertSopUserLogs(List<SopUserLogsInfo> sopUserLogsInfos, SopUserLogsVo logVo, Date sendTime,
     private void insertSopUserLogs(List<SopUserLogsInfo> sopUserLogsInfos, SopUserLogsVo logVo, Date sendTime,
-                                   QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content) {
+                QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content,
+                String qwUserId,String companyUserId,String companyId,String welcomeText,String qwUserName) {
+
         String formattedSendTime = sendTime.toInstant()
         String formattedSendTime = sendTime.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .atZone(ZoneId.systemDefault())
                 .format(DATE_TIME_FORMATTER);
                 .format(DATE_TIME_FORMATTER);
@@ -569,24 +605,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         Long courseId = content.getCourseId();
         Long courseId = content.getCourseId();
         Long videoId = content.getVideoId();
         Long videoId = content.getVideoId();
 
 
-        String[] userKey = logVo.getUserId().split("\\|");
-        if (userKey.length < 3) {
-            log.error("用户 ID {} 格式不正确,跳过处理。", logVo.getUserId());
-            return;
-        }
-        String qwUserId = userKey[0].trim();
-        String companyUserId = userKey[1].trim();
-        String companyId = userKey[2].trim();
+        Integer isOfficial = content.getIsOfficial() != null ? Integer.valueOf(content.getIsOfficial()) : 0;
 
 
         // 发送语言 start
         // 发送语言 start
         if(content.getSetting() == null){
         if(content.getSetting() == null){
             return;
             return;
         }
         }
-        List<QwSopTempSetting.Content.Setting> setting = content.getSetting().stream().filter(e -> "7".equals(e.getContentType())).collect(Collectors.toList());
-        if(!setting.isEmpty()){
-            List<String> valuesList = PubFun.listToNewList(setting, QwSopTempSetting.Content.Setting::getValue);
-            List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
-            Map<Long, Map<String, Optional<QwSopTempVoice>>> collect = voiceList.stream().collect(Collectors.groupingBy(QwSopTempVoice::getCompanyUserId, Collectors.groupingBy(QwSopTempVoice::getVoiceTxt, Collectors.reducing((e, e1) -> e))));
+//        List<QwSopTempSetting.Content.Setting> setting = content.getSetting().stream().filter(e -> "7".equals(e.getContentType())).collect(Collectors.toList());
+//        if(!setting.isEmpty()){
+//            List<String> valuesList = PubFun.listToNewList(setting, QwSopTempSetting.Content.Setting::getValue);
+//            List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
+//            Map<Long, Map<String, Optional<QwSopTempVoice>>> collect = voiceList.stream().collect(Collectors.groupingBy(QwSopTempVoice::getCompanyUserId, Collectors.groupingBy(QwSopTempVoice::getVoiceTxt, Collectors.reducing((e, e1) -> e))));
 //            Consumer<QwSopTempSetting.Content.Setting> buildVoid = st -> {
 //            Consumer<QwSopTempSetting.Content.Setting> buildVoid = st -> {
 //                try {
 //                try {
 //                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), Long.valueOf(companyUserId), false);
 //                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), Long.valueOf(companyUserId), false);
@@ -596,22 +625,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 //                    log.info("音频生成失败-: "+companyUserId);
 //                    log.info("音频生成失败-: "+companyUserId);
 //                }
 //                }
 //            };
 //            };
-            setting.parallelStream().filter(e -> "7".equals(e.getContentType())).forEach(st -> {
-                Map<String, Optional<QwSopTempVoice>> map = collect.get(Long.parseLong(companyUserId));
-                if(map == null) {
-//                buildVoid.accept(st);
-                    return;
-                }
-                Optional<QwSopTempVoice> optional = map.get(st.getValue());
-                if(!optional.isPresent()) {
-//                buildVoid.accept(st);
-                    return;
-                }
-                QwSopTempVoice voice = optional.get();
-                st.setVoiceUrl(voice.getVoiceUrl());
-                st.setVoiceDuration(voice.getDuration()+"");
-            });
-        }
+//            setting.parallelStream().filter(e -> "7".equals(e.getContentType())).forEach(st -> {
+//                Map<String, Optional<QwSopTempVoice>> map = collect.get(Long.parseLong(companyUserId));
+//                if(map == null) {
+////                buildVoid.accept(st);
+//                    return;
+//                }
+//                Optional<QwSopTempVoice> optional = map.get(st.getValue());
+//                if(!optional.isPresent()) {
+////                buildVoid.accept(st);
+//                    return;
+//                }
+//                QwSopTempVoice voice = optional.get();
+//                st.setVoiceUrl(voice.getVoiceUrl());
+//                st.setVoiceDuration(voice.getDuration()+"");
+//            });
+//        }
         // 发送语言 end
         // 发送语言 end
         if (content.getType()==5){
         if (content.getType()==5){
             sopAddTag(logVo,content,sendTime);
             sopAddTag(logVo,content,sendTime);
@@ -626,7 +655,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 Long fsUserId = contactId.getFsUserId();
                 Long fsUserId = contactId.getFsUserId();
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId);
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        type, qwUserId, companyUserId, companyId, externalId, fsUserId);
+                        type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
             } catch (Exception e) {
             } catch (Exception e) {
                 log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
                 log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
             }
             }
@@ -678,14 +707,15 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void handleLogBasedOnType(QwSopLogs sopLogs, QwSopTempSetting.Content content,
     private void handleLogBasedOnType(QwSopLogs sopLogs, QwSopTempSetting.Content content,
                                       SopUserLogsVo logVo, Date sendTime, Long courseId,
                                       SopUserLogsVo logVo, Date sendTime, Long courseId,
                                       Long videoId, int type, String qwUserId,
                                       Long videoId, int type, String qwUserId,
-                                      String companyUserId, String companyId, String externalId, Long fsUserId) {
+                                      String companyUserId, String companyId, String externalId,String welcomeText,
+                                      String qwUserName,Long fsUserId) {
         switch (type) {
         switch (type) {
             case 1:
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId);
                 handleNormalMessage(sopLogs, content,companyUserId);
                 break;
                 break;
             case 2:
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        qwUserId, companyUserId, companyId, externalId, fsUserId);
+                        qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
                 break;
                 break;
             case 3:
             case 3:
                 handleOrderMessage(sopLogs, content);
                 handleOrderMessage(sopLogs, content);
@@ -717,7 +747,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void handleCourseMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,
     private void handleCourseMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,
                                      SopUserLogsVo logVo, Date sendTime, Long courseId,
                                      SopUserLogsVo logVo, Date sendTime, Long courseId,
                                      Long videoId, String qwUserId, String companyUserId,
                                      Long videoId, String qwUserId, String companyUserId,
-                                     String companyId, String externalId, Long fsUserId) {
+                                     String companyId, String externalId,String welcomeText,
+                                     String qwUserName,Long fsUserId) {
         // 深拷贝 Content 对象,避免使用 JSON
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
         if (clonedContent == null) {
@@ -737,30 +768,231 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         for (QwSopTempSetting.Content.Setting setting : settings) {
         for (QwSopTempSetting.Content.Setting setting : settings) {
-            if ("1".equals(setting.getIsBindUrl())&&("3".equals(setting.getContentType())||"1".equals(setting.getContentType()))) {
-                addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
-                String sortLink = generateShortLink(setting, logVo, sendTime, courseId, videoId,
-                        qwUserId, companyUserId, companyId, externalId, fsUserId);
-                if (StringUtils.isNotEmpty(sortLink)) {
-                    if ("3".equals(setting.getContentType())) {
-                        setting.setLinkUrl(sortLink);
-                    } else {
-                        String currentValue = setting.getValue();
-                        if (currentValue == null) {
-                            setting.setValue(sortLink);
+            switch (setting.getContentType()) {
+                //文字和短链一起
+                case "1":
+                case "3":
+                    if ("1".equals(setting.getIsBindUrl())) {
+
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                        String sortLink = generateShortLink(setting, logVo, sendTime, courseId, videoId,
+                                qwUserId, companyUserId, companyId, externalId,fsUserId);
+                        if (StringUtils.isNotEmpty(sortLink)) {
+                            if ("3".equals(setting.getContentType())) {
+                                setting.setLinkUrl(sortLink);
+                            } else {
+                                String currentValue = setting.getValue();
+                                if (currentValue == null) {
+                                    setting.setValue(sortLink);
+                                } else {
+//                                    setting.setValue(currentValue + "\n" + sortLink);
+                                    setting.setValue(currentValue
+                                            .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(welcomeText)?"":welcomeText)
+                                            + "\n" + sortLink);
+                                }
+                            }
                         } else {
                         } else {
-                            setting.setValue(currentValue + "\n" + sortLink);
+                            log.error("生成短链失败,跳过设置 URL。");
+                        }
+
+                    }else {
+                        if ("1".equals(setting.getContentType())) {
+                            setting.setValue(setting.getValue()
+                                    .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(welcomeText)?"":welcomeText));
                         }
                         }
                     }
                     }
-                } else {
-                    log.error("生成短链失败,跳过设置 URL。");
-                }
+                    break;
+                //小程序单独
+                case "4":
+
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+
+                    String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
+                            qwUserId, companyUserId, companyId, externalId,fsUserId);
+
+                    setting.setMiniprogramPage(sortLink);
+
+                    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;
+                //app
+                case "9":
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+
+                    QwCreateLinkByAppDTO linkByApp = createLinkByApp(setting, logVo, sendTime, courseId, videoId,
+                            qwUserId, companyUserId, companyId, externalId,sopLogs.getCorpId(),qwUserName,fsUserId);
+
+                    setting.setLinkUrl(linkByApp.getSortLink());
+                    setting.setAppLinkUrl(linkByApp.getAppMsgLink());
+
+                    break;
+                default:
+                    break;
             }
             }
+//            if ("1".equals(setting.getIsBindUrl())&&("3".equals(setting.getContentType())||"1".equals(setting.getContentType()))) {
+//
+//                addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+//                String sortLink = generateShortLink(setting, logVo, sendTime, courseId, videoId,
+//                        qwUserId, companyUserId, companyId, externalId, fsUserId);
+//
+//                if (StringUtils.isNotEmpty(sortLink)) {
+//                    if ("3".equals(setting.getContentType())) {
+//                        setting.setLinkUrl(sortLink);
+//                    } else {
+//                        String currentValue = setting.getValue();
+//                        if (currentValue == null) {
+//                            setting.setValue(sortLink);
+//                        } else {
+//                            setting.setValue(currentValue
+//                                    .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(welcomeText)?"":welcomeText)
+//                                    + "\n" + sortLink);
+//                        }
+//                    }
+//                } else {
+//                    log.error("生成短链失败,跳过设置 URL。");
+//                }
+//            }
+
         }
         }
         sopLogs.setContentJson(JSON.toJSONString(clonedContent));
         sopLogs.setContentJson(JSON.toJSONString(clonedContent));
         enqueueQwSopLogs(sopLogs);
         enqueueQwSopLogs(sopLogs);
     }
     }
 
 
+    private QwCreateLinkByAppDTO 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,Long fsUserId){
+        // 获取缓存的配置
+        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);
+        courseMap.setFsUserId(fsUserId);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String 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();
+
+        QwCreateLinkByAppDTO byAppDTO=new QwCreateLinkByAppDTO();
+        byAppDTO.setSortLink(sortLink);
+        byAppDTO.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 byAppDTO;
+    }
+
+    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;
+    }
+    private String createLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                       Long courseId, Long videoId, 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);
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(logVo.getCorpId());
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(Long.parseLong(externalId));
+        link.setLinkType(3); //正常链接
+        String randomString = generateRandomStringWithLock();
+        link.setLink(randomString);
+        link.setCreateTime(sendTime);
+
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link,courseMap);
+        courseMap.setFsUserId(fsUserId);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = miniappRealLink + courseJson;
+//        String realLinkFull = config.getMiniprogramPage() + 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();
+    }
+
     /**
     /**
      * 深拷贝 Content 对象,避免使用 JSON 进行序列化和反序列化
      * 深拷贝 Content 对象,避免使用 JSON 进行序列化和反序列化
      */
      */
@@ -771,12 +1003,45 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         return content.clone();
         return content.clone();
     }
     }
 
 
+    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 void handleOrderMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
     private void handleOrderMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
         sopLogs.setContentJson(JSON.toJSONString(content));
         sopLogs.setContentJson(JSON.toJSONString(content));
         enqueueQwSopLogs(sopLogs);
         enqueueQwSopLogs(sopLogs);
     }
     }
 
 
+    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(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();
+        link.setLink(randomString);
+        link.setCreateTime(sendTime);
 
 
+        return link;
+    }
 
 
 
 
     private String generateShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
     private String generateShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
@@ -934,6 +1199,23 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
     }
     }
 
 
+
+    /**
+     * 将 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 队列并进行批量插入
      * 消费 QwSopLogs 队列并进行批量插入
      */
      */
@@ -1021,6 +1303,36 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
     }
     }
 
 
+    /**
+     * 消费 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);
+        }
+    }
+
+
     /**
     /**
      * 批量插入 QwSopLogs
      * 批量插入 QwSopLogs
      */
      */
@@ -1080,6 +1392,26 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     }
     }
 
 
 
 
+    /**
+     * 批量插入 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);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
+
     @Override
     @Override
     public void updateSopLogsByCancel() {
     public void updateSopLogsByCancel() {
         List<QwSopLogs> sopLogs = qwSopLogsMapper.selectQwSopLogsByCancel();
         List<QwSopLogs> sopLogs = qwSopLogsMapper.selectQwSopLogsByCancel();

+ 6 - 17
fs-qw-task/src/main/resources/application-dev.yml

@@ -23,26 +23,15 @@ spring:
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 max-wait: -1ms
                 max-wait: -1ms
     datasource:
     datasource:
-        #        clickhouse:
-        #            type: com.alibaba.druid.pool.DruidDataSource
-        ##            driverClassName: ru.yandex.clickhouse.ClickHouseDriver
-        #            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
-        #            url: jdbc:clickhouse://139.186.211.165:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
-        #            username: default
-        #            password: rt2024
-        #            initialSize: 10
-        #            maxActive: 100
-        #            minIdle: 10
-        #            maxWait: 6000
         mysql:
         mysql:
             type: com.alibaba.druid.pool.DruidDataSource
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
             driverClassName: com.mysql.cj.jdbc.Driver
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://42.194.245.189:3306/rt_fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: YJF_2024
+                    url: jdbc:mysql://139.186.77.83:3306/ylrz_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 从库数据源
                 # 从库数据源
                 slave:
                 slave:
                     # 从数据源开关/默认关闭
                     # 从数据源开关/默认关闭
@@ -95,9 +84,9 @@ spring:
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://42.194.245.189:3306/local_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: YJF_2024
+                    url: jdbc:mysql://139.186.77.83:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 初始连接数
                 # 初始连接数
                 initialSize: 5
                 initialSize: 5
                 # 最小连接池数量
                 # 最小连接池数量

+ 9 - 21
fs-qw-task/src/main/resources/application-druid.yml

@@ -2,17 +2,14 @@
 spring:
 spring:
     # redis 配置
     # redis 配置
     redis:
     redis:
-        # 地址  localhost
+        # 地址
         host: 127.0.0.1
         host: 127.0.0.1
         # 端口,默认为6379
         # 端口,默认为6379
         port: 6379
         port: 6379
-        # 数据库索引
-        database: 0
         # 密码
         # 密码
         password:
         password:
-        #        password:
         # 连接超时时间
         # 连接超时时间
-        timeout: 10s
+        timeout: 30s
         lettuce:
         lettuce:
             pool:
             pool:
                 # 连接池中的最小空闲连接
                 # 连接池中的最小空闲连接
@@ -23,26 +20,17 @@ spring:
                 max-active: 8
                 max-active: 8
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 max-wait: -1ms
                 max-wait: -1ms
+        database: 0
     datasource:
     datasource:
-#        clickhouse:
-#            type: com.alibaba.druid.pool.DruidDataSource
-#            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
-#            url: jdbc:clickhouse://cc-2vc8zzo26w0l7m2l6.public.clickhouse.ads.aliyuncs.com/sop?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
-#            username: rt_2024
-#            password: Yzx_19860213
-#            initialSize: 10
-#            maxActive: 100
-#            minIdle: 10
-#            maxWait: 6000
         mysql:
         mysql:
             type: com.alibaba.druid.pool.DruidDataSource
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
             driverClassName: com.mysql.cj.jdbc.Driver
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://cq-cdb-95qvu08p.sql.tencentcdb.com:63998/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: Rtyy_2023
+                    url: jdbc:mysql://139.186.77.83:3306/ylrz_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 从库数据源
                 # 从库数据源
                 slave:
                 slave:
                     # 从数据源开关/默认关闭
                     # 从数据源开关/默认关闭
@@ -95,9 +83,9 @@ spring:
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://gz-cdb-of55khc9.sql.tencentcdb.com:23620/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: Rtyy_2023
+                    url: jdbc:mysql://139.186.77.83:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 初始连接数
                 # 初始连接数
                 initialSize: 5
                 initialSize: 5
                 # 最小连接池数量
                 # 最小连接池数量

+ 13 - 0
fs-service-system/src/main/java/com/fs/sop/dto/QwCreateLinkByAppDTO.java

@@ -0,0 +1,13 @@
+package com.fs.sop.dto;
+
+import lombok.Data;
+
+@Data
+public class QwCreateLinkByAppDTO {
+
+    //卡片跳转得短链
+    private String sortLink ;
+
+    //发送app消息的链接
+    private String appMsgLink;
+}

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

@@ -6,6 +6,7 @@ import com.fs.sop.domain.SopUserLogsInfo;
 import com.fs.sop.params.BatchSopUserLogsInfoParamU;
 import com.fs.sop.params.BatchSopUserLogsInfoParamU;
 import com.fs.sop.params.DeleteQwSopParam;
 import com.fs.sop.params.DeleteQwSopParam;
 import com.fs.sop.params.SopUserLogsInfoDelParam;
 import com.fs.sop.params.SopUserLogsInfoDelParam;
+import com.fs.sop.params.SopUserLogsInfoParam;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Select;
@@ -111,6 +112,9 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> selectSopUserLogsInfoList(SopUserLogsInfo info);
     List<SopUserLogsInfo> selectSopUserLogsInfoList(SopUserLogsInfo info);
 
 
+    @DataSource(DataSourceType.SOP)
+    List<SopUserLogsInfo> selectSopUserLogsInfoListByIsRegister(@Param("data") SopUserLogsInfoParam logsInfoParam);
+
     @DataSource(DataSourceType.SOP)
     @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  " +
     @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" +
             "        <where>\n" +
@@ -162,6 +166,9 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     public List<SopUserLogsInfo> selectSopUserLogsInfoByIds(@Param("ids") String[] ids);
     public List<SopUserLogsInfo> selectSopUserLogsInfoByIds(@Param("ids") String[] ids);
 
 
+    @DataSource(DataSourceType.SOP)
+    public List<SopUserLogsInfo> selectSopUserLogsInfoByIdsHasUserId(@Param("ids") String[] ids);
+
     /**
     /**
      * 修改sopUserLogsInfo
      * 修改sopUserLogsInfo
      *
      *
@@ -187,12 +194,15 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     @Update("update sop_user_logs_info set fs_user_id=#{fsUserId} where external_id =#{externalId}")
     @Update("update sop_user_logs_info set fs_user_id=#{fsUserId} where external_id =#{externalId}")
     int updateQwExternalContactChangeUserId(@Param("externalId") Long externalId,@Param("fsUserId") Long fsUserId);
     int updateQwExternalContactChangeUserId(@Param("externalId") Long externalId,@Param("fsUserId") Long fsUserId);
+
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     @Select("SELECT external_id  FROM `sop_user_logs_info` where sop_id = #{sopId}  and Date(create_time) = Date(#{minDay})")
     @Select("SELECT external_id  FROM `sop_user_logs_info` where sop_id = #{sopId}  and Date(create_time) = Date(#{minDay})")
     List<SopUserLogsInfo> selectDayBySopId(@Param("sopId")String sopId, @Param("minDay")String minDay);
     List<SopUserLogsInfo> selectDayBySopId(@Param("sopId")String sopId, @Param("minDay")String minDay);
+
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     @Select("SELECT external_id,create_time  FROM sop_user_logs_info where sop_id = #{sopId} ")
     @Select("SELECT external_id,create_time  FROM sop_user_logs_info where sop_id = #{sopId} ")
     List<SopUserLogsInfo> selectSopUserLogsInfoBySopId(@Param("sopId")String sopId);
     List<SopUserLogsInfo> selectSopUserLogsInfoBySopId(@Param("sopId")String sopId);
+
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> repeatProject(@Param("projects") List<Integer> projects, @Param("externalUserID") String externalUserID);
     List<SopUserLogsInfo> repeatProject(@Param("projects") List<Integer> projects, @Param("externalUserID") String externalUserID);
     @DataSource(DataSourceType.SOP)
     @DataSource(DataSourceType.SOP)

+ 13 - 0
fs-service-system/src/main/java/com/fs/sop/params/SopUserLogsInfoParam.java

@@ -0,0 +1,13 @@
+package com.fs.sop.params;
+
+
+import lombok.Data;
+
+@Data
+public class SopUserLogsInfoParam {
+    private String sopId;
+    private String userLogsId;
+
+    // 是否只发送注册用户
+    private Integer isRegister;
+}

+ 138 - 141
fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java

@@ -358,6 +358,7 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
      */
      */
     @Override
     @Override
     public void qwSopLogsResult() {
     public void qwSopLogsResult() {
+
         logger.info("开始执行企业微信群发消息结果查询任务");
         logger.info("开始执行企业微信群发消息结果查询任务");
         long startTime = System.currentTimeMillis();
         long startTime = System.currentTimeMillis();
 
 
@@ -375,9 +376,9 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
 
 
         logger.info("按企业ID分组后,共有{}个企业需要处理", corpGroupedLogs.size());
         logger.info("按企业ID分组后,共有{}个企业需要处理", corpGroupedLogs.size());
 
 
-        // 创建线程池,只对企业进行多线程处理
-        int threadCount = Math.min(10, Runtime.getRuntime().availableProcessors());
-        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+        // 创建线程池(根据企业数量动态调整线程数,上限50)
+        int threadPoolSize = Math.min(corpGroupedLogs.size(), 50);
+        ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
         CountDownLatch mainLatch = new CountDownLatch(corpGroupedLogs.size());
         CountDownLatch mainLatch = new CountDownLatch(corpGroupedLogs.size());
 
 
         // 对每个企业创建一个任务
         // 对每个企业创建一个任务
@@ -412,6 +413,7 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                         logCopy.setSendType(2);
                                         logCopy.setSendType(2);
                                         logCopy.setSendStatus(3L);
                                         logCopy.setSendStatus(3L);
                                         logCopy.setRemark("补发");
                                         logCopy.setRemark("补发");
+                                        logCopy.setReceivingStatus(0L);
                                         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                         LocalDateTime currentTime = LocalDateTime.now();
                                         LocalDateTime currentTime = LocalDateTime.now();
                                         String newTimeString = currentTime.format(formatter);
                                         String newTimeString = currentTime.format(formatter);
@@ -429,7 +431,8 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                     }
                                     }
                                     updateList.add(logCopy);
                                     updateList.add(logCopy);
                                 }
                                 }
-                            } else if (groupmsgSendResult.getErrCode() == 45033 || groupmsgSendResult.getErrCode() == 40001) {
+                            }
+                            else if (groupmsgSendResult.getErrCode() == 45033 || groupmsgSendResult.getErrCode() == 40001) {
                                 // 接口调用频率超过限制或不合法的access_token
                                 // 接口调用频率超过限制或不合法的access_token
                                 logger.warn("企业微信接口调用频率超限,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
                                 logger.warn("企业微信接口调用频率超限,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
@@ -451,10 +454,12 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                             logCopy.setSendStatus(3L);
                                             logCopy.setSendStatus(3L);
                                             logCopy.setSort(3);
                                             logCopy.setSort(3);
                                             logCopy.setRemark("补发");
                                             logCopy.setRemark("补发");
+                                            logCopy.setReceivingStatus(0L);
                                             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                             LocalDateTime currentTime = LocalDateTime.now();
                                             LocalDateTime currentTime = LocalDateTime.now();
                                             String newTimeString = currentTime.format(formatter);
                                             String newTimeString = currentTime.format(formatter);
                                             logCopy.setSendTime(newTimeString);
                                             logCopy.setSendTime(newTimeString);
+                                            logCopy.setSort(3);
                                         }else {
                                         }else {
                                             logCopy.setReceivingStatus(Long.valueOf(itemResult.getStatus()));
                                             logCopy.setReceivingStatus(Long.valueOf(itemResult.getStatus()));
                                             if (itemResult.getStatus() == 0) {
                                             if (itemResult.getStatus() == 0) {
@@ -470,13 +475,13 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                             groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                                             groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                                 }
                                 }
                             } else {
                             } else {
-                                logger.warn("获取群发消息结果失败,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
+                                logger.error("获取群发消息结果失败,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
                                         groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                                         groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                             }
                             }
 
 
-                            // 每处理50条记录,批量更新一次数据库
-                            if (updateList.size() >= 50) {
+                            // 每处理500条记录,批量更新一次数据库
+                            if (updateList.size() >= 500) {
                                 batchUpdateDatabase(updateList);
                                 batchUpdateDatabase(updateList);
                                 updateList.clear();
                                 updateList.clear();
                             }
                             }
@@ -501,9 +506,9 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
 
 
         try {
         try {
             // 等待所有企业处理完成
             // 等待所有企业处理完成
-            boolean completed = mainLatch.await(30, TimeUnit.MINUTES);
+            boolean completed = mainLatch.await(4, TimeUnit.HOURS);
             if (!completed) {
             if (!completed) {
-                logger.warn("群发消息结果查询任务未在规定时间内完成");
+                logger.error("群发消息结果查询任务未在规定时间内完成");
             }
             }
         } catch (InterruptedException e) {
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             Thread.currentThread().interrupt();
@@ -1174,181 +1179,173 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
         long startTime = System.currentTimeMillis();
         long startTime = System.currentTimeMillis();
         logger.info("开始执行企业微信群发消息创建任务");
         logger.info("开始执行企业微信群发消息创建任务");
 
 
-        // 获取需要发送的SOP日志记录
         List<QwSopLogs> qwSopLogs = qwSopLogsMapper.selectSopLogsByCreateCorpMassSending(date);
         List<QwSopLogs> qwSopLogs = qwSopLogsMapper.selectSopLogsByCreateCorpMassSending(date);
-//        List<QwSopLogs> qwSopLogs = qwSopLogsMapper.checkQwSopLogs();
         if (qwSopLogs.isEmpty()) {
         if (qwSopLogs.isEmpty()) {
             logger.error("zyp \n【企微官方群发记录为空】");
             logger.error("zyp \n【企微官方群发记录为空】");
             return;
             return;
         }
         }
 
 
-        // 按照企业员工ID、发送时间、SOP ID和企业ID进行分组
         Map<String, List<QwSopLogs>> groupedLogs = new HashMap<>();
         Map<String, List<QwSopLogs>> groupedLogs = new HashMap<>();
         for (QwSopLogs log : qwSopLogs) {
         for (QwSopLogs log : qwSopLogs) {
             String key = log.getQwUserid() + "|" + log.getSendTime() + "|" + log.getSopId() + "|" + log.getCorpId();
             String key = log.getQwUserid() + "|" + log.getSendTime() + "|" + log.getSopId() + "|" + log.getCorpId();
             groupedLogs.computeIfAbsent(key, k -> new ArrayList<>()).add(log);
             groupedLogs.computeIfAbsent(key, k -> new ArrayList<>()).add(log);
         }
         }
 
 
-        // 创建线程池,使用固定大小的线程池以避免过多线程导致的资源竞争
         int threadCount = Math.min(10, Runtime.getRuntime().availableProcessors() + 1);
         int threadCount = Math.min(10, Runtime.getRuntime().availableProcessors() + 1);
         ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
         ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
-
-        // 创建用于发送消息的嵌套线程池
         ExecutorService messageExecutorService = Executors.newFixedThreadPool(20);
         ExecutorService messageExecutorService = Executors.newFixedThreadPool(20);
-
-        // 用于存储需要批量更新的日志记录,使用线程安全的集合
         List<QwSopLogs> updateList = Collections.synchronizedList(new ArrayList<>());
         List<QwSopLogs> updateList = Collections.synchronizedList(new ArrayList<>());
 
 
-        // 使用CountDownLatch等待所有任务完成
         CountDownLatch latch = new CountDownLatch(groupedLogs.size());
         CountDownLatch latch = new CountDownLatch(groupedLogs.size());
 
 
-        // 处理每个分组
-        for (Map.Entry<String, List<QwSopLogs>> entry : groupedLogs.entrySet()) {
-            String key = entry.getKey();
-            List<QwSopLogs> logs = entry.getValue();
-            String[] keys = key.split("\\|");
-            String qwUserid = keys[0];
-            String corpId = keys[3];
-
-//            QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, qwUserid);
-            // 查询员工信息的id
-            QwUser qwUser = qwExternalContactService.getQwUserByRedis(corpId.trim(),qwUserid.trim());
-            if (qwUser != null && qwUser.getIsDel() == 0) {
-                // 提交到线程池处理每个分组
-                executorService.submit(() -> {
-                    try {
-                        // 按外部用户ID分组
-                        Map<String, List<QwSopLogs>> userLogsMap = new HashMap<>();
-                        for (QwSopLogs log : logs) {
-                            String externalUserId = log.getExternalUserId();
-                            userLogsMap.computeIfAbsent(externalUserId, k -> new ArrayList<>()).add(log);
-                        }
-
-                        // 使用嵌套的CountDownLatch等待所有消息发送完成
-                        CountDownLatch messageLatch = new CountDownLatch(userLogsMap.size());
+        try {
+            for (Map.Entry<String, List<QwSopLogs>> entry : groupedLogs.entrySet()) {
+                String key = entry.getKey();
+                List<QwSopLogs> logs = entry.getValue();
+                String[] keys = key.split("\\|");
+                String qwUserid = keys[0];
+                String corpId = keys[3];
+
+                QwUser qwUser = qwExternalContactService.getQwUserByRedis(corpId.trim(), qwUserid.trim());
+                if (qwUser != null && qwUser.getIsDel() == 0) {
+                    executorService.submit(() -> {
+                        try {
+                            Map<String, List<QwSopLogs>> userLogsMap = new HashMap<>();
+                            for (QwSopLogs log : logs) {
+                                userLogsMap.computeIfAbsent(log.getExternalUserId(), k -> new ArrayList<>()).add(log);
+                            }
 
 
-                        // 处理每个外部用户
-                        for (Map.Entry<String, List<QwSopLogs>> userEntry : userLogsMap.entrySet()) {
-                            String externalUserId = userEntry.getKey();
-                            List<QwSopLogs> userLogs = userEntry.getValue();
+                            CountDownLatch messageLatch = new CountDownLatch(userLogsMap.size());
+                            for (Map.Entry<String, List<QwSopLogs>> userEntry : userLogsMap.entrySet()) {
+                                String externalUserId = userEntry.getKey();
+                                List<QwSopLogs> userLogs = userEntry.getValue();
 
 
-                            // 提交到消息发送线程池
-                            messageExecutorService.submit(() -> {
-                                try {
-                                    QwMsgTemplateSop templateSop = new QwMsgTemplateSop();
-                                    templateSop.setChatType("single");
-                                    templateSop.setAllowSelect(false);
-                                    templateSop.setSender(qwUserid);
-                                    templateSop.setExternalUseridList(Collections.singletonList(externalUserId));
-
-                                    List<QwMsgTemplateSop.Attachment> attachments = new ArrayList<>();
-                                    boolean hasError = false;
-
-                                    for (QwSopLogs log : userLogs) {
-                                        try {
-                                            QwSopTempSetting.Content content = JSON.parseObject(log.getContentJson(), QwSopTempSetting.Content.class);
-                                            if (content == null || content.getSetting() == null) continue;
-                                            Long courseId = content.getCourseId();
-                                            for (QwSopTempSetting.Content.Setting set : content.getSetting()) {
-                                                processContent(set, corpId, templateSop, attachments, courseId);
+                                messageExecutorService.submit(() -> {
+                                    try {
+                                        QwMsgTemplateSop templateSop = new QwMsgTemplateSop();
+                                        templateSop.setChatType("single");
+                                        templateSop.setAllowSelect(false);
+                                        templateSop.setSender(qwUserid);
+                                        templateSop.setExternalUseridList(Collections.singletonList(externalUserId));
+
+                                        List<QwMsgTemplateSop.Attachment> attachments = new ArrayList<>();
+                                        boolean hasError = false;
+                                        for (QwSopLogs log : userLogs) {
+                                            try {
+                                                QwSopTempSetting.Content content = JSON.parseObject(log.getContentJson(), QwSopTempSetting.Content.class);
+                                                if (content == null || content.getSetting() == null) continue;
+                                                Long courseId = content.getCourseId();
+                                                for (QwSopTempSetting.Content.Setting set : content.getSetting()) {
+                                                    processContent(set, corpId, templateSop, attachments, courseId);
+                                                }
+                                            } catch (Exception e) {
+                                                logger.error("消息内容解析失败,logId:{},{},{}", log.getId(), e,key);
+                                                hasError = true;
                                             }
                                             }
-                                        } catch (Exception e) {
-                                            logger.error("消息内容解析失败,logId:{},{},{}", log.getId(), e,key);
-                                            hasError = true;
                                         }
                                         }
-                                    }
-
-                                    if (!hasError && (!attachments.isEmpty() || templateSop.getTextContent() != null)) {
-                                        templateSop.setAttachments(attachments);
-                                        try {
-                                            QwAddMsgTemplateResult result = qwApiService.addMsgTemplateBySop(templateSop, corpId);
-                                            if (result.getErrCode() == 0 || result.getErrCode() == 41063){
-                                                for (QwSopLogs log : userLogs) {
-                                                    log.setSendStatus(1L);
-                                                    log.setMsgId(result.getMsgId());
-                                                    updateList.add(log);
+                                        if (!hasError && (!attachments.isEmpty() || templateSop.getTextContent() != null)) {
+                                            templateSop.setAttachments(attachments);
+                                            try {
+                                                QwAddMsgTemplateResult result = qwApiService.addMsgTemplateBySop(templateSop, corpId);
+                                                if (result.getErrCode() == 0 || result.getErrCode() == 41063){
+                                                    for (QwSopLogs log : userLogs) {
+                                                        log.setSendStatus(1L);
+                                                        log.setMsgId(result.getMsgId());
+                                                        updateList.add(log);
+                                                    }
+                                                }else {
+
+                                                    for (QwSopLogs log : userLogs) {
+                                                        log.setSendType(2);
+                                                        log.setSendStatus(3L);
+                                                        log.setRemark("官方有误,sop补发");
+                                                        log.setReceivingStatus(0L);
+                                                        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+                                                        LocalDateTime currentTime = LocalDateTime.now();
+                                                        String newTimeString = currentTime.format(formatter);
+                                                        log.setSendTime(newTimeString);
+                                                        log.setSort(3);
+                                                        updateList.add(log);
+                                                    }
+
+                                                    logger.error("企业微信接口-消息发送失败-进入sop补偿,corpId:{},errCode:{},errMsg:{},key:{}", corpId, result.getErrCode(), result.getErrMsg(),key);
                                                 }
                                                 }
-                                            }else {
-
+                                            } catch (Exception e) {
+                                                logger.error("消息发送失败,user:{},{},{}", externalUserId, e,key);
                                                 for (QwSopLogs log : userLogs) {
                                                 for (QwSopLogs log : userLogs) {
-                                                    log.setSendType(2);
-                                                    log.setSendStatus(3L);
-                                                    log.setRemark("官方有误,sop补发");
-                                                    log.setReceivingStatus(0L);
-                                                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-                                                    LocalDateTime currentTime = LocalDateTime.now();
-                                                    String newTimeString = currentTime.format(formatter);
-                                                    log.setSendTime(newTimeString);
-                                                    log.setSort(3);
+                                                    log.setSendStatus(0L);
+                                                    log.setRemark("信息异常");
                                                     updateList.add(log);
                                                     updateList.add(log);
                                                 }
                                                 }
-
-                                               logger.error("企业微信接口-消息发送失败-进入sop补偿,corpId:{},errCode:{},errMsg:{},key:{}", corpId, result.getErrCode(), result.getErrMsg(),key);
-
-                                            }
-
-
-                                        } catch (Exception e) {
-                                            logger.error("消息发送失败,user:{},{},{}", externalUserId, e,key);
-                                            for (QwSopLogs log : userLogs) {
-                                                log.setSendStatus(0L);
-                                                log.setRemark("信息有误");
-                                                updateList.add(log);
                                             }
                                             }
                                         }
                                         }
+
+                                    } finally {
+                                        logger.info("执行结束-messageLatch-countDown:"+updateList.size());
+                                        messageLatch.countDown();
                                     }
                                     }
-                                } finally {
-                                    messageLatch.countDown();
-                                }
-                            });
-                        }
+                                });
 
 
-                        // 等待所有消息发送完成
-                        try {
+                            }
+
+                            logger.info("messageExecutorService-updateList总量:"+updateList.size());
+
+                            // 等待所有消息发送完成
                             messageLatch.await();
                             messageLatch.await();
+
                         } catch (InterruptedException e) {
                         } catch (InterruptedException e) {
-                            logger.error("等待消息发送完成时被中断", e);
+                            logger.info("messageExecutorService-Thread.currentThread().interrupt():"+updateList.size());
                             Thread.currentThread().interrupt();
                             Thread.currentThread().interrupt();
+                        } finally {
+                            logger.info("finally-latch.countDown:"+updateList.size());
+                            latch.countDown();
                         }
                         }
-                    } finally {
-                        latch.countDown();
-                    }
-                });
-            }else {
-                logger.error("官方群发 员工信息有误:"+corpId+":"+qwUserid);
+                    });
+                } else {
+                    logger.error("员工信息无效-不存在或被删除,corpId:{}, userId:{}", corpId, qwUserid);
+
+                    latch.countDown(); // 确保每个分组都减少计数
+                }
             }
             }
 
 
-        }
+            latch.await(); // 等待所有分组提交的任务完成
+            logger.info("关闭线程池并等待任务完成:"+updateList.size());
+            // 关闭线程池并等待任务完成
+            executorService.shutdown();
+            messageExecutorService.shutdown();
+            if (!executorService.awaitTermination(300, TimeUnit.SECONDS)) {
+                logger.error("ExecutorService未完全关闭");
+            }
+            if (!messageExecutorService.awaitTermination(300, TimeUnit.SECONDS)) {
+                logger.error("MessageExecutorService未完全关闭");
+            }
 
 
-        // 等待所有分组处理完成
-        try {
-            latch.await();
-        } catch (InterruptedException e) {
-            logger.error("等待分组处理完成时被中断", e);
-            Thread.currentThread().interrupt();
-        }
+            // 5. 同步块生成快照(终极防护),创建快照避免并发修改
+            List<QwSopLogs> batchList;
+            synchronized (updateList) { // 加锁确保无并发修改
+                batchList = new ArrayList<>(updateList);
+            }
 
 
-        // 批量更新发送状态,每500条一批
-        if (!updateList.isEmpty()) {
-            int batchSize = 500;
-            for (int i = 0; i < updateList.size(); i += batchSize) {
-                int endIndex = Math.min(i + batchSize, updateList.size());
-                List<QwSopLogs> batch = updateList.subList(i, endIndex);
-                try {
-                    qwSopLogsMapper.batchUpdateStatus(batch);
-                    logger.info("批量修改 sopLogs 成功,修改数量: " + batch.size());
-                } catch (Exception e) {
-                    logger.error("批量修改 sopLogs 失败", e);
+
+            logger.info("批量修改总数: {}", batchList.size());
+
+            if (!batchList.isEmpty()){
+                int batchSize = 1000;
+                for (int i = 0; i < batchList.size(); i += batchSize) {
+                    int end = Math.min(i + batchSize, batchList.size());
+                    List<QwSopLogs> subList = batchList.subList(i, end);
+                    qwSopLogsMapper.batchUpdateStatus(subList);
                 }
                 }
             }
             }
-        }
 
 
-        // 关闭线程池
-        executorService.shutdown();
-        messageExecutorService.shutdown();
 
 
-        long endTime = System.currentTimeMillis();
-        logger.info("企业微信群发消息创建任务执行完成,总耗时: {} 毫秒", (endTime - startTime));
+        } catch (InterruptedException e) {
+            logger.error("线程中断异常", e);
+            Thread.currentThread().interrupt();
+        } finally {
+            long endTime = System.currentTimeMillis();
+            logger.info("任务完成,耗时: {} 毫秒", endTime - startTime);
+        }
     }
     }
 
 
     // 处理不同类型的内容
     // 处理不同类型的内容

+ 4 - 2
fs-service-system/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -43,6 +43,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import javax.validation.ConstraintViolationException;
 import javax.validation.ConstraintViolationException;
@@ -315,7 +316,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
 
 
 
                 }
                 }
-            } catch (ConstraintViolationException e) {
+            } catch (DuplicateKeyException e) {
                 return R.error().put("msg", "修改营期失败:目标营期已经有此客户,请检查客户信息 是否重复,重复请联系超管 删除此营期数据");
                 return R.error().put("msg", "修改营期失败:目标营期已经有此客户,请检查客户信息 是否重复,重复请联系超管 删除此营期数据");
             } catch (Exception e) {
             } catch (Exception e) {
                 return R.error().put("msg", "修改营期失败:" + e.getMessage());
                 return R.error().put("msg", "修改营期失败:" + e.getMessage());
@@ -358,7 +359,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
 
         List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
         List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
 
 
-        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoByIds(param.getIds());
+//        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoByIds(param.getIds());
+        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoByIdsHasUserId(param.getIds());
 
 
         String[] userKey = param.getUserIdParam().split("\\|");
         String[] userKey = param.getUserIdParam().split("\\|");
         String qwUserId = userKey[0].trim();
         String qwUserId = userKey[0].trim();

+ 24 - 0
fs-service-system/src/main/resources/mapper/sop/QwSopLogsMapper.xml

@@ -247,6 +247,30 @@
         ]]>
         ]]>
     </select>
     </select>
 
 
+
+    <select id="selectSopLogsByCreateCorpMassSending" parameterType="String" resultType="QwSopLogs" >
+        <![CDATA[
+        select * from qw_sop_logs where log_type=2 and send_status=3 and send_type=1 and send_time >= #{date}
+        ]]>
+    </select>
+
+
+    <select id="selectSopLogsByCreateCorpMassSendResult" resultType="QwSopLogs" >
+        <![CDATA[
+        SELECT
+            *
+        FROM
+            qw_sop_logs
+        WHERE
+            log_type = 2
+          AND send_type = 1
+          AND send_status = 1
+          AND receiving_status = 0
+          AND msg_id IS NOT NULL
+          AND DATE(send_time) = CURDATE();
+        ]]>
+    </select>
+
     <select id="qwSopLogsResult" resultType="QwSopLogs">
     <select id="qwSopLogsResult" resultType="QwSopLogs">
         <![CDATA[
         <![CDATA[
          select * from qw_sop_logs
          select * from qw_sop_logs

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

@@ -30,6 +30,15 @@
         where id = #{id}
         where id = #{id}
     </select>
     </select>
 
 
+    <select id="selectSopUserLogsInfoByIdsHasUserId" parameterType="java.util.List" resultMap="SopUserLogsInfoResult">
+        <include refid="selectSopUserLogsInfoVo"/>
+        WHERE fs_user_id is not null and fs_user_id !=0 and id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+
     <select id="selectSopUserLogsInfoByIds" parameterType="java.util.List" resultMap="SopUserLogsInfoResult">
     <select id="selectSopUserLogsInfoByIds" parameterType="java.util.List" resultMap="SopUserLogsInfoResult">
         <include refid="selectSopUserLogsInfoVo"/>
         <include refid="selectSopUserLogsInfoVo"/>
         WHERE id IN
         WHERE id IN
@@ -187,6 +196,17 @@
         order by crt_Time desc
         order by crt_Time desc
     </select>
     </select>
 
 
+
+    <select id="selectSopUserLogsInfoListByIsRegister" parameterType="com.fs.sop.params.SopUserLogsInfoParam" resultMap="SopUserLogsInfoResult">
+        <include refid="selectSopUserLogsInfoVo"/>
+        <where>
+            <if test="data.sopId != null">and sop_id = #{data.sopId}</if>
+            <if test="data.userLogsId != null">and user_logs_id = #{data.userLogsId}</if>
+            <if test="data.isRegister == 1">and fs_user_id is not null and fs_user_id != 0 </if>
+        </where>
+        order by crt_Time desc
+    </select>
+
     <select id="selectSopUserLogsInfo" parameterType="com.fs.sop.domain.SopUserLogsInfo" resultMap="SopUserLogsInfoResult">
     <select id="selectSopUserLogsInfo" parameterType="com.fs.sop.domain.SopUserLogsInfo" resultMap="SopUserLogsInfoResult">
         <include refid="selectSopUserLogsInfoVo"/>
         <include refid="selectSopUserLogsInfoVo"/>
         <where>
         <where>

+ 12 - 32
fs-qw-task/src/main/resources/application-druid-bly.yml → fs-user-app/src/main/resources/application-druid.yml

@@ -3,15 +3,13 @@ spring:
     # redis 配置
     # redis 配置
     redis:
     redis:
         # 地址
         # 地址
-        host: localhost
+        host: 127.0.0.1
         # 端口,默认为6379
         # 端口,默认为6379
         port: 6379
         port: 6379
-        # 数据库索引
-        database: 0
         # 密码
         # 密码
         password:
         password:
         # 连接超时时间
         # 连接超时时间
-        timeout: 20s
+        timeout: 30s
         lettuce:
         lettuce:
             pool:
             pool:
                 # 连接池中的最小空闲连接
                 # 连接池中的最小空闲连接
@@ -22,27 +20,18 @@ spring:
                 max-active: 8
                 max-active: 8
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 max-wait: -1ms
                 max-wait: -1ms
+        database: 0
     datasource:
     datasource:
-        #        clickhouse:
-        #            type: com.alibaba.druid.pool.DruidDataSource
-        ##            driverClassName: ru.yandex.clickhouse.ClickHouseDriver
-        #            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
-        #            url: jdbc:clickhouse://139.186.211.165:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
-        #            username: default
-        #            password: rt2024
-        #            initialSize: 10
-        #            maxActive: 100
-        #            minIdle: 10
-        #            maxWait: 6000
         mysql:
         mysql:
             type: com.alibaba.druid.pool.DruidDataSource
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
             driverClassName: com.mysql.cj.jdbc.Driver
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://42.194.245.189:3306/rt_fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://139.186.77.83:3306/bly_store?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
                     username: root
-                    password: YJF_2024
+                    password: bly@2025
+
                 # 从库数据源
                 # 从库数据源
                 slave:
                 slave:
                     # 从数据源开关/默认关闭
                     # 从数据源开关/默认关闭
@@ -77,8 +66,8 @@ spring:
                     allow:
                     allow:
                     url-pattern: /druid/*
                     url-pattern: /druid/*
                     # 控制台管理用户名和密码
                     # 控制台管理用户名和密码
-                    login-username: fs
-                    login-password: 123456
+                    login-username:
+                    login-password:
                 filter:
                 filter:
                     stat:
                     stat:
                         enabled: true
                         enabled: true
@@ -95,9 +84,9 @@ spring:
             druid:
             druid:
                 # 主库数据源
                 # 主库数据源
                 master:
                 master:
-                    url: jdbc:mysql://42.194.245.189:3306/test_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: YJF_2024
+                    url: jdbc:mysql://139.186.77.83:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 初始连接数
                 # 初始连接数
                 initialSize: 5
                 initialSize: 5
                 # 最小连接池数量
                 # 最小连接池数量
@@ -137,13 +126,4 @@ spring:
                     wall:
                     wall:
                         config:
                         config:
                             multi-statement-allow: true
                             multi-statement-allow: true
-rocketmq:
-    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
-    producer:
-        group: my-producer-group
-        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
-    consumer:
-        group: test-group
-        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+