Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	fs-user-app/src/main/java/com/fs/app/controller/CourseWxH5Controller.java
zyp před 1 měsícem
rodič
revize
0bfd9e9939
52 změnil soubory, kde provedl 1120 přidání a 108 odebrání
  1. 2 3
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  2. 5 0
      fs-company/src/main/java/com/fs/qw/QwGroupChatController.java
  3. 1 1
      fs-company/src/main/resources/application.yml
  4. 99 42
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  5. 1 0
      fs-service-system/src/main/java/com/fs/course/domain/FsCourseLink.java
  6. 112 0
      fs-service-system/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java
  7. 61 0
      fs-service-system/src/main/java/com/fs/course/mapper/FsUserCoursePeriodMapper.java
  8. 4 0
      fs-service-system/src/main/java/com/fs/course/param/FsCourseLinkCreateParam.java
  9. 2 15
      fs-service-system/src/main/java/com/fs/course/param/newfs/FsCourseSortLinkParam.java
  10. 23 0
      fs-service-system/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoUParam.java
  11. 61 0
      fs-service-system/src/main/java/com/fs/course/service/IFsUserCoursePeriodService.java
  12. 8 0
      fs-service-system/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  13. 1 0
      fs-service-system/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  14. 96 0
      fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java
  15. 8 8
      fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  16. 37 1
      fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  17. 6 0
      fs-service-system/src/main/java/com/fs/qw/domain/QwGroupChat.java
  18. 4 0
      fs-service-system/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java
  19. 1 0
      fs-service-system/src/main/java/com/fs/qw/mapper/QwGroupChatUserMapper.java
  20. 4 0
      fs-service-system/src/main/java/com/fs/qw/service/IQwGroupChatService.java
  21. 2 0
      fs-service-system/src/main/java/com/fs/qw/service/IQwGroupChatUserService.java
  22. 90 0
      fs-service-system/src/main/java/com/fs/qw/service/impl/AsyncChatSopService.java
  23. 0 1
      fs-service-system/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java
  24. 11 4
      fs-service-system/src/main/java/com/fs/qw/service/impl/QwGroupChatServiceImpl.java
  25. 6 0
      fs-service-system/src/main/java/com/fs/qw/service/impl/QwGroupChatUserServiceImpl.java
  26. 93 0
      fs-service-system/src/main/java/com/fs/qw/vo/ChatSopRuleTimeVO.java
  27. 1 0
      fs-service-system/src/main/java/com/fs/qw/vo/QwSopRuleTimeVO.java
  28. 1 0
      fs-service-system/src/main/java/com/fs/qw/vo/QwSopTempSetting.java
  29. 1 0
      fs-service-system/src/main/java/com/fs/qw/vo/WxSopRuleTimeVO.java
  30. 25 14
      fs-service-system/src/main/java/com/fs/sop/domain/QwSop.java
  31. 2 0
      fs-service-system/src/main/java/com/fs/sop/domain/QwSopLogs.java
  32. 2 0
      fs-service-system/src/main/java/com/fs/sop/domain/QwSopTemp.java
  33. 1 0
      fs-service-system/src/main/java/com/fs/sop/domain/SopUserLogs.java
  34. 2 1
      fs-service-system/src/main/java/com/fs/sop/mapper/QwSopLogsMapper.java
  35. 5 0
      fs-service-system/src/main/java/com/fs/sop/mapper/QwSopMapper.java
  36. 1 0
      fs-service-system/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java
  37. 4 0
      fs-service-system/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java
  38. 2 0
      fs-service-system/src/main/java/com/fs/sop/service/IQwSopTempRulesService.java
  39. 3 0
      fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java
  40. 32 0
      fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java
  41. 6 0
      fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopTempRulesServiceImpl.java
  42. 22 1
      fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopTempServiceImpl.java
  43. 1 0
      fs-service-system/src/main/java/com/fs/sop/vo/SopUserLogsVo.java
  44. 2 0
      fs-service-system/src/main/resources/mapper/course/FsCourseLinkMapper.xml
  45. 87 0
      fs-service-system/src/main/resources/mapper/course/FsUserCoursePeriodMapper.xml
  46. 3 0
      fs-service-system/src/main/resources/mapper/qw/QwGroupChatMapper.xml
  47. 3 0
      fs-service-system/src/main/resources/mapper/qw/QwGroupChatUserMapper.xml
  48. 15 1
      fs-service-system/src/main/resources/mapper/sop/QwSopLogsMapper.xml
  49. 26 0
      fs-service-system/src/main/resources/mapper/sop/QwSopMapper.xml
  50. 21 1
      fs-service-system/src/main/resources/mapper/sop/SopUserLogsMapper.xml
  51. 11 15
      fs-user-app/src/main/java/com/fs/app/controller/CourseWxH5Controller.java
  52. 103 0
      fs-user-app/src/main/java/com/fs/app/controller/FsUserCoursePeriodController.java

+ 2 - 3
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -100,11 +100,10 @@ public class FsUserCourseVideoController extends AppBaseController {
     }
 
     @Login
-    @GetMapping("/courseSortLink")
+    @PostMapping("/courseSortLink")
     @ApiOperation("生成课程分享短链")
-    public R createCourseSortLink(FsCourseSortLinkParam param) {
+    public R createCourseSortLink(@RequestBody FsCourseSortLinkParam param) {
         FsCourseLinkCreateParam fsCourseLinkCreateParam = new FsCourseLinkCreateParam();
-        // todo 入参需要确认调整
         BeanUtils.copyProperties(param, fsCourseLinkCreateParam);
 
         R courseSortLink = fsUserCourseService.createCourseSortLink(fsCourseLinkCreateParam);

+ 5 - 0
fs-company/src/main/java/com/fs/qw/QwGroupChatController.java

@@ -90,4 +90,9 @@ public class QwGroupChatController extends BaseController
         List<QwGroupChatOptionsVO> list = qwGroupChatService.selectGroupChatOptionsVOList(corpId);
         return AjaxResult.success(list);
     }
+    @GetMapping("/listAll")
+    public AjaxResult listAll(String qwUserIds){
+        List<QwGroupChatOptionsVO> list = qwGroupChatService.listAllByQwUserList(qwUserIds);
+        return AjaxResult.success(list);
+    }
 }

+ 1 - 1
fs-company/src/main/resources/application.yml

@@ -34,7 +34,7 @@ server:
 # 日志配置
 logging:
   level:
-    com.fs: info
+    com.fs: debug
     org.springframework: warn
 # Spring配置
 spring:

+ 99 - 42
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -2,24 +2,31 @@ package com.fs.app.taskService.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.app.taskService.SopLogsTaskService;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.BatchUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.date.DateUtil;
 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.*;
+import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsCourseLinkService;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwGroupChat;
+import com.fs.qw.domain.QwGroupChatUser;
 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.IQwGroupChatService;
+import com.fs.qw.service.IQwGroupChatUserService;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopRuleTimeVO;
@@ -55,6 +62,7 @@ import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
@@ -92,9 +100,15 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private QwSopTagMapper qwSopTagMapper ;
     @Autowired
     private QwSopMapper sopMapper;
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
 
     @Autowired
     private IQwExternalContactService iQwExternalContactService;
+    @Autowired
+    private IQwGroupChatService qwGroupChatService;
+    @Autowired
+    private IQwGroupChatUserService qwGroupChatUserService;
 
     @Autowired
     private QwExternalContactServiceImpl qwExternalContactService;
@@ -263,7 +277,15 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             log.info("没有需要处理的 SOP 用户日志。");
             return;
         }
-
+        String[] array = sopUserLogsVos.stream().map(SopUserLogsVo::getChatId).filter(StringUtils::isNotEmpty).toArray(String[]::new);
+        Map<String, QwGroupChat> groupChatMap = new HashMap<>();
+        if(array.length > 0){
+            List<QwGroupChat> qwGroupChatList = qwGroupChatService.selectQwGroupChatByChatIds(array);
+            List<QwGroupChatUser> qwGroupChatUserList = qwGroupChatUserService.selectQwGroupChatUserByChatIds(array);
+            Map<String, List<QwGroupChatUser>> chatUserMap = PubFun.listToMapByGroupList(qwGroupChatUserList, QwGroupChatUser::getChatId);
+            qwGroupChatList.stream().filter(e -> chatUserMap.containsKey(e.getChatId())).forEach(e -> e.setChatUserList(chatUserMap.get(e.getChatId())));
+            groupChatMap = PubFun.listToMapByGroupObject(qwGroupChatList, QwGroupChat::getChatId);
+        }
         // 查询销售二级域名
         Set<Long> ids = sopUserLogsVos.stream().map(s -> {
             String[] userKey = s.getUserId().split("\\|");
@@ -308,7 +330,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         for (Map.Entry<String, List<SopUserLogsVo>> entry : sopLogsGroupedById.entrySet()) {
             String sopId = entry.getKey();
             List<SopUserLogsVo> userLogsVos = entry.getValue();
-            processSopGroupAsync(sopId, userLogsVos, sopGroupLatch,currentTime);
+            processSopGroupAsync(sopId, userLogsVos, sopGroupLatch,currentTime, groupChatMap);
         }
 
         // 等待所有 SOP 分组处理完成
@@ -328,9 +350,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             maxAttempts = 3,
             backoff = @Backoff(delay = 2000)
     )
-    public void processSopGroupAsync(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch ,LocalDateTime currentTime) {
+    public void processSopGroupAsync(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch , LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap) {
         try {
-            processSopGroup(sopId, userLogsVos,currentTime);
+            processSopGroup(sopId, userLogsVos,currentTime, groupChatMap);
         } catch (Exception e) {
             log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
         } finally {
@@ -339,7 +361,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     }
 
 
-    private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos,LocalDateTime currentTime) throws Exception {
+    private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap) throws Exception {
         QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
 
         if (ruleTimeVO == null) {
@@ -373,7 +395,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
         CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
         for (SopUserLogsVo logVo : userLogsVos) {
-            processUserLogAsync(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime);
+            processUserLogAsync(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime, groupChatMap);
         }
 
         // 等待所有用户日志处理完成
@@ -392,9 +414,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             maxAttempts = 3,
             backoff = @Backoff(delay = 2000)
     )
-    public void processUserLogAsync(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings, CountDownLatch latch, LocalDateTime currentTime) {
+    public void processUserLogAsync(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings, CountDownLatch latch, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap) {
         try {
-            processUserLog(logVo, ruleTimeVO, tempSettings,currentTime);
+            processUserLog(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap);
         } catch (Exception e) {
             log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
         } finally {
@@ -403,7 +425,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     }
 
 
-    private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings, LocalDateTime currentTime) {
+    private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap) {
         try {
             LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
             LocalDate currentDate = currentTime.toLocalDate();
@@ -429,11 +451,11 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 day++;
             }
             List<QwSopTempSetting.Content> contents = getDay(tempSettings, day);
+
             if (contents == null || contents.isEmpty()) {
                 log.error("SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
                 return;
             }
-
             String[] userKey = logVo.getUserId().split("\\|");
             if (userKey.length < 3) {
                 log.error("用户 ID {} 格式不正确,跳过处理。", logVo.getUserId());
@@ -487,7 +509,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
             for (QwSopTempSetting.Content content : contents) {
                 try {
-
                     LocalTime elementLocalTime = LocalTime.parse(content.getTime());
                     LocalDateTime elementDateTime = LocalDateTime.of(currentTime.toLocalDate(), elementLocalTime);
 
@@ -508,7 +529,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                         // 如果时间差在目标范围内,更新记录
                         // 组合年月日和element的时间
-                        LocalDate targetDate = startDate.plusDays(intervalDay * tempGap);
+                        LocalDate targetDate = startDate.plusDays((long) intervalDay * tempGap);
 
                         // 将 targetDate 和 elementTime 组合成 LocalDateTime
                         LocalDateTime dateTime = LocalDateTime.of(targetDate, elementLocalTime);
@@ -548,7 +569,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 //                        }
 
 //                        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, qwUserId, companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName(), groupChatMap);
 
 
                     }
@@ -565,6 +586,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     private List<QwSopTempSetting.Content> getDay(List<QwSopTempRules> tempSettings, long days){
         List<QwSopTempRules> collect = tempSettings.stream().filter(e -> e.getDayNum() == days).collect(Collectors.toList());
+        AtomicInteger i = new AtomicInteger();
         return collect.stream().map(e -> {
             QwSopTempSetting.Content content = new QwSopTempSetting.Content();
             content.setId(e.getId());
@@ -583,13 +605,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             content.setCourseType(e.getCourseType());
             content.setAiTouch(e.getAiTouch());
             return content;
-        }).collect(Collectors.toList());
+        }).sorted(Comparator.comparing(e -> LocalTime.parse(e.getTime() + ":00"))).peek(e -> e.setIndex(i.getAndIncrement())).collect(Collectors.toList());
     }
 
     //消息处理
     private void insertSopUserLogs(List<SopUserLogsInfo> sopUserLogsInfos, SopUserLogsVo logVo, Date sendTime,
-                QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content,
-                String qwUserId,String companyUserId,String companyId,String welcomeText,String qwUserName) {
+                                   QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content,
+                                   String qwUserId, String companyUserId, String companyId, String welcomeText, String qwUserName, Map<String, QwGroupChat> groupChatMap) {
 
         String formattedSendTime = sendTime.toInstant()
                 .atZone(ZoneId.systemDefault())
@@ -639,20 +661,37 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             sopAddTag(logVo,content,sendTime);
         }
 
-
-        // 处理每个 externalContactId
-        sopUserLogsInfos.forEach(contactId -> {
-            try {
-                String externalId = contactId.getExternalId().toString();
-                String externalUserName = contactId.getExternalUserName();
-                Long fsUserId = contactId.getFsUserId();
-                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId);
+        if(StringUtils.isNotEmpty(logVo.getChatId())){
+            QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
+            ruleTimeVO.setSendType(6);
+            ruleTimeVO.setType(2);
+            if(content.getIndex() == 0){
+                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
-            } catch (Exception e) {
-                log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
+                        type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText,qwUserName, null, true);
+            }else{
+                groupChat.getChatUserList().forEach(user -> {
+                    QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, user.getUserId(), user.getName(), null);
+                    handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
+                            type, qwUserId, companyUserId, companyId, user.getUserId(), welcomeText,qwUserName, null, false);
+                });
             }
-        });
+        }else{
+            // 处理每个 externalContactId
+            sopUserLogsInfos.forEach(contactId -> {
+                try {
+                    String externalId = contactId.getExternalId().toString();
+                    String externalUserName = contactId.getExternalUserName();
+                    Long fsUserId = contactId.getFsUserId();
+                    QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId);
+                    handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
+                            type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId, false);
+                } catch (Exception e) {
+                    log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
+                }
+            });
+        }
+
     }
 
     private void sopAddTag(SopUserLogsVo logVo, QwSopTempSetting.Content content, Date sendTime) {
@@ -700,15 +739,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,String welcomeText,
-                                      String qwUserName,Long fsUserId) {
+                                      String companyUserId, String companyId, String externalId, String welcomeText,
+                                      String qwUserName, Long fsUserId, boolean isGroupChat) {
         switch (type) {
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId);
                 break;
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId);
+                        qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName,fsUserId, isGroupChat);
                 break;
             case 3:
                 handleOrderMessage(sopLogs, content);
@@ -740,8 +779,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,String welcomeText,
-                                     String qwUserName,Long fsUserId) {
+                                     String companyId, String externalId, String welcomeText,
+                                     String qwUserName, Long fsUserId, boolean isGroupChat) {
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
@@ -766,22 +805,40 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 case "1":
                 case "3":
                     if ("1".equals(setting.getIsBindUrl())) {
+                        String link;
+                        if(isGroupChat){
+                            FsCourseLinkCreateParam createParam = new FsCourseLinkCreateParam();
+                            createParam.setCourseId(courseId);
+                            createParam.setVideoId(videoId);
+                            createParam.setCorpId(logVo.getCorpId());
+                            createParam.setCompanyUserId(Long.parseLong(companyUserId));
+                            createParam.setCompanyId(Long.parseLong(companyId));
+                            createParam.setChatId(logVo.getChatId());
+                            createParam.setQwUserId(qwUserId);
+                            createParam.setDays(setting.getExpiresDays());
+                            R createLink = courseLinkService.createRoomLinkUrl(createParam);
+                            if (createLink.get("code").equals(500)){
+                                throw new BaseException("链接生成失败!");
+                            }
+                            link = (String) createLink.get("url");
+                        }else{
+                            addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                            link = generateShortLink(setting, logVo, sendTime, courseId, videoId,
+                                    qwUserId, companyUserId, companyId, externalId,fsUserId);
+                        }
 
-                        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 (StringUtils.isNotEmpty(link)) {
                             if ("3".equals(setting.getContentType())) {
-                                setting.setLinkUrl(sortLink);
+                                setting.setLinkUrl(link);
                             } else {
                                 String currentValue = setting.getValue();
                                 if (currentValue == null) {
-                                    setting.setValue(sortLink);
+                                    setting.setValue(link);
                                 } else {
 //                                    setting.setValue(currentValue + "\n" + sortLink);
                                     setting.setValue(currentValue
                                             .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(welcomeText)?"":welcomeText)
-                                            + "\n" + sortLink);
+                                            + "\n" + link);
                                 }
                             }
                         } else {

+ 1 - 0
fs-service-system/src/main/java/com/fs/course/domain/FsCourseLink.java

@@ -57,5 +57,6 @@ public class FsCourseLink extends BaseEntity
 //    private String link_uuid;
 
     private Integer isRoom;//是否发群
+    private String chatId;//是否发群
 
 }

+ 112 - 0
fs-service-system/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java

@@ -0,0 +1,112 @@
+package com.fs.course.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 会员营期对象 fs_user_course_period
+ *
+ * @author fs
+ * @date 2025-04-11
+ */
+public class FsUserCoursePeriod extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 营期id */
+    private Long periodId;
+
+    /** 营期名称 */
+    @Excel(name = "营期名称")
+    private String periodName;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 课程id */
+    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 视频id */
+    @Excel(name = "视频id")
+    private Long videoId;
+
+    /** 开课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "开课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date startingTime;
+
+    public void setPeriodId(Long periodId)
+    {
+        this.periodId = periodId;
+    }
+
+    public Long getPeriodId()
+    {
+        return periodId;
+    }
+    public void setPeriodName(String periodName)
+    {
+        this.periodName = periodName;
+    }
+
+    public String getPeriodName()
+    {
+        return periodName;
+    }
+    public void setCompanyId(Long companyId)
+    {
+        this.companyId = companyId;
+    }
+
+    public Long getCompanyId()
+    {
+        return companyId;
+    }
+    public void setCourseId(Long courseId)
+    {
+        this.courseId = courseId;
+    }
+
+    public Long getCourseId()
+    {
+        return courseId;
+    }
+    public void setVideoId(Long videoId)
+    {
+        this.videoId = videoId;
+    }
+
+    public Long getVideoId()
+    {
+        return videoId;
+    }
+    public void setStartingTime(Date startingTime)
+    {
+        this.startingTime = startingTime;
+    }
+
+    public Date getStartingTime()
+    {
+        return startingTime;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("periodId", getPeriodId())
+            .append("periodName", getPeriodName())
+            .append("companyId", getCompanyId())
+            .append("courseId", getCourseId())
+            .append("videoId", getVideoId())
+            .append("startingTime", getStartingTime())
+            .append("createTime", getCreateTime())
+            .append("updateTime", getUpdateTime())
+            .toString();
+    }
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/course/mapper/FsUserCoursePeriodMapper.java

@@ -0,0 +1,61 @@
+package com.fs.course.mapper;
+
+import java.util.List;
+import com.fs.course.domain.FsUserCoursePeriod;
+
+/**
+ * 会员营期Mapper接口
+ * 
+ * @author fs
+ * @date 2025-04-11
+ */
+public interface FsUserCoursePeriodMapper 
+{
+    /**
+     * 查询会员营期
+     * 
+     * @param periodId 会员营期ID
+     * @return 会员营期
+     */
+    public FsUserCoursePeriod selectFsUserCoursePeriodById(Long periodId);
+
+    /**
+     * 查询会员营期列表
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 会员营期集合
+     */
+    public List<FsUserCoursePeriod> selectFsUserCoursePeriodList(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 新增会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    public int insertFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 修改会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    public int updateFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 删除会员营期
+     * 
+     * @param periodId 会员营期ID
+     * @return 结果
+     */
+    public int deleteFsUserCoursePeriodById(Long periodId);
+
+    /**
+     * 批量删除会员营期
+     * 
+     * @param periodIds 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteFsUserCoursePeriodByIds(Long[] periodIds);
+}

+ 4 - 0
fs-service-system/src/main/java/com/fs/course/param/FsCourseLinkCreateParam.java

@@ -1,5 +1,6 @@
 package com.fs.course.param;
 
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.util.Date;
@@ -26,6 +27,9 @@ public class FsCourseLinkCreateParam {
     private Integer sendType;
 
     private Date sendTime;
+    private String chatId;
 
+    @ApiModelProperty(value = "链接有效时长(分钟)")
+    private Integer effectiveDuration;
 
 }

+ 2 - 15
fs-service-system/src/main/java/com/fs/course/param/newfs/FsCourseSortLinkParam.java

@@ -11,10 +11,6 @@ public class FsCourseSortLinkParam {
     @ApiModelProperty(value = "视频id")
     private Long videoId;
 
-    private String qwUserId;
-
-    private String corpId;
-
     @ApiModelProperty(value = "公司id")
     private Long companyId;
 
@@ -24,16 +20,7 @@ public class FsCourseSortLinkParam {
     @ApiModelProperty(value = "课程id")
     private Long courseId;
 
-    @ApiModelProperty(value = "标题")
-    private String title;
-
-    @ApiModelProperty(value = "封面")
-    private String imageUrl;
-
-    @ApiModelProperty(value = "描述")
-    private String desc;
-
-//    @ApiModelProperty(value = "跳转的链接地址,格式举例:/course/pages/xxx?course=")
-//    private String realLink;
+    @ApiModelProperty(value = "链接有效时长")
+    private Integer effectiveDuration;
 
 }

+ 23 - 0
fs-service-system/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoUParam.java

@@ -0,0 +1,23 @@
+package com.fs.course.param.newfs;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Data
+@ApiModel(value = "更新看课时长的入参")
+public class FsUserCourseVideoUParam implements Serializable {
+    @NotNull(message = "时长不能为空")
+    private Long duration;
+
+    @NotNull(message = "视频id不能为空")
+    private Long videoId;
+
+    @NotNull(message = "用户id不能为空")
+    private Long userId;
+
+    private Integer linkType;
+
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/course/service/IFsUserCoursePeriodService.java

@@ -0,0 +1,61 @@
+package com.fs.course.service;
+
+import java.util.List;
+import com.fs.course.domain.FsUserCoursePeriod;
+
+/**
+ * 会员营期Service接口
+ * 
+ * @author fs
+ * @date 2025-04-11
+ */
+public interface IFsUserCoursePeriodService 
+{
+    /**
+     * 查询会员营期
+     * 
+     * @param periodId 会员营期ID
+     * @return 会员营期
+     */
+    public FsUserCoursePeriod selectFsUserCoursePeriodById(Long periodId);
+
+    /**
+     * 查询会员营期列表
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 会员营期集合
+     */
+    public List<FsUserCoursePeriod> selectFsUserCoursePeriodList(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 新增会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    public int insertFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 修改会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    public int updateFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod);
+
+    /**
+     * 批量删除会员营期
+     * 
+     * @param periodIds 需要删除的会员营期ID
+     * @return 结果
+     */
+    public int deleteFsUserCoursePeriodByIds(Long[] periodIds);
+
+    /**
+     * 删除会员营期信息
+     * 
+     * @param periodId 会员营期ID
+     * @return 结果
+     */
+    public int deleteFsUserCoursePeriodById(Long periodId);
+}

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

@@ -6,6 +6,7 @@ import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.param.*;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
+import com.fs.course.param.newfs.FsUserCourseVideoUParam;
 import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.vo.FsUserCourseVideoListUVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoDetailsVO;
@@ -136,4 +137,11 @@ public interface IFsUserCourseVideoService
 
     R addWatchLogByLink(FsUserCourseAddCompanyUserParam param);
 
+    /**
+     * 更新看课时长
+     * @param param 入参
+     * @return
+     */
+    R updateWatchDurationWx(FsUserCourseVideoUParam param);
+
 }

+ 1 - 0
fs-service-system/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -211,6 +211,7 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         link.setRealLink(realLink+course);
         String randomString = generateRandomString();
         link.setLink(randomString);
+        link.setChatId(param.getChatId());
         link.setCreateTime(new Date());
         Integer expireDays = 0;
         if (param.getDays() == null || param.getDays() == 0){

+ 96 - 0
fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java

@@ -0,0 +1,96 @@
+package com.fs.course.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.course.mapper.FsUserCoursePeriodMapper;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.service.IFsUserCoursePeriodService;
+
+/**
+ * 会员营期Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-04-11
+ */
+@Service
+public class FsUserCoursePeriodServiceImpl implements IFsUserCoursePeriodService 
+{
+    @Autowired
+    private FsUserCoursePeriodMapper fsUserCoursePeriodMapper;
+
+    /**
+     * 查询会员营期
+     * 
+     * @param periodId 会员营期ID
+     * @return 会员营期
+     */
+    @Override
+    public FsUserCoursePeriod selectFsUserCoursePeriodById(Long periodId)
+    {
+        return fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(periodId);
+    }
+
+    /**
+     * 查询会员营期列表
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 会员营期
+     */
+    @Override
+    public List<FsUserCoursePeriod> selectFsUserCoursePeriodList(FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        return fsUserCoursePeriodMapper.selectFsUserCoursePeriodList(fsUserCoursePeriod);
+    }
+
+    /**
+     * 新增会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        fsUserCoursePeriod.setCreateTime(DateUtils.getNowDate());
+        return fsUserCoursePeriodMapper.insertFsUserCoursePeriod(fsUserCoursePeriod);
+    }
+
+    /**
+     * 修改会员营期
+     * 
+     * @param fsUserCoursePeriod 会员营期
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserCoursePeriod(FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        fsUserCoursePeriod.setUpdateTime(DateUtils.getNowDate());
+        return fsUserCoursePeriodMapper.updateFsUserCoursePeriod(fsUserCoursePeriod);
+    }
+
+    /**
+     * 批量删除会员营期
+     * 
+     * @param periodIds 需要删除的会员营期ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserCoursePeriodByIds(Long[] periodIds)
+    {
+        return fsUserCoursePeriodMapper.deleteFsUserCoursePeriodByIds(periodIds);
+    }
+
+    /**
+     * 删除会员营期信息
+     * 
+     * @param periodId 会员营期ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserCoursePeriodById(Long periodId)
+    {
+        return fsUserCoursePeriodMapper.deleteFsUserCoursePeriodById(periodId);
+    }
+}

+ 8 - 8
fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -58,8 +58,8 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     @Autowired
     private CompanyUserMapper companyUserMapper;
 
-    // todo 链接url需要调整
-    private static final String realLink = "/courseh5/pages/course/learning?course=";
+    private static final String realLink = "/courseH5/pages/course/learning?course=";
+    private static final String shortLink = "/courseH5/pages/course/learning?s=";
 
     /**
      * 查询课程
@@ -423,23 +423,23 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         int i = fsCourseLinkMapper.insertFsCourseLink(link);
         if (i > 0){
             String domainName = getDomainName(param.getCompanyUserId(), config);
-            String sortLink = domainName + "/s/" + link.getLink();
+            String sortLink = domainName + shortLink + link.getLink();
             return R.ok().put("url", sortLink);
         }
         return R.error("生成链接失败!");
     }
 
     private static Calendar getExpireDay(FsCourseLinkCreateParam param, CourseConfig config, Date createTime) {
-        Integer expireDays;
-        if (param.getDays() == null || param.getDays() == 0){
-            expireDays = config.getVideoLinkExpireDate();
+        Integer expireDuration;
+        if (param.getEffectiveDuration() == null || param.getEffectiveDuration() == 0){
+            expireDuration = config.getVideoLinkExpireDate();
         }else {
-            expireDays = param.getDays();
+            expireDuration = param.getEffectiveDuration();
         }
         // 设置过期时间
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(createTime);
-        calendar.add(Calendar.DAY_OF_MONTH, expireDays);
+        calendar.add(Calendar.MINUTE, expireDuration);
         return calendar;
     }
 

+ 37 - 1
fs-service-system/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -19,6 +19,7 @@ import com.fs.course.mapper.*;
 import com.fs.course.param.*;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
+import com.fs.course.param.newfs.FsUserCourseVideoUParam;
 import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseVideoListUVO;
@@ -945,7 +946,6 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             ResponseResult.ok(vo);
         }
         // 从Redis中获取用户目前的观看时长
-        //todo 需要在修改看课记录中设置此key
         String redisKey = "h5wxuser:watch:duration:" + param.getFsUserId() + ":" + param.getVideoId();
         String durationCurrent = redisCache.getCacheObject(redisKey);
 
@@ -981,4 +981,40 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
         return null;
     }
+
+    @Override
+    public R updateWatchDurationWx(FsUserCourseVideoUParam param) {
+        //临时短链不做记录
+        if (param.getLinkType() != null && param.getLinkType() == 1){
+            return R.ok();
+        }
+
+        // 从Redis中获取观看时长
+        String redisKey = "h5wxuser:watch:duration:" + param.getUserId() + ":" + param.getVideoId();
+        try {
+            String durationStr = redisCache.getCacheObject(redisKey);
+            long duration = durationStr != null ? Long.parseLong(durationStr) : 0L;
+
+            // 更新Redis中的观看时长
+            if (param.getDuration() != null && param.getDuration() > duration) {
+                //24小时过期
+                redisCache.setCacheObject(redisKey, param.getDuration().toString(),2,TimeUnit.HOURS);
+            }
+
+            //更新缓存中的心跳时间
+            updateHeartbeatWx(param);
+            return R.ok();
+        } catch (Exception e) {
+            logger.error("更新看课时长失败:{}", redisKey, e.getMessage());
+            return R.error("更新看课时长失败");
+        }
+    }
+
+    //会员-更新心跳时间
+    public void updateHeartbeatWx(FsUserCourseVideoUParam param) {
+        String redisKey = "h5wxuser:watch:heartbeat:" + param.getUserId() + ":" + param.getVideoId();
+        redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
+        // 设置 Redis 记录的过期时间(例如 5 分钟)
+        redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
+    }
 }

+ 6 - 0
fs-service-system/src/main/java/com/fs/qw/domain/QwGroupChat.java

@@ -1,9 +1,12 @@
 package com.fs.qw.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 客户群详情对象 qw_group_chat
  *
@@ -71,4 +74,7 @@ public class QwGroupChat extends BaseEntity
     @Excel(name = "累计退群人数")
     private Long allOutGroup;
 
+    @TableField(exist = false)
+    private List<QwGroupChatUser> chatUserList;
+
 }

+ 4 - 0
fs-service-system/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java

@@ -110,4 +110,8 @@ public interface QwGroupChatMapper
     @Select("select chat_id,name from qw_group_chat where  corp_id=#{corpId}")
     List<QwGroupChatOptionsVO> selectGroupChatOptionsVOList(String corpId);
 
+    @Select("select chat_id,name from qw_group_chat where  find_in_set(owner,#{qwUserIds})")
+    List<QwGroupChatOptionsVO> listAllByQwUserList(String qwUserIds);
+
+    List<QwGroupChat> selectQwGroupChatByChatIds(@Param("ids") String[] ids);
 }

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

@@ -139,4 +139,5 @@ public interface QwGroupChatUserMapper
     @Delete("delete from qw_group_chat_user where chat_id=#{chatId} and corp_id=#{corpId}")
     public int deleteQwGroupChatUserByIdAndCompanyId(@Param("chatId") String chatId,@Param("corpId") String corpId);
 
+    List<QwGroupChatUser> selectQwGroupChatUserByChatIds(@Param("ids") String[] ids);
 }

+ 4 - 0
fs-service-system/src/main/java/com/fs/qw/service/IQwGroupChatService.java

@@ -62,4 +62,8 @@ public interface IQwGroupChatService
     public int deleteQwGroupChatByChatIdAndCompanyId(String chatId,String corpId);
 
     List<QwGroupChatOptionsVO> selectGroupChatOptionsVOList(String corpId);
+
+    List<QwGroupChatOptionsVO> listAllByQwUserList(String qwUserIds);
+
+    List<QwGroupChat> selectQwGroupChatByChatIds(String[] ids);
 }

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

@@ -46,4 +46,6 @@ public interface IQwGroupChatUserService
     /** 根据成员id和公司corpId等查询群成员信息 */
 
     public QwGroupChatUser selectQwGroupChatUserByExternalUserId(QwGroupChatUser qwGroupChatUser);
+
+    List<QwGroupChatUser> selectQwGroupChatUserByChatIds(String[] array);
 }

+ 90 - 0
fs-service-system/src/main/java/com/fs/qw/service/impl/AsyncChatSopService.java

@@ -0,0 +1,90 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.utils.PubFun;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.date.DateUtil;
+import com.fs.qw.domain.QwGroupChat;
+import com.fs.qw.service.IQwGroupChatService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.ChatSopRuleTimeVO;
+import com.fs.qw.vo.QwUserVO;
+import com.fs.sop.domain.QwSop;
+import com.fs.sop.domain.QwSopTemp;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.QwSopTempMapper;
+import com.fs.sop.mapper.SopUserLogsMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class AsyncChatSopService {
+
+    private final QwSopMapper qwSopMapper;
+    private final QwSopTempMapper qwSopTempMapper;
+    private final SopUserLogsMapper sopUserLogsMapper;
+    private final IQwUserService qwUserService;
+    private final IQwGroupChatService qwGroupChatService;
+
+    /**
+     * 立即执行SOP任务
+     */
+    @Async("scheduledExecutorService")
+    public void executeChatSopByIds(String[] ids) {
+        try {
+            List<ChatSopRuleTimeVO> ruleTimeVOList = qwSopMapper.executeSopChatByIds(ids);
+            List<QwSopTemp> tempList = qwSopTempMapper.selectListByIds(PubFun.listToNewList(ruleTimeVOList, ChatSopRuleTimeVO::getTempId));
+            Map<String, QwSopTemp> tempMap = PubFun.listToMapByGroupObject(tempList, QwSopTemp::getId);
+            ruleTimeVOList = ruleTimeVOList.stream().filter(e -> tempMap.containsKey(e.getTempId())).collect(Collectors.toList());
+            List<QwUserVO> qwUserVOList = qwUserService.selectQwUserVOByIds(ruleTimeVOList.stream().flatMap(e -> Arrays.stream(e.getQwUserIds().split(",")).map(Long::parseLong)).toArray(Long[]::new));
+            Map<String, QwUserVO> qwUserMap = PubFun.listToMapByGroupObject(qwUserVOList, QwUserVO::getQwUserId);
+            List<QwGroupChat> qwGroupChatList = qwGroupChatService.selectQwGroupChatByChatIds(ruleTimeVOList.stream().flatMap(e -> Arrays.stream(e.getChatId().split(","))).toArray(String[]::new));
+            Map<String, QwGroupChat> groupChatMap = PubFun.listToMapByGroupObject(qwGroupChatList, QwGroupChat::getChatId);
+            ruleTimeVOList.forEach(ruleTimeVO -> {
+                QwSopTemp qwSopTemp = tempMap.get(ruleTimeVO.getTempId());
+                if (!qwSopTemp.getStatus().equals("0")) {
+                    processInternal(ruleTimeVO, qwSopTemp, qwUserMap, groupChatMap);
+                }
+                QwSop qwSop = new QwSop();
+                qwSop.setStatus(3L);
+            });
+            qwSopMapper.updateStatusQwSopById2(PubFun.listToNewList(ruleTimeVOList, ChatSopRuleTimeVO::getId));
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("立即执行执行失败", e);
+        }
+    }
+
+    private void processInternal(ChatSopRuleTimeVO timeVO, QwSopTemp temp, Map<String, QwUserVO> qwUserMap, Map<String, QwGroupChat> groupChatMap) {
+        if(StringUtils.isNotEmpty(timeVO.getChatId())){
+            List<SopUserLogs> list = Arrays.stream(timeVO.getChatId().split(",")).map(e -> {
+                SopUserLogs sopUserLogs = new SopUserLogs();
+                sopUserLogs.setSopId(timeVO.getId());
+                sopUserLogs.setSopTempId(temp.getId());
+                QwGroupChat groupChat = groupChatMap.get(e);
+                sopUserLogs.setQwUserId(groupChat.getOwner());
+                sopUserLogs.setChatId(e);
+                sopUserLogs.setCorpId(timeVO.getCorpId());
+                sopUserLogs.setStartTime(DateUtil.formatLocalDate(timeVO.getStartTime()));
+                sopUserLogs.setStatus(1);
+                QwUserVO qwUserVO = qwUserMap.get(groupChat.getOwner());
+                if(qwUserVO != null){
+                    sopUserLogs.setUserId(qwUserVO.getId() + "|" + qwUserVO.getCompanyUserId() + "|" + qwUserVO.getCompanyId());
+                }
+                return sopUserLogs;
+            }).collect(Collectors.toList());
+            sopUserLogsMapper.batchInsertSopUserLogs(list);
+        }
+    }
+
+}

+ 0 - 1
fs-service-system/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java

@@ -512,5 +512,4 @@ public class AsyncSopTestService {
         });
 
     }
-
 }

+ 11 - 4
fs-service-system/src/main/java/com/fs/qw/service/impl/QwGroupChatServiceImpl.java

@@ -30,10 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 
 /**
  * 客户群详情Service业务层处理
@@ -290,6 +287,16 @@ public class QwGroupChatServiceImpl implements IQwGroupChatService
         return qwGroupChatMapper.selectGroupChatOptionsVOList(corpId);
     }
 
+    @Override
+    public List<QwGroupChatOptionsVO> listAllByQwUserList(String qwUserIds) {
+        return qwGroupChatMapper.listAllByQwUserList(qwUserIds);
+    }
+
+    @Override
+    public List<QwGroupChat> selectQwGroupChatByChatIds(String[] ids) {
+        return qwGroupChatMapper.selectQwGroupChatByChatIds(ids);
+    }
+
     /**
      *  同步客户群列表
      */

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

@@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.text.SimpleDateFormat;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -214,4 +215,9 @@ public class QwGroupChatUserServiceImpl implements IQwGroupChatUserService
         return qwGroupChatUserMapper.selectQwGroupChatUserByExternalUserId(qwGroupChatUser);
     }
 
+    @Override
+    public List<QwGroupChatUser> selectQwGroupChatUserByChatIds(String[] array) {
+        return qwGroupChatUserMapper.selectQwGroupChatUserByChatIds(array);
+    }
+
 }

+ 93 - 0
fs-service-system/src/main/java/com/fs/qw/vo/ChatSopRuleTimeVO.java

@@ -0,0 +1,93 @@
+package com.fs.qw.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+//查出要执行的SOP
+@Data
+public class ChatSopRuleTimeVO extends BaseEntity {
+
+    /** id */
+    private String id;
+
+    private String corpId;
+
+    /** 规则名称 */
+    @Excel(name = "规则名称")
+    private String name;
+
+    /** 状态 */
+    @Excel(name = "状态")
+    private Long status;
+
+    /** 类别 1个人 2企业微信 */
+    @Excel(name = "类别 1个人 2企业微信")
+    private Integer type;
+
+    /** qw_user主表的主键 */
+    @Excel(name = "qw_user主表的主键")
+    private String qwUserIds;
+
+    @Excel(name = "公司编号")
+    private Long companyId;
+
+    /**
+     发送类型 1定时接口发送 2 Ai接口发送  3wanke  4AIduihua
+     **/
+    private Integer sendType;
+
+    /**
+    * 筛选的标签
+    */
+    private String tags;
+
+    /**
+    * 排除的标签
+    */
+    private String excludeTags;
+
+    /**
+    * 开始时间
+    */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private LocalDate startTime;
+    /**
+     *  是否开启新客户添加自动创建sop 1 否 2 是
+     */
+    private Integer isAutoSop;
+    /**
+     *   自动添加SOP的时间段
+     */
+    private String autoSopTime;
+
+    /**
+    * 模板id
+    */
+    private String tempId;
+
+    /**
+     * 过滤类别 1 包含全部 2 包含任意
+     */
+    private Integer filterType;
+
+    /**
+    * 模板内容
+    */
+//    private String tempSetting;
+
+    /**
+    * 模板状态 正常  停用
+    */
+    private String tempStatus;
+
+    /**
+    * 间隔天数
+    */
+    private Integer tempGap;
+    private String chatId;
+
+}

+ 1 - 0
fs-service-system/src/main/java/com/fs/qw/vo/QwSopRuleTimeVO.java

@@ -89,5 +89,6 @@ public class QwSopRuleTimeVO extends BaseEntity {
     */
     private Integer tempGap;
     private Integer project;
+    private String chatId;
 
 }

+ 1 - 0
fs-service-system/src/main/java/com/fs/qw/vo/QwSopTempSetting.java

@@ -21,6 +21,7 @@ public class QwSopTempSetting implements Serializable{
         //普通/课程/订单/ai
         private Long id;
         private int type;
+        private int index;
 
         private String contentType;
 

+ 1 - 0
fs-service-system/src/main/java/com/fs/qw/vo/WxSopRuleTimeVO.java

@@ -89,5 +89,6 @@ public class WxSopRuleTimeVO extends BaseEntity {
     * 间隔天数
     */
     private Integer tempGap;
+    private String chatId;
 
 }

+ 25 - 14
fs-service-system/src/main/java/com/fs/sop/domain/QwSop.java

@@ -15,26 +15,35 @@ import java.io.Serializable;
  * @date 2024-07-31
  */
 @Data
-public class QwSop implements Serializable
-{
+public class QwSop implements Serializable {
 
-    /** id */
+    /**
+     * id
+     */
     @TableId(type = IdType.UUID)
     private String id;
 
-    /** 规则名称 */
+    /**
+     * 规则名称
+     */
     @Excel(name = "规则名称")
     private String name;
 
-    /** 状态 */
+    /**
+     * 状态
+     */
     @Excel(name = "状态")
     private Long status;
 
-    /** 类别 1个人 2企业微信 */
+    /**
+     * 类别 1个人 2企业微信
+     */
     @Excel(name = "类别 1个人 2企业微信")
     private Integer type;
 
-    /** qw_user主表的主键 */
+    /**
+     * qw_user主表的主键
+     */
     @Excel(name = "qw_user主表的主键")
     private String qwUserIds;
 
@@ -42,7 +51,7 @@ public class QwSop implements Serializable
     private Long companyId;
 
     /**
-     发送类型 1定时接口发送 2 Ai接口发送 3完课 4ai新客对话
+     * 发送类型 1定时接口发送 2 Ai接口发送 3完课 4ai新客对话
      **/
     private Integer sendType;
 
@@ -68,12 +77,12 @@ public class QwSop implements Serializable
     private Integer expiryTime;
 
     /**
-    *  是否开启新客户添加自动创建sop 1 否 2 是
-    */
+     * 是否开启新客户添加自动创建sop 1 否 2 是
+     */
     private Integer isAutoSop;
     /**
-    *   自动添加SOP的时间段
-    */
+     * 自动添加SOP的时间段
+     */
     private String autoSopTime;
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -88,8 +97,8 @@ public class QwSop implements Serializable
     private Integer voice;
 
     /**
-    * 是否开启评级 1否 2是
-    */
+     * 是否开启评级 1否 2是
+     */
     private Integer isRating;
 
     private Integer courseDay;
@@ -110,5 +119,7 @@ public class QwSop implements Serializable
     // 新课对话模板
     private String newTemplateId;
     private Integer project;
+    // 群聊ID
+    private String chatId;
 
 }

+ 2 - 0
fs-service-system/src/main/java/com/fs/sop/domain/QwSopLogs.java

@@ -17,6 +17,8 @@ public class QwSopLogs implements Serializable {
 
     private String id;
 
+    private Integer type;
+
     private String corpId;
 
     private Long customerId;

+ 2 - 0
fs-service-system/src/main/java/com/fs/sop/domain/QwSopTemp.java

@@ -65,6 +65,8 @@ public class QwSopTemp implements Serializable
     @TableField(exist = false)
     private List<Map<String, Object>> rules;
     @TableField(exist = false)
+    private boolean cuoser;
+    @TableField(exist = false)
     private List<QwSopTempDay> list = new ArrayList<>();
 
 }

+ 1 - 0
fs-service-system/src/main/java/com/fs/sop/domain/SopUserLogs.java

@@ -13,6 +13,7 @@ public class SopUserLogs implements Serializable {
     private String sopTempId;
 
     private String qwUserId;
+    private String chatId;
     private String corpId;
     @JsonFormat(pattern = "yyyy-MM-dd")
     private String startTime;

+ 2 - 1
fs-service-system/src/main/java/com/fs/sop/mapper/QwSopLogsMapper.java

@@ -274,5 +274,6 @@ public interface QwSopLogsMapper extends BaseMapper<QwSopLogs> {
 
     @DataSource(DataSourceType.SOP)
     List<QwSopLogs> selectSopLogsByCreateCorpMassSendResult();
-
+    @DataSource(DataSourceType.SOP)
+    List<QwSopLogsListCVO> selectQwSopLogsListByChatSopId(@Param("map") QwSopLogsParam param);
 }

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

@@ -8,6 +8,7 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.qw.domain.QwSopUpdateStatus;
 import com.fs.qw.result.QwFilterSopCustomersResult;
+import com.fs.qw.vo.ChatSopRuleTimeVO;
 import com.fs.qw.vo.QwSopRuleTimeVO;
 import com.fs.qw.vo.WxSopRuleTimeVO;
 import com.fs.sop.domain.QwSop;
@@ -246,6 +247,8 @@ public interface QwSopMapper extends BaseMapper<QwSop> {
     public List<QwSopRuleTimeVO> executeSopByIds(@Param("ids") String[] ids);
     @DataSource(DataSourceType.SOP)
     public List<WxSopRuleTimeVO> executeSopWxByIds(@Param("ids") String[] ids);
+    @DataSource(DataSourceType.SOP)
+    public List<ChatSopRuleTimeVO> executeSopChatByIds(@Param("ids") String[] ids);
 
 //    @Select("<script> " +
 //            "SELECT\n" +
@@ -271,6 +274,8 @@ public interface QwSopMapper extends BaseMapper<QwSop> {
 
     @DataSource(DataSourceType.SOP)
     public int updateStatusQwSopById(@Param("ids") String[] ids);
+    @DataSource(DataSourceType.SOP)
+    public int updateStatusQwSopById2(@Param("ids") List<String> ids);
 
 
     @DataSource(DataSourceType.SOP)

+ 1 - 0
fs-service-system/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java

@@ -78,4 +78,5 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules>{
     List<QwSopTempRules> listByTempIdAndNameAndDayNum(@Param("id") String id, @Param("dayNum") Integer dayNum);
 
     void deleteByIdList(@Param("ids") List<String> ids);
+
 }

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

@@ -17,6 +17,7 @@ import java.util.List;
 
 
 @Repository
+@DataSource(DataSourceType.SOP)
 public interface SopUserLogsMapper {
 
     @DataSource(DataSourceType.SOP)
@@ -162,4 +163,7 @@ public interface SopUserLogsMapper {
             "order by start_time desc" +
             "</script>"})
     List<SopUserLogsVO> selectSopUserLogsListByParam(@Param("maps") SopUserLogsParam param);
+
+    @DataSource(DataSourceType.SOP)
+    void batchInsertSopUserLogs(@Param("list") List<SopUserLogs> list);
 }

+ 2 - 0
fs-service-system/src/main/java/com/fs/sop/service/IQwSopTempRulesService.java

@@ -88,4 +88,6 @@ public interface IQwSopTempRulesService extends IService<QwSopTempRules>{
     List<QwSopTempRules> listById(List<Long> rulesIds);
 
     void updateSiFenTemp();
+
+    List<QwSopTempRules> selectByTemplateIds(List<String> ids);
 }

+ 3 - 0
fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java

@@ -134,6 +134,9 @@ public class QwSopLogsServiceImpl implements IQwSopLogsService
             //企微
             case 2:
                 return qwSopLogsMapper.selectQwSopLogsListByQwSopId(param);
+            //企微
+            case 3:
+                return qwSopLogsMapper.selectQwSopLogsListByChatSopId(param);
         }
        return null;
     }

+ 32 - 0
fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java

@@ -16,6 +16,7 @@ import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.param.QwAutoSopTimeParam;
 import com.fs.qw.result.QwFilterSopCustomersResult;
+import com.fs.qw.service.impl.AsyncChatSopService;
 import com.fs.qw.service.impl.AsyncSopService;
 import com.fs.qw.service.impl.AsyncSopTestService;
 import com.fs.qw.service.impl.AsyncWxSopService;
@@ -49,6 +50,7 @@ import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 /**
@@ -92,6 +94,8 @@ public class QwSopServiceImpl implements IQwSopService
     private AsyncSopTestService asyncSopTestService;
     @Autowired
     private AsyncWxSopService asyncWxSopService;
+    @Autowired
+    private AsyncChatSopService asyncChatSopService;
 
 
     @Autowired
@@ -751,6 +755,7 @@ public class QwSopServiceImpl implements IQwSopService
         List<QwSop> qwSops = qwSopMapper.selectStatusQwSopById(ids);
         List<QwSop> qwSopList = qwSops.stream().filter(e -> e.getType() == 2).collect(Collectors.toList());
         List<QwSop> wxSopList = qwSops.stream().filter(e -> e.getType() == 1).collect(Collectors.toList());
+        List<QwSop> chatSopList = qwSops.stream().filter(e -> e.getType() == 3).collect(Collectors.toList());
 
         if(!wxSopList.isEmpty()){
             // 筛选出 status == 1 的 IDs
@@ -806,6 +811,33 @@ public class QwSopServiceImpl implements IQwSopService
                 return R.error().put("err",areadyList);
             }
         }
+        if(!chatSopList.isEmpty()){
+            // 筛选出 status == 1 的 IDs
+            String[] toBeSent = chatSopList.stream()
+                    .filter(qwSop -> qwSop.getStatus() == 1)
+                    .map(QwSop::getId)
+                    .toArray(String[]::new);
+
+            // 筛选出 status != 1 的 IDs
+            List<String> areadyList = chatSopList.stream()
+                    .filter(qwSop -> qwSop.getStatus() != 1)
+                    .map(QwSop::getId)
+                    .collect(Collectors.toList());
+
+            //异步执行
+            asyncChatSopService.executeChatSopByIds(toBeSent);
+
+            if (toBeSent.length > 0) {
+                int i = qwSopMapper.updateStatusQwSopById(toBeSent);
+                if (i > 0) {
+                    return R.ok().put("suc",toBeSent).put("err",areadyList);
+                }else {
+                    return R.error();
+                }
+            }else {
+                return R.error().put("err",areadyList);
+            }
+        }
         return R.error();
     }
 

+ 6 - 0
fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopTempRulesServiceImpl.java

@@ -245,4 +245,10 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
             contentMapper.updateQwSopTempContent(content1);
         }
     }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTempRules> selectByTemplateIds(List<String> ids) {
+        return list(new QueryWrapper<QwSopTempRules>().in("temp_id", ids));
+    }
 }

+ 22 - 1
fs-service-system/src/main/java/com/fs/sop/service/impl/QwSopTempServiceImpl.java

@@ -23,6 +23,7 @@ import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.stereotype.Service;
 
 import java.text.SimpleDateFormat;
+import java.time.LocalTime;
 import java.util.*;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -262,7 +263,27 @@ public class QwSopTempServiceImpl implements IQwSopTempService
 
     @Override
     public List<QwSopTemp> selectQwSopTempListNew(QwSopTemp qwSopTemp) {
-        return qwSopTempMapper.selectQwSopTempListNew(qwSopTemp);
+        List<QwSopTemp> list = qwSopTempMapper.selectQwSopTempListNew(qwSopTemp);
+        if(qwSopTemp.isCuoser()){
+            List<QwSopTempRules> rulesList = qwSopTempRulesService.selectByTemplateIds(PubFun.listToNewList(list, QwSopTemp::getId));
+            rulesList.sort(Comparator.comparing(
+                    e -> {
+                        String timeStr = e.getTime();
+                        if (timeStr == null || timeStr.isEmpty()) {
+                            return null; // 返回 null 表示该条目需排在最后
+                        }
+                        return LocalTime.parse(timeStr + ":00"); // 正常解析有效时间
+                    },
+                    Comparator.nullsLast(Comparator.naturalOrder()) // 处理 null 并排序
+            ));
+            Map<String, List<QwSopTempRules>> ruleMap = PubFun.listToMapByGroupList(rulesList, QwSopTempRules::getTempId);
+            list.stream().filter(e -> ruleMap.containsKey(e.getId())).forEach(e -> {
+                List<QwSopTempRules> rulesListSub = ruleMap.get(e.getId());
+                Map<Long, QwSopTempRules> dayMap = PubFun.listToMapByGroupObject(rulesListSub, QwSopTempRules::getDayId);
+                e.setCuoser(dayMap.entrySet().stream().allMatch(d -> d.getValue().getContentType() == 2));
+            });
+        }
+        return list;
     }
 
     @Override

+ 1 - 0
fs-service-system/src/main/java/com/fs/sop/vo/SopUserLogsVo.java

@@ -38,5 +38,6 @@ public class SopUserLogsVo  {
     private Integer isFixed;
     // 是否只发送注册用户
     private Integer isRegister;
+    private String chatId;
 
 }

+ 2 - 0
fs-service-system/src/main/resources/mapper/course/FsCourseLinkMapper.xml

@@ -62,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qwExternalId != null">qw_external_id,</if>
             <if test="linkType != null">link_type,</if>
             <if test="isRoom != null">is_room,</if>
+            <if test="chatId != null">chat_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="link != null">#{link},</if>
@@ -77,6 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qwExternalId != null">#{qwExternalId},</if>
             <if test="linkType != null">#{linkType},</if>
             <if test="isRoom != null">#{isRoom},</if>
+            <if test="chatId != null">#{chatId},</if>
          </trim>
     </insert>
 

+ 87 - 0
fs-service-system/src/main/resources/mapper/course/FsUserCoursePeriodMapper.xml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.course.mapper.FsUserCoursePeriodMapper">
+    
+    <resultMap type="FsUserCoursePeriod" id="FsUserCoursePeriodResult">
+        <result property="periodId"    column="period_id"    />
+        <result property="periodName"    column="period_name"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="videoId"    column="video_id"    />
+        <result property="startingTime"    column="starting_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsUserCoursePeriodVo">
+        select period_id, period_name, company_id, course_id, video_id, starting_time, create_time, update_time from fs_user_course_period
+    </sql>
+
+    <select id="selectFsUserCoursePeriodList" parameterType="FsUserCoursePeriod" resultMap="FsUserCoursePeriodResult">
+        <include refid="selectFsUserCoursePeriodVo"/>
+        <where>  
+            <if test="periodName != null  and periodName != ''"> and period_name like concat('%', #{periodName}, '%')</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="courseId != null "> and course_id = #{courseId}</if>
+            <if test="videoId != null "> and video_id = #{videoId}</if>
+            <if test="startingTime != null "> and starting_time = #{startingTime}</if>
+        </where>
+    </select>
+    
+    <select id="selectFsUserCoursePeriodById" parameterType="Long" resultMap="FsUserCoursePeriodResult">
+        <include refid="selectFsUserCoursePeriodVo"/>
+        where period_id = #{periodId}
+    </select>
+        
+    <insert id="insertFsUserCoursePeriod" parameterType="FsUserCoursePeriod">
+        insert into fs_user_course_period
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">period_id,</if>
+            <if test="periodName != null">period_name,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="videoId != null">video_id,</if>
+            <if test="startingTime != null">starting_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">#{periodId},</if>
+            <if test="periodName != null">#{periodName},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="videoId != null">#{videoId},</if>
+            <if test="startingTime != null">#{startingTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserCoursePeriod" parameterType="FsUserCoursePeriod">
+        update fs_user_course_period
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="periodName != null">period_name = #{periodName},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="startingTime != null">starting_time = #{startingTime},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where period_id = #{periodId}
+    </update>
+
+    <delete id="deleteFsUserCoursePeriodById" parameterType="Long">
+        delete from fs_user_course_period where period_id = #{periodId}
+    </delete>
+
+    <delete id="deleteFsUserCoursePeriodByIds" parameterType="String">
+        delete from fs_user_course_period where period_id in 
+        <foreach item="periodId" collection="array" open="(" separator="," close=")">
+            #{periodId}
+        </foreach>
+    </delete>
+    
+</mapper>

+ 3 - 0
fs-service-system/src/main/resources/mapper/qw/QwGroupChatMapper.xml

@@ -49,6 +49,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectQwGroupChatVo"/>
         where chat_id = #{chatId}
     </select>
+    <select id="selectQwGroupChatByChatIds" resultType="com.fs.qw.domain.QwGroupChat">
+        select * from qw_group_chat where chat_id in <foreach collection="ids" open="(" separator="," close=")" item="item">#{item}</foreach>
+    </select>
     <insert id="insertOrUpdateQwGroupChat" parameterType="QwGroupChat">
         insert into qw_group_chat
         <trim prefix="(" suffix=")" suffixOverrides=",">

+ 3 - 0
fs-service-system/src/main/resources/mapper/qw/QwGroupChatUserMapper.xml

@@ -56,6 +56,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectQwGroupChatUserVo"/>
         where chat_id = #{chatId} AND user_id=#{userId}  AND corp_id=#{corpId}
     </select>
+    <select id="selectQwGroupChatUserByChatIds" resultType="com.fs.qw.domain.QwGroupChatUser">
+        select * from qw_group_chat_user where is_out = 1 and type = 2 and chat_id in <foreach collection="ids" open="(" separator="," close=")" item="item">#{item}</foreach>
+    </select>
 
     <insert id="insertOrUpdateQwGroupChatUser" parameterType="QwGroupChatUser" useGeneratedKeys="true" keyProperty="id">
         insert into qw_group_chat_user

+ 15 - 1
fs-service-system/src/main/resources/mapper/sop/QwSopLogsMapper.xml

@@ -510,7 +510,21 @@
         AND real_send_time < now()
         ]]>
     </select>
-
+    <select id="selectQwSopLogsListByChatSopId" resultType="com.fs.sop.vo.QwSopLogsListCVO">
+        SELECT * FROM qw_sop_logs
+        <where>
+            <if test="map.sopId != null">sop_id = #{map.sopId}</if>
+            <if test="map.corpId != null">AND corp_id = #{map.corpId}</if>
+            <if test="map.sendType != null">AND send_type = #{map.sendType}</if>
+            <if test="map.sendStatus != null">AND send_status = #{map.sendStatus}</if>
+            <if test="map.receivingStatus != null">AND receiving_status = #{map.receivingStatus}</if>
+            <if test="map.qwUserid != null and map.qwUserid !='' ">AND qw_userid = #{map.qwUserid}</if>
+            <if test="map.externalUserName != null and map.externalUserName!= '' ">AND external_user_name = #{map.externalUserName}</if>
+            <if test="map.scheduleStartTime != null">AND send_time &gt;= #{map.scheduleStartTime}</if>
+            <if test="map.scheduleEndTime != null">AND send_time &lt;=  #{map.scheduleEndTime}</if>
+        </where>
+        ORDER BY send_time desc
+    </select>
 
 
     <!-- 批量更新 QwSopLogs -->

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

@@ -395,6 +395,24 @@
         select a.* from qw_sop a
         where a.send_type != 4 and a.status in (2,3) and a.type = 1
     </select>
+    <select id="executeSopChatByIds" resultType="com.fs.qw.vo.ChatSopRuleTimeVO">
+
+        SELECT qs.*,
+        qst.name AS temp_name,
+        qst.setting AS temp_setting,
+        qst.status AS temp_status,
+        qst.gap AS temp_gap,
+        qst.sort AS temp_sort,
+        qst.create_time AS temp_create_time,
+        qst.create_by AS temp_create_by,
+        qst.corp_id AS temp_company_id
+        FROM qw_sop qs
+        LEFT JOIN qw_sop_temp qst ON qs.temp_id = qst.id
+        WHERE qs.id IN
+        <foreach item="id" collection="ids"  open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
     <update id="updateQwSop" parameterType="QwSop" useGeneratedKeys="false" keyProperty="id" >
         UPDATE  qw_sop
         <trim prefix="SET" suffixOverrides=",">
@@ -456,6 +474,14 @@
             #{id}
         </foreach>
     </update>
+    <update id="updateStatusQwSopById2" useGeneratedKeys="false" keyProperty="id" >
+        UPDATE qw_sop
+        SET status = 3
+        WHERE id IN
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
 
     <update id="updateMinSendStatus">
         UPDATE qw_sop

+ 21 - 1
fs-service-system/src/main/resources/mapper/sop/SopUserLogsMapper.xml

@@ -107,6 +107,26 @@
             <if test="data.userId != null  ">#{data.userId},</if>
         </trim>
     </insert>
+    <insert id="batchInsertSopUserLogs">
+        INSERT INTO sop_user_logs
+        (
+        sop_id, sop_temp_id, qw_user_id,chat_id,corp_id, start_time,
+        status, user_id
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.sopId},
+            #{log.sopTempId},
+            #{log.qwUserId},
+            #{log.chatId},
+            #{log.corpId},
+            #{log.startTime},
+            #{log.status},
+            #{log.userId}
+            )
+        </foreach>
+    </insert>
 
     <update id="updateSopUserLogsByTempId" parameterType="String">
         update  sop_user_logs
@@ -158,7 +178,7 @@
 
 
     <select id="selectSopUserLogsListByTime" resultType="com.fs.sop.vo.SopUserLogsVo">
-        select a.*,b.min_conversion_day,b.max_conversion_day,b.min_send,b.max_send,b.is_fixed,b.is_register from sop_user_logs a
+        select a.*,b.min_conversion_day,b.max_conversion_day,b.min_send,b.max_send,b.is_fixed,b.is_register,b.chat_id from sop_user_logs a
         inner join qw_sop b on a.sop_id = b.id
         where a.start_time &lt;= Now() and a.status = 1 and b.send_type != 4 and b.status in (2,3)
     </select>

+ 11 - 15
fs-user-app/src/main/java/com/fs/app/controller/CourseWxH5Controller.java

@@ -3,13 +3,10 @@ package com.fs.app.controller;
 
 import cn.hutool.json.JSONUtil;
 import com.fs.app.annotation.Login;
-import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCourseWatchLog;
-import com.fs.course.param.FsCourseQuestionAnswerUParam;
-import com.fs.course.param.FsCourseSendRewardUParam;
 import com.fs.course.param.FsUserCourseVideoFinishUParam;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
@@ -59,21 +56,21 @@ public class CourseWxH5Controller extends AppBaseController {
         return courseVideoService.isAddCompanyUser(param);
     }
 
-    //    @ApiOperation("h5课程简介")
-//    @GetMapping("/getH5CourseByVideoId")
-//    public R getCourseByVideoId(@RequestParam("videoId") Long videoId,HttpServletRequest request)
-//    {
-//        FsUserCourseVideoH5VO course = courseService.selectFsUserCourseVideoH5VOByVideoId(videoId);
-//        return R.ok().put("data",course);
-//    }
-//
+        @ApiOperation("h5课程简介")
+    @GetMapping("/getH5CourseByVideoId")
+    public R getCourseByVideoId(@RequestParam("videoId") Long videoId,HttpServletRequest request)
+    {
+        FsUserCourseVideoH5VO course = courseService.selectFsUserCourseVideoH5VOByVideoId(videoId);
+        return R.ok().put("data",course);
+    }
+
+    @Login
     @ApiOperation("H5课程详情")
     @GetMapping("/videoDetails")
     public ResponseResult<FsUserCourseVideoLinkDetailsVO> getCourseVideoDetails(FsUserCourseVideoLinkParam param) {
         return courseVideoService.getLinkCourseVideoDetails(param);
     }
 
-
     @ApiOperation("获取真实链接")
     @GetMapping("/getRealLink")
     public R getRealLink(@RequestParam("sortLink")String link)
@@ -81,12 +78,11 @@ public class CourseWxH5Controller extends AppBaseController {
         return courseLinkService.getRealLink(link);
     }
 
-
     @ApiOperation("更新看课时长")
     @PostMapping("/updateWatchDuration")
-    public R updateWatchDuration(@RequestBody FsUserCourseVideoFinishUParam param)
+    public R updateWatchDuration(@RequestBody FsUserCourseVideoUParam param)
     {
-        return courseVideoService.updateWatchDuration(param);
+        return courseVideoService.updateWatchDurationWx(param);
     }
 
 

+ 103 - 0
fs-user-app/src/main/java/com/fs/app/controller/FsUserCoursePeriodController.java

@@ -0,0 +1,103 @@
+package com.fs.app.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 会员营期Controller
+ *
+ * @author fs
+ * @date 2025-04-11
+ */
+@RestController
+@RequestMapping("/course/userCoursePeriod")
+public class FsUserCoursePeriodController extends BaseController
+{
+    @Autowired
+    private IFsUserCoursePeriodService fsUserCoursePeriodService;
+
+    /**
+     * 查询会员营期列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        startPage();
+        List<FsUserCoursePeriod> list = fsUserCoursePeriodService.selectFsUserCoursePeriodList(fsUserCoursePeriod);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员营期列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:export')")
+    @Log(title = "会员营期", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        List<FsUserCoursePeriod> list = fsUserCoursePeriodService.selectFsUserCoursePeriodList(fsUserCoursePeriod);
+        ExcelUtil<FsUserCoursePeriod> util = new ExcelUtil<FsUserCoursePeriod>(FsUserCoursePeriod.class);
+        return util.exportExcel(list, "userCoursePeriod");
+    }
+
+    /**
+     * 获取会员营期详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:query')")
+    @GetMapping(value = "/{periodId}")
+    public AjaxResult getInfo(@PathVariable("periodId") Long periodId)
+    {
+        return AjaxResult.success(fsUserCoursePeriodService.selectFsUserCoursePeriodById(periodId));
+    }
+
+    /**
+     * 新增会员营期
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:add')")
+    @Log(title = "会员营期", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        return toAjax(fsUserCoursePeriodService.insertFsUserCoursePeriod(fsUserCoursePeriod));
+    }
+
+    /**
+     * 修改会员营期
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:edit')")
+    @Log(title = "会员营期", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserCoursePeriod fsUserCoursePeriod)
+    {
+        return toAjax(fsUserCoursePeriodService.updateFsUserCoursePeriod(fsUserCoursePeriod));
+    }
+
+    /**
+     * 删除会员营期
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCoursePeriod:remove')")
+    @Log(title = "会员营期", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{periodIds}")
+    public AjaxResult remove(@PathVariable Long[] periodIds)
+    {
+        return toAjax(fsUserCoursePeriodService.deleteFsUserCoursePeriodByIds(periodIds));
+    }
+}