三七 2 nedēļas atpakaļ
vecāks
revīzija
a9bcadb125

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

@@ -77,3 +77,51 @@ spring:
                     wall:
                         config:
                             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.stereotype.Component;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 
 @Component
@@ -98,18 +100,27 @@ public class qwTask {
         sopLogsTaskChatService.createAiChatSopLogs(today);
     }
 
+
+
+
+
     /**
-    * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息
-    */
-    @Scheduled(cron = "0 0/15 * * * ?")
+     * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息
+     */
+    @Scheduled(cron = "0 15 0 * * ?")
     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(){
         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.course.config.CourseConfig;
 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.vo.AudioVO;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopRuleTimeVO;
 import com.fs.qw.vo.QwSopTempSetting;
 import com.fs.sop.domain.*;
+import com.fs.sop.dto.QwCreateLinkByAppDTO;
 import com.fs.sop.mapper.*;
+import com.fs.sop.params.SopUserLogsInfoParam;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempRulesService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.retry.annotation.Backoff;
 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 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 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
     private CourseConfig cachedCourseConfig;
@@ -83,8 +89,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private QwSopTagMapper qwSopTagMapper ;
     @Autowired
     private QwSopMapper sopMapper;
+
+    @Autowired
+    private IQwExternalContactService iQwExternalContactService;
+
     @Autowired
-    private IQwExternalContactService qwExternalContactService;
+    private QwExternalContactServiceImpl qwExternalContactService;
 
     @Autowired
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
@@ -97,6 +107,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     @Autowired
     private FsCourseLinkMapper fsCourseLinkMapper;
+
+    @Autowired
+    private FsCourseSopAppLinkMapper fsCourseSopAppLinkMapper;
+
     @Autowired
     private ISysConfigService configService;
 
@@ -118,11 +132,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
 
     // Executors for consumer threads
     private ExecutorService qwSopLogsExecutor;
     private ExecutorService watchLogsExecutor;
     private ExecutorService courseLinkExecutor;
+    private ExecutorService courseSopAppLinkExecutor;
 
     // Shutdown flags
     private volatile boolean running = true;
@@ -181,9 +197,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return t;
         });
 
+        courseSopAppLinkExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "courseSopAppLinkConsumer");
+            t.setDaemon(true);
+            return t;
+        });
+
+
         qwSopLogsExecutor.submit(this::consumeQwSopLogs);
         watchLogsExecutor.submit(this::consumeWatchLogs);
         courseLinkExecutor.submit(this::consumeCourseLink);
+        courseSopAppLinkExecutor.submit(this::consumeCourseSopAppLink);
     }
 
     // Scheduled tasks to refresh configurations and domain names periodically
@@ -332,7 +356,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
         if (qwSopTemp == null) {
-            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
+//            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
             log.info("SOP ID {} 模板不存在,相关日志已清除。", sopId);
             return;
         }
@@ -340,10 +364,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         ruleTimeVO.setTempGap(qwSopTemp.getGap());
 
         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);
             return;
         }
@@ -417,9 +441,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 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分钟后的时间段
             LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
@@ -438,17 +475,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 }else{
                     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
                 contents = getDay(tempSettings, day);
@@ -497,32 +523,40 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         // 将 LocalDateTime 转换为 Date
                         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
-                        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) {
@@ -561,7 +595,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     //消息处理
     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()
                 .atZone(ZoneId.systemDefault())
                 .format(DATE_TIME_FORMATTER);
@@ -569,24 +605,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         Long courseId = content.getCourseId();
         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
         if(content.getSetting() == null){
             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 -> {
 //                try {
 //                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), Long.valueOf(companyUserId), false);
@@ -596,22 +625,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 //                    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
         if (content.getType()==5){
             sopAddTag(logVo,content,sendTime);
@@ -626,7 +655,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 Long fsUserId = contactId.getFsUserId();
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        type, qwUserId, companyUserId, companyId, externalId, fsUserId);
+                        type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
             } catch (Exception 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,
                                       SopUserLogsVo logVo, Date sendTime, Long courseId,
                                       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) {
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId);
                 break;
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        qwUserId, companyUserId, companyId, externalId, fsUserId);
+                        qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
                 break;
             case 3:
                 handleOrderMessage(sopLogs, content);
@@ -717,7 +747,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void handleCourseMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,
                                      SopUserLogsVo logVo, Date sendTime, Long courseId,
                                      Long videoId, String qwUserId, String companyUserId,
-                                     String companyId, String externalId, Long fsUserId) {
+                                     String companyId, String externalId,String welcomeText,
+                                     String qwUserName,Long fsUserId) {
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
@@ -737,30 +768,231 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         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 {
-                            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));
         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 进行序列化和反序列化
      */
@@ -771,12 +1003,45 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         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) {
         sopLogs.setContentJson(JSON.toJSONString(content));
         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,
@@ -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 队列并进行批量插入
      */
@@ -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
      */
@@ -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
     public void updateSopLogsByCancel() {
         List<QwSopLogs> sopLogs = qwSopLogsMapper.selectQwSopLogsByCancel();

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

@@ -23,26 +23,15 @@ spring:
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 max-wait: -1ms
     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:
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
             druid:
                 # 主库数据源
                 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:
                     # 从数据源开关/默认关闭
@@ -95,9 +84,9 @@ spring:
             druid:
                 # 主库数据源
                 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
                 # 最小连接池数量

+ 0 - 149
fs-qw-task/src/main/resources/application-druid-bly.yml

@@ -1,149 +0,0 @@
-# 数据源配置
-spring:
-    # redis 配置
-    redis:
-        # 地址
-        host: localhost
-        # 端口,默认为6379
-        port: 6379
-        # 数据库索引
-        database: 0
-        # 密码
-        password:
-        # 连接超时时间
-        timeout: 20s
-        lettuce:
-            pool:
-                # 连接池中的最小空闲连接
-                min-idle: 0
-                # 连接池中的最大空闲连接
-                max-idle: 8
-                # 连接池的最大数据库连接数
-                max-active: 8
-                # #连接池最大阻塞等待时间(使用负值表示没有限制)
-                max-wait: -1ms
-    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:
-            type: com.alibaba.druid.pool.DruidDataSource
-            driverClassName: com.mysql.cj.jdbc.Driver
-            druid:
-                # 主库数据源
-                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
-                # 从库数据源
-                slave:
-                    # 从数据源开关/默认关闭
-                    enabled: false
-                    url:
-                    username:
-                    password:
-                # 初始连接数
-                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
-        sop:
-            type: com.alibaba.druid.pool.DruidDataSource
-            driverClassName: com.mysql.cj.jdbc.Driver
-            druid:
-                # 主库数据源
-                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
-                # 初始连接数
-                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
-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

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

@@ -2,17 +2,14 @@
 spring:
     # redis 配置
     redis:
-        # 地址  localhost
+        # 地址
         host: 127.0.0.1
         # 端口,默认为6379
         port: 6379
-        # 数据库索引
-        database: 0
         # 密码
         password:
-        #        password:
         # 连接超时时间
-        timeout: 10s
+        timeout: 30s
         lettuce:
             pool:
                 # 连接池中的最小空闲连接
@@ -23,26 +20,17 @@ spring:
                 max-active: 8
                 # #连接池最大阻塞等待时间(使用负值表示没有限制)
                 max-wait: -1ms
+        database: 0
     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:
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
             druid:
                 # 主库数据源
                 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:
                     # 从数据源开关/默认关闭
@@ -95,9 +83,9 @@ spring:
             druid:
                 # 主库数据源
                 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
                 # 最小连接池数量

+ 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.DeleteQwSopParam;
 import com.fs.sop.params.SopUserLogsInfoDelParam;
+import com.fs.sop.params.SopUserLogsInfoParam;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -111,6 +112,9 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> selectSopUserLogsInfoList(SopUserLogsInfo info);
 
+    @DataSource(DataSourceType.SOP)
+    List<SopUserLogsInfo> selectSopUserLogsInfoListByIsRegister(@Param("data") SopUserLogsInfoParam logsInfoParam);
+
     @DataSource(DataSourceType.SOP)
     @Select("<script> SELECT li.*,ul.start_time FROM sop_user_logs_info li left join  sop_user_logs ul on li.user_logs_id=ul.id  " +
             "        <where>\n" +
@@ -162,6 +166,9 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     public List<SopUserLogsInfo> selectSopUserLogsInfoByIds(@Param("ids") String[] ids);
 
+    @DataSource(DataSourceType.SOP)
+    public List<SopUserLogsInfo> selectSopUserLogsInfoByIdsHasUserId(@Param("ids") String[] ids);
+
     /**
      * 修改sopUserLogsInfo
      *
@@ -187,12 +194,15 @@ public interface SopUserLogsInfoMapper {
     @DataSource(DataSourceType.SOP)
     @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);
+
     @DataSource(DataSourceType.SOP)
     @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);
+
     @DataSource(DataSourceType.SOP)
     @Select("SELECT external_id,create_time  FROM sop_user_logs_info where sop_id = #{sopId} ")
     List<SopUserLogsInfo> selectSopUserLogsInfoBySopId(@Param("sopId")String sopId);
+
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> repeatProject(@Param("projects") List<Integer> projects, @Param("externalUserID") String externalUserID);
 }

+ 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
     public void qwSopLogsResult() {
+
         logger.info("开始执行企业微信群发消息结果查询任务");
         long startTime = System.currentTimeMillis();
 
@@ -375,9 +376,9 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
 
         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());
 
         // 对每个企业创建一个任务
@@ -412,6 +413,7 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                         logCopy.setSendType(2);
                                         logCopy.setSendStatus(3L);
                                         logCopy.setRemark("补发");
+                                        logCopy.setReceivingStatus(0L);
                                         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                         LocalDateTime currentTime = LocalDateTime.now();
                                         String newTimeString = currentTime.format(formatter);
@@ -429,7 +431,8 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                     }
                                     updateList.add(logCopy);
                                 }
-                            } else if (groupmsgSendResult.getErrCode() == 45033 || groupmsgSendResult.getErrCode() == 40001) {
+                            }
+                            else if (groupmsgSendResult.getErrCode() == 45033 || groupmsgSendResult.getErrCode() == 40001) {
                                 // 接口调用频率超过限制或不合法的access_token
                                 logger.warn("企业微信接口调用频率超限,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
@@ -451,10 +454,12 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                             logCopy.setSendStatus(3L);
                                             logCopy.setSort(3);
                                             logCopy.setRemark("补发");
+                                            logCopy.setReceivingStatus(0L);
                                             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                             LocalDateTime currentTime = LocalDateTime.now();
                                             String newTimeString = currentTime.format(formatter);
                                             logCopy.setSendTime(newTimeString);
+                                            logCopy.setSort(3);
                                         }else {
                                             logCopy.setReceivingStatus(Long.valueOf(itemResult.getStatus()));
                                             if (itemResult.getStatus() == 0) {
@@ -470,13 +475,13 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
                                             groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                                 }
                             } else {
-                                logger.warn("获取群发消息结果失败,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
+                                logger.error("获取群发消息结果失败,企业ID: {},错误码: {},错误信息: {},MsgId: {}",
                                         sopLog.getCorpId(), groupmsgSendResult.getErrCode(),
                                         groupmsgSendResult.getErrMsg(), sopLog.getMsgId());
                             }
 
-                            // 每处理50条记录,批量更新一次数据库
-                            if (updateList.size() >= 50) {
+                            // 每处理500条记录,批量更新一次数据库
+                            if (updateList.size() >= 500) {
                                 batchUpdateDatabase(updateList);
                                 updateList.clear();
                             }
@@ -501,9 +506,9 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
 
         try {
             // 等待所有企业处理完成
-            boolean completed = mainLatch.await(30, TimeUnit.MINUTES);
+            boolean completed = mainLatch.await(4, TimeUnit.HOURS);
             if (!completed) {
-                logger.warn("群发消息结果查询任务未在规定时间内完成");
+                logger.error("群发消息结果查询任务未在规定时间内完成");
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -1174,181 +1179,173 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
         long startTime = System.currentTimeMillis();
         logger.info("开始执行企业微信群发消息创建任务");
 
-        // 获取需要发送的SOP日志记录
         List<QwSopLogs> qwSopLogs = qwSopLogsMapper.selectSopLogsByCreateCorpMassSending(date);
-//        List<QwSopLogs> qwSopLogs = qwSopLogsMapper.checkQwSopLogs();
         if (qwSopLogs.isEmpty()) {
             logger.error("zyp \n【企微官方群发记录为空】");
             return;
         }
 
-        // 按照企业员工ID、发送时间、SOP ID和企业ID进行分组
         Map<String, List<QwSopLogs>> groupedLogs = new HashMap<>();
         for (QwSopLogs log : qwSopLogs) {
             String key = log.getQwUserid() + "|" + log.getSendTime() + "|" + log.getSopId() + "|" + log.getCorpId();
             groupedLogs.computeIfAbsent(key, k -> new ArrayList<>()).add(log);
         }
 
-        // 创建线程池,使用固定大小的线程池以避免过多线程导致的资源竞争
         int threadCount = Math.min(10, Runtime.getRuntime().availableProcessors() + 1);
         ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
-
-        // 创建用于发送消息的嵌套线程池
         ExecutorService messageExecutorService = Executors.newFixedThreadPool(20);
-
-        // 用于存储需要批量更新的日志记录,使用线程安全的集合
         List<QwSopLogs> updateList = Collections.synchronizedList(new ArrayList<>());
 
-        // 使用CountDownLatch等待所有任务完成
         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) {
-                                                    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);
                                                 }
-
-                                               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();
+
                         } catch (InterruptedException e) {
-                            logger.error("等待消息发送完成时被中断", e);
+                            logger.info("messageExecutorService-Thread.currentThread().interrupt():"+updateList.size());
                             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.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 
 import javax.validation.ConstraintViolationException;
@@ -315,7 +316,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
 
                 }
-            } catch (ConstraintViolationException e) {
+            } catch (DuplicateKeyException e) {
                 return R.error().put("msg", "修改营期失败:目标营期已经有此客户,请检查客户信息 是否重复,重复请联系超管 删除此营期数据");
             } catch (Exception e) {
                 return R.error().put("msg", "修改营期失败:" + e.getMessage());
@@ -358,7 +359,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
         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 qwUserId = userKey[0].trim();

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

@@ -247,6 +247,30 @@
         ]]>
     </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">
         <![CDATA[
          select * from qw_sop_logs

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

@@ -30,6 +30,15 @@
         where id = #{id}
     </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">
         <include refid="selectSopUserLogsInfoVo"/>
         WHERE id IN
@@ -187,6 +196,17 @@
         order by crt_Time desc
     </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">
         <include refid="selectSopUserLogsInfoVo"/>
         <where>

+ 102 - 53
fs-user-app/src/main/resources/application-druid.yml

@@ -22,59 +22,108 @@ spring:
                 max-wait: -1ms
         database: 0
     datasource:
-        type: com.alibaba.druid.pool.DruidDataSource
-        driverClassName: com.mysql.cj.jdbc.Driver
-        druid:
-            # 主库数据源
-            master:
-                url: jdbc:mysql://139.186.77.83:3306/bly_store?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                username: root
-                password: bly@2025
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://139.186.77.83:3306/bly_store?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: bly@2025
 
-            # 从库数据源
-            slave:
-                # 从数据源开关/默认关闭
-                enabled: false
-                url:
-                username:
-                password:
-            # 初始连接数
-            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:
-                login-password:
-            filter:
-                stat:
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    enabled: false
+                    url:
+                    username:
+                    password:
+                # 初始连接数
+                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
-                    # 慢SQL记录
-                    log-slow-sql: true
-                    slow-sql-millis: 1000
-                    merge-sql: true
-                wall:
-                    config:
-                        multi-statement-allow: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username:
+                    login-password:
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            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