Bladeren bron

外呼流程优化

吴树波 2 dagen geleden
bovenliggende
commit
79e6f33037
56 gewijzigde bestanden met toevoegingen van 1054 en 6420 verwijderingen
  1. 8 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 8 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  3. 7 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  4. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticWx.java
  5. 28 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java
  6. 10 4
      fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java
  7. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  8. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  9. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWxAccountService.java
  10. 3 2
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  11. 2 2
      fs-service/src/main/java/com/fs/company/service/ICompanyWxDialogService.java
  12. 44 27
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  13. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  14. 32 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  15. 81 0
      fs-service/src/main/java/com/fs/company/util/ObjectPlaceholderResolver.java
  16. 17 0
      fs-service/src/main/java/com/fs/company/vo/SendMsgVo.java
  17. 77 0
      fs-service/src/main/java/com/fs/course/config/WxConfig.java
  18. 78 0
      fs-service/src/main/java/com/fs/wxcid/domain/WxContact.java
  19. 6 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/ContactItem.java
  20. 4 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/GetContactDetailsResponseData.java
  21. 61 0
      fs-service/src/main/java/com/fs/wxcid/mapper/WxContactMapper.java
  22. 10 0
      fs-service/src/main/java/com/fs/wxcid/service/FriendService.java
  23. 64 0
      fs-service/src/main/java/com/fs/wxcid/service/IWxContactService.java
  24. 4 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/FriendServiceImpl.java
  25. 121 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/WxContactServiceImpl.java
  26. 12 0
      fs-service/src/main/java/com/fs/wxcid/vo/AddContactVo.java
  27. 16 0
      fs-service/src/main/java/com/fs/wxcid/vo/VerifyUserValidTicketListVo.java
  28. 1 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  29. 10 0
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  30. 136 0
      fs-service/src/main/resources/mapper/wxcid/WxContactMapper.xml
  31. 5 1
      fs-wx-api/src/main/java/com/fs/app/controller/WebscoketServer.java
  32. 1 1
      fs-wx-task/src/main/java/com/fs/FsWxTaskApplication.java
  33. 21 497
      fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java
  34. 0 19
      fs-wx-task/src/main/java/com/fs/app/controller/VoiceController.java
  35. 132 0
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java
  36. 0 155
      fs-wx-task/src/main/java/com/fs/app/task/CourseWatchLogScheduler.java
  37. 0 37
      fs-wx-task/src/main/java/com/fs/app/task/UserCourseWatchCountTask.java
  38. 40 0
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java
  39. 0 484
      fs-wx-task/src/main/java/com/fs/app/task/qwTask.java
  40. 0 10
      fs-wx-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingMoreSevenDaysService.java
  41. 0 10
      fs-wx-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingService.java
  42. 0 8
      fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsChatTaskService.java
  43. 0 22
      fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java
  44. 0 11
      fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsTestService.java
  45. 0 9
      fs-wx-task/src/main/java/com/fs/app/taskService/SopUserLogsInfoByIsDaysNotStudy.java
  46. 0 8
      fs-wx-task/src/main/java/com/fs/app/taskService/SopWxLogsService.java
  47. 0 10
      fs-wx-task/src/main/java/com/fs/app/taskService/SyncQwExternalContactService.java
  48. 0 374
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/AsyncCourseWatchFinishService.java
  49. 0 333
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingMoreSevenDaysServiceImpl.java
  50. 0 410
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingServiceImpl.java
  51. 0 212
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsChatTaskServiceImpl.java
  52. 0 2336
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  53. 0 965
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsTestServiceImpl.java
  54. 0 262
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopUserLogsInfoByIsDaysNotStudyImpl.java
  55. 0 127
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsServiceImpl.java
  56. 0 78
      fs-wx-task/src/main/java/com/fs/app/taskService/impl/SyncQwExternalContactServiceImpl.java

+ 8 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java

@@ -197,4 +197,12 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.dispenseWx(id);
         return R.ok();
     }
+    /**
+     * 启动任务
+     */
+	@GetMapping("/taskRun")
+    public R taskRun(Long id){
+        companyVoiceRoboticService.taskRun(id);
+        return R.ok();
+    }
 }

+ 8 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -222,4 +222,12 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.cidList(companyId);
         return R.ok();
     }
+    /**
+     * 启动任务
+     */
+    @GetMapping("/taskRun")
+    public R taskRun(Long id){
+        companyVoiceRoboticService.taskRun(id);
+        return R.ok();
+    }
 }

+ 7 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java

@@ -1,6 +1,8 @@
 package com.fs.company.domain;
 
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.company.vo.RoboticWxVo;
@@ -21,6 +23,7 @@ public class CompanyVoiceRobotic {
     private static final long serialVersionUID = 1L;
 
     /** ID */
+    @TableId(type = IdType.AUTO)
     private Long id;
 
     /** 任务名称 */
@@ -96,6 +99,10 @@ public class CompanyVoiceRobotic {
     private Long createUser;
     private Long companyId;
     private Long companyUserId;
+    private String taskFlow;
+    private String runTaskFlow;
+    // 任务状态0待执行1执行中2执行中断3执行完成
+    private Integer taskStatus;
     /** 创建人 */
     @Excel(name = "创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

+ 2 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticWx.java

@@ -54,4 +54,6 @@ public class CompanyVoiceRoboticWx extends BaseEntityTow {
     private String dialogName;
     @TableField(exist = false)
     private int indexNumber;
+    @TableField(exist = false)
+    private CompanyWxAccount account;
 }

+ 28 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java

@@ -6,6 +6,7 @@ import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
 /**
@@ -66,6 +67,33 @@ public class CompanyWxAccount extends BaseEntity
     private LocalDateTime outTime;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime syncFriendTime;
+    /**
+     * 上一次添加微信时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime lastAddWxTime;
+    /**
+     * 账号创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private LocalDate accountCreateTime;
+    /**
+     * 每天添加账号数量
+     */
+    private Integer addNum;
+    /**
+     * 发送消息条数限制
+     */
+    private String sendMsgJson;
+    /**
+     * 是否新账号0是1否
+     */
+    private Integer isNew;
+    /**
+     * 当天已经添加数量
+     */
+    private Integer isAddNum;
+    private Integer allocateNum;
 
     private String outRemark;
 

+ 10 - 4
fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java

@@ -59,11 +59,19 @@ public class CompanyWxClient extends BaseEntityTow
     /** 是否添加;0否1是 */
     @Excel(name = "是否添加;0否1是2待添加3作废")
     private Integer isAdd;
+    private Long accountId;
 
     /** 添加时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "添加时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "添加时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime addTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "添加完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime successAddTime;
+    private String wxV3;
+    private String wxV4;
+    private Long companyId;
+    private Long companyUserId;
 
     @TableField(exist = false)
     private String wxNickName;
@@ -77,7 +85,5 @@ public class CompanyWxClient extends BaseEntityTow
     private String roboticName;
     @TableField(exist = false)
     private String memo;
-    @TableField(exist = false)
-    private Long companyUserId;
 
 }

+ 4 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java

@@ -68,4 +68,8 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
     CompanyUser selectCompanyByAccountId(@Param("wxId") Long wxId);
 
     List<CompanyWxClient> listCompanyIds(@Param("ids") String[] ids);
+
+    List<CompanyWxClient> getAddWxList(@Param("accountIdList") List<Long> accountIdList);
+
+    CompanyWxClient selectWx(@Param("accountId") Long accountId, @Param("v3") String v3);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -75,4 +75,6 @@ public interface ICompanyVoiceRoboticService
     void addCompany(AddWxClientVo vo);
 
     void cidList(Long companyId);
+
+    void taskRun(Long id);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyWxAccountService.java

@@ -82,4 +82,6 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
     void wxLoginOut(Long accountId);
 
     void syncWx(Long accountId);
+
+    void isCheckContact(String formUser, Long accountId);
 }

+ 3 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java

@@ -1,5 +1,6 @@
 package com.fs.company.service;
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.AddWxResultVo;
 
@@ -11,8 +12,7 @@ import java.util.List;
  * @author fs
  * @date 2024-12-09
  */
-public interface ICompanyWxClientService 
-{
+public interface ICompanyWxClientService extends IService<CompanyWxClient> {
     /**
      * 查询添加个微信账号
      * 
@@ -68,4 +68,5 @@ public interface ICompanyWxClientService
 
     void addWxTrueResult(AddWxResultVo vo);
 
+    List<CompanyWxClient> getAddWxList(List<Long> accountIdList);
 }

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyWxDialogService.java

@@ -1,5 +1,6 @@
 package com.fs.company.service;
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyWxDialog;
 
 import java.util.List;
@@ -10,8 +11,7 @@ import java.util.List;
  * @author fs
  * @date 2024-12-06
  */
-public interface ICompanyWxDialogService 
-{
+public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
     /**
      * 查询添加微信话术
      * 

+ 44 - 27
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -22,6 +22,7 @@ import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticWxMapper;
 import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.service.ICompanyWxAccountService;
 import com.fs.company.vo.AddWxClientVo;
 import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
 import com.fs.company.vo.RoboticWxAccountVo;
@@ -37,10 +38,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 
@@ -61,6 +59,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
 
     private final CompanyVoiceRoboticCalleesServiceImpl companyVoiceRoboticCalleesService;
+    private final ICompanyWxAccountService companyWxAccountService;
 
     private final CompanyVoiceRoboticWxMapper companyVoiceRoboticWxMapper;
 
@@ -112,6 +111,22 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
      */
     @Override
     public int insertCompanyVoiceRobotic(CompanyVoiceRobotic companyVoiceRobotic){
+        int i = companyVoiceRoboticMapper.insert(companyVoiceRobotic);
+        // 设置加微微信列表
+        List<RoboticWxVo> qwUserList = companyVoiceRobotic.getQwUserList();
+        List<CompanyVoiceRoboticWx> collect = qwUserList.stream().map(e -> {
+            CompanyVoiceRoboticWx entity = new CompanyVoiceRoboticWx();
+            entity.setIntention(e.getIntention());
+            entity.setRoboticId(companyVoiceRobotic.getId());
+            entity.setAccountId(e.getCompanyUserId());
+            entity.setWxDialogId(e.getWxDialogId());
+            return entity;
+        }).collect(Collectors.toList());
+        companyVoiceRoboticWxService.saveBatch(collect);
+        return i;
+    }
+
+    public CalltaskcreateaiCustomizeResult addTask(CompanyVoiceRobotic companyVoiceRobotic){
         List<CrmCustomer> customerList = crmCustomerService.selectCrmCustomerListByIds(String.join(",", companyVoiceRobotic.getUserIds()));
         if(customerList.isEmpty()){
             throw new BaseException("拨打电话不能为空");
@@ -121,7 +136,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         param.setRobot(companyVoiceRobotic.getRobot());
         param.setDialogID(companyVoiceRobotic.getDialogId());
         param.setMode(companyVoiceRobotic.getMode());
-        int i = companyVoiceRoboticMapper.insertCompanyVoiceRobotic(companyVoiceRobotic);
         // 保存外呼电话列表
         List<CompanyVoiceRoboticCallees> calleesList = customerList.stream().map(e -> {
             CompanyVoiceRoboticCallees entity = new CompanyVoiceRoboticCallees();
@@ -156,18 +170,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         companyVoiceRobotic.setTaskId(result.getTaskID());
         companyVoiceRobotic.setTaskName(result.getTaskName());
         companyVoiceRoboticMapper.updateById(companyVoiceRobotic);
-        // 设置加微微信列表
-        List<RoboticWxVo> qwUserList = companyVoiceRobotic.getQwUserList();
-        List<CompanyVoiceRoboticWx> collect = qwUserList.stream().map(e -> {
-            CompanyVoiceRoboticWx entity = new CompanyVoiceRoboticWx();
-            entity.setIntention(e.getIntention());
-            entity.setRoboticId(companyVoiceRobotic.getId());
-            entity.setAccountId(e.getCompanyUserId());
-            entity.setWxDialogId(e.getWxDialogId());
-            return entity;
-        }).collect(Collectors.toList());
-        companyVoiceRoboticWxService.saveBatch(collect);
-        return i;
+        return result;
     }
 
     /**
@@ -286,9 +289,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     @Override
     public void dispenseWx(Long roboticId){
-        String json=configService.selectConfigByKey("add.wx");
-        AddWxConfig bean = JSONUtil.toBean(json, AddWxConfig.class);
-        Integer addWxNum = bean.getDayAddNum();
         // 任务详情
 //        CompanyVoiceRobotic robotic = getById(roboticId);
         // 拨打电话列表
@@ -298,6 +298,9 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         if(calleesList.isEmpty()) return;
         // 分配任务列表
         List<CompanyVoiceRoboticWx> roboticWxList = companyVoiceRoboticWxMapper.selectByRoboticId(roboticId);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(PubFun.listToNewList(roboticWxList, CompanyVoiceRoboticWx::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountList, CompanyWxAccount::getId);
+        roboticWxList.forEach(e -> e.setAccount(accountMap.get(e.getAccountId())));
         // 客户列表
         List<CrmCustomer> customerList = crmCustomerMapper.selectCrmCustomerListByIds(calleesList.stream().map(e -> e.getUserId().toString()).collect(Collectors.joining(",")));
         Map<String, List<CrmCustomer>> customerMap = PubFun.listToMapByGroupList(customerList, CrmCustomer::getIntention);
@@ -312,7 +315,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return v.stream().map(e -> {
                 CompanyWxClient companyWxClient = new CompanyWxClient();
                 // 绑定销售
-                bindCompany(companyWxClient, wxList, addWxNum);
+                bindCompany(companyWxClient, wxList);
                 // 任务ID
                 companyWxClient.setRoboticId(roboticId);
                 // 客户名称
@@ -334,17 +337,17 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     @Override
     public void addCompany(AddWxClientVo vo) {
-        String json=configService.selectConfigByKey("add.wx");
-        AddWxConfig bean = JSONUtil.toBean(json, AddWxConfig.class);
-        Integer addWxNum = bean.getDayAddNum();
         // 任务详情
         List<RoboticWxAccountVo> qwUserList = vo.getQwUserList();
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(PubFun.listToNewList(qwUserList, RoboticWxAccountVo::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountList, CompanyWxAccount::getId);
         List<CompanyVoiceRoboticWx> collect = qwUserList.stream().map(e -> {
             CompanyVoiceRoboticWx entity = new CompanyVoiceRoboticWx();
             entity.setIntention(e.getIntention());
             entity.setRoboticId(vo.getRoboticId());
             entity.setAccountId(e.getAccountId());
             entity.setWxDialogId(e.getWxDialogId());
+            entity.setAccount(accountMap.get(e.getAccountId()));
             entity.setNum(0);
             return entity;
         }).collect(Collectors.toList());
@@ -359,7 +362,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             // 对应销售账号列表
             List<CompanyVoiceRoboticWx> wxList = qwMap.get(k);
             // 组装任务数据
-            v.forEach(e -> bindCompany(e, wxList, addWxNum));
+            v.forEach(e -> bindCompany(e, wxList));
         });
         companyVoiceRoboticWxService.updateBatchById(collect);
         companyWxClientServiceImpl.updateBatchById(list);
@@ -370,16 +373,30 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     }
 
+    @Override
+    public void taskRun(Long id) {
+        CompanyVoiceRobotic robotic = getById(id);
+        robotic.setTaskStatus(1);
+        updateById(robotic);
+    }
+
 
     // 绑定销售
-    private static void bindCompany(CompanyWxClient client, List<CompanyVoiceRoboticWx> wxList, Integer addWxNum) {
-        List<CompanyVoiceRoboticWx> wx = wxList.stream().filter(f -> f.getNum() < addWxNum).collect(Collectors.toList());
+    private void bindCompany(CompanyWxClient client, List<CompanyVoiceRoboticWx> wxList) {
+        List<CompanyVoiceRoboticWx> wx = wxList.stream().filter(f -> f.getAccount() != null && f.getAccount().getAllocateNum() < f.getAccount().getAddNum()).collect(Collectors.toList());
         // 绑定销售,添加值达到阈值后设置为空,等待下次绑定
         if (!wx.isEmpty()) {
             CompanyVoiceRoboticWx companyVoiceRoboticWx = wx.get(0);
             companyVoiceRoboticWx.setNum(companyVoiceRoboticWx.getNum() + 1);
             client.setDialogId(companyVoiceRoboticWx.getWxDialogId());
+            client.setAccountId(companyVoiceRoboticWx.getAccountId());
             client.setRoboticWxId(companyVoiceRoboticWx.getId());
+            client.setCompanyId(companyVoiceRoboticWx.getAccount().getCompanyId());
+            client.setCompanyUserId(companyVoiceRoboticWx.getAccount().getCompanyUserId());
+            CompanyWxAccount account = new CompanyWxAccount();
+            account.setId(companyVoiceRoboticWx.getAccount().getId());
+            account.setAllocateNum(companyVoiceRoboticWx.getAccount().getAllocateNum() + 1);
+            companyWxAccountService.updateById(account);
         }
     }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java

@@ -223,4 +223,9 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
             companyWxUserMapper.updateCompanyWxUser(companyWx);
         }
     }
+
+    @Override
+    public List<CompanyWxClient> getAddWxList(List<Long> accountIdList) {
+        return baseMapper.getAddWxList(accountIdList);
+    }
 }

+ 32 - 5
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -9,14 +9,18 @@ import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyWxAccountService;
 import com.fs.wxcid.domain.CidIpadServerUser;
+import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.dto.friend.ContactListResponse;
-import com.fs.wxcid.dto.login.LoginStatusData;
+import com.fs.wxcid.dto.friend.GetContactDetailsResponseData;
 import com.fs.wxcid.dto.login.LoginStatusResponseData;
+import com.fs.wxcid.dto.message.UserNameWrapper;
 import com.fs.wxcid.dto.user.UserInfo;
 import com.fs.wxcid.dto.user.UserInfoExt;
 import com.fs.wxcid.dto.user.UserProfileData;
@@ -24,15 +28,13 @@ import com.fs.wxcid.mapper.wx.ModContactDbMapper;
 import com.fs.wxcid.service.*;
 import com.fs.wxcid.service.impl.LoginServiceImpl;
 import com.fs.wxcid.service.impl.UserServiceImpl;
+import com.fs.wxcid.vo.VerifyUserValidTicketListVo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -50,6 +52,8 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Autowired
     private CompanyWxAccountMapper companyWxAccountMapper;
     @Autowired
+    private CompanyWxClientMapper companyWxClientMapper;
+    @Autowired
     private LoginServiceImpl loginService;
     @Autowired
     private ICompanyService companyService;
@@ -71,6 +75,8 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     private RedisCacheT<List<String>> friendListRedis;
     @Autowired
     private ModContactDbMapper modContactDbMapper;
+    @Autowired
+    private IWxContactService wxContactService;
 
     /**
      * 查询企微账号
@@ -278,4 +284,25 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         friendListRedis.setCacheObject(key, friendList);
         updateById(account);
     }
+
+    @Override
+    public void isCheckContact(String formUser, Long accountId) {
+        GetContactDetailsResponseData details = friendService.getContactDetailsList(accountId, Collections.singletonList(formUser));
+        details.getContactList().forEach(e -> {
+            String v3 = e.getEncryptUserName();
+//            String v4 = null;
+//            Optional<VerifyUserValidTicketListVo> optional = details.getVerifyUserValidTicketList().stream().filter(v -> formUser.equals(v.getUsername())).findAny();
+//            if(optional.isPresent()){
+//                v4 = optional.get().getAntispamticket();
+//            }
+            CompanyWxClient companyWxClient = companyWxClientMapper.selectWx(accountId, v3);
+            companyWxClient.setIsAdd(1);
+            companyWxClient.setWxNo(e.getAlias());
+            companyWxClient.setWxName(e.getNickName().getStr());
+            companyWxClient.setAvatar(e.getBigHeadImgUrl());
+            companyWxClient.setSuccessAddTime(LocalDateTime.now());
+            companyWxClientMapper.updateById(companyWxClient);
+            wxContactService.add(companyWxClient.getCustomerId(), companyWxClient.getPhone(), accountId, e);
+        });
+    }
 }

+ 81 - 0
fs-service/src/main/java/com/fs/company/util/ObjectPlaceholderResolver.java

@@ -0,0 +1,81 @@
+package com.fs.company.util;
+
+import org.springframework.stereotype.Component;
+import java.lang.reflect.Method;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 基于Java对象的文本占位符替换工具类
+ * 支持替换 ${属性名} 或 ${属性名:默认值} 格式的占位符
+ */
+@Component
+public class ObjectPlaceholderResolver {
+
+    // 正则匹配 ${xxx} 或 ${xxx:默认值} 格式的占位符
+    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}:]+)(?::([^}]*))?\\}");
+
+    /**
+     * 替换文本中的占位符为指定对象的属性值
+     * @param targetObject 提供属性值的目标对象(不能为null)
+     * @param text 包含占位符的原始文本
+     * @return 替换后的文本
+     */
+    public String resolvePlaceholders(Object targetObject, String text) {
+        // 容错:对象为空或文本为空,直接返回原文本
+        if (targetObject == null || text == null || text.isEmpty()) {
+            return text;
+        }
+
+        Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
+        StringBuffer result = new StringBuffer();
+
+        while (matcher.find()) {
+            // 提取占位符中的属性名(如 ${name} → name)
+            String propName = matcher.group(1);
+            // 提取默认值(如 ${hobby:打篮球} → 打篮球)
+            String defaultValue = matcher.group(2);
+
+            // 核心:通过反射获取对象的属性值
+            Object propValue = getPropertyValue(targetObject, propName);
+
+            // 确定最终替换值:有属性值→用属性值 → 无则用默认值 → 都无则保留原占位符
+            String replaceValue = null;
+            if (propValue != null) {
+                replaceValue = propValue.toString();
+            } else if (defaultValue != null) {
+                replaceValue = defaultValue;
+            } else {
+                replaceValue = matcher.group(0); // 保留原占位符
+            }
+
+            // 替换(处理特殊字符,避免替换失败)
+            matcher.appendReplacement(result, Matcher.quoteReplacement(replaceValue));
+        }
+        matcher.appendTail(result);
+
+        return result.toString();
+    }
+
+    /**
+     * 反射获取对象的属性值(通过Getter方法)
+     * 规则:属性名name → Getter方法getName()
+     * @param obj 目标对象
+     * @param propName 属性名
+     * @return 属性值(不存在则返回null)
+     */
+    private Object getPropertyValue(Object obj, String propName) {
+        try {
+            // 拼接Getter方法名:首字母大写,比如 name → getName
+            String methodName = "get" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
+            // 获取Getter方法
+            Method getterMethod = obj.getClass().getMethod(methodName);
+            // 执行方法获取属性值(设置可访问,兼容私有方法)
+            getterMethod.setAccessible(true);
+            return getterMethod.invoke(obj);
+        } catch (Exception e) {
+            // 捕获所有反射异常(方法不存在、执行失败等),返回null
+            return null;
+        }
+    }
+}

+ 17 - 0
fs-service/src/main/java/com/fs/company/vo/SendMsgVo.java

@@ -0,0 +1,17 @@
+package com.fs.company.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SendMsgVo {
+
+    private Integer txt;
+    private Integer img;
+
+}

+ 77 - 0
fs-service/src/main/java/com/fs/course/config/WxConfig.java

@@ -0,0 +1,77 @@
+package com.fs.course.config;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalTime;
+import java.util.List;
+
+@Data
+public class WxConfig implements Serializable {
+    /**
+     *新账号判断时间
+     */
+    private Integer newAccountTime;
+    /**
+     * 新账号每天添加数量
+     */
+    private Integer newAccountAddNum;
+    /**
+     * 普通账号每天添加数量
+     */
+    private Integer accountAddMin;
+    /**
+     * 普通账号每天添加数量
+     */
+    private Integer accountAddMax;
+    /**
+     * 文字消息(条/分)
+     */
+    private Integer txtMsgMinNum;
+    /**
+     * 文字消息(条/分)
+     */
+    private Integer txtMsgMaxNum;
+    /**
+     * 图片消息(条/分)
+     */
+    private Integer imgMsgMinNum;
+    /**
+     * 图片消息(条/分)
+     */
+    private Integer imgMsgMaxNum;
+    /**
+     * 新号添加好友频率(条/分)
+     */
+    private Integer newAccountAddWxMax;
+    /**
+     * 新号添加好友频率(条/分)
+     */
+    private Integer newAccountAddWxMin;
+    /**
+     * 新号消息发送频率(条/秒)
+     */
+    private Integer newAccountSendMsgMax;
+    /**
+     * 新号消息发送频率(条/秒)
+     */
+    private Integer newAccountSendMsgMin;
+    /**
+     * 添加好友频率(条/分)
+     */
+    private Integer accountAddWxMax;
+    /**
+     * 添加好友频率(条/分)
+     */
+    private Integer accountAddWxMin;
+    /**
+     * 消息发送频率(条/秒)
+     */
+    private Integer accountSendMsgMax;
+    /**
+     * 消息发送频率(条/秒)
+     */
+    private Integer accountSendMsgMin;
+}

+ 78 - 0
fs-service/src/main/java/com/fs/wxcid/domain/WxContact.java

@@ -0,0 +1,78 @@
+package com.fs.wxcid.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 个微联系人对象 wx_contact
+ *
+ * @author fs
+ * @date 2025-12-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxContact extends BaseEntityTow {
+
+    /** crm客户ID */
+    private Long id;
+
+    /** 微信ID */
+    @Excel(name = "微信ID")
+    private String userName;
+
+    /** 微信昵称 */
+    @Excel(name = "微信昵称")
+    private String nickName;
+
+    /** 全拼 */
+    @Excel(name = "全拼")
+    private String quanPin;
+
+    /** 性别 */
+    @Excel(name = "性别")
+    private Integer sex;
+
+    /** 微信号 */
+    @Excel(name = "微信号")
+    private String alias;
+
+    /** 电话号码 */
+    @Excel(name = "电话号码")
+    private String phone;
+
+    /** 头像 */
+    @Excel(name = "头像")
+    private String headImgUrl;
+
+    /** 微信V3 */
+    @Excel(name = "微信V3")
+    private String encryptUserName;
+
+    /** 所在地区 */
+    @Excel(name = "所在地区")
+    private String province;
+
+    /** 所在地区 */
+    @Excel(name = "所在地区")
+    private String city;
+
+    /** 加微的账号ID */
+    @Excel(name = "加微的账号ID")
+    private Long accountId;
+
+    /** 销售公司ID */
+    @Excel(name = "销售公司ID")
+    private Long companyId;
+
+    /** 销售人员ID */
+    @Excel(name = "销售人员ID")
+    private Long companyUserId;
+
+    private Long customerId;
+
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/ContactItem.java

@@ -28,6 +28,9 @@ public class ContactItem {
     @JSONField(name = "country")
     private String country;
 
+    @JSONField(name = "city")
+    private String city;
+
     @JSONField(name = "bigHeadImgUrl")
     private String bigHeadImgUrl;
 
@@ -43,5 +46,8 @@ public class ContactItem {
     @JSONField(name = "pyinitial")
     private UserNameWrapper pyinitial;
 
+    @JSONField(name = "encryptUserName")
+    private String encryptUserName;
+
     // 可根据需要补充其他字段...
 }

+ 4 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/GetContactDetailsResponseData.java

@@ -1,6 +1,7 @@
 package com.fs.wxcid.dto.friend;
 
 import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.vo.VerifyUserValidTicketListVo;
 import lombok.Data;
 
 import java.util.List;
@@ -19,4 +20,7 @@ public class GetContactDetailsResponseData {
 
     @JSONField(name = "contactList")
     private List<ContactItem> contactList;
+
+    @JSONField(name = "verifyUserValidTicketList")
+    private List<VerifyUserValidTicketListVo> verifyUserValidTicketList;
 }

+ 61 - 0
fs-service/src/main/java/com/fs/wxcid/mapper/WxContactMapper.java

@@ -0,0 +1,61 @@
+package com.fs.wxcid.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.wxcid.domain.WxContact;
+
+/**
+ * 个微联系人Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-24
+ */
+public interface WxContactMapper extends BaseMapper<WxContact>{
+    /**
+     * 查询个微联系人
+     * 
+     * @param id 个微联系人主键
+     * @return 个微联系人
+     */
+    WxContact selectWxContactById(Long id);
+
+    /**
+     * 查询个微联系人列表
+     * 
+     * @param wxContact 个微联系人
+     * @return 个微联系人集合
+     */
+    List<WxContact> selectWxContactList(WxContact wxContact);
+
+    /**
+     * 新增个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    int insertWxContact(WxContact wxContact);
+
+    /**
+     * 修改个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    int updateWxContact(WxContact wxContact);
+
+    /**
+     * 删除个微联系人
+     * 
+     * @param id 个微联系人主键
+     * @return 结果
+     */
+    int deleteWxContactById(Long id);
+
+    /**
+     * 批量删除个微联系人
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxContactByIds(Long[] ids);
+}

+ 10 - 0
fs-service/src/main/java/com/fs/wxcid/service/FriendService.java

@@ -4,8 +4,10 @@ package com.fs.wxcid.service;
 
 import com.fs.wxcid.dto.common.BaseResponse;
 import com.fs.wxcid.dto.friend.*;
+import com.fs.wxcid.vo.AddContactVo;
 import com.fs.wxcid.vo.VerifyUserVo;
 
+import javax.validation.constraints.NotBlank;
 import java.util.List;
 
 /**
@@ -50,4 +52,12 @@ public interface FriendService {
      */
     BaseResponse verifyUser(Long accountId, VerifyUserVo vo);
     ContactListResponse getContactListNotKey(Long accountId);
+
+    /**
+     * 添加联系人
+     * @param id     账号ID
+     * @param mobile 手机号
+     * @param txt 招呼内容
+     */
+    AddContactVo addContact(Long id, String mobile, String txt);
 }

+ 64 - 0
fs-service/src/main/java/com/fs/wxcid/service/IWxContactService.java

@@ -0,0 +1,64 @@
+package com.fs.wxcid.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.dto.friend.ContactItem;
+
+/**
+ * 个微联系人Service接口
+ * 
+ * @author fs
+ * @date 2025-12-24
+ */
+public interface IWxContactService extends IService<WxContact>{
+    /**
+     * 查询个微联系人
+     * 
+     * @param id 个微联系人主键
+     * @return 个微联系人
+     */
+    WxContact selectWxContactById(Long id);
+
+    /**
+     * 查询个微联系人列表
+     * 
+     * @param wxContact 个微联系人
+     * @return 个微联系人集合
+     */
+    List<WxContact> selectWxContactList(WxContact wxContact);
+
+    /**
+     * 新增个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    int insertWxContact(WxContact wxContact);
+
+    /**
+     * 修改个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    int updateWxContact(WxContact wxContact);
+
+    /**
+     * 批量删除个微联系人
+     * 
+     * @param ids 需要删除的个微联系人主键集合
+     * @return 结果
+     */
+    int deleteWxContactByIds(Long[] ids);
+
+    /**
+     * 删除个微联系人信息
+     * 
+     * @param id 个微联系人主键
+     * @return 结果
+     */
+    int deleteWxContactById(Long id);
+
+    void add(Long customerId, String phone, Long accountId, ContactItem e);
+}

File diff suppressed because it is too large
+ 4 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/FriendServiceImpl.java


+ 121 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/WxContactServiceImpl.java

@@ -0,0 +1,121 @@
+package com.fs.wxcid.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.wxcid.dto.friend.ContactItem;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import com.fs.wxcid.mapper.WxContactMapper;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.service.IWxContactService;
+
+/**
+ * 个微联系人Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-24
+ */
+@Service
+@AllArgsConstructor
+public class WxContactServiceImpl extends ServiceImpl<WxContactMapper, WxContact> implements IWxContactService {
+
+    private final CompanyWxAccountMapper companyWxAccountMapper;
+
+    /**
+     * 查询个微联系人
+     * 
+     * @param id 个微联系人主键
+     * @return 个微联系人
+     */
+    @Override
+    public WxContact selectWxContactById(Long id)
+    {
+        return baseMapper.selectWxContactById(id);
+    }
+
+    /**
+     * 查询个微联系人列表
+     * 
+     * @param wxContact 个微联系人
+     * @return 个微联系人
+     */
+    @Override
+    public List<WxContact> selectWxContactList(WxContact wxContact)
+    {
+        return baseMapper.selectWxContactList(wxContact);
+    }
+
+    /**
+     * 新增个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    @Override
+    public int insertWxContact(WxContact wxContact)
+    {
+        wxContact.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxContact(wxContact);
+    }
+
+    /**
+     * 修改个微联系人
+     * 
+     * @param wxContact 个微联系人
+     * @return 结果
+     */
+    @Override
+    public int updateWxContact(WxContact wxContact)
+    {
+        wxContact.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxContact(wxContact);
+    }
+
+    /**
+     * 批量删除个微联系人
+     * 
+     * @param ids 需要删除的个微联系人主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxContactByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxContactByIds(ids);
+    }
+
+    /**
+     * 删除个微联系人信息
+     * 
+     * @param id 个微联系人主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxContactById(Long id)
+    {
+        return baseMapper.deleteWxContactById(id);
+    }
+
+    @Override
+    public void add(Long customerId, String phone, Long accountId, ContactItem e) {
+        CompanyWxAccount account = companyWxAccountMapper.selectById(accountId);
+        WxContact wxContact = new WxContact();
+        wxContact.setUserName(e.getUserName().getStr());
+        wxContact.setNickName(e.getNickName().getStr());
+        wxContact.setQuanPin(e.getQuanPin().getStr());
+        wxContact.setSex(e.getSex());
+        wxContact.setAlias(e.getAlias());
+        wxContact.setPhone(phone);
+        wxContact.setHeadImgUrl(e.getBigHeadImgUrl());
+        wxContact.setEncryptUserName(e.getEncryptUserName());
+        wxContact.setProvince(e.getProvince());
+        wxContact.setCity(e.getCity());
+        wxContact.setAccountId(accountId);
+        wxContact.setCompanyId(account.getCompanyId());
+        wxContact.setCompanyUserId(account.getCompanyUserId());
+        wxContact.setCustomerId(customerId);
+        save(wxContact);
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/wxcid/vo/AddContactVo.java

@@ -0,0 +1,12 @@
+package com.fs.wxcid.vo;
+
+import lombok.Data;
+
+@Data
+public class AddContactVo {
+
+    private boolean success;
+    private String v3;
+    private String v4;
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/wxcid/vo/VerifyUserValidTicketListVo.java

@@ -0,0 +1,16 @@
+package com.fs.wxcid.vo;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.dto.message.UserNameWrapper;
+import lombok.Data;
+
+@Data
+public class VerifyUserValidTicketListVo {
+
+    @JSONField(name = "username")
+    private String username;
+    @JSONField(name = "antispamticket")
+    private String antispamticket;
+
+
+}

+ 1 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

@@ -27,7 +27,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectCompanyVoiceRoboticVo">
-        select id, name, task_name, task_id, add_type, robot, dialog_id, mode, multiplier, auto_recall, recall_times, cid_group_id, week_day1, start_time1, end_time1, week_day2, start_time2, end_time2, create_time, create_user from company_voice_robotic
+        select * from company_voice_robotic
     </sql>
 
     <select id="selectCompanyVoiceRoboticList" parameterType="CompanyVoiceRobotic" resultMap="CompanyVoiceRoboticResult">

+ 10 - 0
fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml

@@ -153,4 +153,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_wx_account b on a.account_id = b.id
         where b.company_user_id in <foreach collection="ids" open="(" separator="," close=")" item="item">#{item}</foreach>
     </select>
+    <select id="getAddWxList" resultType="com.fs.company.domain.CompanyWxClient">
+        SELECT * FROM company_wx_client where is_add = 0 and account_id is not null
+        <if test="accountIdList">
+            and account_id in <foreach collection="accountIdList" open="(" separator="," close=")" item="item">#{item}</foreach>
+        </if>
+        group by account_id
+    </select>
+    <select id="selectWx" resultType="com.fs.company.domain.CompanyWxClient">
+        select * from company_wx_client where account_id = #{accountId} and wx_v3 = #{v3}
+    </select>
 </mapper>

+ 136 - 0
fs-service/src/main/resources/mapper/wxcid/WxContactMapper.xml

@@ -0,0 +1,136 @@
+<?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.wxcid.mapper.WxContactMapper">
+    
+    <resultMap type="WxContact" id="WxContactResult">
+        <result property="id"    column="id"    />
+        <result property="userName"    column="user_name"    />
+        <result property="nickName"    column="nick_name"    />
+        <result property="quanPin"    column="quan_pin"    />
+        <result property="sex"    column="sex"    />
+        <result property="alias"    column="alias"    />
+        <result property="phone"    column="phone"    />
+        <result property="headImgUrl"    column="head_img_url"    />
+        <result property="encryptUserName"    column="encrypt_user_name"    />
+        <result property="province"    column="province"    />
+        <result property="city"    column="city"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxContactVo">
+        select id, user_name, nick_name, quan_pin, sex, alias, phone, head_img_url, encrypt_user_name, province, city, account_id, company_id, company_user_id, create_time, create_by, update_time, update_by, remark from wx_contact
+    </sql>
+
+    <select id="selectWxContactList" parameterType="WxContact" resultMap="WxContactResult">
+        <include refid="selectWxContactVo"/>
+        <where>  
+            <if test="userName != null  and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
+            <if test="nickName != null  and nickName != ''"> and nick_name like concat('%', #{nickName}, '%')</if>
+            <if test="quanPin != null  and quanPin != ''"> and quan_pin = #{quanPin}</if>
+            <if test="sex != null "> and sex = #{sex}</if>
+            <if test="alias != null  and alias != ''"> and alias = #{alias}</if>
+            <if test="phone != null  and phone != ''"> and phone = #{phone}</if>
+            <if test="headImgUrl != null  and headImgUrl != ''"> and head_img_url = #{headImgUrl}</if>
+            <if test="encryptUserName != null  and encryptUserName != ''"> and encrypt_user_name like concat('%', #{encryptUserName}, '%')</if>
+            <if test="province != null  and province != ''"> and province = #{province}</if>
+            <if test="city != null  and city != ''"> and city = #{city}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+        </where>
+    </select>
+    
+    <select id="selectWxContactById" parameterType="Long" resultMap="WxContactResult">
+        <include refid="selectWxContactVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertWxContact" parameterType="WxContact" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_contact
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userName != null">user_name,</if>
+            <if test="nickName != null">nick_name,</if>
+            <if test="quanPin != null">quan_pin,</if>
+            <if test="sex != null">sex,</if>
+            <if test="alias != null">alias,</if>
+            <if test="phone != null">phone,</if>
+            <if test="headImgUrl != null">head_img_url,</if>
+            <if test="encryptUserName != null">encrypt_user_name,</if>
+            <if test="province != null">province,</if>
+            <if test="city != null">city,</if>
+            <if test="accountId != null">account_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userName != null">#{userName},</if>
+            <if test="nickName != null">#{nickName},</if>
+            <if test="quanPin != null">#{quanPin},</if>
+            <if test="sex != null">#{sex},</if>
+            <if test="alias != null">#{alias},</if>
+            <if test="phone != null">#{phone},</if>
+            <if test="headImgUrl != null">#{headImgUrl},</if>
+            <if test="encryptUserName != null">#{encryptUserName},</if>
+            <if test="province != null">#{province},</if>
+            <if test="city != null">#{city},</if>
+            <if test="accountId != null">#{accountId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxContact" parameterType="WxContact">
+        update wx_contact
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userName != null">user_name = #{userName},</if>
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="quanPin != null">quan_pin = #{quanPin},</if>
+            <if test="sex != null">sex = #{sex},</if>
+            <if test="alias != null">alias = #{alias},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="headImgUrl != null">head_img_url = #{headImgUrl},</if>
+            <if test="encryptUserName != null">encrypt_user_name = #{encryptUserName},</if>
+            <if test="province != null">province = #{province},</if>
+            <if test="city != null">city = #{city},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxContactById" parameterType="Long">
+        delete from wx_contact where id = #{id}
+    </delete>
+
+    <delete id="deleteWxContactByIds" parameterType="String">
+        delete from wx_contact where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 5 - 1
fs-wx-api/src/main/java/com/fs/app/controller/WebscoketServer.java

@@ -106,7 +106,11 @@ public class WebscoketServer {
         public void onMessage(String message) {
             WxCallbackVo callbackVo = JSON.parseObject(message, WxCallbackVo.class);
             log.info("📩 收到第三方 WebSocket 消息:{}", callbackVo);
-            String formUser = callbackVo.getMessage().getFromUserName().getStr();
+            WxCallbackVo.Message msgVo = callbackVo.getMessage();
+            String formUser = msgVo.getFromUserName().getStr();
+            if(msgVo.getMsgType() == 10000 && "以上是打招呼的消息".equals(msgVo.getContent().getStr())){
+                companyWxAccountService.isCheckContact(formUser, account.getId());
+            }
             wxMsgLogService.insertLog(callbackVo, account, !formUser.equals(account.getWxNo()) ? 0 : 1);
         }
 

+ 1 - 1
fs-wx-task/src/main/java/com/fs/FsWxTaskApplication.java

@@ -19,6 +19,6 @@ public class FsWxTaskApplication
     public static void main(String[] args){
         // System.setProperty("spring.devtools.restart.enabled", "false");
         SpringApplication.run(FsWxTaskApplication.class, args);
-        System.out.println("QwTask启动成功");
+        System.out.println("WxTask启动成功");
     }
 }

+ 21 - 497
fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -1,522 +1,46 @@
 package com.fs.app.controller;
 
 
-import cn.hutool.core.date.DateUtil;
-import com.alibaba.fastjson.JSON;
-import com.fs.app.task.qwTask;
-import com.fs.app.taskService.*;
+import com.fs.app.service.WxTaskService;
 import com.fs.common.core.domain.R;
-import com.fs.common.core.domain.ResponseResult;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.common.utils.StringUtils;
-import com.fs.company.service.ICompanyService;
-import com.fs.company.vo.RedPacketMoneyVO;
-import com.fs.course.mapper.FsCourseRedPacketLogMapper;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
-import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
-import com.fs.course.service.*;
-import com.fs.course.vo.FsUserCourseVideoQVO;
-import com.fs.his.domain.FsUser;
-import com.fs.his.service.IFsInquiryOrderService;
-import com.fs.his.utils.qrcode.QRCodeUtils;
-import com.fs.qw.domain.QwCompany;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwIpadServerLog;
-import com.fs.qw.domain.QwUser;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwUserMapper;
-import com.fs.qw.service.*;
-import com.fs.qwApi.domain.QwExternalContactResult;
-import com.fs.qwApi.service.QwApiService;
-import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.sop.mapper.QwSopMapper;
-import com.fs.sop.mapper.SopUserLogsMapper;
-import com.fs.sop.service.*;
-import com.fs.sop.vo.QwSopLogsDoSendListTVO;
-import com.fs.store.service.IFsUserCourseCountService;
-import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
-import com.fs.wxwork.service.WxWorkService;
+import com.fs.company.service.ICompanyWxAccountService;
 import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-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.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
-import com.fs.app.task.qwTask;
 
-import java.time.Duration;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
 
+@Slf4j
 @Api("公共接口")
 @RestController
+@AllArgsConstructor
 @RequestMapping(value="/app/common")
-@Slf4j
 public class CommonController {
 
-    @Autowired
-    private SopLogsTaskService service;
-    @Autowired
-    private IFsUserCourseVideoService courseVideoService;
-    @Autowired
-    private SopLogsTaskService sopLogsTaskService;
-    @Autowired
-    private SopWxLogsService sopWxLogsService;
-    @Autowired
-    private IQwExternalContactService qwExternalContactService;
-    @Autowired
-    private qwTask qwTask1;
-    @Autowired
-    private IFsUserVideoService fsUserVideoService;
-    @Autowired
-    private IHuaweiObsService huaweiObsService;
-    @Autowired
-    private IFsCourseWatchLogService watchLogService;
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-    @Autowired
-    private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
-
-    @Autowired
-    private IQwSopLogsService qwSopLogsService;
-
-    @Autowired
-    private QwSopMapper qwSopMapper;
-
-    @Autowired
-    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-
-    @Autowired
-    private IFsCourseLinkService courseLinkService;
-    @Autowired
-    private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
-    @Autowired
-    private ICompanyService companyService;
-
-    @Autowired
-    private SopUserLogsMapper sopUserLogsMapper;
-
-    @Autowired
-    private QwSopLogsMapper qwSopLogsMapper;
-    @Autowired
-    private IQwSopTempRulesService tempRulesService;
-    @Autowired
-    private IQwSopTempVoiceService qwSopTempVoiceService;
-
-    @Autowired
-    private QwExternalContactRatingService qwExternalContactRatingService;
-
-    @Autowired
-    private ISopUserLogsService iSopUserLogsService;
-
-    @Autowired
-    private IFsUserCourseCountService userCourseCountService;
-
-    @Autowired
-    private ISopUserLogsInfoService iSopUserLogsInfoService;
-
-    @Autowired
-    private IFsInquiryOrderService inquiryOrderService;
-
-    @Autowired
-    private IQwMaterialService iQwMaterialService;
-
-    @Autowired
-    private IFsCourseLinkService iFsCourseLinkService;
-
-    @Autowired
-    private SyncQwExternalContactService syncQwExternalContactService;
-    @Autowired
-    private IFsUserCourseVideoService fsUserCourseVideoService;
-
-    @Autowired
-    public RedisCache redisCache;
-
-    @Autowired
-    private QwUserMapper qwUserMapper;
-
-
-    @Autowired
-    IQwIpadServerService ipadServerService;
-
-    @Autowired
-    IQwIpadServerLogService qwIpadServerLogService;
-    @Autowired
-    IQwIpadServerUserService qwIpadServerUserService;
-
-    @Autowired
-    IQwExternalContactService externalContactService;
-    @Autowired
-    WxWorkService wxWorkService;
-
-    /**
-     *
-     */
-    @GetMapping("/selectQwUserByTest")
-    public void selectQwUserByTest() {
-        try {
-            List<QwUser> list = qwUserMapper.selectQwUserByTest();
-            for (QwUser qwUser : list) {
-                try {
-
-                     Long serverId = qwUser.getServerId();
-
-                    if (serverId==null){
-                        System.out.println("serverId不存在");
-                    }else {
-                        //没绑定销售 或者 已经离职
-                        if (qwUser.getStatus()==0 || qwUser.getIsDel()==2){
-
-                            updateIpadStatus(qwUser,serverId);
-                        }
-
-                        //绑定了销售-也绑定了ipad,但是长时间离线的(离线状态,无操作超过2天的,也自动解绑)
-                        if(qwUser.getUpdateTime()!=null){
-                            Date createTime = qwUser.getUpdateTime();
-                            Integer serverStatus = qwUser.getServerStatus();
-                            Integer ipadStatus = qwUser.getIpadStatus();
-
-                            boolean result = isCreateTimeMoreThanDaysWithOptional(createTime, 2);
-                            //大于2天 ,绑定了ipad,离线
-                            if(result && serverStatus==1 && ipadStatus==0){
-                                updateIpadStatus(qwUser,serverId);
-
-                            }
-                        }
-
-
-                    }
-
-
-                } catch (Exception e) {
-                    System.out.println("解绑ipad报错"+e);
-
-                }
-            }
-        } catch (Exception e) {
-            log.error("定时处理未绑定员工企微异常",e);
-        }
-
-    }
-
-
-    public void updateIpadStatus(QwUser qwUser,Long serverId){
-        QwUser u = new QwUser();
-        u.setId(qwUser.getId());
-        u.setServerId(null);
-        u.setServerStatus(0);
-        qwUserMapper.updateQwUser(u);
-        ipadServerService.addServer(serverId);
-        QwIpadServerLog qwIpadServerLog = new QwIpadServerLog();
-        qwIpadServerLog.setType(2);
-        qwIpadServerLog.setTilie("解绑");
-        qwIpadServerLog.setServerId(serverId);
-        qwIpadServerLog.setQwUserId(qwUser.getId());
-        qwIpadServerLog.setCompanyUserId(qwUser.getCompanyUserId());
-        qwIpadServerLog.setCompanyId(qwUser.getCompanyId());
-        qwIpadServerLog.setCreateTime(new Date());
-        qwIpadServerLogService.insertQwIpadServerLog(qwIpadServerLog);
-        qwIpadServerUserService.deleteQwIpadServerUserByQwUserId(qwUser.getId());
-        WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
-        wxWorkGetQrCodeDTO.setUuid(qwUser.getUid());
-        wxWorkService.LoginOut(wxWorkGetQrCodeDTO,qwUser.getServerId());
-        updateIpadStatus(qwUser.getId(),0);
-    }
-
-    public static boolean isCreateTimeMoreThanDaysWithOptional(Date createTime, int days) {
-        return Optional.ofNullable(createTime)
-                .map(time -> {
-                    LocalDateTime createDateTime = time.toInstant()
-                            .atZone(ZoneId.systemDefault())
-                            .toLocalDateTime();
-                    LocalDateTime now = LocalDateTime.now();
-                    Duration duration = Duration.between(createDateTime, now);
-                    return duration.toDays() > days;
-                })
-                .orElse(false); // 为null时返回false,可根据需求调整
-    }
-
-    void updateIpadStatus(Long id ,Integer status){
-        QwUser u = new QwUser();
-        u.setId(id);
-        u.setIpadStatus(status);
-        qwUserMapper.updateQwUser(u);
-    }
-    /**
-     *
-     */
-    @GetMapping("/countQwApiAopLogToken")
-    public void countQwApiAopLogToken() {
-
-        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-        // 获取当前日期(只包含年月日)
-        LocalDate currentDate = LocalDate.now();
-
-        String todayStr = currentDate.format(dateFormatter);
-        qwSopLogsService.countQwApiAopLogToken(todayStr);
-
-    }
-
-    /**
-     * 查询视频时长
-     */
-    @GetMapping("/getVideoDuration")
-    public Long getVideoDuration(Long videoId) {
-
-            String redisKey = "h5user:video:duration:" + videoId;
-            Long duration = redisCache.getCacheObject(redisKey);
-
-            if (duration == null) {
-                FsUserCourseVideoQVO videoInfo = fsUserCourseVideoService.selectFsUserCourseVideoByVideoIdVO(videoId,null);
-                if (videoInfo == null || videoInfo.getDuration() == null) {
-                    throw new IllegalArgumentException("视频时长信息不存在");
-                }
-                duration = videoInfo.getDuration();
-
-                // 将查询结果缓存到Redis,设置适当过期时间
-                redisCache.setCacheObject(redisKey, duration);
-            }
-
-            return duration;
-
-    }
-
-
-
-    /**
-     * 获取跳转微信小程序的链接地址
-     */
-    @GetMapping("/getGotoWxAppLink")
-    @ApiOperation("获取跳转微信小程序的链接地址")
-    public ResponseResult<String> getGotoWxAppLink(String linkStr,String appid) {
-        return ResponseResult.ok(courseLinkService.getGotoWxAppLink(linkStr,appid));
-    }
-
-    /**
-    * 发官方通连
-    */
-    @GetMapping("/sopguanfanone")
-    public R sopguanfanone(String dateTime) throws Exception {
-
-        LocalDateTime localDateTime = DateUtil.parseLocalDateTime(dateTime);
-
-        int currentHour = localDateTime.getHour();
-        LocalDate localDate = localDateTime.toLocalDate();
-
-        String taskStartTime = localDate.atTime(currentHour, 0, 0)
-                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
-        String taskEndTime = localDate.atTime(currentHour, 59, 59)
-                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
-
-        qwSopLogsService.createCorpMassSendingByUserLogs( taskStartTime, taskEndTime);
-        return R.ok();
-    }
-
-    /**
-    * 发一对一
-    */
-    @GetMapping("/sopguanfantwo")
-    public R sopguanfantwo(String dateTime) throws Exception {
-
-        LocalDateTime localDateTime = DateUtil.parseLocalDateTime(dateTime);
-
-
-        LocalDate localDate = localDateTime.toLocalDate();
-        String date = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-
-        qwSopLogsService.createCorpMassSending(date);
-        return R.ok();
-    }
-
-    /**
-     * 查官方的执行结果
-     */
-    @GetMapping("/sopguanfantResult")
-    public R sopguanfantResult() throws Exception {
-        qwSopLogsService.qwSopLogsResultNew();
-        return R.ok();
-    }
-
-
-    @GetMapping("/testMaterial")
-    public void testMaterial() throws Exception {
-
-        iQwMaterialService.updateQwMaterialByQw();
+    private final WxTaskService taskService;
+    private final ICompanyWxAccountService companyWxAccountService;
 
+    @GetMapping("initAccountNum")
+    public void initAccountNum(){
+        taskService.initAccountNum();
     }
 
-    @GetMapping("/testSop")
-    public R testSop() throws Exception {
-
-        return iFsCourseLinkService.getWxaCodeGenerateScheme("/pages_course/video.html?course={\"companyId\":100,\"companyUserId\":2020,\"corpId\":\"wweb0666cc79d79da5\",\"courseId\":61,\"link\":\"1950497651577323520\",\"linkType\":3,\"qwExternalId\":2356946,\"qwUserId\":\"1682\",\"uNo\":\"b8b010e1-ee0f-42ec-8ad8-06681d1b449a\",\"videoId\":366}","wx34bba1ae94d34986");
+    @GetMapping("initAccountMsg")
+    public void initAccountMsg(){
+        taskService.initAccountMsg();
     }
 
-    @GetMapping("/testRatingSop")
-    public R testRatingSop(String sopId) throws Exception {
-
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 sop营期-用户分级 ======");
-
-        iSopUserLogsService.ratingUserLogs(sopId);
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== sop营期-用户分级处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-        return R.ok();
+    @GetMapping("addWx")
+    public void addWx(Long accountId) {
+        taskService.addWx(Collections.singletonList(accountId));
     }
-
-    // 定义一个方法来批量处理插入逻辑,支持每 500 条数据一次的批量插入
-    private void processAndInsertQwSopLogs(List<QwSopLogsDoSendListTVO> logsByJsApiNotExtId) {
-        // 定义批量插入的大小
-        int batchSize = 500;
-
-        // 循环处理外部用户 ID,每次处理批量大小的子集
-        for (int i = 0; i < logsByJsApiNotExtId.size(); i += batchSize) {
-
-            int endIndex = Math.min(i + batchSize, logsByJsApiNotExtId.size());
-            List<QwSopLogsDoSendListTVO> batchList = logsByJsApiNotExtId.subList(i, endIndex);  // 获取当前批次的子集
-
-            // 直接使用批次数据进行批量更新,不需要额外的 List
-            try {
-                qwSopLogsMapper.batchUpdateQwSopLogsBySendTime(batchList);
-            } catch (Exception e) {
-                // 记录异常日志,方便后续排查问题
-                log.error("批量更新数据时发生异常,处理的批次起始索引为: " + i, e);
-            }
-        }
-    }
-
-    @GetMapping("/test")
-    public R test(String time, String sopId) throws Exception {
-        log.info("进入sop任务");
-//        LocalDateTime currentTime = DateUtil.parseLocalDateTime(time);
-//        // 计算下一个整点时间
-//        LocalDateTime nextHourTime = currentTime.withMinute(0).withSecond(0).withNano(0).plusHours(1);
-//
-//        // 打印日志,确认时间
-//        log.info("任务实际执行时间: {}", currentTime);
-//        log.info("传递给任务的时间参数: {}", nextHourTime);
-        List<String> sopidList = new ArrayList<>();
-        if(StringUtils.isNotEmpty(sopId)){
-            sopidList = Arrays.asList(sopId.split(","));
-        }
-        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
-        return R.ok();
-    }
-    @GetMapping("/testWx")
-    public R testWx(String time) throws Exception {
-        sopWxLogsService.wxSopLogsByTime(DateUtil.parseLocalDateTime(time));
-        return R.ok();
-    }
-
-
-    @GetMapping("/testVideo")
-    public R testVideo(String sopId) throws Exception {
-        qwSopTempVoiceService.synchronous(sopId, Arrays.asList(Arrays.asList(2020L, 100L), Arrays.asList(2758L, 170L)));
-        return R.ok();
-    }
-
-    @Autowired
-    IQwCompanyService iQwCompanyService;
-    @GetMapping("/testSop2")
-    public R testSop2() throws Exception {
-
-        String cropId="ww401085d7b785aae8";
-
-        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(cropId);
-
-        String status="100_asddas_6666";
-
-        String url="https://open.weixin.qq.com/connect/oauth2/authorize?appid="+cropId+"&redirect_uri=" +
-                "http://"+qwCompany.getRealmNameUrl()+"/qwh5/pages/user/index?corpId="+cropId +
-                "&response_type=code&scope=snsapi_base&state="+status+"&agentid="+qwCompany.getServerAgentId()+"#wechat_redirect";
-
-        R andUpload = QRCodeUtils.createAndUpload(url);
-
-        return R.ok().put("data",andUpload);
-    }
-
-    @Autowired
-    private QwApiService qwApiService;
-
-    @GetMapping("/testSop3")
-    public R testSop3(String date) throws Exception {
-//        qwSopLogsService.createCorpMassSending(date);
-//        QwGetGroupmsgSendParam qwGetGroupmsgSendParam = new QwGetGroupmsgSendParam();
-//        qwGetGroupmsgSendParam.setMsgid("msg7tWFCgAAjJC-HqurNKsOJif5oUHQiA");
-//        qwGetGroupmsgSendParam.setUserid("ZhangZhanYue");
-//
-//        QwGroupmsgSendResult groupmsgSendResult = qwApiService.getGroupmsgSendResult(qwGetGroupmsgSendParam, "ww5a88c4f879f204c5");
-        return R.ok();
-    }
-
-    @Autowired
-    IQwSopTagService qwSopTagService;
-    @GetMapping("/tag")
-    public R tag() throws Exception {
-        qwSopTagService.addTag();
-        return R.ok();
-    }
-
-
-    @Autowired
-    private SopLogsChatTaskService sopLogsChatTaskService;
-    @GetMapping("/test2")
-    public String selectChatSopUserLogsListByTime() throws Exception {
-        userCourseCountService.insertFsUserCourseCountTask();
-        return "s";
-    }
-    @GetMapping("/isAddkf")
-    public ResponseResult<FsUser> isAddkf(FsUserCourseAddCompanyUserParam param) throws Exception {
-        return courseVideoService.isAddCompanyUser(param);
-    }
-
-    @PostMapping("/updateUrl")
-    public R updateUrl()
-    {
-        log.info("开始更新URL");
-        try {
-            fsUserVideoService.updateVideoUrl();
-            huaweiObsService.uploadByCOS();
-            log.info("更新URL成功完成");
-
-
-        } catch (Exception e) {
-            log.error("开始更新URL执行失败", e);
-        }
-        return R.ok();
-    }
-    @GetMapping("/updateRedPack")
-    public R updateRedPack(String start , String end    ){
-        LocalDateTime startTime = DateUtil.parseLocalDateTime(start);
-        LocalDateTime endTime = DateUtil.parseLocalDateTime(end);
-        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogHourseByCompany(startTime, endTime);
-        for (RedPacketMoneyVO redPacketMoneyVO : redPacketMoneyVOS) {
-            companyService.subtractCompanyMoneyHourse(redPacketMoneyVO.getMoney(),redPacketMoneyVO.getCompanyId(), startTime.toLocalTime(), endTime.toLocalTime());
-        }
-        return R.ok();
-    }
-
-    @GetMapping("/syncQwExternalContactUnionid")
-    public R syncQwExternalContactUnionid(){
-        return syncQwExternalContactService.syncQwExternalContactUnionid();
-    }
-
-
-    @GetMapping("/queryRedPacketResult")
-    public R queryRedPacketResult(String startTime , String  endTime) {
-        fsCourseRedPacketLogService.queryRedPacketResult(startTime, endTime);
-        return R.ok();
-    }
-
-    @GetMapping("/autoPullGroup")
-    public R autoPullGroup(){
-        qwTask1.autoPullGroup();
-        return R.ok();
+    @GetMapping("isCheckContact")
+    public void isCheckContact(String formUser, Long accountId){
+        companyWxAccountService.isCheckContact(formUser, accountId);
     }
 
 }

+ 0 - 19
fs-wx-task/src/main/java/com/fs/app/controller/VoiceController.java

@@ -1,19 +0,0 @@
-package com.fs.app.controller;
-
-
-import io.swagger.annotations.Api;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@Api("公共接口")
-@RestController
-@RequestMapping(value="/app/common/voice")
-@Slf4j
-public class VoiceController {
-    @GetMapping
-    public void voice() {
-
-    }
-}

+ 132 - 0
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -0,0 +1,132 @@
+package com.fs.app.service;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.domain.CompanyWxClient;
+import com.fs.company.domain.CompanyWxDialog;
+import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.company.service.ICompanyWxClientService;
+import com.fs.company.service.ICompanyWxDialogService;
+import com.fs.company.util.ObjectPlaceholderResolver;
+import com.fs.company.vo.SendMsgVo;
+import com.fs.course.config.WxConfig;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.system.service.ISysConfigService;
+import com.fs.wxcid.service.FriendService;
+import com.fs.wxcid.vo.AddContactVo;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class WxTaskService {
+
+    private final ICompanyWxAccountService companyWxAccountService;
+    private final ISysConfigService sysConfigService;
+    private final ICompanyWxClientService companyWxClientService;
+    private final ICompanyWxDialogService companyWxDialogService;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+    private final ObjectPlaceholderResolver objectPlaceholderResolver;
+    private final ICrmCustomerService crmCustomerService;
+    private final FriendService friendService;
+
+    public void addWx(List<Long> accountIdList) {
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        // 需要添加微信的列表
+        List<CompanyWxClient> list = companyWxClientService.getAddWxList(accountIdList);
+        if(list.isEmpty()) return;
+        List<CompanyWxClient> addList = new ArrayList<>();
+        Map<Long, CompanyWxClient> clientMap = PubFun.listToMapByGroupObject(list, CompanyWxClient::getAccountId);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(clientMap.keySet()));
+        List<CompanyWxAccount> addAccountList = accountList.stream().filter(e -> {
+            int newAddWxMinute = RandomUtil.randomInt(config.getNewAccountAddWxMin(), config.getNewAccountAddWxMax());
+            int addWxMinute = RandomUtil.randomInt(config.getAccountAddWxMin(), config.getAccountAddWxMax());
+            if (e.getLastAddWxTime() == null) {
+                return true;
+            }
+            int minute = addWxMinute;
+            if (e.getIsNew() == 0) {
+                minute = newAddWxMinute;
+            }
+            long until = e.getLastAddWxTime().until(LocalDateTime.now(), ChronoUnit.MINUTES);
+            return until > minute;
+        }).collect(Collectors.toList());
+        addAccountList.forEach(e -> {
+            CompanyWxClient client = clientMap.get(e.getId());
+            if(client != null){
+                CompanyWxDialog dialog = companyWxDialogService.getById(client.getDialogId());
+                CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+                String newTxt = objectPlaceholderResolver.resolvePlaceholders(crmCustomer, dialog.getTemplateDetails());
+                AddContactVo vo = friendService.addContact(e.getId(), crmCustomer.getMobile(), newTxt);
+                if(vo.isSuccess()){
+                    e.setLastAddWxTime(LocalDateTime.now());
+                    e.setIsAddNum(e.getIsAddNum() + 1);
+                    client.setIsAdd(2);
+                    client.setAddTime(LocalDateTime.now());
+                    client.setWxV3(vo.getV3());
+                    client.setWxV4(vo.getV4());
+                    addList.add(client);
+                }
+            }
+        });
+        if(!addList.isEmpty()){
+            companyWxClientService.updateBatchById(addList);
+        }
+        if(!addAccountList.isEmpty()){
+            companyWxAccountService.updateBatchById(addAccountList);
+        }
+
+    }
+
+    public void initAccountNum() {
+        LocalDateTime now = LocalDateTime.now();
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        List<CompanyWxAccount> list = companyWxAccountService.list();
+        list.forEach(e -> {
+            if(e.getAccountCreateTime() != null){
+                long until = e.getAccountCreateTime().until(now.toLocalDate(), ChronoUnit.DAYS);
+                if(until > config.getNewAccountTime()){
+                    e.setIsNew(1);
+                }
+            }
+            if(e.getIsNew() == 0){
+                e.setAddNum(config.getNewAccountAddNum());
+            }else{
+                e.setAddNum(RandomUtil.randomInt(config.getAccountAddMax(), config.getAccountAddMin()));
+            }
+            e.setIsAddNum(0);
+            e.setAllocateNum(0);
+        });
+        companyWxAccountService.updateBatchById(list);
+    }
+
+
+    public void initAccountMsg() {
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        List<CompanyWxAccount> list = companyWxAccountService.list();
+        list.forEach(e -> {
+            int txtNum = RandomUtil.randomInt(config.getTxtMsgMinNum(), config.getTxtMsgMaxNum());
+            int imgNum = RandomUtil.randomInt(config.getImgMsgMinNum(), config.getImgMsgMaxNum());
+            e.setSendMsgJson(JSON.toJSONString(SendMsgVo.builder().txt(txtNum).img(imgNum).build()));
+        });
+        companyWxAccountService.updateBatchById(list);
+    }
+}

+ 0 - 155
fs-wx-task/src/main/java/com/fs/app/task/CourseWatchLogScheduler.java

@@ -1,155 +0,0 @@
-package com.fs.app.task;
-
-import com.fs.app.taskService.SopLogsTaskService;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
-import com.fs.course.mapper.FsUserCourseVideoMapper;
-import com.fs.course.service.IFsCourseLinkService;
-import com.fs.course.service.IFsCourseWatchLogService;
-import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.system.service.ISysConfigService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-@Component
-@Slf4j
-public class CourseWatchLogScheduler {
-    private final AtomicBoolean isRunning1 = new AtomicBoolean(false);
-
-    private final AtomicBoolean isRunning2 = new AtomicBoolean(false);
-
-    private final AtomicBoolean isRunning3 = new AtomicBoolean(false);
-
-    private final AtomicBoolean isRunning4 = new AtomicBoolean(false);
-
-    @Autowired
-    private FsCourseWatchLogMapper courseWatchLogMapper;
-
-    @Autowired
-    private QwSopLogsMapper qwSopLogsMapper;
-
-    @Autowired
-    RedisCache redisCache;
-
-    @Autowired
-    private FsUserCourseVideoMapper courseVideoMapper;
-
-    @Autowired
-    private ISysConfigService configService;
-
-    @Autowired
-    private IFsCourseWatchLogService courseWatchLogService;
-
-    @Autowired
-    private SopLogsTaskService sopLogsTaskService;
-
-//    // 定时任务批量更新到数据库
-//    @Scheduled(fixedRate = 60000) // 每分钟执行一次
-//    public void scheduleBatchUpdateToDatabase() {
-//        courseWatchLogService.scheduleBatchUpdateToDatabase();
-//    }
-
-
-
-    /**
-     * 检查看课状态
-     */
-    @Scheduled(fixedRate = 60000) // 每分钟执行一次
-    public void checkWatchStatus() {
-        // 尝试设置标志为 true,表示任务开始执行
-        if (!isRunning1.compareAndSet(false, true)) {
-            log.warn("检查看课中任务执行 - 上一个任务尚未完成,跳过此次执行");
-            return;
-        }
-        try {
-            log.info("检查看课中任务执行>>>>>>>>>>>>");
-            courseWatchLogService.scheduleBatchUpdateToDatabase();
-            courseWatchLogService.scheduleBatchUpdateToDatabaseIsOpen();
-            courseWatchLogService.checkWatchStatus();
-            log.info("检查看课中任务执行完成>>>>>>>>>>>>");
-        }catch (Exception e) {
-            log.error("检查看课中任务执行完成 - 定时任务执行失败", e);
-        } finally {
-            // 重置标志为 false,表示任务已完成
-            isRunning1.set(false);
-        }
-
-    }
-
-
-
-    /**
-     * 创建完课消息
-     */
-    @Scheduled(fixedRate = 300000) // 每五分钟执行一次
-    public void createCourseFinishMsg() {
-        // 尝试设置标志为 true,表示任务开始执行
-        if (!isRunning3.compareAndSet(false, true)) {
-            log.warn("创建完课消息 - 上一个任务尚未完成,跳过此次执行");
-            return;
-        }
-
-        try {
-            log.info("创建完课消息 - 定时任务开始");
-            sopLogsTaskService.createCourseFinishMsg();
-            log.info("创建完课消息 - 定时任务成功完成");
-        } catch (Exception e) {
-            log.error("创建完课消息 - 定时任务执行失败", e);
-        } finally {
-            // 重置标志为 false,表示任务已完成
-            isRunning3.set(false);
-        }
-
-    }
-
-    @Autowired
-    private IFsCourseLinkService courseLinkService;
-
-
-    // 定时任务,每天0点执行
-
-    /**
-     * 每天删除过期短链
-     */
-    @Scheduled(cron = "0 0 0 * * ?")  // 0点0分0秒执行
-    public void delCourseExpireLink() {
-        try {
-            log.info("删除过期短链 - 定时任务开始");
-            courseLinkService.delCourseExpireLink();
-            log.info("删除过期短链 - 定时任务成功完成");
-        } catch (Exception e) {
-            log.error("删除过期短链 - 定时任务执行失败", e);
-        }
-
-    }
-
-    @Scheduled(fixedRate = 30000) // 每分钟执行一次
-    public void checkFsUserWatchStatus() {
-        // 尝试设置标志为 true,表示任务开始执行
-        if (!isRunning4.compareAndSet(false, true)) {
-            log.warn("WXH5-检查会员看课中任务执行 - 上一个任务尚未完成,跳过此次执行");
-            return;
-        }
-        try {
-            log.info("WXH5-检查会员看课中任务执行>>>>>>>>>>>>");
-            courseWatchLogService.scheduleUpdateDurationToDatabase();
-            courseWatchLogService.checkFsUserWatchStatus();
-            log.info("WXH5-检查会员看课中任务执行完成>>>>>>>>>>>>");
-        }catch (Exception e) {
-            log.error("WXH5-检查会员看课中任务执行完成 - 定时任务执行失败", e);
-        } finally {
-            // 重置标志为 false,表示任务已完成
-            isRunning4.set(false);
-        }
-
-    }
-
-
-
-
-
-}

+ 0 - 37
fs-wx-task/src/main/java/com/fs/app/task/UserCourseWatchCountTask.java

@@ -1,37 +0,0 @@
-package com.fs.app.task;
-
-import com.fs.store.service.IFsUserCourseCountService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Component
-@Slf4j
-public class UserCourseWatchCountTask {
-    @Autowired
-    private IFsUserCourseCountService userCourseCountService;
-
-
-    /**
-     * 每15分钟执行一次
-     */
-    @Scheduled(cron = "0 */10 * * * ?")  // 每10分钟执行一次
-    public void userCourseCountTask() {
-        try {
-            log.info("==============会员看课统计任务执行===============开始");
-            long startTime = System.currentTimeMillis();
-
-            userCourseCountService.insertFsUserCourseCountTask();
-
-            log.info("会员看课统计任务执行==============结束");
-            long endTime = System.currentTimeMillis();
-            log.info("会员看课统计任务执行----------执行时长:{}", (endTime - startTime));
-        } catch (Exception e) {
-            log.error("会员看课统计任务执行----------定时任务执行失败", e);
-        }
-
-    }
-
-
-}

+ 40 - 0
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -0,0 +1,40 @@
+package com.fs.app.task;
+
+import com.fs.app.service.WxTaskService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 企业微信SOP定时任务管理类
+ * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
+ *
+ * @author 系统
+ * @version 1.0
+ */
+@Component
+@Slf4j
+public class WxTask {
+
+    private WxTaskService taskService;
+
+    @Scheduled(cron = "0 0/30 * * * ?")
+    public void addWx() {
+        taskService.addWx(null);
+    }
+
+    @Scheduled(cron = "0 0 0 * * ?")
+    public void initAccountNum() {
+        taskService.initAccountNum();
+    }
+
+    @Scheduled(cron = "0 0 * * * ?")
+    public void initAccountMsg() {
+        taskService.initAccountMsg();
+    }
+
+    @Scheduled(cron = "0 10 * * * ?")
+    public void initAccountMsg() {
+        taskService.initAccountMsg();
+    }
+}

+ 0 - 484
fs-wx-task/src/main/java/com/fs/app/task/qwTask.java

@@ -1,484 +0,0 @@
-package com.fs.app.task;
-
-import com.fs.app.taskService.*;
-import com.fs.common.utils.PubFun;
-import com.fs.ipad.IpadSendUtils;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwGroupChat;
-import com.fs.qw.domain.QwUser;
-import com.fs.qw.service.*;
-import com.fs.sop.domain.QwSop;
-import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.sop.mapper.QwSopMapper;
-import com.fs.sop.service.IQwSopLogsService;
-import com.fs.sop.service.IQwSopTagService;
-import com.fs.sop.service.ISopUserLogsService;
-import com.fs.sop.service.impl.QwSopLogsServiceImpl;
-import com.fs.sop.service.impl.QwSopServiceImpl;
-import com.fs.sop.vo.QwSopLogsDoSendListTVO;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-import static com.fs.qw.service.impl.AsyncChatSopService.MAX_GROUP_USER_NUM;
-import static com.fs.qw.service.impl.AsyncChatSopService.MAX_GROUP_NUM;
-
-/**
- * 企业微信SOP定时任务管理类
- * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
- *
- * @author 系统
- * @version 1.0
- */
-@Component
-@Slf4j
-public class qwTask {
-
-    @Autowired
-    private QwSopMapper qwSopMapper;
-
-    @Autowired
-    private IQwExternalContactService qwExternalContactService;
-
-    @Autowired
-    private IQwUserService qwUserService;
-    @Autowired
-    private IQwGroupChatService qwGroupChatService;
-
-    @Autowired
-    private IpadSendUtils ipadSendUtils;
-
-    @Autowired
-    private QwSopServiceImpl qwSopService;
-
-    @Autowired
-    private IQwSopLogsService iQwSopLogsService;
-
-    @Autowired
-    private QwSopLogsServiceImpl qwSopLogsService;
-
-    @Autowired
-    private IQwGroupMsgService qwGroupMsgService;
-
-    @Autowired
-    private ISopUserLogsService sopUserLogsService;
-
-    @Autowired
-    private SopLogsTaskService sopLogsTaskService;
-
-    @Autowired
-    private SopWxLogsService sopWxLogsService;
-
-    @Autowired
-    private SopLogsChatTaskService sopLogsTaskChatService;
-
-    @Autowired
-    private IQwExternalErrRetryService errRetryService;
-
-    @Autowired
-    private QwExternalContactRatingService qwExternalContactRatingService;
-
-    @Autowired
-    private IQwWorkUserService qwWorkUserService;
-
-    @Autowired
-    private QwSopLogsMapper qwSopLogsMapper;
-
-    @Autowired
-    private IQwSopTagService qwSopTagService;
-
-    @Autowired
-    private SopUserLogsInfoByIsDaysNotStudy logsInfoByIsDaysNotStudy;
-
-    @Autowired
-    private QwExternalContactRatingMoreSevenDaysService qwExternalContactRatingMoreSevenDaysService;
-
-    @Autowired
-    private SyncQwExternalContactService syncQwExternalContactService;
-
-    /**
-     * 定时任务:检查SOP规则时间
-     * 执行时间:每天凌晨 1:10:00
-     * 功能:将符合条件的qw_sop任务录入到sop_user_Logs(clickHouse)
-     */
-    @Scheduled(cron = "0 10 1 * * ?")
-    public void qwCheckSopRuleTime() {
-        qwSopService.checkSopRuleTime();
-    }
-
-    /**
-     * 定时任务:添加标签
-     * 执行时间:每20分钟执行一次
-     * 功能:自动为符合条件的记录添加标签
-     */
-    @Scheduled(cron = "0 0/20 * * * ?")
-    public void addTag() {
-        qwSopTagService.addTag();
-    }
-
-    /**
-     * 定时任务:根据营期生成sopLogs待发记录
-     * 执行时间:每小时的第5分钟执行
-     * 功能:根据营期时间生成需要发送的SOP日志记录
-     *
-     * @throws Exception 执行异常
-     */
-    @Scheduled(cron = "0 5 * * * ?") // 每小时的第5分钟触发
-    @Async
-    public void selectSopUserLogsListByTime() throws Exception {
-        // 获取当前时间,精确到小时
-        LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
-        // 打印日志,确认任务执行时间
-        log.info("任务实际执行时间: {}", currentTime);
-
-        // 调用服务方法处理SOP用户日志
-        sopLogsTaskService.selectSopUserLogsListByTime(currentTime, null);
-    }
-
-    /**
-     * 定时任务:微信SOP处理
-     * 执行时间:每小时的第5分钟执行
-     * 功能:处理微信相关的SOP日志
-     *
-     * @throws Exception 执行异常
-     */
-    @Scheduled(cron = "0 5 * * * ?") // 每小时的第5分钟触发
-    public void wxSop() throws Exception {
-        // 获取当前时间,精确到小时
-        LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
-        // 打印日志,确认任务执行时间
-        log.info("任务实际执行时间: {}", currentTime);
-
-        // 调用服务方法处理微信SOP日志
-        sopWxLogsService.wxSopLogsByTime(currentTime);
-    }
-
-    /**
-     * 定时任务:处理聊天SOP用户日志
-     * 执行时间:已注释,原为每分钟的第5秒执行
-     * 功能:将clickHouse的sopUserLogsChat(营期表)按每分钟巡回处理
-     *
-     * @throws Exception 执行异常
-     */
-//    @Scheduled(cron = "5 0/1 * * * ?")
-    public void selectChatSopUserLogsListByTime() throws Exception {
-        // 获取当前时间,精确到分钟
-        LocalDateTime today = LocalDateTime.now().withSecond(0).withNano(0);
-
-        // 创建AI聊天SOP日志
-        sopLogsTaskChatService.createAiChatSopLogs(today);
-    }
-
-    /**
-     * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息(按单链发)
-     */
-    @Scheduled(cron = "0 20 1 * * ?")
-    public void SendQwApiSopLogTimer(){
-        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 10 0,1 * * ?")
-    public void SendQwApiSopLogTimerNew(){
-
-        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"));
-
-        int currentHour = LocalDateTime.now().getHour();
-        String taskStartTime = LocalDate.now().atTime(currentHour, 0, 0)
-                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
-        String taskEndTime = LocalDate.now().atTime(currentHour, 59, 59)
-                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
-
-        qwSopLogsService.createCorpMassSendingByUserLogs(taskStartTime,taskEndTime);
-    }
-
-    /**
-     *
-     * 执行时间:每天上午 8:00:00
-     * 功能:获取通过企业微信接口发送的SOP客户群发消息的反馈结果
-     */
-    @Scheduled(cron = "0 0 8 * * ?")
-    public void GetQwApiSopLogResultTimerNew() {
-        qwSopLogsService.qwSopLogsResultNew();
-    }
-
-    /**
-     * 定时任务:群发API接口的客户/群群发
-     * 执行时间:每10分钟执行一次
-     * 功能:定时处理群发消息任务
-     */
-    @Scheduled(cron = "0 0/10 * * * ?")
-    public void sendQwGroupMsgTask() {
-        qwGroupMsgService.qwGroupMsgTask();
-    }
-
-    /**
-     * 定时任务:发送转换消息
-     * 执行时间:每天上午 8:00:00
-     * 功能:根据SOP规则发送转换消息
-     */
-    @Scheduled(cron = "0 0 8 * * ?")
-//    @Scheduled(cron = "0/10 * * * * ?") // 测试用:每10秒执行一次
-    public void sendQwBySop() {
-        sopUserLogsService.sendQwBySop();
-    }
-
-    /**
-     * 定时任务:企业微信自动打标签/备注补偿机制
-     * 执行时间:每3分钟执行一次
-     * 功能:对没有成功打标签或备注的记录进行补偿处理
-     */
-    @Scheduled(cron = "0 0/3 * * * ?")
-    public void qwExternalErrRetryTimer() {
-        log.info("补偿机制开始");
-        errRetryService.qwExternalErrRetryTimer();
-    }
-
-    /**
-     * 定时任务:补发过期完课消息
-     * 执行时间:每小时的第0分钟执行
-     * 功能:补发已过期但未发送的完课消息
-     */
-    @Scheduled(cron = "0 0 * * * ?")  // 每小时的第0分钟0秒执行
-    public void updateQwSopLogsByCancel() {
-        log.info("补发过期完课消息 - 定时任务开始");
-        try {
-            sopLogsTaskService.updateSopLogsByCancel();
-            log.info("补发过期完课消息 - 定时任务成功完成");
-        } catch (Exception e) {
-            log.error("补发过期完课消息 - 定时任务执行失败", e);
-        }
-    }
-
-    /**
-     * 定时任务:批量处理SOP待发送记录中已过期的消息
-     * 执行时间:每8分钟执行一次
-     * 功能:批量更新已过期的SOP待发送记录
-     */
-    @Scheduled(cron = "0 0/8 * * * ?")
-    public void batchProcessingExpiredMessages() {
-        log.info("批量处理sop待发送记录中已过期的消息");
-        try {
-            // 步骤1:批量获取已过期的记录
-            List<QwSopLogsDoSendListTVO> expireded = iQwSopLogsService.expiredMessagesByQwSopLogs();
-            if (!expireded.isEmpty()) {
-                // 步骤2:批量处理并插入记录
-                processAndInsertQwSopLogs(expireded);
-            }
-            log.info("处理已过期 - 定时任务成功完成");
-        } catch (Exception e) {
-            log.error("处理已过期 - 定时任务执行失败", e);
-        }
-    }
-
-    /**
-     * 批量处理插入逻辑,支持每500条数据一次的批量插入
-     *
-     * @param logsByJsApiNotExtId 需要处理的日志列表
-     */
-    private void processAndInsertQwSopLogs(List<QwSopLogsDoSendListTVO> logsByJsApiNotExtId) {
-        // 定义批量插入的大小
-        int batchSize = 500;
-
-        // 循环处理外部用户ID,每次处理批量大小的子集
-        for (int i = 0; i < logsByJsApiNotExtId.size(); i += batchSize) {
-            // 计算当前批次的结束索引
-            int endIndex = Math.min(i + batchSize, logsByJsApiNotExtId.size());
-            // 获取当前批次的子集
-            List<QwSopLogsDoSendListTVO> batchList = logsByJsApiNotExtId.subList(i, endIndex);
-
-            // 直接使用批次数据进行批量更新
-            try {
-                qwSopLogsMapper.batchUpdateQwSopLogsBySendTime(batchList);
-            } catch (Exception e) {
-                // 记录异常日志,方便后续排查问题
-                log.error("批量更新数据时发生异常,处理的批次起始索引为: " + i, e);
-            }
-        }
-    }
-
-    /**
-     * 定时任务:清除2天以前的SOP任务记录
-     * 执行时间:每天凌晨 0:10:00
-     * 功能:清理历史数据,保持数据库性能
-     */
-    @Scheduled(cron = "0 10 0 * * ?")
-    public void deleteQwSopLogsByDate() {
-        qwSopLogsMapper.deleteQwSopLogsByDate();
-    }
-
-    /**
-     * 定时任务:处理营期异常的数据
-     * 执行时间:每3小时的第30分钟执行
-     * 功能:修复营期相关的异常数据
-     */
-    @Scheduled(cron = "0 30 0/3 * * ? ")
-    public void processRepairQwSopLogsTimer() {
-        sopUserLogsService.repairSopUserLogsTimer();
-    }
-
-
-    /**
-     * 凌晨 2点35开始,将营期小于7天中标记为 是否7天未看课的(E级) 客户的 但是看课了的恢复一下
-     */
-    @Scheduled(cron = "0 35 2 * * ?")
-    @Async
-    public void processSopUserLogsInfoByIsDaysNotStudy() {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 是否7天未看课的(E级) 客户的 恢复一下 ======");
-
-        logsInfoByIsDaysNotStudy.restoreByIsDaysNotStudy();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== 用户E级恢复处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    /**
-     * 定时任务:客户评级处理
-     * 执行时间:每天凌晨 3:45:00
-     * 功能:对SOP营期用户进行分级评级
-     * 备注:异步执行,避免阻塞其他任务
-     */
-    @Scheduled(cron = "0 45 3 * * ?")
-    @Async
-    public void processQwSopExternalContactRatingTimer() {
-        // 记录任务开始时间
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 sop营期-用户分级 ======");
-
-        // 执行用户分级评级
-        qwExternalContactRatingService.ratingUserLogs();
-
-        // 计算并记录任务执行耗时
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== sop营期-用户分级处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    /**
-     * 凌晨4点35开始 客户超过7天没有看课的 标记E级
-     */
-    @Scheduled(cron = "0 30 3 * * ?")
-    @Async
-    public void processQwSopExternalContactRatingMoreSevenDaysTimer() {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 sop营期-用户超7天的看课情况 ======");
-
-        qwExternalContactRatingMoreSevenDaysService.ratingMoreSevenDaysUserLogs();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== sop营期-用户超7天处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-
-    /**
-     * 更新掉所有前一天的所有待发送
-     */
-    @Scheduled(cron = "0 3 0 * * ?")
-    public void updateQwSopLogsDayBefore() {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 更新掉所有前一天的所有待发送 ======");
-        qwSopLogsMapper.updateQwSopLogsByDayBefore();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== 更新掉所有前一天的所有待发送,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    @Scheduled(cron = "0 1 0 */2 * ?")
-    public void updateQwExternalContactUnionid() {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 同步外部联系人的UnionId ======");
-        syncQwExternalContactService.syncQwExternalContactUnionid();
-
-    }
-
-    /**
-     * 定时拉人进群
-     */
-    @Scheduled(cron = "0 0 16 * * ?")
-    public void autoPullGroup(){
-        //  拉群 ,①保持群号 ②每日拉群 ③创建建群记录
-        // 计算每个人最大拉人数量
-        long maxNum = (long) MAX_GROUP_NUM * MAX_GROUP_USER_NUM;
-        // 获取当前时间
-        LocalDate now = LocalDate.now();
-        // 获取需要自动拉群的SOP任务
-        List<QwSop> list = qwSopMapper.selectGroup(now);
-        if(list == null || list.isEmpty()) return;
-        list.forEach(sop -> {
-            // 获取这个SOP下面的企微ID
-            List<Long> qwUserIdList = Arrays.stream(sop.getQwUserIds().split(",")).map(Long::parseLong).distinct().collect(Collectors.toList());
-            // 获取企微ID下面的所有用户
-            List<QwExternalContact> qwExternalContactList = qwExternalContactService.selectQwUserAndLevel(qwUserIdList, Arrays.asList(sop.getAutoGroupLevel().split(",")), sop.getAutoUserReg() == 1);
-            // 根据企微ID进行分组
-            Map<Long, List<QwExternalContact>> qwUserMap = PubFun.listToMapByGroupList(qwExternalContactList, QwExternalContact::getQwUserId);
-            // 获取企微列表
-            List<QwUser> qwUserList = qwUserService.selectQwUserByIds(qwUserIdList);
-            try {
-                // 每个企微都拉人
-                qwUserList.stream().filter(qwUser -> qwUserMap.containsKey(qwUser.getId())).forEach(qwUser -> {
-                    List<QwExternalContact> userList = qwUserMap.get(qwUser.getId()).stream().limit(maxNum).collect(Collectors.toList());
-                    // 创建群 如果没人或者人数没达到满群的要求,不进行建群
-                    if(userList.isEmpty() || userList.size() < MAX_GROUP_USER_NUM) return;
-                    List<QwGroupChat> chatList = qwGroupChatService.selectSopAndQwUser(qwUser.getQwUserId(), sop.getId());
-                    int groupNum = 0;
-                    if (chatList != null && !chatList.isEmpty()) {
-                        groupNum = extractLastNumber(chatList.get(0).getName())  == null ? 0 : extractLastNumber(chatList.get(0).getName());
-                    }
-                    try {
-                        // 建群
-                        ipadSendUtils.createRoom(sop, sop.getGroupName(), qwUser, userList, MAX_GROUP_NUM, MAX_GROUP_USER_NUM,groupNum);
-                    }catch (Exception e){
-                        log.error("群聊拉人进群错误:{},企微ID:{},企微名称:{},外部联系人:{}", e.getMessage(), qwUser.getId(), qwUser.getQwUserName(), PubFun.listToNewList(userList, QwExternalContact::getId));
-                        log.error("群聊拉人进群错误", e);
-                    }
-                });
-            }catch (Exception e){
-                log.error("SOP拉人进群错误", e);
-            }
-        });
-    }
-    /**
-     * 提取字符串中最后的数字
-     * @param str 待处理的字符串
-     * @return 提取到的数字,若没有数字则返回null
-     */
-    public static Integer extractLastNumber(String str) {
-        if (str == null || str.isEmpty()) {
-            return null;
-        }
-
-        // 正则表达式:匹配字符串末尾的一个或多个数字
-        Pattern pattern = Pattern.compile("\\d+$");
-        Matcher matcher = pattern.matcher(str);
-
-        if (matcher.find()) {
-            String numberStr = matcher.group();
-            return Integer.parseInt(numberStr);
-        }
-
-        // 没有找到数字
-        return null;
-    }
-}

+ 0 - 10
fs-wx-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingMoreSevenDaysService.java

@@ -1,10 +0,0 @@
-package com.fs.app.taskService;
-
-import com.fs.common.core.domain.R;
-
-public interface QwExternalContactRatingMoreSevenDaysService {
-    /**
-     * Sop客户超7天评次
-     */
-    public R ratingMoreSevenDaysUserLogs();
-}

+ 0 - 10
fs-wx-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingService.java

@@ -1,10 +0,0 @@
-package com.fs.app.taskService;
-
-import com.fs.common.core.domain.R;
-
-public interface QwExternalContactRatingService {
-    /**
-     * Sop客户评级
-     */
-    public R ratingUserLogs();
-}

+ 0 - 8
fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsChatTaskService.java

@@ -1,8 +0,0 @@
-package com.fs.app.taskService;
-
-import java.time.LocalDateTime;
-
-public interface SopLogsChatTaskService {
-
-    public void createAiChatSopLogs(LocalDateTime today) throws Exception;
-}

+ 0 - 22
fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java

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

+ 0 - 11
fs-wx-task/src/main/java/com/fs/app/taskService/SopLogsTestService.java

@@ -1,11 +0,0 @@
-package com.fs.app.taskService;
-
-public interface SopLogsTestService {
-
-    /**
-     *手动生成sopLogs
-     * @throws Exception
-     */
-    public void selectSopUserLogsListByTest() throws Exception;
-
-}

+ 0 - 9
fs-wx-task/src/main/java/com/fs/app/taskService/SopUserLogsInfoByIsDaysNotStudy.java

@@ -1,9 +0,0 @@
-package com.fs.app.taskService;
-
-public interface SopUserLogsInfoByIsDaysNotStudy {
-
-    /**
-     * 将前7天营期中标记为 是否7天未看课的(E级) 客户的 恢复一下,突然有的恢复一下 (复刻版)
-     */
-    public void restoreByIsDaysNotStudy();
-}

+ 0 - 8
fs-wx-task/src/main/java/com/fs/app/taskService/SopWxLogsService.java

@@ -1,8 +0,0 @@
-package com.fs.app.taskService;
-
-import java.time.LocalDateTime;
-
-public interface SopWxLogsService {
-
-    void wxSopLogsByTime(LocalDateTime currentTime) throws Exception;
-}

+ 0 - 10
fs-wx-task/src/main/java/com/fs/app/taskService/SyncQwExternalContactService.java

@@ -1,10 +0,0 @@
-package com.fs.app.taskService;
-
-import com.fs.common.core.domain.R;
-
-public interface SyncQwExternalContactService {
-
-    R syncQwExternalContactUnionid();
-
-
-}

+ 0 - 374
fs-wx-task/src/main/java/com/fs/app/taskService/impl/AsyncCourseWatchFinishService.java

@@ -1,374 +0,0 @@
-package com.fs.app.taskService.impl;
-
-
-import com.alibaba.fastjson.JSON;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.course.domain.FsCourseWatchLog;
-import com.fs.qw.domain.QwCompany;
-import com.fs.qw.domain.QwUser;
-import com.fs.qw.service.IQwCompanyService;
-import com.fs.qw.service.impl.QwExternalContactServiceImpl;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-import org.apache.rocketmq.client.exception.MQClientException;
-import org.apache.rocketmq.client.producer.SendCallback;
-import org.apache.rocketmq.client.producer.SendResult;
-import org.apache.rocketmq.common.message.MessageConst;
-import org.apache.rocketmq.spring.core.RocketMQTemplate;
-import org.apache.rocketmq.spring.support.RocketMQHeaders;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.messaging.support.MessageBuilder;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import java.util.Optional;
-import java.util.concurrent.*;
-
-@Slf4j
-@Service
-@AllArgsConstructor
-public class AsyncCourseWatchFinishService {
-
-    @Autowired
-    private RocketMQTemplate rocketMQTemplate;
-
-    @Autowired
-    private IQwCompanyService iQwCompanyService;
-
-    @Autowired
-    private QwExternalContactServiceImpl qwExternalContactService;
-
-    @Autowired
-    RedisCache redisCache;
-
-    // 重试队列和调度器
-    private final BlockingQueue<RetryMessage> retryQueue = new LinkedBlockingQueue<>(10000);
-    private final ScheduledExecutorService retryExecutor = Executors.newSingleThreadScheduledExecutor();
-
-    // 主题映射配置
-    private static final String TOPIC = "course-finish-notes";
-
-    @PostConstruct
-    public void init() {
-        // 启动重试任务,每5秒处理一次重试队列
-        retryExecutor.scheduleWithFixedDelay(this::processRetryQueue, 10, 5, TimeUnit.SECONDS);
-        log.info("AsyncCourseWatchFinishService 重试队列处理器已启动");
-    }
-
-    /**
-    * 异步处理完课打备注的
-    */
-    @Async("scheduledExecutorService")
-    public void executeCourseWatchFinish(FsCourseWatchLog finishLog) {
-//        原代码
-//        FsCourseWatchLog watchLog = new FsCourseWatchLog();
-//        watchLog.setQwExternalContactId(finishLog.getQwExternalContactId());
-//        watchLog.setFinishTime(finishLog.getFinishTime());
-//        watchLog.setQwUserId(finishLog.getQwUserId());
-//
-//
-//        QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(finishLog.getQwUserId()));
-//        if (qwUserByRedis == null) {
-//            log.error("无企微员工信息 {} 跳过处理。", finishLog.getQwUserId());
-//            return;
-//        }
-//
-//        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUserByRedis.getCorpId());
-//
-//        if (qwCompany == null) {
-//            log.error("企业微信主体为空 {} 跳过处理。{} ", qwUserByRedis.getCorpId(),watchLog);
-//            return;
-//        }
-//
-//        rocketMQTemplate.asyncSend("course-finish-notes", JSON.toJSONString(finishLog),     new SendCallback() {
-//            @Override public void onSuccess(SendResult sendResult) {
-//                log.info("推送完课打备注成功1:{},{}",JSON.toJSONString(finishLog),sendResult.getMsgId());
-//            }  // 空实现
-//            @Override public void onException(Throwable e) {log.error("推送完课打备注失败1:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
-//        });
-
-
-//        // 定义默认值
-//         final Integer DEFAULT_SERVER_NUM = 99;
-//
-//        // 使用
-//        Integer companyServerNum = Optional.ofNullable(qwCompany.getCompanyServerNum())
-//                .orElse(DEFAULT_SERVER_NUM);
-//        switch (companyServerNum){
-//            case 1:
-//                rocketMQTemplate.asyncSend("course-finish-notes", JSON.toJSONString(finishLog),     new SendCallback() {
-//                    @Override public void onSuccess(SendResult sendResult) {
-//                     log.info("推送完课打备注成功1:{},{}",JSON.toJSONString(finishLog),sendResult.getMsgId());
-//                     }  // 空实现
-//                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败1:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
-//                });
-//                break;
-//            case 2:
-//
-//                rocketMQTemplate.asyncSend("course-finish-notesTwo", JSON.toJSONString(finishLog),     new SendCallback() {
-//                    @Override public void onSuccess(SendResult sendResult) {}  // 空实现
-//                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败2:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
-//                });
-//                break;
-//            case 3:
-//                rocketMQTemplate.asyncSend("course-finish-notesThree", JSON.toJSONString(finishLog),     new SendCallback() {
-//                    @Override public void onSuccess(SendResult sendResult) {}  // 空实现
-//                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败3:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
-//                });
-//                break;
-//            default:
-//                break;
-//        }
-
-
-        // 1. 数据验证和准备
-        ValidationResult validationResult = validateAndPrepareData(finishLog);
-        if (!validationResult.isValid()) {
-            return;
-        }
-
-
-        //  2. 发送消息(使用Tag区分)
-        sendWithFlowControl(finishLog, validationResult, 0);
-
-    }
-
-    /**
-     * 数据验证和准备
-     */
-    private ValidationResult validateAndPrepareData(FsCourseWatchLog finishLog) {
-        // 准备日志对象
-        FsCourseWatchLog watchLog = new FsCourseWatchLog();
-        watchLog.setQwExternalContactId(finishLog.getQwExternalContactId());
-        watchLog.setFinishTime(finishLog.getFinishTime());
-        watchLog.setQwUserId(finishLog.getQwUserId());
-
-        // 验证企微用户信息
-        QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(finishLog.getQwUserId()));
-        if (qwUserByRedis == null) {
-            log.error("无企微员工信息 {} 跳过处理。", finishLog.getQwUserId());
-            return ValidationResult.invalid();
-        }
-
-        // 验证企业主体
-        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUserByRedis.getCorpId());
-        if (qwCompany == null) {
-            log.error("企业微信主体为空 {} 跳过处理。{} ", qwUserByRedis.getCorpId(), watchLog);
-            return ValidationResult.invalid();
-        }
-
-        return ValidationResult.valid(watchLog, qwUserByRedis, qwCompany);
-    }
-
-
-    /**
-     * 带流控处理的消息发送
-     */
-    private void sendWithFlowControl(FsCourseWatchLog finishLog,
-                                     ValidationResult validationResult, int retryCount) {
-        if (retryCount >= 3) {
-            log.warn("消息重试超过最大次数,转入重试队列: topic={}, qwUserId={}",
-                    TOPIC, finishLog.getQwUserId());
-            offerToRetryQueue(finishLog, validationResult);
-            return;
-        }
-
-        rocketMQTemplate.asyncSend(TOPIC, JSON.toJSONString(finishLog), new SendCallback() {
-            @Override
-            public void onSuccess(SendResult sendResult) {
-                log.info("推送完课打备注成功1:{},{}",JSON.toJSONString(finishLog),sendResult.getMsgId());
-            }
-
-            @Override
-            public void onException(Throwable e) {
-//                if (isFlowControlException(e)) {
-//                    // 流控异常处理
-//                    handleFlowControlRetry(TOPIC, finishLog, validationResult, retryCount, e);
-//                    log.error("推送完课打备注失败1流控异常:finishLog={},e={}",JSON.toJSONString(finishLog),e.getMessage());
-//                } else {
-//                    // 其他异常
-//                    log.error("推送完课打备注失败1:{},{}",JSON.toJSONString(finishLog),e.getMessage());
-//                }
-                if (isFlowControlException(e)) {
-                    // 流控异常处理
-                    handleFlowControlRetry(TOPIC, finishLog, validationResult, retryCount, e);
-                    log.error("推送完课打备注流控异常,准备重试。retryCount: {}, topic: {}, finishLogId: {}",
-                            retryCount, TOPIC, finishLog.getLogId()); // 只记录关键信息
-                } else {
-                    // 其他异常 - 记录完整堆栈
-                    log.error("推送完课打备注失败,非流控异常。finishLog: {}",
-                            JSON.toJSONString(finishLog), e); // 注意这里传 e 而不是 e.getMessage()
-                }
-            }
-        });
-    }
-
-    /**
-     * 放入重试队列
-     */
-    private void offerToRetryQueue(FsCourseWatchLog finishLog,
-                                   ValidationResult validationResult) {
-        RetryMessage retryMessage = new RetryMessage(finishLog, validationResult);
-        boolean offered = retryQueue.offer(retryMessage);
-        if (offered) {
-            log.info("消息已加入重试队列: topic={}, qwUserId={}", TOPIC, finishLog.getQwUserId());
-        } else {
-            log.error("重试队列已满,消息可能丢失: topic={}, qwUserId={}", TOPIC, finishLog.getQwUserId());
-            // 这里可以接入告警系统
-        }
-    }
-
-    /**
-     * 处理重试队列
-     */
-    private void processRetryQueue() {
-        try {
-            int processedCount = 0;
-            RetryMessage retryMessage;
-
-            while (processedCount < 100 && (retryMessage = retryQueue.poll()) != null) {
-                try {
-                    // 重新发送消息
-                    sendWithFlowControl(retryMessage.getFinishLog(),
-                            retryMessage.getValidationResult(), 0);
-                    processedCount++;
-
-                    Thread.sleep(10);
-                } catch (Exception e) {
-                    log.error("重试队列处理失败: {}", e.getMessage());
-                    offerToRetryQueue(retryMessage.getFinishLog(), retryMessage.getValidationResult());
-                }
-            }
-
-            if (processedCount > 0) {
-                log.debug("重试队列处理完成,本次处理数量: {}", processedCount);
-            }
-        } catch (Exception e) {
-            log.error("处理重试队列异常: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 判断是否为流控异常
-     */
-    private boolean isFlowControlException(Throwable e) {
-        // 检查异常消息中是否包含流控关键词(双重保障)
-        String errorMessage = e.getMessage();
-        if (errorMessage != null && (
-                errorMessage.contains("flow control") ||
-                errorMessage.contains("exhausted the send quota") ||
-                errorMessage.contains("CODE: 215"))) {
-            return true;
-        }
-
-        if (e instanceof MQClientException) {
-            return ((MQClientException) e).getResponseCode() == 215;
-        }
-        // 检查异常链
-        Throwable cause = e.getCause();
-        if (cause instanceof MQClientException) {
-            return ((MQClientException) cause).getResponseCode() == 215;
-        }
-        return false;
-    }
-
-    private static final int MAX_FLOW_CONTROL_RETRY = 5;
-    /**
-     * 流控重试处理
-     */
-    private void handleFlowControlRetry(String topic, FsCourseWatchLog finishLog,
-                                        ValidationResult validationResult, int retryCount, Throwable e) {
-        // 检查重试次数限制
-//        if (retryCount >= MAX_FLOW_CONTROL_RETRY) {
-//            log.error("流控重试达到最大次数 {},放弃重试。topic: {}, qwUserId: {}, logId: {}",
-//                    MAX_FLOW_CONTROL_RETRY, topic, finishLog.getQwUserId(), finishLog.getLogId());
-//            //todo 可以记录到数据库或发送告警
-//            return;
-//        }
-
-        long backoffTime = calculateBackoffTime(retryCount);
-        log.warn("流控触发,{}ms后第{}次重试: topic={}, qwUserId={}",
-                backoffTime, retryCount + 1, topic, finishLog.getQwUserId());
-
-        // 使用 ScheduledExecutorService 进行延迟执行
-        retryExecutor.schedule(() -> {
-            try {
-                sendWithFlowControl(finishLog, validationResult, retryCount + 1);
-            } catch (Exception ex) {
-                log.error("延迟重试执行异常 - qwUserId: {}, logId: {}, error: {}",
-                        finishLog.getQwUserId(), finishLog.getLogId(), ex.getMessage(), ex);
-            }
-        }, backoffTime, TimeUnit.MILLISECONDS);
-    }
-    /**
-     * 计算退避时间(指数退避)
-     */
-    private long calculateBackoffTime(int retryCount) {
-//        return Math.min(1000 * (long) Math.pow(2, retryCount), 10000); // 最大10秒
-        // 基础退避:1s, 2s, 4s, 8s, 16s, 32s
-        long baseDelay = Math.min(1000L * (1L << Math.min(retryCount, 5)), 32000L);
-        // 添加随机抖动 (0~2s),避免多个客户端同时重试
-        long jitter = (long) (Math.random() * 1000);
-
-        return baseDelay + jitter;
-    }
-
-    @PreDestroy
-    public void destroy() {
-        retryExecutor.shutdown();
-        try {
-            if (!retryExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
-                retryExecutor.shutdownNow();
-            }
-        } catch (InterruptedException e) {
-            retryExecutor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
-        log.info("AsyncCourseWatchFinishService 已关闭");
-    }
-
-    // 内部辅助类
-    private static class ValidationResult {
-        private final boolean valid;
-        private final FsCourseWatchLog watchLog;
-        private final QwUser qwUser;
-        private final QwCompany qwCompany;
-
-        public ValidationResult(boolean valid, FsCourseWatchLog watchLog, QwUser qwUser, QwCompany qwCompany) {
-            this.valid = valid;
-            this.watchLog = watchLog;
-            this.qwUser = qwUser;
-            this.qwCompany = qwCompany;
-        }
-
-        public static ValidationResult valid(FsCourseWatchLog watchLog, QwUser qwUser, QwCompany qwCompany) {
-            return new ValidationResult(true, watchLog, qwUser, qwCompany);
-        }
-
-        public static ValidationResult invalid() {
-            return new ValidationResult(false, null, null, null);
-        }
-
-        public boolean isValid() { return valid; }
-        public FsCourseWatchLog getWatchLog() { return watchLog; }
-        public QwUser getQwUser() { return qwUser; }
-        public QwCompany getQwCompany() { return qwCompany; }
-    }
-
-    private static class RetryMessage {
-        private final FsCourseWatchLog finishLog;
-        private final ValidationResult validationResult;
-
-        public RetryMessage(FsCourseWatchLog finishLog, ValidationResult validationResult) {
-            this.finishLog = finishLog;
-            this.validationResult = validationResult;
-        }
-
-        public FsCourseWatchLog getFinishLog() { return finishLog; }
-        public ValidationResult getValidationResult() { return validationResult; }
-    }
-
-}

+ 0 - 333
fs-wx-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingMoreSevenDaysServiceImpl.java

@@ -1,333 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.QwExternalContactRatingMoreSevenDaysService;
-import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.sop.domain.SopUserLogs;
-import com.fs.sop.domain.SopUserLogsInfo;
-import com.fs.sop.mapper.SopUserLogsInfoMapper;
-import com.fs.sop.mapper.SopUserLogsMapper;
-import com.fs.sop.params.QwRatingConfig;
-import com.fs.sop.service.IQwSopTempDayService;
-import com.fs.sop.service.ISopUserLogsInfoService;
-import com.fs.sop.vo.QwRatingVO;
-import com.fs.system.service.ISysConfigService;
-import com.fs.voice.utils.StringUtil;
-import com.google.common.util.concurrent.AtomicDouble;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.*;
-import java.util.stream.Collectors;
-
-@Service
-@Slf4j
-public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExternalContactRatingMoreSevenDaysService {
-
-
-
-    @Autowired
-    private ISysConfigService configService;
-
-    @Autowired
-    private RedisCache redisCache;
-
-    @Autowired
-    private SopUserLogsMapper sopUserLogsMapper;
-
-    @Autowired
-    private IQwSopTempDayService qwSopTempDayService;
-
-    @Autowired
-    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
-
-    @Autowired
-    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-
-    @Autowired
-    private ISopUserLogsInfoService iSopUserLogsInfoService;
-
-    @Autowired
-    private ExecutorService sopRatingExecutor;  // 自定义线程池
-
-    // 任务队列
-    private final BlockingQueue<SopUserLogs> taskQueue = new LinkedBlockingQueue<>(10000);
-
-    private volatile boolean running = true;
-    //批量更新队列
-    private final List<CompletableFuture<Void>> updateFutures = Collections.synchronizedList(new ArrayList<>());
-
-    private final Object configLock = new Object();
-
-    // 启动时初始化消费者线程
-    @PostConstruct
-    public void init() {
-
-        loadCourseConfig();
-
-        int consumerCount = Runtime.getRuntime().availableProcessors(); // 消费者线程数,默认 CPU 核心数
-        for (int i = 0; i < consumerCount; i++) {
-            sopRatingExecutor.submit(this::consumeTasks); // 提交消费者任务
-        }
-
-        log.info("初始化 {} 个消费者线程", consumerCount);
-    }
-
-    private  volatile QwRatingConfig qwRatingConfig;
-
-    private void loadCourseConfig() {
-        try {
-            String json = configService.selectConfigByKey("qwRating:config");
-            QwRatingConfig config = JSON.parseObject(json, QwRatingConfig.class);
-            if (!StringUtil.strIsNullOrEmpty(json) && config != null) {
-                qwRatingConfig = config;
-                log.info("Loaded qwRating.config successfully.");
-            } else {
-                log.error("Failed to load course.config from configService.");
-            }
-        } catch (Exception e) {
-            log.error("Exception while loading qwRating.config: {}", e.getMessage(), e);
-        }
-    }
-
-
-    @Override
-    public R ratingMoreSevenDaysUserLogs() {
-        // 分页加载并放入队列
-        int pageSize = 1000;
-        int offset = 0;
-        List<SopUserLogs> sopUserLogs;
-
-        // 获取缓存的配置
-        QwRatingConfig config;
-        synchronized(configLock) {
-            config = qwRatingConfig;
-        }
-
-        do {
-            sopUserLogs = sopUserLogsMapper.meetsTheRatingByUserInfoWithPaginationStudyDays(offset, pageSize,config.getNotStudyDays());
-            if (!sopUserLogs.isEmpty()) {
-                sopUserLogs.forEach(item -> {
-                    try {
-                        taskQueue.put(item); // 将任务放入队列
-                    } catch (InterruptedException e) {
-                        log.error("任务放入队列失败,sopId: {}", item.getSopId(), e);
-                        Thread.currentThread().interrupt();
-                    }
-                });
-                offset += pageSize;
-            }
-        } while (!sopUserLogs.isEmpty());
-
-
-        // 等待队列处理完成
-        CompletableFuture.runAsync(() -> {
-            while (!taskQueue.isEmpty()) {
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    log.error("等待队列处理时中断", e);
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }).join(); // 等待任务完成
-
-        return R.ok();
-    }
-
-
-    private void consumeTasks() {
-
-        if (!running && taskQueue.isEmpty()) {
-            log.info("没有评级任务需要处理");
-            return; // 如果队列为空且没有正在运行的线程,则直接返回
-        }
-
-        while (running) {
-            try {
-                SopUserLogs item = taskQueue.poll(1, TimeUnit.SECONDS); // 等待 1 秒
-                if (item != null) {
-                    processSingleTask(item);
-                }
-            } catch (Exception e) {
-                log.error("消费者线程异常", e);
-            }
-        }
-    }
-
-    private void processSingleTask(SopUserLogs item) {
-
-        // 获取缓存的配置
-        QwRatingConfig config;
-        synchronized(configLock) {
-            config = qwRatingConfig;
-        }
-
-        List<SopUserLogsInfo> sopUserLogsInfosList = sopUserLogsInfoMapper
-                .selectSopUserLogsInfoListBySopId(item.getSopId(), item.getId());
-
-        if (sopUserLogsInfosList == null || sopUserLogsInfosList.isEmpty()) {
-            log.error("当前营期没有客户-sopId:{},营期id:{}", item.getSopId(), item.getId());
-            return;
-        }
-
-        List<QwExternalContact> batchQwExternalContact = sopUserLogsInfosList.stream()
-                .map(logsInfo -> processUserLog(logsInfo, config))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-
-        if (!batchQwExternalContact.isEmpty()) {
-            batchUpdateQwExternalContact(batchQwExternalContact);
-        }
-    }
-
-    private QwExternalContact processUserLog(SopUserLogsInfo logsInfo, QwRatingConfig config) {
-        try {
-            Long externalId = logsInfo.getExternalId();
-            if (externalId == null) {
-                return null;
-            }
-
-            List<QwRatingVO> ratingVOS = fsCourseWatchLogMapper
-                    .selectFsCourseWatchLogByExtIdRatingMoreStudyDays(externalId, config.getNotStudyDays());
-
-            if (ratingVOS == null || ratingVOS.isEmpty() || ratingVOS.size() < 6) {
-                log.info("没有记录或不满足条件不评级或看课记录小于6 不评级,externalId: {}", externalId);
-                return null;
-            }
-
-
-            //判断 7天的时长是否大于0
-            boolean scoreMoreStudyLevel = getScoreMoreStudyLevel(ratingVOS);
-
-            if (!scoreMoreStudyLevel) {
-                QwExternalContact externalContact = new QwExternalContact();
-                externalContact.setId(externalId);
-                externalContact.setLevel(5);
-                externalContact.setIsDaysNotStudy(1);
-                return externalContact;
-            }else {
-                QwExternalContact externalContact = new QwExternalContact();
-                externalContact.setId(externalId);
-                externalContact.setLevel(ratingVOS.get(0).getLevel());
-                externalContact.setIsDaysNotStudy(0);
-                return externalContact;
-            }
-
-
-        } catch (Exception e) {
-            log.error("计算用户积分异常,用户:{}", logsInfo, e);
-            return null;
-        }
-    }
-
-    private void batchUpdateQwExternalContact(List<QwExternalContact> notInExternalUseridList) {
-        int batchSize = 300;
-
-        for (int i = 0; i < notInExternalUseridList.size(); i += batchSize) {
-            int endIndex = Math.min(i + batchSize, notInExternalUseridList.size());
-            List<QwExternalContact> batchList = notInExternalUseridList.subList(i, endIndex);
-
-            int finalI = i;
-            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
-                try {
-                    qwExternalContactMapper.batchUpdateQwExternalContactByMoreStudy(batchList);
-                    iSopUserLogsInfoService.batchUpdateSopUserLogsInfoByMoreStudy(batchList);
-                    log.info("成功更新看课7天数据,起始索引: {}, 数量: {}", finalI, batchList.size());
-                } catch (Exception e) {
-                    log.error("批量更新异常,批次起始索引: {}", finalI, e);
-                }
-
-            }, sopRatingExecutor);
-
-            updateFutures.add(future);
-        }
-    }
-
-    @PreDestroy
-    public void shutdown() {
-        running = false;  // 标记消费者停止
-        log.info("正在关闭线程池...");
-
-        // **等待任务队列处理完毕**
-        while (!taskQueue.isEmpty()) {
-            try {
-                Thread.sleep(500);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.warn("等待任务队列处理完成时被中断", e);
-            }
-        }
-
-        // **确保所有 `batchUpdateQwExternalContact` 的任务完成**
-        log.info("等待所有批量更新任务完成...");
-        CompletableFuture.allOf(updateFutures.toArray(new CompletableFuture[0])).join();
-
-        // 关闭线程池
-        sopRatingExecutor.shutdown();
-        try {
-            if (!sopRatingExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                List<Runnable> pendingTasks = sopRatingExecutor.shutdownNow();
-                log.warn("强制关闭线程池,未完成任务数: {}", pendingTasks.size());
-            }
-        } catch (InterruptedException e) {
-            sopRatingExecutor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
-        log.info("线程池和消费者已完全关闭");
-    }
-
-
-    /**
-     * 每6小时更新一次
-     */
-    @Scheduled(cron = "0 50 0/6 * * ?")
-    public void refreshRatingConfig() {
-
-        synchronized(configLock) {
-            try {
-                String json = configService.selectConfigByKey("qwRating:config");
-                QwRatingConfig config = JSON.parseObject(json, QwRatingConfig.class);
-                if (!StringUtil.strIsNullOrEmpty(json) && config != null) {
-                    qwRatingConfig = config;
-                    log.info("LoadedTime qwRating.config successfully.");
-                } else {
-                    log.error("Failed to load course.config from configService.");
-                }
-            } catch (Exception e) {
-                log.error("Exception while refreshing course.config: {}", e.getMessage(), e);
-            }
-        }
-
-    }
-
-
-    //查 E级
-    public boolean getScoreMoreStudyLevel(List<QwRatingVO> qwRatingVOS) {
-
-        AtomicDouble watchCount= new AtomicDouble();
-
-        qwRatingVOS.forEach(vo -> {
-            watchCount.addAndGet(vo.getWatchDuration());
-        });
-
-        // 判断总 watchDuration 是否 > 0
-        return watchCount.get() > 0;
-    }
-
-}

+ 0 - 410
fs-wx-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingServiceImpl.java

@@ -1,410 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.QwExternalContactRatingService;
-import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.sop.domain.SopUserLogs;
-import com.fs.sop.domain.SopUserLogsInfo;
-import com.fs.sop.mapper.SopUserLogsInfoMapper;
-import com.fs.sop.mapper.SopUserLogsMapper;
-import com.fs.sop.params.QwRatingConfig;
-import com.fs.sop.service.IQwSopTempDayService;
-import com.fs.sop.service.ISopUserLogsInfoService;
-import com.fs.sop.vo.QwRatingVO;
-import com.fs.system.service.ISysConfigService;
-import com.fs.voice.utils.StringUtil;
-import com.google.common.util.concurrent.AtomicDouble;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.*;
-import java.util.stream.Collectors;
-
-@Service
-@Slf4j
-public class QwExternalContactRatingServiceImpl implements QwExternalContactRatingService {
-
-
-    @Autowired
-    private ISysConfigService configService;
-
-    @Autowired
-    private RedisCache redisCache;
-
-    @Autowired
-    private SopUserLogsMapper sopUserLogsMapper;
-
-    @Autowired
-    private IQwSopTempDayService qwSopTempDayService;
-
-    @Autowired
-    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
-
-    @Autowired
-    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-
-    @Autowired
-    private ISopUserLogsInfoService iSopUserLogsInfoService;
-
-    @Autowired
-    private ExecutorService sopRatingExecutor;  // 自定义线程池
-
-    // 任务队列
-    private final BlockingQueue<SopUserLogs> taskQueue = new LinkedBlockingQueue<>(10000);
-    private volatile boolean running = true;
-    //批量更新队列
-    private final List<CompletableFuture<Void>> updateFutures = Collections.synchronizedList(new ArrayList<>());
-
-    private final Object configLock = new Object();
-
-    // 启动时初始化消费者线程
-    @PostConstruct
-    public void init() {
-
-        loadCourseConfig();
-
-        int consumerCount = Runtime.getRuntime().availableProcessors(); // 消费者线程数,默认 CPU 核心数
-        for (int i = 0; i < consumerCount; i++) {
-            sopRatingExecutor.submit(this::consumeTasks); // 提交消费者任务
-        }
-
-        log.info("初始化 {} 个消费者线程", consumerCount);
-    }
-
-    private  volatile QwRatingConfig qwRatingConfig;
-
-    private void loadCourseConfig() {
-        try {
-            String json = configService.selectConfigByKey("qwRating:config");
-            QwRatingConfig config = JSON.parseObject(json, QwRatingConfig.class);
-            if (!StringUtil.strIsNullOrEmpty(json) && config != null) {
-                qwRatingConfig = config;
-                log.info("Loaded qwRating.config successfully.");
-            } else {
-                log.error("Failed to load course.config from configService.");
-            }
-        } catch (Exception e) {
-            log.error("Exception while loading qwRating.config: {}", e.getMessage(), e);
-        }
-    }
-
-    @Override
-    public R ratingUserLogs() {
-
-        // 分页加载并放入队列
-        int pageSize = 1000;
-        int offset = 0;
-        List<SopUserLogs> sopUserLogs;
-
-        do {
-            sopUserLogs = sopUserLogsMapper.meetsTheRatingByUserInfoWithPagination(offset, pageSize);
-            if (!sopUserLogs.isEmpty()) {
-                sopUserLogs.forEach(item -> {
-                    try {
-                        taskQueue.put(item); // 将任务放入队列
-                    } catch (InterruptedException e) {
-                        log.error("任务放入队列失败,sopId: {}", item.getSopId(), e);
-                        Thread.currentThread().interrupt();
-                    }
-                });
-                offset += pageSize;
-            }
-        } while (!sopUserLogs.isEmpty());
-
-
-        // 等待队列处理完成
-        CompletableFuture.runAsync(() -> {
-            while (!taskQueue.isEmpty()) {
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    log.error("等待队列处理时中断", e);
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }).join(); // 等待任务完成
-
-        return R.ok();
-    }
-
-
-    private void consumeTasks() {
-
-        if (!running && taskQueue.isEmpty()) {
-            return; // 如果队列为空且没有正在运行的线程,则直接返回
-        }
-
-        while (running) {
-            try {
-                SopUserLogs item = taskQueue.poll(1, TimeUnit.SECONDS); // 等待 1 秒
-                if (item != null) {
-                    processSingleTask(item);
-                }
-            } catch (Exception e) {
-                log.error("消费者线程异常", e);
-            }
-        }
-    }
-
-    private void processSingleTask(SopUserLogs item) {
-
-        // 获取缓存的配置
-        QwRatingConfig config;
-        synchronized(configLock) {
-            config = qwRatingConfig;
-        }
-
-        Integer countDays = item.getCountDays();
-        String cacheKey = "sop-tempId:" + item.getSopTempId();
-        Integer sopTemIdNum = redisCache.getCacheObject(cacheKey);
-
-        if (sopTemIdNum == null) {
-            sopTemIdNum = qwSopTempDayService.getDayNumByIdLimitOne(item.getSopTempId());
-            redisCache.setCacheObject(cacheKey, sopTemIdNum, 3, TimeUnit.HOURS);
-        }
-
-        if (sopTemIdNum < countDays) {
-            log.info("当前营期的伦次中,模板天数不足。不评级:{}|sopId:{}", item.getSopTempId(), item.getSopId());
-            return;
-        }
-
-        List<SopUserLogsInfo> sopUserLogsInfosList = sopUserLogsInfoMapper
-                .selectSopUserLogsInfoListBySopId(item.getSopId(), item.getId());
-
-        if (sopUserLogsInfosList == null || sopUserLogsInfosList.isEmpty()) {
-            log.error("当前营期没有客户-sopId:{},营期id:{}", item.getSopId(), item.getId());
-            return;
-        }
-
-        List<QwExternalContact> batchQwExternalContact = sopUserLogsInfosList.stream()
-                .map(logsInfo -> processUserLog(logsInfo, config))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-
-        if (!batchQwExternalContact.isEmpty()) {
-            batchUpdateQwExternalContact(batchQwExternalContact);
-        }
-    }
-
-    private QwExternalContact processUserLog(SopUserLogsInfo logsInfo, QwRatingConfig config) {
-        try {
-            Long externalId = logsInfo.getExternalId();
-            if (externalId == null) {
-                return null;
-            }
-
-            List<QwRatingVO> ratingVOS = fsCourseWatchLogMapper
-                    .selectFsCourseWatchLogByExtIdRating(externalId, config.getLevelDay());
-
-            if (ratingVOS == null || ratingVOS.isEmpty()) {
-                log.info("没有记录不评级,externalId: {}", externalId);
-                return null;
-            }
-
-            int scoreLevel = getScoreLevel(ratingVOS, config);
-            int latestTime = getLatestTime(ratingVOS);
-            int levelUpFall = ratingVOS.get(0).getLevelType() != null
-                    ? getLevelUpFall(scoreLevel, ratingVOS.get(0).getLevelType())
-                    : 3;
-
-            QwExternalContact externalContact = new QwExternalContact();
-            externalContact.setId(externalId);
-            externalContact.setLevel(scoreLevel);
-            externalContact.setLastWatchTime(latestTime);
-            externalContact.setLevelType(levelUpFall);
-
-            return externalContact;
-
-        } catch (Exception e) {
-            log.error("计算用户积分异常,用户:{}", logsInfo, e);
-            return null;
-        }
-    }
-
-    private void batchUpdateQwExternalContact(List<QwExternalContact> notInExternalUseridList) {
-        int batchSize = 300;
-
-        for (int i = 0; i < notInExternalUseridList.size(); i += batchSize) {
-            int endIndex = Math.min(i + batchSize, notInExternalUseridList.size());
-            List<QwExternalContact> batchList = notInExternalUseridList.subList(i, endIndex);
-
-            int finalI = i;
-            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
-                try {
-                    qwExternalContactMapper.batchUpdateQwExternalContact(batchList);
-                    iSopUserLogsInfoService.batchUpdateSopUserLogsInfoByLevel(batchList);
-                    log.info("成功更新评级数据,起始索引: {}, 数量: {}", finalI, batchList.size());
-                } catch (Exception e) {
-                    log.error("批量更新异常,批次起始索引: {}", finalI, e);
-                }
-            }, sopRatingExecutor);
-
-            updateFutures.add(future);
-        }
-    }
-
-    @PreDestroy
-    public void shutdown() {
-        running = false;  // 标记消费者停止
-        log.info("正在关闭线程池...");
-
-        // **等待任务队列处理完毕**
-        while (!taskQueue.isEmpty()) {
-            try {
-                Thread.sleep(500);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.warn("等待任务队列处理完成时被中断", e);
-            }
-        }
-
-        // **确保所有 `batchUpdateQwExternalContact` 的任务完成**
-        log.info("等待所有批量更新任务完成...");
-        CompletableFuture.allOf(updateFutures.toArray(new CompletableFuture[0])).join();
-
-        // 关闭线程池
-        sopRatingExecutor.shutdown();
-        try {
-            if (!sopRatingExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                List<Runnable> pendingTasks = sopRatingExecutor.shutdownNow();
-                log.warn("强制关闭线程池,未完成任务数: {}", pendingTasks.size());
-            }
-        } catch (InterruptedException e) {
-            sopRatingExecutor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
-        log.info("线程池和消费者已完全关闭");
-    }
-
-
-    /**
-    * 每6小时更新一次
-    */
-    @Scheduled(cron = "0 50 0/6 * * ?")
-    public void refreshRatingConfig() {
-
-        synchronized(configLock) {
-            try {
-                String json = configService.selectConfigByKey("qwRating:config");
-                QwRatingConfig config = JSON.parseObject(json, QwRatingConfig.class);
-                if (!StringUtil.strIsNullOrEmpty(json) && config != null) {
-                    qwRatingConfig = config;
-                    log.info("LoadedTime qwRating.config successfully.");
-                } else {
-                    log.error("Failed to load course.config from configService.");
-                }
-            } catch (Exception e) {
-                log.error("Exception while refreshing course.config: {}", e.getMessage(), e);
-            }
-        }
-
-    }
-
-
-    //评级
-    public int getScoreLevel(List<QwRatingVO> qwRatingVOS, QwRatingConfig config) {
-
-        AtomicDouble watchCount= new AtomicDouble();
-
-        qwRatingVOS.forEach(vo -> {
-            // 计算 watchDuration 除以 allDuration,并保留2位小数,四舍五入
-            BigDecimal watchDuration = new BigDecimal(vo.getWatchDuration());
-            BigDecimal allDuration = new BigDecimal(vo.getAllDuration());
-
-            BigDecimal ratio = watchDuration.divide(allDuration, 3, RoundingMode.DOWN);
-
-            // 将结果四舍五入后加到 watchCount
-            watchCount.addAndGet(ratio.doubleValue());
-        });
-
-
-        // 计算 watchCount 除以 allSize 的结果并四舍五入为整数
-        BigDecimal result = new BigDecimal(watchCount.get())
-                .divide(new BigDecimal(qwRatingVOS.size()), 3, RoundingMode.DOWN);
-
-        // 将结果乘以 100
-        BigDecimal resultMultiplied = result.multiply(new BigDecimal(100));
-
-        // 四舍五入到整数
-        BigDecimal roundedResult = resultMultiplied.setScale(0, RoundingMode.HALF_UP);
-
-        // 转换为 int 类型
-        int score = roundedResult.intValue();
-
-        if (score >= config.getALevelMin()) {
-            return 1; // A 等级
-        } else if (score >= config.getBLevelMin() && score < config.getBLevelMax()) {
-            return 2; // B 等级
-        } else if (score >= config.getCLevelMin() && score < config.getCLevelMax()) {
-            return 3; // C 等级
-        } else if (score >= config.getDLevelMin() && score < config.getDLevelMax()) {
-            return 4; // D 等级
-        } else {
-            throw new IllegalArgumentException("分数不在任何等级范围内: " + score);
-        }
-    }
-    //升降等级
-    public int getLevelUpFall(int scoreLevel,int levelUpFall){
-
-        if (scoreLevel > levelUpFall) {
-            return  1;//升级
-        }else if (scoreLevel < levelUpFall) {
-            return  2;//降级
-        }else {
-            return  3;//不变
-        }
-    }
-
-    //计算最晚看课时间
-    public int getLatestTime(List<QwRatingVO> qwRatingVOS){
-
-        // 定义日期时间格式
-        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-        DateTimeFormatter outputFormatter = DateTimeFormatter.ofPattern("HHmm");
-
-        // 用于存储提取出的时分
-        List<String> timeOnlyList = new ArrayList<>();
-
-        qwRatingVOS.forEach(vos->{
-
-            String finishTime = vos.getFinishTime();
-
-            if (!StringUtil.strIsNullOrEmpty(finishTime)){
-                LocalTime localTime = LocalTime.parse(finishTime, formatter);
-                String formattedTime = localTime.format(outputFormatter);
-                timeOnlyList.add(formattedTime);
-            }
-
-        });
-        String latestTime=null;
-        if (!timeOnlyList.isEmpty()){
-            latestTime  = Collections.max(timeOnlyList);
-        }else {
-            latestTime = "0";
-        }
-
-
-        return Integer.parseInt(latestTime);
-    }
-
-
-}

+ 0 - 212
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsChatTaskServiceImpl.java

@@ -1,212 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import cn.hutool.json.JSONUtil;
-import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.SopLogsChatTaskService;
-import com.fs.fastGpt.param.SendHookAIParam;
-import com.fs.qw.domain.QwUser;
-import com.fs.qw.mapper.QwUserMapper;
-import com.fs.qw.service.IQwExternalContactInfoService;
-import com.fs.qw.vo.QwChatSopTempSetting;
-import com.fs.qw.vo.QwSopRuleTimeVO;
-import com.fs.qwHookApi.param.QwHookSendMsgParam;
-import com.fs.sop.domain.QwSop;
-import com.fs.sop.domain.QwSopTempRules;
-import com.fs.sop.domain.SopUserLogsInfo;
-import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.sop.mapper.QwSopMapper;
-import com.fs.sop.mapper.SopUserLogsInfoMapper;
-import com.fs.sop.service.IQwSopLogsService;
-import com.fs.sop.service.IQwSopTempRulesService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.retry.annotation.Backoff;
-import org.springframework.retry.annotation.Retryable;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Service;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.stream.Collectors;
-
-@Service
-@Slf4j
-public class SopLogsChatTaskServiceImpl implements SopLogsChatTaskService {
-
-
-    @Autowired
-    private QwSopMapper sopMapper;
-
-    @Autowired
-    private IQwSopLogsService qwSopLogsService;
-
-    @Autowired
-    private IQwSopTempRulesService qwSopTempRulesService;
-
-    @Autowired
-    private QwSopLogsMapper qwSopLogsMapper;
-
-    @Autowired
-    private IQwExternalContactInfoService qwExternalContactInfoService;
-    @Autowired
-    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
-    @Autowired
-    private QwUserMapper qwUserMapper;
-    @Autowired
-    RedisTemplate<String, String> redisTemplate;
-
-    /**
-     * 查询所有的AIsop任务
-     * @throws Exception
-     */
-    @Override
-    public void createAiChatSopLogs(LocalDateTime today) throws Exception {
-        long startTimeMillis = System.currentTimeMillis();
-        List<QwSop> sopByChats = sopMapper.selectChatQwSopList();
-
-        CountDownLatch sopLatch = new CountDownLatch(sopByChats.size());
-        if (sopByChats.isEmpty()) {
-            log.info("没有需要处理的 Ai对话SOP 任务。");
-            return;
-        }
-        for (QwSop sop : sopByChats) {
-            processAiChatSopAsync(sop, sopLatch,today);
-        }
-
-        // 等待所有 SOP 分组处理完成
-        sopLatch.await();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== Ai对话SOP 日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    @Async("sopChatTaskExecutor")
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void processAiChatSopAsync(QwSop sop, CountDownLatch latch,LocalDateTime today) {
-        try {
-            processAiChatSop(sop,today);
-        } catch (Exception e) {
-            log.error("处理 SOP ID {} 时发生异常: {}", sop.getId(), e.getMessage(), e);
-        } finally {
-            latch.countDown();
-        }
-    }
-
-    /**
-     * 查询任务中对应模板
-     * @throws Exception
-     */
-    private void processAiChatSop(QwSop sop,LocalDateTime today) throws Exception {
-        QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sop.getId());
-        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId(ruleTimeVO.getTempId());
-        if (rulesList.isEmpty()) {
-            log.warn("SOP ID {} 的 TempSetting 为空,跳过处理。", sop.getId());
-            return;
-        }
-        String[] userIdArray  = sop.getQwUserIds().split(",");
-
-        List<String> qwIds = Arrays.asList(userIdArray);
-
-        List<QwUser> qwUsers = qwUserMapper.selectQwUserByUserIds(qwIds);
-
-        CountDownLatch userLogsLatch = new CountDownLatch(qwUsers.size());
-        for (QwUser qwUser : qwUsers) {
-            processAiChatUserLogInfoAsync(qwUser, ruleTimeVO, rulesList, userLogsLatch,sop,today);
-        }
-
-        // 等待所有用户日志处理完成
-        try {
-            userLogsLatch.await();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("等待用户日志处理完成时被中断: {}", e.getMessage(), e);
-        }
-        log.info("SOP ID {} 的所有用户日志已处理完毕。", sop.getId());
-    }
-
-
-    /**
-     * 处理chatSop消息
-     */
-    @Async("sopChatTaskExecutor")
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void processAiChatUserLogInfoAsync(QwUser qwUser, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings, CountDownLatch latch,QwSop sop,LocalDateTime today) {
-        try {
-            processAiChatUserInfoLog(qwUser, tempSettings,today);
-        } catch (Exception e) {
-            log.error("处理用户日志 {} 时发生异常: {}", qwUser.getId(), e.getMessage(), e);
-        } finally {
-            latch.countDown();
-        }
-    }
-
-    private List<QwChatSopTempSetting.Content> getDay(List<QwSopTempRules> tempSettings, long days){
-        List<QwSopTempRules> collect = tempSettings.stream().filter(e -> e.getDayNum() == days).collect(Collectors.toList());
-        return collect.stream().map(e -> {
-            QwChatSopTempSetting.Content content = new QwChatSopTempSetting.Content();
-            content.setType(e.getType());
-            content.setContentType(e.getContentType() != null ? e.getContentType().toString() : null);
-            content.setSetting(e.getSettingList().stream().map(s -> JSON.parseObject(s.getContent(), QwChatSopTempSetting.Content.Setting.class)).collect(Collectors.toList()));
-            content.setTime(e.getTime());
-            return content;
-        }).collect(Collectors.toList());
-    }
-
-    private void processAiChatUserInfoLog(QwUser qwUser, List<QwSopTempRules> tempSettings,LocalDateTime today){
-        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectChatSopUserLogsByQwUserId(qwUser.getQwUserId(), qwUser.getCorpId());
-        for (SopUserLogsInfo sopUserLogsInfo : sopUserLogsInfos) {
-            String crtTime = sopUserLogsInfo.getCrtTime();
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-            LocalDateTime createTime = LocalDateTime.parse(crtTime, formatter);
-
-            int size = tempSettings.size();
-            if (size>0){
-                    //第一天的
-                    List<QwChatSopTempSetting.Content> content = getDay(tempSettings, 1);
-                    for (QwChatSopTempSetting.Content settCont : content) {
-                        List<QwChatSopTempSetting.Content.Setting> setting = settCont.getSetting();
-                        for (QwChatSopTempSetting.Content.Setting s1 : setting) {
-                            LocalDateTime plusTime = createTime.plusMinutes(s1.getIntervalTime());
-                            boolean sameMinute = today.withSecond(0).withNano(0).isEqual(plusTime.withSecond(0).withNano(0));
-                            if(sameMinute) {
-//                            log.info("发送新客消息内容:"+s1.getValue());
-                            if (s1.getTalkType()!=null&& !s1.getTalkType().isEmpty()){
-                                qwExternalContactInfoService.updateQwExternalContactInfoBytalk(s1.getTalkType(),sopUserLogsInfo.getExternalId());
-                            }
-                            QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
-                            QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
-                            sendMsgParam.setType(101003);
-                            sendMsgData.setMsg(s1.getValue());
-                            sendMsgData.setOpenId(sopUserLogsInfo.getExternalContactId());
-                            sendMsgData.setSyncKey("1");
-                            sendMsgParam.setData(sendMsgData);
-                            SendHookAIParam sendAIParam = new SendHookAIParam();
-                            sendAIParam.setCmd("aiReplyMsg");
-                            sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
-                            sendAIParam.setKey(qwUser.getAppKey());
-                            redisTemplate.opsForList().leftPush("AiMsg:"+qwUser.getAppKey(), JSON.toJSONString(sendAIParam));
-                            }
-                        }
-                    }
-
-
-            }
-
-
-        }
-
-    }
-
-}

+ 0 - 2336
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -1,2336 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import cn.hutool.core.util.ObjectUtil;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.fs.app.taskService.SopLogsTaskService;
-import com.fs.common.config.FSSysConfig;
-import com.fs.common.utils.PubFun;
-import com.fs.common.utils.StringUtils;
-import com.fs.company.domain.Company;
-import com.fs.company.domain.CompanyMiniapp;
-import com.fs.company.domain.CompanyUser;
-import com.fs.company.mapper.CompanyMapper;
-import com.fs.company.service.ICompanyMiniappService;
-import com.fs.company.service.ICompanyUserService;
-import com.fs.config.cloud.CloudHostProper;
-import com.fs.course.config.CourseConfig;
-import com.fs.course.domain.*;
-import com.fs.course.mapper.*;
-import com.fs.course.service.IFsCourseLinkService;
-import com.fs.course.service.IFsUserCompanyBindService;
-import com.fs.qw.domain.*;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwUserMapper;
-import com.fs.qw.service.IQwCompanyService;
-import com.fs.qw.service.IQwGroupChatService;
-import com.fs.qw.service.IQwGroupChatUserService;
-import com.fs.qw.service.impl.QwExternalContactServiceImpl;
-import com.fs.qw.vo.GroupUserExternalVo;
-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.mapper.*;
-import com.fs.sop.service.IQwSopLogsService;
-import com.fs.sop.service.IQwSopTempContentService;
-import com.fs.sop.service.IQwSopTempRulesService;
-import com.fs.sop.service.IQwSopTempVoiceService;
-import com.fs.sop.vo.QwCreateLinkByAppVO;
-import com.fs.sop.vo.SopUserLogsVo;
-import com.fs.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;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.*;
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
-
-import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
-
-@Service
-@Slf4j
-public class SopLogsTaskServiceImpl implements SopLogsTaskService {
-
-
-    private static final String REAL_LINK_PREFIX = "/courseH5/pages/course/learning?course=";
-    private static final String SHORT_LINK_PREFIX = "/courseH5/pages/course/learning?s=";
-    private static final String miniappRealLink = "/pages_course/video.html?course=";
-    private static final String appRealLink = "/pages/courseAnswer/index?link=";
-    private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
-
-//    private static final String miniappRealLink = "/pages/index/index?course=";
-
-    private static final String QWSOP_KEY_PREFIX = "qwsop:";
-    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-    private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd 07:00:00");
-
-
-    // Cached configurations and domain names
-    private CourseConfig cachedCourseConfig;
-    private final Object configLock = new Object();
-
-    private List<FsCourseDomainName> cachedDomainNames;
-    private final Object domainLock = new Object();
-
-
-    // Batch size for database inserts, configurable via application.properties
-    private final int BATCH_SIZE = 500;
-
-    @Autowired
-    private IFsCourseLinkService courseLinkService;
-    @Autowired
-    private SopUserLogsMapper sopUserLogsMapper;
-    @Autowired
-    private QwSopTagMapper qwSopTagMapper ;
-    @Autowired
-    private QwSopMapper sopMapper;
-
-
-    @Autowired
-    private QwExternalContactServiceImpl qwExternalContactService;
-
-    @Autowired
-    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-
-    @Autowired
-    private IQwSopLogsService qwSopLogsService;
-
-    @Autowired
-    private QwSopLogsMapper qwSopLogsMapper;
-
-    @Autowired
-    private FsCourseLinkMapper fsCourseLinkMapper;
-
-    @Autowired
-    private FsCourseSopAppLinkMapper fsCourseSopAppLinkMapper;
-
-    @Autowired
-    private ISysConfigService configService;
-
-    @Autowired
-    private FsCourseDomainNameMapper fsCourseDomainNameMapper;
-
-    @Autowired
-    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
-    @Autowired
-    private QwUserMapper qwUserMapper;
-    @Autowired
-    private IQwSopTempRulesService qwSopTempRulesService;
-    @Autowired
-    private IQwSopTempContentService qwSopTempContentService;
-    @Autowired
-    private IQwSopTempVoiceService qwSopTempVoiceService;
-    @Autowired
-    private CloudHostProper cloudHostProper;
-
-    // Blocking queues with bounded capacity to implement backpressure
-    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;
-    @Autowired
-    private IQwGroupChatService qwGroupChatService;
-    @Autowired
-    private IQwGroupChatUserService qwGroupChatUserService;
-    @Autowired
-    private ICompanyMiniappService companyMiniappService;
-    // Shutdown flags
-    private volatile boolean running = true;
-    @Autowired
-    private QwSopTempMapper qwSopTempMapper;
-
-    @Autowired
-    private ICompanyUserService companyUserService;
-
-    @Autowired
-    private IQwCompanyService iQwCompanyService;
-
-    @Autowired
-    private CompanyMapper companyMapper;
-
-    @Autowired
-    private AsyncCourseWatchFinishService asyncCourseWatchFinishService;
-
-    @Autowired
-    private IFsUserCompanyBindService fsUserCompanyBindService;
-
-
-    @Autowired
-    private IQwSopTempVoiceService sopTempVoiceService;
-
-    @PostConstruct
-    public void init() {
-        loadCourseConfig();
-        startConsumers();
-    }
-
-    private void loadCourseConfig() {
-        try {
-            String json = configService.selectConfigByKey("course.config");
-            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
-            if (config != null) {
-                cachedCourseConfig = config;
-                log.info("Loaded course.config successfully.");
-            } else {
-                log.error("Failed to load course.config from configService.");
-            }
-        } catch (Exception e) {
-            log.error("Exception while loading course.config: {}", e.getMessage(), e);
-        }
-    }
-
-
-
-    private void startConsumers() {
-        qwSopLogsExecutor = Executors.newSingleThreadExecutor(r -> {
-            Thread t = new Thread(r, "QwSopLogsConsumer");
-            t.setDaemon(true);
-            return t;
-        });
-        watchLogsExecutor = Executors.newSingleThreadExecutor(r -> {
-            Thread t = new Thread(r, "WatchLogsConsumer");
-            t.setDaemon(true);
-            return t;
-        });
-        courseLinkExecutor = Executors.newSingleThreadExecutor(r -> {
-            Thread t = new Thread(r, "courseLinkConsumer");
-            t.setDaemon(true);
-            return t;
-        });
-
-        courseSopAppLinkExecutor = Executors.newSingleThreadExecutor(r -> {
-            Thread t = new Thread(r, "courseSopAppLinkConsumer");
-            t.setDaemon(true);
-            return t;
-        });
-
-
-        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
-    @Scheduled(fixedDelay = 60000) // 每60秒刷新一次
-    public void refreshCourseConfig() {
-        synchronized(configLock) {
-            try {
-                String json = configService.selectConfigByKey("course.config");
-                CourseConfig newConfig = JSON.parseObject(json, CourseConfig.class);
-                if (newConfig != null) {
-                    cachedCourseConfig = newConfig;
-                    log.info("Refreshed course.config.");
-                } else {
-                    log.error("Failed to refresh course.config.");
-                }
-            } catch (Exception e) {
-                log.error("Exception while refreshing course.config: {}", e.getMessage(), e);
-            }
-        }
-    }
-
-
-
-    @PreDestroy
-    public void shutdownConsumers() {
-        running = false;
-        qwSopLogsExecutor.shutdown();
-        watchLogsExecutor.shutdown();
-        courseLinkExecutor.shutdown();
-        courseSopAppLinkExecutor.shutdown();
-        try {
-            if (!qwSopLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                qwSopLogsExecutor.shutdownNow();
-            }
-            if (!watchLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                watchLogsExecutor.shutdownNow();
-            }
-            if (!courseLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                courseLinkExecutor.shutdownNow();
-            }
-            if (!courseSopAppLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                courseSopAppLinkExecutor.shutdownNow();
-            }
-        } catch (InterruptedException e) {
-            qwSopLogsExecutor.shutdownNow();
-            watchLogsExecutor.shutdownNow();
-            courseLinkExecutor.shutdownNow();
-            courseSopAppLinkExecutor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
-    }
-
-    @Override
-    public void selectSopUserLogsListByTime(LocalDateTime currentTime, List<String> sopidList) throws Exception {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 SOP 用户日志 ======");
-
-        // 获取缓存的配置
-        CourseConfig config;
-        synchronized(configLock) {
-            config = cachedCourseConfig;
-        }
-
-        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTime(sopidList);
-        if (sopUserLogsVos.isEmpty()) {
-            log.info("没有需要处理的 SOP 用户日志。");
-            return;
-        }
-        sopUserLogsVos = sopUserLogsVos.stream().filter(e -> e.getFilterMode() == 1 || (e.getFilterMode() == 2 && StringUtils.isNotEmpty(e.getChatId()))).collect(Collectors.toList());
-
-        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);
-            List<String> groupChatUserIds = PubFun.listToNewList(qwGroupChatUserList, QwGroupChatUser::getUserId);
-            if(!groupChatUserIds.isEmpty()){
-                List<GroupUserExternalVo> userList = qwExternalContactMapper.selectByGroupUser(groupChatUserIds);
-                Map<String, List<GroupUserExternalVo>> userMap = PubFun.listToMapByGroupList(userList, GroupUserExternalVo::getExternalUserId);
-                qwGroupChatUserList.forEach(e -> {
-                    e.setUserList(userMap.getOrDefault(e.getUserId(), Collections.emptyList()));
-                });
-            }
-            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);
-        }
-
-        Map<String, List<SopUserLogsVo>> sopLogsGroupedById = sopUserLogsVos.stream()
-                .collect(Collectors.groupingBy(SopUserLogsVo::getSopId));
-
-        // 查询公司关联小程序数据
-        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
-
-        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
-
-
-        List<Company> companies = companyMapper.selectCompanyAllList();
-
-        log.info("共分组 {} 个 SOP ID 进行处理。", sopLogsGroupedById.size());
-
-        CountDownLatch sopGroupLatch = new CountDownLatch(sopLogsGroupedById.size());
-
-        for (Map.Entry<String, List<SopUserLogsVo>> entry : sopLogsGroupedById.entrySet()) {
-            String sopId = entry.getKey();
-            List<SopUserLogsVo> userLogsVos = entry.getValue();
-            processSopGroupAsync(sopId, userLogsVos, sopGroupLatch,currentTime, groupChatMap,config,miniMap,companies);
-        }
-
-        // 等待所有 SOP 分组处理完成
-        sopGroupLatch.await();
-
-        // 触发批量插入(可选,如果需要立即插入队列中的数据)
-        // batchInsertQwSopLogs();
-        // batchInsertFsCourseWatchLogs();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    @Async("sopTaskExecutor")
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void processSopGroupAsync(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch ,LocalDateTime currentTime,
-                                     Map<String, QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                     List<Company> companies) {
-        try {
-            processSopGroup(sopId, userLogsVos,currentTime, groupChatMap, config,miniMap,companies);
-        } catch (Exception e) {
-            log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
-        } finally {
-            latch.countDown();
-        }
-    }
-
-
-    private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos,LocalDateTime currentTime, Map<String,
-                                         QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                 List<Company> companies) throws Exception {
-        QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
-
-        if (ruleTimeVO == null) {
-//            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
-            log.error("SOP ID {} 已删除或不存在,相关日志已清除。", sopId);
-            return;
-        }
-        QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
-        if (qwSopTemp == null) {
-//            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
-            log.error("SOP ID {} 模板不存在,相关日志已清除。", sopId);
-            return;
-        }
-
-        ruleTimeVO.setTempStatus(qwSopTemp.getStatus());
-        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);
-            log.error("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
-            return;
-        }
-
-        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId(ruleTimeVO.getTempId());
-        if (rulesList.isEmpty()) {
-            log.error("SOP ID {} 的 TempSetting 为空,跳过处理。", sopId);
-            return;
-        }
-
-        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(ruleTimeVO.getCorpId());
-
-        if (qwCompany == null ) {
-            log.error("SOP ID {} 的 公司信息为空 为空,跳过处理。", sopId);
-            return ;
-        }
-
-        CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
-        for (SopUserLogsVo logVo : userLogsVos) {
-            processUserLogAsync(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime, groupChatMap,qwCompany.getMiniAppId(),
-                    config,miniMap,companies);
-        }
-
-        // 等待所有用户日志处理完成
-        try {
-            userLogsLatch.await();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("等待用户日志处理完成时被中断: {}", e.getMessage(), e);
-        }
-        log.info("SOP ID {} 的所有用户日志已处理完毕。", sopId);
-    }
-
-    @Async("sopTaskExecutor")
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void processUserLogAsync(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
-                                    CountDownLatch latch, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,
-                                    String miniAppId,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                    List<Company> companies) {
-        try {
-            processUserLog(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap, miniAppId, config,miniMap,companies);
-        } catch (Exception e) {
-            log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
-        } finally {
-            latch.countDown();
-        }
-    }
-
-
-    private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
-                                LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,String miniAppId,
-                                CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                List<Company> companies) {
-        try {
-
-            LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
-            LocalDate currentDate = currentTime.toLocalDate();
-
-            long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
-            int tempGap = ruleTimeVO.getTempGap();
-
-            if (tempGap <= 0) {
-                log.error("SOP ID {} 的 TempGap {} 无效,跳过处理。", logVo.getSopId(), tempGap);
-                return;
-            }
-
-            int intervalDay = (int) (daysBetween / tempGap);
-            if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
-                log.info("用户日志 {} 的 intervalDay {} 超出 TempSettings 范围,跳过处理。", logVo.getId(), intervalDay);
-                return;
-            }
-            long day = daysBetween;
-            if(day == 0 && ruleTimeVO.getIsAutoSop() == 1){
-                day = 1;
-            }else{
-                day++;
-            }
-            List<QwSopTempSetting.Content> contents = getDay(tempSettings, day);
-            if (contents == null || contents.isEmpty()) {
-                log.error("SOP ID {} 的 TempSetting 内容为空,跳过处理。天数 {}", logVo.getSopId(),day);
-                return;
-            }
-
-
-            //获取企业微信员工的称呼//从redis里或者从库里取
-            QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedis(logVo.getCorpId(),logVo.getQwUserId());
-            if (qwUserByRedis==null){
-                log.error("无企微员工信息 {} 跳过处理。:{}", logVo.getUserId(),logVo.getCorpId());
-                return;
-            }
-
-            String qwUserId = String.valueOf(qwUserByRedis.getId()).trim();
-            String companyUserId = String.valueOf(qwUserByRedis.getCompanyUserId()).trim();
-            String companyId = String.valueOf(qwUserByRedis.getCompanyId()).trim();
-            Integer sendMsgType = qwUserByRedis.getSendMsgType();
-
-            if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
-                log.error("员工未绑定销售账号或公司,跳过处理:"+qwUserId);
-                return;
-            }
-
-            CompanyUser companyUser = companyUserService.selectCompanyUserByIdForRedis(Long.valueOf(companyUserId));
-            if (Objects.nonNull(companyUser)) {
-                if (!StringUtil.strIsNullOrEmpty(companyUser.getDomain())) {
-                    logVo.setDomain(companyUser.getDomain().trim());
-                } else {
-                    logVo.setDomain(config.getRealLinkDomainName().trim());
-                }
-            } else {
-                logVo.setDomain(config.getRealLinkDomainName().trim());
-            }
-
-            //寻找时间
-//            LocalDateTime currentTime = LocalDateTime.of(2024, 12, 25,23 , 40);
-
-            // 先算好 60分钟后 ~ 再60分钟后的时间段
-            LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
-
-            // 如果发现已经跨天
-            if (!startRangeFirst.toLocalDate().equals(currentDate)) {
-                // 更新 currentDate
-                currentDate = startRangeFirst.toLocalDate();
-
-                // 重新计算 daysBetween
-                daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
-                intervalDay = (int) (daysBetween / tempGap);
-                day = daysBetween;
-                if(day == 0 && ruleTimeVO.getIsAutoSop() == 1){
-                    day = 1;
-                }else{
-                    day++;
-                }
-//
-//                // 再次验证 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);
-                if (contents == null || contents.isEmpty()) {
-                    log.error("跨天-SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
-                    return;
-                }
-            }
-
-
-            // 只有整倍数才做事
-            if (daysBetween % tempGap != 0) {
-                log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap,logVo.getSopId());
-                return;
-            }
-
-
-            for (QwSopTempSetting.Content content : contents) {
-                try {
-
-                    LocalTime elementLocalTime = LocalTime.parse(content.getTime());
-                    LocalDateTime elementDateTime = LocalDateTime.of(currentTime.toLocalDate(), elementLocalTime);
-
-                    // 动态调整 elementDateTime 的日期
-                    if (elementLocalTime.isBefore(currentTime.toLocalTime())) {
-                        elementDateTime = elementDateTime.plusDays(1);
-                    }
-
-                    LocalDateTime startRange = currentTime.plusMinutes(60);
-                    LocalDateTime endRange = startRange.plusMinutes(60);
-
-                    // 跨天逻辑修正:仅当 startRange 的时间晚于 endRange 的时间时调整
-                    if (startRange.toLocalTime().isAfter(endRange.toLocalTime())
-                            && startRange.toLocalDate().equals(endRange.toLocalDate())) {
-                        endRange = endRange.plusDays(1); // 将 endRange 调整为第二天
-                    }
-                    if (!elementDateTime.isBefore(startRange) && !elementDateTime.isAfter(endRange.minusMinutes(1))) {
-
-                        // 如果时间差在目标范围内,更新记录
-                        // 组合年月日和element的时间
-                        LocalDate targetDate = startDate.plusDays(intervalDay * tempGap);
-
-                        // 将 targetDate 和 elementTime 组合成 LocalDateTime
-                        LocalDateTime dateTime = LocalDateTime.of(targetDate, elementLocalTime);
-
-                        // 将 LocalDateTime 转换为 Date
-                        Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
-
-                        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);
-                            if (!externalContactIdList.isEmpty()) {
-                                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 TODO
-//                        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, qwUserId,
-                                companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName(),
-                                groupChatMap, miniAppId,config,miniMap, sendMsgType,companies);
-
-                    }
-                } catch (Exception e) {
-                    log.error("解析模板内容 {} 失败: {}", content.getTime(), e.getMessage(), e);
-                }
-            }
-
-        } catch (Exception e) {
-            log.error("解析解析模板 {} 失败: {}", logVo.getStartTime(), e.getMessage(), e);
-        }
-    }
-
-
-    private List<QwSopTempSetting.Content> getDay(List<QwSopTempRules> tempSettings, long days){
-        List<QwSopTempRules> collect = tempSettings.stream().filter(e -> e.getDayNum() == days && e.getTime() != null).collect(Collectors.toList());
-        AtomicInteger i = new AtomicInteger();
-        return collect.stream().map(e -> {
-            QwSopTempSetting.Content content = new QwSopTempSetting.Content();
-            content.setId(e.getId());
-            content.setType(e.getType());
-            content.setContentType(e.getContentType() != null ? e.getContentType().toString() : null);
-            content.setSetting(e.getSettingList().stream().map(s -> {
-                QwSopTempSetting.Content.Setting setting = JSON.parseObject(s.getContent(), QwSopTempSetting.Content.Setting.class);
-                setting.setId(s.getId());
-                return setting;
-            }).collect(Collectors.toList()));
-            content.setAddTag(e.getAddTag());
-            content.setDelTag(e.getDelTag());
-            content.setTime(e.getTime());
-            content.setIsOfficial(e.getIsOfficial());
-            content.setCourseId(e.getCourseId());
-            content.setVideoId(e.getVideoId());
-            content.setCourseType(e.getCourseType());
-            content.setAiTouch(e.getAiTouch());
-            return content;
-        }).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,
-                                   Map<String, QwGroupChat> groupChatMap,String miniAppId,CourseConfig config,
-                                   Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
-                                   List<Company> companies) {
-        String formattedSendTime = sendTime.toInstant()
-                .atZone(ZoneId.systemDefault())
-                .format(DATE_TIME_FORMATTER);
-        int type = content.getType();
-        Long courseId = content.getCourseId();
-        Long videoId = content.getVideoId();
-        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);
-            if (valuesList != null && !valuesList.isEmpty()) {
-                try {
-                    List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
-                    if (voiceList != null && !voiceList.isEmpty()) {
-                        Map<String, QwSopTempVoice> collect = voiceList.stream().collect(Collectors.toMap(QwSopTempVoice::getVoiceTxt, e -> e));
-                        setting.parallelStream().filter(e -> "7".equals(e.getContentType())).forEach(st -> {
-                            QwSopTempVoice voice = collect.get(st.getValue());
-                            if (voice.getVoiceUrl() == null) {
-                                return;
-                            }
-                            st.setVoiceUrl(voice.getVoiceUrl());
-                            st.setVoiceDuration(voice.getDuration() + "");
-                        });
-                    }
-                } catch (NumberFormatException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        }
-//        // 发送语音 end
-        if (content.getType()==5){
-            sopAddTag(logVo,content,sendTime);
-        }
-
-        //当语音模板的qw_sop_temp_voice中无对应语音,就不生成qw_sop_logs记录
-        if (content.getType() == 7 && content.getSetting() != null && !content.getSetting().isEmpty()) {
-            if (content.getSetting().get(0).getVoiceUrl() == null) {
-                return;
-            }
-        }
-
-        if (StringUtils.isNotEmpty(logVo.getChatId())) {
-            QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
-            ruleTimeVO.setSendType(6);
-            ruleTimeVO.setType(2);
-            if (groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()) {
-                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null,null);
-                handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                        type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
-            }
-//            if (content.getIndex() == 0) {
-//                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
-//                handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-//                        type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-//                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
-//            } else {
-//                if(groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()){
-//                    groupChat.getChatUserList().forEach(user -> {
-//                        ruleTimeVO.setSendType(2);
-//                        ruleTimeVO.setRemark("客户群催课");
-//                        QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, user.getUserId(), user.getName(), null, isOfficial, null);
-//                        handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-//                                type, qwUserId, companyUserId, companyId, user.getId().toString(), welcomeText, qwUserName,
-//                                null, false, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
-//                    });
-//                }
-//            }
-        } else {
-            // 处理每个 externalContactId
-            sopUserLogsInfos.forEach(contactId -> {
-                try {
-                    String externalId = contactId.getExternalId().toString();
-                    String externalUserName = contactId.getExternalUserName();
-                    Long fsUserId = contactId.getFsUserId();
-                    Integer grade = contactId.getGrade();
-                    QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(),contactId.getIsDaysNotStudy());
-                    handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                            type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
-                            null,config, miniMap, grade, sendMsgType,companies);
-                } catch (Exception e) {
-                    log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
-                }
-            });
-        }
-//        // 处理每个 externalContactId
-//        sopUserLogsInfos.forEach(contactId -> {
-//            try {
-//                String externalId = contactId.getExternalId().toString();
-//                String externalUserName = contactId.getExternalUserName();
-//                Long fsUserId = contactId.getFsUserId();
-//                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId,isOfficial,contactId.getExternalId());
-//                handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-//                        type, qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName);
-//            } catch (Exception e) {
-//                log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
-//            }
-//        });
-    }
-
-    private void sopAddTag(SopUserLogsVo logVo, QwSopTempSetting.Content content, Date sendTime) {
-        String id = logVo.getId();
-        String addTag = content.getAddTag();
-        String delTag = content.getDelTag();
-        String corpId = logVo.getCorpId();
-        if (addTag!=null || delTag!=null) {
-            QwSopTag qwSopTag = new QwSopTag();
-            qwSopTag.setAddTags(addTag);
-            qwSopTag.setDelTags(delTag);
-            qwSopTag.setCorpId(corpId);
-            qwSopTag.setSopUserLogsId(id);
-            qwSopTag.setType(1);
-            qwSopTag.setStatus(1);
-            qwSopTag.setSendTime(sendTime);
-            qwSopTag.setCreateTime(new Date());
-            qwSopTagMapper.insertQwSopTag(qwSopTag);
-        }
-    }
-
-    private QwSopLogs createBaseLog(String formattedSendTime, SopUserLogsVo logVo,
-                                    QwSopRuleTimeVO ruleTimeVO, String externalContactId,
-                                    String externalUserName, Long fsUserId,Integer isOfficial,
-                                    Long externalId,Integer isDaysNotStudy) {
-        QwSopLogs sopLogs = new QwSopLogs();
-        sopLogs.setSendTime(formattedSendTime);
-        sopLogs.setQwUserid(logVo.getQwUserId());
-        sopLogs.setCorpId(logVo.getCorpId());
-        sopLogs.setLogType(ruleTimeVO.getType());
-        sopLogs.setTakeRecords(0);
-
-        if (isOfficial != 1 && Integer.valueOf(1).equals(isDaysNotStudy)) {
-            sopLogs.setSendStatus(5L);
-            sopLogs.setRemark("E级客户不发送");
-        }else {
-            sopLogs.setSendStatus(3L);
-        }
-
-        sopLogs.setReceivingStatus(0L);
-
-        if (isOfficial == 1) {
-
-            if (logVo.getIsSampSend()== 1) {
-                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
-                    sopLogs.setSendType(2);
-                    sopLogs.setRemark("未绑定小程序用户,单链补发");
-                    //时间设置成固定8点
-                    LocalDateTime dateTime = LocalDateTime.parse(formattedSendTime, DATE_TIME_FORMATTER);
-                    sopLogs.setSendTime(OUTPUT_FORMATTER.format(dateTime));
-                } else {
-                    sopLogs.setSendType(1);
-                }
-
-            }else {
-                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
-                    sopLogs.setTakeRecords(1);
-                    sopLogs.setSendType(1);
-                }else {
-                    sopLogs.setSendType(1);
-                }
-            }
-
-        } else if (isOfficial == 0) {
-            sopLogs.setSendType(ruleTimeVO.getSendType() == 1 ? 2 : ruleTimeVO.getSendType());
-        } else {
-            sopLogs.setSendType(ruleTimeVO.getSendType());
-        }
-
-
-
-
-        String[] userKey = logVo.getUserId().split("\\|");
-        sopLogs.setCompanyId(Long.valueOf(userKey[2].trim()));
-        if (StringUtils.isNotEmpty(userKey[0].trim())){
-            sopLogs.setQwUserKey(Long.valueOf(userKey[0].trim()));
-        }
-        sopLogs.setSopId(logVo.getSopId());
-        sopLogs.setSort(Integer.valueOf(logVo.getStartTime().replaceAll("-","")));
-        sopLogs.setExternalUserId(externalContactId);
-        sopLogs.setExternalId(externalId);
-        sopLogs.setExternalUserName(externalUserName);
-        sopLogs.setFsUserId(fsUserId);
-        sopLogs.setUserLogsId(logVo.getId());
-
-        if (ObjectUtil.isNotEmpty(logVo.getActualQwId())){
-            sopLogs.setQwUserKey(logVo.getActualQwId());
-        }
-        return sopLogs;
-    }
-
-    private void handleLogBasedOnType(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, boolean isGroupChat, String miniAppId,
-                                      QwGroupChat groupChat,CourseConfig config,
-                                      Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                      Integer grade, Integer sendMsgType ,List<Company> companies ) {
-        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,
-                        isGroupChat, miniAppId, groupChat,config,miniMap, grade, sendMsgType,companies);
-                break;
-            case 3:
-                handleOrderMessage(sopLogs, content);
-                break;
-            case 4:
-//                handleAIMessage(sopLogs, content);
-                break;
-            case 5:
-//                handleTagMessage(sopLogs, content);
-                break;
-            case 7:
-                handleVoiceMessage(sopLogs, content, companyUserId);
-                break;
-            default:
-                log.error("未知的消息类型 {},跳过处理。", type);
-                break;
-        }
-    }
-    private void handleVoiceMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId) {
-        sopLogs.setContentJson(JSON.toJSONString(content));
-        enqueueQwSopLogs(sopLogs);
-    }
-
-    private void handleNormalMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,String companyUserId) {
-
-        sopLogs.setContentJson(JSON.toJSONString(content));
-        enqueueQwSopLogs(sopLogs);
-    }
-
-    private void handleAIMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
-        sopLogs.setContentJson(JSON.toJSONString(content));
-        sopLogs.setSort(3);
-        enqueueQwSopLogs(sopLogs);
-    }
-
-    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, boolean isGroupChat, String miniAppId, QwGroupChat groupChat,CourseConfig config,Map<Long,
-                    Map<Integer, List<CompanyMiniapp>>> miniMap,Integer grade, Integer sendMsgType,
-                                     List<Company> companies) {
-        QwExternalContact contact = null;
-        if(logVo.getExternalId() != null){
-            contact = qwExternalContactMapper.selectById(logVo.getExternalId());
-        }
-        // 深拷贝 Content 对象,避免使用 JSON
-        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
-        if (clonedContent == null) {
-            log.error("Failed to clone content, skipping handleCourseMessage.");
-            return;
-        }
-
-//
-//        Integer courseType = clonedContent.getCourseType();
-
-        String isOfficial = clonedContent.getIsOfficial();
-
-        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
-        if (settings == null || settings.isEmpty()) {
-            log.error("Cloned content settings are empty, skipping.");
-            return;
-        }
-
-        // 顺序处理每个 Setting,避免过多的并行导致线程开销
-        for (QwSopTempSetting.Content.Setting setting : settings) {
-            switch (setting.getContentType()) {
-                //文字和短链一起
-                case "1":
-                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(Long.valueOf(qwUserId));
-//                            createParam.setDays(setting.getExpiresDays());
-//                            R createLink = courseLinkService.createRoomLinkUrl(createParam);
-//                            if (createLink.get("code").equals(500)) {
-//                                throw new BaseException("链接生成失败!");
-//                            }
-//                            try {
-//                                groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
-//                                    Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
-//                                    GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
-//                                    if (vo != null && vo.getId() != null) {
-//                                        sopLogs.setFsUserId(vo.getFsUserId());
-//                                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo);
-//                                    }
-//                                });
-//                            } catch (Exception e) {
-//                                log.error("群聊创建看课记录失败!", e);
-//                            }
-//                            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,isOfficial,sopLogs.getFsUserId());
-//                        }
-
-//                        if (StringUtils.isNotEmpty(link)) {
-//                            if ("3".equals(setting.getContentType())) {
-//                                setting.setLinkUrl(link);
-//                            } else {
-//                                String currentValue = setting.getValue();
-//                                if (currentValue == null) {
-//                                    setting.setValue(link);
-//                                } else {
-//                                    setting.setValue(currentValue
-//                                            .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(welcomeText) ? "" : welcomeText)
-//                                            .replaceAll("#客户称呼#", contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus())|| "0".equals(contact.getStageStatus())?"同学":contact.getStageStatus())
-//                                            + "\n" + link);
-//                                }
-//                            }
-//                        } else {
-//                            log.error("生成短链失败,跳过设置 URL。");
-//                        }
-
-//                    } else {
-                        if ("1".equals(setting.getContentType())) {
-                            String defaultName = "同学";
-                            if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
-                                defaultName = contact.getName();
-                            }
-                            setting.setValue(setting.getValue()
-                                    .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(welcomeText) ? "" : welcomeText)
-                                    .replaceAll("#客户称呼#", contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus())|| "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
-                        }
-//                    }
-                    break;
-                //小程序单独
-                case "4":
-                    if (isGroupChat) {
-                        try {
-                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
-                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
-                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
-                                if (vo != null && vo.getId() != null) {
-                                    sopLogs.setFsUserId(vo.getFsUserId());
-                                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo);
-                                }
-                            });
-                        } catch (Exception e) {
-                            log.error("群聊创建看课记录失败!", e);
-                        }
-                    } else {
-                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
-                    }
-
-                    String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
-                            qwUserId, companyUserId, companyId, externalId,isOfficial,sopLogs.getFsUserId(), isGroupChat ? groupChat.getChatId() : null);
-
-                    if(sopLogs.getSendType()==1){
-                        setting.setMiniprogramAppid(miniAppId);
-                    }else {
-                        int miniType = getLevel(grade);
-                        //算主备小程序
-                        String finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
-
-                        if (StringUtil.strIsNullOrEmpty(finalAppId)) {
-                            finalAppId = miniAppId;
-                        }
-
-                        setting.setMiniType(miniType);
-                        if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
-                            setting.setMiniprogramAppid(finalAppId);
-                        } else {
-                            log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
-                        }
-
-                    }
-
-                    setting.setMiniprogramPage(sortLink.replaceAll("^[\\s\\u2005]+", ""));
-
-                    try {
-                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl())? config.getSidebarImageUrl():setting.getMiniprogramPicUrl());
-                    } catch (Exception e) {
-                        log.error("赋值-小程序封面地址失败-" + e);
-                    }
-
-                    break;
-                //app
-                case "9":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
-
-                    QwCreateLinkByAppVO linkByApp = createLinkByApp(setting, logVo, sendTime, courseId, videoId,
-                            qwUserId, companyUserId, companyId, externalId,sopLogs.getCorpId(),qwUserName);
-
-                    setting.setLinkUrl(linkByApp.getSortLink().replaceAll("^[\\s\\u2005]+", ""));
-                    setting.setAppLinkUrl(linkByApp.getAppMsgLink().replaceAll("^[\\s\\u2005]+", ""));
-                    setting.setCourseUrl(setting.getLinkImageUrl());
-                    setting.setTitle(setting.getLinkDescribe()); //小节名称
-
-                    break;
-                //自定义小程序
-                case "10":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
-
-                    Optional<Company> matchedCompany = companies.stream()
-                            .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
-                            .findFirst();
-                    if (matchedCompany.isPresent()) {
-                        Company company = matchedCompany.get();
-
-                        String customMiniAppId = company.getCustomMiniAppId();
-
-                        if (customMiniAppId != null && !customMiniAppId.trim().isEmpty()) {
-                            setting.setMiniprogramAppid(customMiniAppId);
-                        } else {
-                            setting.setMiniprogramAppid("该公司未配置自定义小程序:"+companyId);
-                        }
-                    } else {
-                        setting.setMiniprogramAppid("未找到匹配的公司的自定义小程序:"+companyId);
-                    }
-
-                    break;
-                //直播小程序单独
-                case "12":
-                    String sortLiveLink;
-                    sortLiveLink = "/pages_course/living?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId();
-
-
-                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
-                    int maxLiveLength = 17;
-                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
-                    String json = configService.selectConfigByKey("his.config");
-                    FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
-                    setting.setMiniprogramAppid(sysConfig.getAppId());
-                    setting.setMiniprogramPage(sortLiveLink);
-                    setting.setContentType("4");
-                    try {
-                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
-                    } catch (Exception e) {
-                        log.error("赋值-小程序封面地址失败-" + e);
-                    }
-
-                    break;
-                default:
-                    break;
-            }
-
-        }
-        clonedContent.getSetting().stream().filter(e -> "1".equals(e.getIsBindUrl())).forEach(e -> {
-            e.setIsBindUrl("0");
-//            e.setLinkDescribe(null);
-            e.setLinkUrl(null);
-//            e.setLinkImageUrl(null);
-        });
-        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
-        enqueueQwSopLogs(sopLogs);
-    }
-
-    private String getAppIdFromMiniMap(Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                       String companyId,
-                                       int sendMsgType,
-                                       Integer grade) {
-        if (miniMap.isEmpty() || sendMsgType != 1) {
-            return null;
-        }
-
-        Map<Integer, List<CompanyMiniapp>> gradeMap = miniMap.get(Long.valueOf(companyId));
-        if (gradeMap == null) {
-            return null;
-        }
-
-        int listIndex = getLevel(grade);
-        List<CompanyMiniapp> miniapps = gradeMap.get(listIndex);
-
-        if (miniapps == null || miniapps.isEmpty()) {
-            return null;
-        }
-
-        CompanyMiniapp companyMiniapp = miniapps.get(0);
-        return (companyMiniapp != null && !StringUtil.strIsNullOrEmpty(companyMiniapp.getAppId()))
-                ? companyMiniapp.getAppId()
-                : null;
-    }
-
-    private static int getLevel(Integer grade) {
-        int effectiveGrade = (grade == null) ? 5 : grade;
-        int listIndex = (effectiveGrade == 1 || effectiveGrade == 2) ? 0 : 1;
-        return listIndex;
-    }
-
-    /**
-     * 深拷贝 Content 对象,避免使用 JSON 进行序列化和反序列化
-     */
-    private QwSopTempSetting.Content deepCopyContent(QwSopTempSetting.Content content) {
-        if (content == null) {
-            return null;
-        }
-        return content.clone();
-    }
-
-    private void handleOrderMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
-        sopLogs.setContentJson(JSON.toJSONString(content));
-        enqueueQwSopLogs(sopLogs);
-    }
-
-
-
-
-    private String generateShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-                                     Long courseId, Long videoId, String qwUserId,
-                                     String companyUserId, String companyId, String externalId,String isOfficial, Long fsUserId) {
-        // 获取缓存的配置
-        CourseConfig config;
-        synchronized(configLock) {
-            config = cachedCourseConfig;
-        }
-
-        if (config == null) {
-            log.error("CourseConfig is not loaded.");
-            return "";
-        }
-
-        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
-        FsCourseLink link = new FsCourseLink();
-        link.setCompanyId(Long.parseLong(companyId));
-        link.setQwUserId(Long.parseLong(qwUserId));
-        link.setCompanyUserId(Long.parseLong(companyUserId));
-        link.setVideoId(videoId.longValue());
-        link.setCorpId(logVo.getCorpId());
-        link.setCourseId(courseId.longValue());
-        link.setQwExternalId(Long.parseLong(externalId));
-
-        if (StringUtil.strIsNullOrEmpty(isOfficial)){
-            link.setLinkType(0);
-        }else {
-            if (isOfficial.equals("1")) {
-                if (fsUserId== null || Long.valueOf(0L).equals(fsUserId)){
-                    link.setLinkType(0);
-                }else {
-                    link.setLinkType(5);
-                }
-            }else if (isOfficial.equals("0")){
-                link.setLinkType(0);
-            }else{
-                link.setLinkType(0);
-            }
-        }
-
-        FsCourseRealLink courseMap = new FsCourseRealLink();
-        courseMap.setCompanyId(link.getCompanyId());
-        courseMap.setQwUserId(link.getQwUserId());
-        courseMap.setCompanyUserId(link.getCompanyUserId());
-        courseMap.setVideoId(link.getVideoId());
-        courseMap.setCorpId(link.getCorpId());
-        courseMap.setCourseId(link.getCourseId());
-        courseMap.setQwExternalId(link.getQwExternalId());
-        courseMap.setFsUserId(fsUserId);
-
-        if (StringUtil.strIsNullOrEmpty(isOfficial)){
-            courseMap.setLinkType(0);
-        }else {
-            if (isOfficial.equals("1")) {
-                if (fsUserId== null || Long.valueOf(0L).equals(fsUserId)){
-                    courseMap.setLinkType(0);
-                }else {
-                    courseMap.setLinkType(5);
-                }
-            }else if (isOfficial.equals("0")){
-                courseMap.setLinkType(0);
-            }else{
-                courseMap.setLinkType(0);
-            }
-        }
-
-
-        String courseJson = JSON.toJSONString(courseMap);
-        String realLinkFull = REAL_LINK_PREFIX + courseJson;
-        link.setRealLink(realLinkFull);
-
-        String randomString = generateRandomStringWithLock();
-        if (StringUtil.strIsNullOrEmpty(randomString)){
-            link.setLink(UUID.randomUUID().toString().replace("-", ""));
-        }else {
-            link.setLink(randomString);
-        }
-
-        link.setCreateTime(sendTime);
-
-        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
-                ? config.getVideoLinkExpireDate()
-                : setting.getExpiresDays();
-
-        // 使用 Java 8 时间 API 计算过期时间
-        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
-        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
-        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
-        link.setUpdateTime(updateTime);
-
-        //取销售绑定的二级域名
-        String sortLink = logVo.getDomain() + SHORT_LINK_PREFIX + link.getLink();
-        enqueueCourseLink(link);
-        return sortLink.replaceAll("^[\\s\\u2005]+", "");
-    }
-
-    private QwCreateLinkByAppVO createLinkByApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-                                                Long courseId, Long videoId, String qwUserId,
-                                                String companyUserId, String companyId, String externalId,String corpId,String qwUserName){
-        // 获取缓存的配置
-        CourseConfig config;
-        synchronized(configLock) {
-            config = cachedCourseConfig;
-        }
-
-        if (config == null) {
-            log.error("CourseConfig is not loaded.");
-            return null;
-        }
-        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
-                companyUserId, companyId, externalId, 4);
-
-        FsCourseRealLink courseMap = new FsCourseRealLink();
-        BeanUtils.copyProperties(link,courseMap);
-
-        String courseJson = JSON.toJSONString(courseMap);
-        String realLinkFull = REAL_LINK_PREFIX + courseJson;
-        link.setRealLink(realLinkFull);
-
-        Date updateTime = createUpdateTime(setting, sendTime, config);
-
-        link.setUpdateTime(updateTime);
-
-        String sortLink = appLink+link.getLink()+"&videoId="+videoId;
-
-        String appMsgLink=appRealLink+link.getLink();
-
-        QwCreateLinkByAppVO byAppVO=new QwCreateLinkByAppVO();
-        byAppVO.setSortLink(sortLink);
-        byAppVO.setAppMsgLink(appMsgLink);
-
-        FsCourseSopAppLink fsCourseSopAppLink = createFsCourseSopAppLink(link.getLink(), sendTime, updateTime, companyId, companyUserId, qwUserId,
-                qwUserName, corpId, courseId, setting.getLinkTitle(), setting.getLinkImageUrl(), videoId,
-                setting.getLinkDescribe(), appMsgLink, externalId);
-
-        enqueueCourseSopAppLink(fsCourseSopAppLink);
-
-        enqueueCourseLink(link);
-
-        return byAppVO;
-    }
-
-
-    public FsCourseSopAppLink createFsCourseSopAppLink(String link, Date sendTime, Date updateTime, String companyId,
-                                                       String companyUserId,String qwUserId,String qwUserName,String corpId,
-                                                       Long courseId,String linkTile,String linkImageUrl,Long videoId,
-                                                       String linkDescribe,String appMsgLink,String externalId){
-
-        FsCourseSopAppLink sopAppLink=new FsCourseSopAppLink();
-        sopAppLink.setLink(link);
-        sopAppLink.setCreateTime(sendTime);
-        sopAppLink.setUpdateTime(updateTime);
-        sopAppLink.setCompanyId(Long.parseLong(companyId));
-        sopAppLink.setCompanyUserId(Long.parseLong(companyUserId));
-        sopAppLink.setQwUserId(Long.parseLong(qwUserId));
-        sopAppLink.setQwUserName(qwUserName);
-        sopAppLink.setCorpId(corpId);
-        sopAppLink.setCourseId(courseId);
-        sopAppLink.setCourseTitle(linkTile);
-        sopAppLink.setCourseUrl(linkImageUrl);
-        sopAppLink.setVideoId(videoId);
-        sopAppLink.setVideoTitle(linkDescribe);
-        sopAppLink.setAppRealLink(appMsgLink);
-        sopAppLink.setQwExternalId(Long.parseLong(externalId));
-
-
-        return sopAppLink;
-    }
-
-    public FsCourseLink createFsCourseLink(String corpId, Date sendTime,Long courseId,Long videoId, String qwUserId,
-                                           String companyUserId, String companyId,String externalId,Integer type){
-        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
-        FsCourseLink link = new FsCourseLink();
-        link.setCompanyId(Long.parseLong(companyId));
-        link.setQwUserId(Long.parseLong(qwUserId));
-        link.setCompanyUserId(Long.parseLong(companyUserId));
-        link.setVideoId(videoId.longValue());
-        link.setCorpId(corpId);
-        link.setCourseId(courseId.longValue());
-        link.setQwExternalId(Long.parseLong(externalId));
-        link.setLinkType(type); //小程序
-
-        String randomString = generateRandomStringWithLock();
-        if (StringUtil.strIsNullOrEmpty(randomString)){
-            link.setLink(UUID.randomUUID().toString().replace("-", ""));
-        }else {
-            link.setLink(randomString);
-        }
-
-        link.setCreateTime(sendTime);
-
-        return link;
-    }
-
-    private Date createUpdateTime(QwSopTempSetting.Content.Setting setting,Date sendTime,CourseConfig config){
-
-        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
-                ? config.getVideoLinkExpireDate()
-                : setting.getExpiresDays();
-
-//         使用 Java 8 时间 API 计算过期时间
-        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
-        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
-        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
-
-        return updateTime;
-    }
-
-    private String createLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-                                       Long courseId, Long videoId, String qwUserId,
-                                       String companyUserId, String companyId, String externalId,String isOfficial,Long fsUserId, String chatId) {
-        // 获取缓存的配置
-        CourseConfig config;
-        synchronized(configLock) {
-            config = cachedCourseConfig;
-        }
-
-        if (config == null) {
-            log.error("CourseConfig is not loaded.");
-            return "";
-        }
-
-
-//        if (StringUtils.isEmpty(config.getMiniprogramPage())){
-//            log.error("miniprogramPage is not loaded.");
-//            return "";
-//        }
-
-        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
-        FsCourseLink link = new FsCourseLink();
-        link.setCompanyId(Long.parseLong(companyId));
-        link.setQwUserId(Long.parseLong(qwUserId));
-        link.setCompanyUserId(Long.parseLong(companyUserId));
-        link.setVideoId(videoId);
-        link.setCorpId(logVo.getCorpId());
-        link.setCourseId(courseId);
-        if(StringUtils.isEmpty(chatId)){
-            link.setQwExternalId(Long.parseLong(externalId));
-        }
-        link.setProjectCode(cloudHostProper.getProjectCode());
-        link.setChatId(chatId);
-
-        if (StringUtil.strIsNullOrEmpty(isOfficial)){
-            link.setLinkType(3);
-        }else {
-            if (isOfficial.equals("1")) {
-                if (fsUserId== null || Long.valueOf(0L).equals(fsUserId)){
-                    link.setLinkType(3);
-                }else {
-                    link.setLinkType(5);
-                }
-            }else if (isOfficial.equals("0")){
-                link.setLinkType(3);
-            }else{
-                link.setLinkType(3);
-            }
-        }
-
-        String randomString = generateRandomStringWithLock();
-        if (StringUtil.strIsNullOrEmpty(randomString)){
-            link.setLink(UUID.randomUUID().toString().replace("-", ""));
-        }else {
-            link.setLink(randomString);
-        }
-
-        link.setCreateTime(sendTime);
-
-        FsCourseRealLink courseMap = new FsCourseRealLink();
-        BeanUtils.copyProperties(link,courseMap);
-
-        String courseJson = JSON.toJSONString(courseMap);
-        String realLinkFull = miniappRealLink + courseJson;
-        link.setRealLink(realLinkFull);
-
-
-        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
-                ? config.getVideoLinkExpireDate()
-                : setting.getExpiresDays();
-
-        // 使用 Java 8 时间 API 计算过期时间
-        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
-        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
-        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
-        link.setUpdateTime(updateTime);
-
-        //存短链-
-        enqueueCourseLink(link);
-        return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
-    }
-
-    private void addWatchLogIfNeeded(QwSopLogs sopLogs, Long videoId, Long courseId,
-                                     Date sendTime, String qwUserId, String companyUserId,
-                                     String companyId, String externalId,SopUserLogsVo logsVo) {
-        FsCourseWatchLog watchLog = new FsCourseWatchLog();
-        watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
-        watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
-        watchLog.setSendType(2);
-        watchLog.setQwUserId(Long.parseLong(qwUserId));
-        watchLog.setSopId(sopLogs.getSopId());
-        watchLog.setDuration(0L);
-        watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
-        watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
-        watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
-        watchLog.setCreateTime(convertStringToDate(sopLogs.getSendTime(),"yyyy-MM-dd HH:mm:ss"));
-        watchLog.setUpdateTime(new Date());
-        watchLog.setLogType(3);
-        watchLog.setUserId(sopLogs.getFsUserId());
-        watchLog.setCampPeriodTime(convertStringToDate(logsVo.getStartTime(),"yyyy-MM-dd"));
-        enqueueWatchLog(watchLog);
-    }
-
-    /**
-     * 时间字符串转Date时间
-     * @param dateString
-     * @return
-     */
-    public static Date convertStringToDate(String dateString,String pattern) {
-        if (dateString == null || dateString.isEmpty()) {
-            return null;
-        }
-        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
-        LocalDateTime localDateTime;
-        LocalDate localDate;
-        // 先解析成 LocalDate(只含年月日)
-        if (pattern.equals("yyyy-MM-dd")){
-            // 先解析成 LocalDate(只含年月日)
-            localDate = LocalDate.parse(dateString, formatter);
-            // 将 LocalDate 转为当天 00:00:00 的 LocalDateTime
-            localDateTime = localDate.atStartOfDay();
-        }else {
-            localDateTime = LocalDateTime.parse(dateString, formatter);
-        }
-        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
-    }
-
-
-    /**
-     * 将 QwSopLogs 放入队列
-     */
-    private void enqueueQwSopLogs(QwSopLogs sopLogs) {
-        try {
-            boolean offered = qwSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
-            if (!offered) {
-                log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
-                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("插入 QwSopLogs 队列时被中断: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 将 FsCourseWatchLog 放入队列
-     */
-    private void enqueueWatchLog(FsCourseWatchLog watchLog) {
-        try {
-            boolean offered = watchLogsQueue.offer(watchLog, 5, TimeUnit.SECONDS);
-            if (!offered) {
-                log.error("FsCourseWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(watchLog));
-                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("插入 FsCourseWatchLog 队列时被中断: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 将 FsCourseWatchLog 放入队列
-     */
-    private void enqueueCourseLink(FsCourseLink courseLink) {
-        try {
-            boolean offered = linkQueue.offer(courseLink, 5, TimeUnit.SECONDS);
-            if (!offered) {
-                log.error("FsCourseLink 队列已满,无法添加日志: {}", JSON.toJSONString(courseLink));
-                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("插入 FsCourseLink 队列时被中断: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 将 FsCourseSopAppLing 放入队列
-     */
-    private void enqueueCourseSopAppLink(FsCourseSopAppLink sopAppLink) {
-        try {
-            boolean offered = sopAppLinks.offer(sopAppLink, 5, TimeUnit.SECONDS);
-            if (!offered) {
-                log.error("FsCourseSopAppLink 队列已满,无法添加日志: {}", JSON.toJSONString(sopAppLink));
-                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("插入 FsCourseLink 队列时被中断: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 消费 QwSopLogs 队列并进行批量插入
-     */
-    private void consumeQwSopLogs() {
-        List<QwSopLogs> batch = new ArrayList<>(BATCH_SIZE);
-        while (running || !qwSopLogsQueue.isEmpty()) {
-            try {
-                QwSopLogs log = qwSopLogsQueue.poll(1, TimeUnit.SECONDS);
-                if (log != null) {
-                    batch.add(log);
-                }
-                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && log == null)) {
-                    if (!batch.isEmpty()) {
-                        batchInsertQwSopLogs(new ArrayList<>(batch));
-                        batch.clear();
-                    }
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("QwSopLogs 消费线程被中断: {}", e.getMessage(), e);
-            }
-        }
-
-        // 处理剩余的数据
-        if (!batch.isEmpty()) {
-            batchInsertQwSopLogs(batch);
-        }
-    }
-
-    /**
-     * 消费 FsCourseWatchLog 队列并进行批量插入
-     */
-    private void consumeCourseLink() {
-        List<FsCourseLink> batch = new ArrayList<>(BATCH_SIZE);
-        while (running || !linkQueue.isEmpty()) {
-            try {
-                FsCourseLink courseLink = linkQueue.poll(1, TimeUnit.SECONDS);
-                if (courseLink != null) {
-                    batch.add(courseLink);
-                }
-                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && courseLink == null)) {
-                    if (!batch.isEmpty()) {
-                        batchInsertFsCourseLink(new ArrayList<>(batch));
-                        batch.clear();
-                    }
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("FsCourseLink 消费线程被中断: {}", e.getMessage(), e);
-            }
-        }
-
-        // 处理剩余的数据
-        if (!batch.isEmpty()) {
-            batchInsertFsCourseLink(batch);
-        }
-    }
-
-    /**
-     * 消费 FsCourseSopAppLink 队列并进行批量插入
-     */
-    private void consumeCourseSopAppLink() {
-        List<FsCourseSopAppLink> batch = new ArrayList<>(BATCH_SIZE);
-        while (running || !sopAppLinks.isEmpty()) {
-            try {
-                FsCourseSopAppLink courseSopAppLink = sopAppLinks.poll(1, TimeUnit.SECONDS);
-                if (courseSopAppLink != null) {
-                    batch.add(courseSopAppLink);
-                }
-                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && courseSopAppLink == null)) {
-                    if (!batch.isEmpty()) {
-                        batchInsertFsCourseSopAppLink(new ArrayList<>(batch));
-                        batch.clear();
-                    }
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("FsCourseSopAppLink 消费线程被中断: {}", e.getMessage(), e);
-            }
-        }
-
-        // 处理剩余的数据
-        if (!batch.isEmpty()) {
-            batchInsertFsCourseSopAppLink(batch);
-        }
-    }
-
-    /**
-     * 消费 FsCourseWatchLog 队列并进行批量插入
-     */
-    private void consumeWatchLogs() {
-        List<FsCourseWatchLog> batch = new ArrayList<>(BATCH_SIZE);
-        while (running || !watchLogsQueue.isEmpty()) {
-            try {
-                FsCourseWatchLog watchLog = watchLogsQueue.poll(1, TimeUnit.SECONDS);
-                if (watchLog != null) {
-                    batch.add(watchLog);
-                }
-                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && watchLog == null)) {
-                    if (!batch.isEmpty()) {
-                        batchInsertFsCourseWatchLogs(new ArrayList<>(batch));
-                        batch.clear();
-                    }
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("FsCourseWatchLog 消费线程被中断: {}", e.getMessage(), e);
-            }
-        }
-
-        // 处理剩余的数据
-        if (!batch.isEmpty()) {
-            batchInsertFsCourseWatchLogs(batch);
-        }
-    }
-
-    /**
-     * 批量插入 QwSopLogs
-     */
-    @Transactional
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void batchInsertQwSopLogs(List<QwSopLogs> logsToInsert) {
-        try {
-            qwSopLogsService.batchInsertQwSopLogs(logsToInsert);
-            log.info("批量插入 QwSopLogs 完成,共插入 {} 条记录。", logsToInsert.size());
-        } catch (Exception e) {
-            log.error("批量插入 QwSopLogs 失败: {}", e.getMessage(), e);
-            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-        }
-    }
-
-    /**
-     * 批量插入 FsCourseWatchLog
-     */
-    @Transactional
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void batchInsertFsCourseWatchLogs(List<FsCourseWatchLog> watchLogsToInsert) {
-        try {
-            fsCourseWatchLogMapper.insertFsCourseWatchLogBatch(watchLogsToInsert);
-            log.info("批量插入 FsCourseWatchLog 完成,共插入 {} 条记录。", watchLogsToInsert.size());
-        } catch (Exception e) {
-            log.error("批量插入 FsCourseWatchLog 失败: {}", e.getMessage(), e);
-            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-        }
-    }
-
-
-    /**
-     * 批量插入 FsCourseLink
-     */
-    @Transactional
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void batchInsertFsCourseLink(List<FsCourseLink> courseLinkToInsert) {
-        try {
-            fsCourseLinkMapper.insertFsCourseLinkBatch(courseLinkToInsert);
-            log.info("批量插入 FsCourseLink 完成,共插入 {} 条记录。", courseLinkToInsert.size());
-        } catch (Exception e) {
-            log.error("批量插入 FsCourseLink 失败: {}", e.getMessage(), e);
-            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-        }
-    }
-
-
-    /**
-     * 批量插入 FsCourseSopAppLink
-     */
-    @Transactional
-    @Retryable(
-            value = { Exception.class },
-            maxAttempts = 3,
-            backoff = @Backoff(delay = 2000)
-    )
-    public void batchInsertFsCourseSopAppLink(List<FsCourseSopAppLink> courseSopAppLinkToInsert) {
-        try {
-            fsCourseSopAppLinkMapper.insertFsCourseSopAppLinkBatch(courseSopAppLinkToInsert);
-            log.info("批量插入 FsCourseSopAppLink 完成,共插入 {} 条记录。", courseSopAppLinkToInsert.size());
-        } catch (Exception e) {
-            log.error("批量插入 FsCourseSopAppLink 失败: {}", e.getMessage(), e);
-            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-        }
-    }
-
-
-    @Override
-    public void updateSopLogsByCancel() {
-        List<QwSopLogs> sopLogs = qwSopLogsMapper.selectQwSopLogsByCancel();
-        log.info("补发过期完课消息总条数:{}",sopLogs.size());
-        processUpdateQwSopLogs(sopLogs);
-    }
-
-
-    // 定义一个方法来批量处理插入逻辑,支持每 500 条数据一次的批量插入
-    private void processUpdateQwSopLogs(List<QwSopLogs> sopLogs) {
-        // 定义批量插入的大小
-        int batchSize = 500;
-
-        // 循环处理外部用户 ID,每次处理批量大小的子集
-        for (int i = 0; i < sopLogs.size(); i += batchSize) {
-
-            int endIndex = Math.min(i + batchSize, sopLogs.size());
-            List<QwSopLogs> batchList = sopLogs.subList(i, endIndex);  // 获取当前批次的子集
-
-            // 直接使用批次数据进行批量更新,不需要额外的 List
-            try {
-                qwSopLogsMapper.batchUpdateQwSopLogsByCancel(batchList);
-                log.info("正在补发条数:{}",batchSize);
-            } catch (Exception e) {
-                // 记录异常日志,方便后续排查问题
-                log.error("批量更新数据时发生异常,处理的批次起始索引为: " + i, e);
-            }
-        }
-    }
-
-    @Autowired
-    private FsCourseFinishTempMapper fsCourseFinishTempMapper;
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-
-
-//    @Override
-//    @Transactional
-//    public void creatMessMessage(QwSopLogs logs) {
-//       // qwSopLogsMapper.insertQwSopLogs(logs);
-//        QwSopTempSetting.Content content = JSON.parseObject(logs.getContentJson(), QwSopTempSetting.Content.class);
-//        handleNormalMessage(logs, content,null);
-//    }
-
-    @Override
-    public void createCourseFinishMsg() {
-        long startTime = System.currentTimeMillis();
-        log.info("创建完课消息 - 定时任务开始 {}", startTime);
-
-        // 线程池配置
-        int threadPoolSize = 4;
-        ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
-
-        // 用于收集所有处理结果的队列
-        BlockingQueue<List<FsCourseWatchLog>> batchQueue = new LinkedBlockingQueue<>();
-
-        try {
-            // 查询当天日期范围
-            LocalDate today = LocalDate.now();
-            Date startDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
-            Date endDate = Date.from(today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
-
-            // 启动生产者线程 - 流式分批查询数据
-            executorService.submit(() -> {
-                try {
-                    int batchSize = 1000;
-                    long  maxId = 0;
-                    boolean hasMore = true;
-
-                    while (hasMore) {
-                        // 查询当前批次数据
-                        List<FsCourseWatchLog> batch = fsCourseWatchLogMapper.selectFsCourseWatchLogFinishBatchByDate(
-                                startDate, endDate, maxId, batchSize);
-
-                        if (!batch.isEmpty()) {
-                            // 将批次放入队列
-                            batchQueue.put(batch);
-                            // 更新maxId为当前批次的最后一个ID
-                            maxId = batch.get(batch.size() - 1).getLogId();
-                            log.debug("已生产批次数据,最后logId: {}, 数量: {}", maxId, batch.size());
-                        }
-
-                        if (batch.size() < batchSize) {
-                            hasMore = false;
-                            batchQueue.put(Collections.emptyList());// 结束标志
-                            log.info("数据生产完成,最后logId: {}", maxId);
-                        }
-                    }
-                } catch (Exception e) {
-                    log.error("生产数据时出错", e);
-                    try {
-                        batchQueue.put(Collections.emptyList()); // 确保消费者能退出
-                    } catch (InterruptedException ie) {
-                        Thread.currentThread().interrupt();
-                    }
-                }
-            });
-
-            // 消费者线程处理数据
-            List<Future<?>> futures = new ArrayList<>();
-            for (int i = 0; i < threadPoolSize; i++) {
-                futures.add(executorService.submit(() -> {
-                    try {
-                        while (true) {
-                            List<FsCourseWatchLog> batch = batchQueue.take();
-
-                            // 空列表表示处理结束
-                            if (batch.isEmpty()) {
-                                batchQueue.put(Collections.emptyList()); // 传递给其他消费者
-                                break;
-                            }
-                            log.info("开始处理批次数据");
-                            processBatch(batch); // 处理批次数据
-                        }
-                    } catch (InterruptedException e) {
-                        Thread.currentThread().interrupt();
-                        log.error("处理数据时被中断", e);
-                    } catch (Exception e) {
-                        log.error("处理数据时出错", e);
-                    }
-                }));
-            }
-
-            // 等待所有任务完成
-            for (Future<?> future : futures) {
-                try {
-                    future.get();
-                } catch (InterruptedException | ExecutionException e) {
-                    log.error("等待任务完成时出错", e);
-                    Thread.currentThread().interrupt();
-                }
-            }
-
-            log.info("所有批次处理完成,总耗时: {}ms", System.currentTimeMillis() - startTime);
-
-        } finally {
-            executorService.shutdown();
-            try {
-                if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
-                    executorService.shutdownNow();
-                }
-            } catch (InterruptedException e) {
-                executorService.shutdownNow();
-                Thread.currentThread().interrupt();
-            }
-        }
-    }
-
-    // 处理单个批次的方法
-    private void processBatch(List<FsCourseWatchLog> batch) {
-        List<FsCourseWatchLog> finishLogsToUpdate = new ArrayList<>();
-        List<QwSopLogs> sopLogsToInsert = new ArrayList<>();
-        log.info("开始执行处理批次方法-数量:{}",batch.size());
-        for (FsCourseWatchLog finishLog : batch) {
-            try {
-
-                try {
-
-                    asyncCourseWatchFinishService.executeCourseWatchFinish(finishLog);
-
-                }catch (Exception e){
-                    log.error("添加完课打备注失败",e);
-                }
-
-                // 查询外部联系人信息
-                QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(finishLog.getQwExternalContactId());
-                if (externalContact == null) {
-                    log.error("外部联系人不存在: {}", finishLog.getQwExternalContactId());
-                    continue;
-                }
-
-                // 查询完课模板信息
-                FsCourseFinishTemp finishTemp = fsCourseFinishTempMapper.selectFsCourseFinishTempByCompanyId(finishLog.getCompanyUserId(),finishLog.getCompanyId(), finishLog.getVideoId());
-
-                // 设置 finishLog 为已发送状态,并加入批量更新列表
-                finishLog.setSendFinishMsg(1);
-                finishLogsToUpdate.add(finishLog);
-
-                if (finishTemp == null) {
-//                    log.error("完课模板不存在: " + finishLog.getQwUserId() + ", " + finishLog.getVideoId());
-                    continue;
-                }
-
-                // 构建 sopLogs 对象
-                QwSopLogs sopLogs = buildSopLogs(finishLog, externalContact, finishTemp);
-                if (sopLogs == null) {
-                    log.error("生成完课发送记录为空-:{}", finishLog.getQwExternalContactId());
-                    continue;
-                }
-
-                // 如果客户状态有效,则加入批量插入列表
-                if (isValidExternalContact(externalContact)) {
-                    sopLogsToInsert.add(sopLogs);
-                } else {
-                    log.info("完课消息-客户信息有误,不生成完课消息: {}", finishLog.getQwExternalContactId());
-                }
-                try {
-                    fsUserCompanyBindService.finish(externalContact.getFsUserId(), externalContact.getQwUserId(), externalContact.getCompanyUserId(), finishLog);
-                }catch (Exception e){
-                    log.error("更新重粉看课状态失败",e);
-                }
-            } catch (Exception e) {
-                log.error("处理完课记录失败: {}", finishLog.getLogId(), e);
-            }
-        }
-
-        // 批量更新和插入
-        if (!finishLogsToUpdate.isEmpty()) {
-            try {
-                fsCourseWatchLogMapper.batchUpdateWatchLogSendMsg(finishLogsToUpdate);
-                log.info("批量更新 finishLog 成功,数量: {}", finishLogsToUpdate.size());
-            } catch (Exception e) {
-                log.error("批量更新 finishLog 失败", e);
-            }
-        }
-
-        if (!sopLogsToInsert.isEmpty()) {
-            try {
-                qwSopLogsService.batchInsertQwSopLogs(sopLogsToInsert);
-                log.info("批量插入 sopLogs 成功,数量: {}", sopLogsToInsert.size());
-            } catch (Exception e) {
-                log.error("批量插入 sopLogs 失败", e);
-            }
-        }
-        log.info("结束处理批次方法-数量:{}",batch.size());
-    }
-
-    /**
-     * 构建 QwSopLogs 对象
-     */
-    private QwSopLogs buildSopLogs(FsCourseWatchLog finishLog, QwExternalContact externalContact, FsCourseFinishTemp finishTemp) {
-        QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
-        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-        LocalDateTime currentTime = LocalDateTime.now();
-        LocalDateTime newTime = currentTime.plusMinutes(3);
-        String newTimeString = newTime.format(formatter);
-
-        QwSopLogs sopLogs = new QwSopLogs();
-        sopLogs.setSendTime(newTimeString);
-        sopLogs.setQwUserid(externalContact.getUserId());
-        sopLogs.setCorpId(externalContact.getCorpId());
-        sopLogs.setLogType(2);
-        sopLogs.setSendType(3);
-        sopLogs.setSendStatus(3L);
-        sopLogs.setReceivingStatus(0L);
-        sopLogs.setSort(40000000);
-        sopLogs.setCompanyId(finishLog.getCompanyId());
-        sopLogs.setSopId(finishLog.getSopId());
-        sopLogs.setExternalUserId(externalContact.getExternalUserId());
-        sopLogs.setExternalUserName(externalContact.getName());
-        sopLogs.setFsUserId(finishLog.getUserId() != null ? finishLog.getUserId() : null );
-        sopLogs.setExternalId(finishLog.getQwExternalContactId());
-        sopLogs.setUserLogsId("-");
-
-        sopLogs.setQwUserKey(finishLog.getQwUserId() != null ? finishLog.getQwUserId() : null);
-
-        // 解析模板设置
-        List<QwSopCourseFinishTempSetting.Setting> settings = parseSettings(finishTemp.getSetting());
-        if (settings == null) {
-            return null;
-        }
-        //完课后若是小程序发送另外一堂课
-        saveWacthLogOfCourseLink(settings,sopLogs,newTimeString,finishLog,finishTemp);
-        // 处理音频内容
-        for (QwSopCourseFinishTempSetting.Setting st : settings) {
-            if (st.getContentType().equals("7")) {
-                Long companyUserId = finishLog.getCompanyUserId();
-                QwSopTempVoice qwSopTempVoice = sopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(companyUserId, st.getValue());
-                if (qwSopTempVoice != null && qwSopTempVoice.getVoiceUrl() != null && qwSopTempVoice.getRecordType() == 1) {
-                    st.setVoiceUrl(qwSopTempVoice.getVoiceUrl());
-                    st.setVoiceDuration(String.valueOf(qwSopTempVoice.getDuration()));
-                } else if (qwSopTempVoice == null) {
-                    if(companyUserId != null && st.getValue() != null){
-                        qwSopTempVoice = new QwSopTempVoice();
-                        qwSopTempVoice.setCompanyUserId(companyUserId);
-                        qwSopTempVoice.setVoiceTxt(st.getValue());
-                        qwSopTempVoice.setRecordType(0);
-                        sopTempVoiceService.insertQwSopTempVoice(qwSopTempVoice);
-                    }
-                }
-            }
-        }
-//        for (QwSopCourseFinishTempSetting.Setting st : settings) {
-//            if (st.getContentType().equals("7")) {
-//                try {
-//                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), finishLog.getCompanyUserId(), false);
-//                    st.setVoiceUrl(audioVO.getUrl());
-//                    st.setVoiceDuration(audioVO.getDuration() + "");
-//                } catch (Exception e) {
-//                    log.error("音频生成失败: " + finishLog.getCompanyUserId(), e);
-//                }
-//            }
-//        }
-
-        setting.setSetting(settings);
-        sopLogs.setContentJson(JSON.toJSONString(setting));
-        return sopLogs;
-    }
-
-    /**
-     * 判定小程序的话新增创建看课记录,以及fsCourseLink
-     *
-     * @param settings
-     */
-    public void saveWacthLogOfCourseLink(List<QwSopCourseFinishTempSetting.Setting> settings, QwSopLogs sopLogs,  String newTimeString, FsCourseWatchLog finishLog, FsCourseFinishTemp finishTemp){
-        String json = configService.selectConfigByKey("course.config");
-        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
-        Date dataTime = new Date();
-        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
-        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
-
-        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(sopLogs.getCorpId());
-        QwUser qwUser = qwExternalContactService.getQwUserByRedis(sopLogs.getCorpId(), sopLogs.getQwUserid());
-        if (qwUser == null){
-            return;
-        }
-        for (QwSopCourseFinishTempSetting.Setting st : settings) {
-            switch (st.getContentType()) {
-                //小程序单独
-                case "4":
-                    addWatchLogIfNeeded(sopLogs.getSopId(), st.getVideoId().intValue(), st.getCourseId().intValue(), sopLogs.getFsUserId(),  String.valueOf(qwUser.getId()),qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),
-                            sopLogs.getExternalId(), newTimeString.substring(0, 10), dataTime);
-
-                    String linkByMiniApp = createLinkByMiniApp(st, sopLogs.getCorpId(), dataTime, finishTemp.getCourseId().intValue(), Integer.valueOf(st.getVideoId().toString()),
-                            String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), sopLogs.getExternalId(), config);
-
-
-                    String miniAppId = null;
-                    if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
-                        Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(qwUser.getCompanyId()));
-                        if (integerListMap != null) {
-                            int listIndex = 0;
-                            List<CompanyMiniapp> miniapps = integerListMap.get(listIndex);
-
-                            if (miniapps != null && !miniapps.isEmpty()) {
-                                CompanyMiniapp companyMiniapp = miniapps.get(0);
-                                if (companyMiniapp != null && !StringUtil.strIsNullOrEmpty(companyMiniapp.getAppId())) {
-                                    miniAppId = companyMiniapp.getAppId();
-                                }
-                            }
-                        }
-                    }
-
-                    if (StringUtil.strIsNullOrEmpty(miniAppId) && !StringUtil.strIsNullOrEmpty(qwCompany.getMiniAppId())) {
-                        miniAppId = qwCompany.getMiniAppId();
-                    }
-
-                    if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
-                        st.setMiniprogramAppid(miniAppId);
-                    } else {
-                        log.error("企业未配置小程序-" + sopLogs.getCorpId());
-                    }
-
-                    String miniprogramTitle = st.getMiniprogramTitle();
-                    int maxLength = 17;
-                    st.setMiniprogramTitle(miniprogramTitle.length() > maxLength ? miniprogramTitle.substring(0, maxLength)+"..." : miniprogramTitle);
-                    st.setMiniprogramPage(linkByMiniApp);
-                    break;
-                default:
-                    break;
-
-            }
-        }
-    }
-    private Date processDate(String sendTimeParam) {
-        // 1. 获取当前日期(年月日)
-        LocalDate currentDate = LocalDate.now();
-
-        // 2. 解析传入的时分(支持 "HH:mm" 或 "H:mm")
-        LocalTime sendTime = LocalTime.parse(sendTimeParam);
-
-        // 3. 合并为 LocalDateTime
-        LocalDateTime dateTime = LocalDateTime.of(currentDate, sendTime);
-
-        // 4. 转换为 Date(需通过 Instant 和系统默认时区)
-        Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
-
-        return date;
-    }
-
-    /**
-     * 新增courseLink
-     *
-     * @param setting
-     * @param corpId
-     * @param sendTime
-     * @param courseId
-     * @param videoId
-     * @param qwUserId
-     * @param companyUserId
-     * @param companyId
-     * @param externalId
-     * @param config
-     * @return
-     */
-    private String createLinkByMiniApp(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
-                                       Integer courseId, Integer videoId, String qwUserId,
-                                       String companyUserId, String companyId, Long externalId, CourseConfig config) {
-
-        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
-                companyUserId, companyId, externalId, 3, null);
-
-        FsCourseRealLink courseMap = new FsCourseRealLink();
-        BeanUtils.copyProperties(link, courseMap);
-
-        String courseJson = JSON.toJSONString(courseMap);
-        String realLinkFull = miniappRealLink + courseJson;
-        link.setRealLink(realLinkFull);
-
-        Date updateTime = createUpdateTime(setting, sendTime, config);
-
-        link.setUpdateTime(updateTime);
-        //存短链-
-        fsCourseLinkMapper.insertFsCourseLink(link);
-        return link.getRealLink();
-    }
-
-    /**
-     * 创建courselink
-     * @param corpId
-     * @param sendTime
-     * @param courseId
-     * @param videoId
-     * @param qwUserId
-     * @param companyUserId
-     * @param companyId
-     * @param externalId
-     * @param type
-     * @param chatId
-     * @return
-     */
-    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, String qwUserId,
-                                           String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
-        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
-        FsCourseLink link = new FsCourseLink();
-        link.setCompanyId(Long.parseLong(companyId));
-        link.setQwUserId(Long.valueOf(qwUserId));
-        link.setCompanyUserId(Long.parseLong(companyUserId));
-        link.setVideoId(videoId.longValue());
-        link.setCorpId(corpId);
-        link.setCourseId(courseId.longValue());
-        link.setChatId(chatId);
-        link.setQwExternalId(externalId);
-        link.setLinkType(type); //小程序
-        link.setUNo(UUID.randomUUID().toString());
-        link.setProjectCode(cloudHostProper.getProjectCode());
-        String randomString = generateRandomStringWithLock();
-        if (StringUtil.strIsNullOrEmpty(randomString)) {
-            link.setLink(UUID.randomUUID().toString().replace("-", ""));
-        } else {
-            link.setLink(randomString);
-        }
-
-        link.setCreateTime(sendTime);
-
-        return link;
-    }
-
-
-    /**
-     * 计算过期时间
-     * @param setting
-     * @param sendTime
-     * @param config
-     * @return
-     */
-    private Date createUpdateTime(QwSopCourseFinishTempSetting.Setting setting, Date sendTime, CourseConfig config) {
-
-        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
-                ? config.getVideoLinkExpireDate()
-                : setting.getExpiresDays();
-
-//         使用 Java 8 时间 API 计算过期时间
-        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
-        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
-        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
-
-        return updateTime;
-    }
-
-    /**
-     * 增加看课记录
-     *
-     * @param sopId
-     * @param videoId
-     * @param courseId
-     * @param fsUserId
-     * @param qwUserId
-     * @param companyUserId
-     * @param companyId
-     * @param externalId
-     * @param startTime
-     * @param createTime
-     * @return
-     */
-    private Long addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
-                                     Long fsUserId, String qwUserId, String companyUserId,
-                                     String companyId, Long externalId, String startTime, Date createTime) {
-
-        try {
-            FsCourseWatchLog watchLog = new FsCourseWatchLog();
-            watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
-            watchLog.setQwExternalContactId(externalId);
-            watchLog.setSendType(2);
-            watchLog.setQwUserId(Long.valueOf(qwUserId));
-            watchLog.setSopId(sopId);
-            watchLog.setDuration(0L);
-            watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
-            watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
-            watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
-            watchLog.setCreateTime(createTime);
-            watchLog.setUpdateTime(createTime);
-            watchLog.setLogType(3);
-            watchLog.setUserId(fsUserId);
-            watchLog.setCampPeriodTime(convertStringToDate(startTime, "yyyy-MM-dd"));
-
-            //存看课记录
-            int i = fsCourseWatchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
-            return watchLog.getLogId();
-        } catch (Exception e) {
-            log.error("插入观看记录失败:" + e.getMessage());
-            return null;
-        }
-    }
-
-
-    /**
-     * 解析模板设置
-     */
-    private List<QwSopCourseFinishTempSetting.Setting> parseSettings(String jsonData) {
-        try {
-            if (jsonData.startsWith("[") && jsonData.endsWith("]")) {
-                return JSONArray.parseArray(jsonData, QwSopCourseFinishTempSetting.Setting.class);
-            } else {
-                String fixedJson = JSON.parseObject(jsonData, String.class);
-                return JSONArray.parseArray(fixedJson, QwSopCourseFinishTempSetting.Setting.class);
-            }
-        } catch (Exception e) {
-            log.error("解析模板设置失败", e);
-            return null;
-        }
-    }
-
-    /**
-     * 检查外部联系人状态是否有效
-     */
-    private boolean isValidExternalContact(QwExternalContact externalContact) {
-        return externalContact.getStatus() == 0 || externalContact.getStatus() == 2 || externalContact.getStatus() == 3;
-    }
-}

+ 0 - 965
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopLogsTestServiceImpl.java

@@ -1,965 +0,0 @@
-package com.fs.app.taskService.impl;//package com.fs.app.taskService.impl;
-//
-//import com.alibaba.fastjson.JSON;
-//import com.alibaba.fastjson.JSONArray;
-//import com.fs.app.taskService.SopLogsTaskService;
-//import com.fs.app.taskService.SopLogsTestService;
-//import com.fs.common.utils.StringUtils;
-//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.fastgptApi.util.AudioUtils;
-//import com.fs.fastgptApi.vo.AudioVO;
-//import com.fs.qw.domain.QwExternalContact;
-//import com.fs.qw.mapper.QwExternalContactMapper;
-//import com.fs.qw.mapper.QwUserMapper;
-//import com.fs.qw.vo.QwSopCourseFinishTempSetting;
-//import com.fs.qw.vo.QwSopRuleTimeVO;
-//import com.fs.qw.vo.QwSopTempSetting;
-//import com.fs.sop.domain.QwSopLogs;
-//import com.fs.sop.domain.SopUserLogs;
-//import com.fs.sop.domain.SopUserLogsInfo;
-//import com.fs.sop.mapper.QwSopLogsMapper;
-//import com.fs.sop.mapper.QwSopMapper;
-//import com.fs.sop.mapper.SopUserLogsInfoMapper;
-//import com.fs.sop.mapper.SopUserLogsMapper;
-//import com.fs.sop.service.IQwSopLogsService;
-//import com.fs.sop.vo.SopUserLogsVo;
-//import com.fs.system.service.ISysConfigService;
-//import lombok.extern.slf4j.Slf4j;
-//import org.springframework.beans.factory.annotation.Autowired;
-//import org.springframework.retry.annotation.Backoff;
-//import org.springframework.retry.annotation.Retryable;
-//import org.springframework.scheduling.annotation.Async;
-//import org.springframework.scheduling.annotation.Scheduled;
-//import org.springframework.stereotype.Service;
-//import org.springframework.transaction.annotation.Transactional;
-//
-//import javax.annotation.PostConstruct;
-//import javax.annotation.PreDestroy;
-//import java.time.LocalDate;
-//import java.time.LocalDateTime;
-//import java.time.LocalTime;
-//import java.time.ZoneId;
-//import java.time.format.DateTimeFormatter;
-//import java.time.temporal.ChronoUnit;
-//import java.util.ArrayList;
-//import java.util.Date;
-//import java.util.List;
-//import java.util.Map;
-//import java.util.concurrent.*;
-//import java.util.stream.Collectors;
-//
-//import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
-//
-//@Service
-//@Slf4j
-//public class SopLogsTestServiceImpl implements SopLogsTestService {
-//
-//
-//    private static final String REAL_LINK_PREFIX = "https://h5api.ylrzcloud.com/courseh5/pages/course/learning?course=";
-//    private static final String QWSOP_KEY_PREFIX = "qwsop:";
-//    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-//    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-//
-//    // Cached configurations and domain names
-//    private CourseConfig cachedCourseConfig;
-//    private final Object configLock = new Object();
-//
-//    private List<FsCourseDomainName> cachedDomainNames;
-//    private final Object domainLock = new Object();
-//
-//
-//    // Batch size for database inserts, configurable via application.properties
-//    private final int BATCH_SIZE = 1000;
-//
-//    @Autowired
-//    private SopUserLogsMapper sopUserLogsMapper;
-//
-//    @Autowired
-//    private QwSopMapper sopMapper;
-//
-//    @Autowired
-//    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-//
-//    @Autowired
-//    private IQwSopLogsService qwSopLogsService;
-//
-//    @Autowired
-//    private QwSopLogsMapper qwSopLogsMapper;
-//
-//    @Autowired
-//    private FsCourseLinkMapper fsCourseLinkMapper;
-//    @Autowired
-//    private ISysConfigService configService;
-//
-//    @Autowired
-//    private FsCourseDomainNameMapper fsCourseDomainNameMapper;
-//
-//    @Autowired
-//    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
-//    @Autowired
-//    private QwUserMapper qwUserMapper;
-//
-//    // Blocking queues with bounded capacity to implement backpressure
-//    private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(10000);
-//    private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(10000);
-//    private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(10000);
-//
-//    // Executors for consumer threads
-//    private ExecutorService qwSopLogsExecutor;
-//    private ExecutorService watchLogsExecutor;
-//    private ExecutorService courseLinkExecutor;
-//
-//    // Shutdown flags
-//    private volatile boolean running = true;
-//
-//
-//    @PostConstruct
-//    public void init() {
-//        loadCourseConfig();
-//        loadDomainNames();
-//        startConsumers();
-//    }
-//
-//    private void loadCourseConfig() {
-//        try {
-//            String json = configService.selectConfigByKey("course.config");
-//            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
-//            if (config != null) {
-//                cachedCourseConfig = config;
-//                log.info("Loaded course.config successfully.");
-//            } else {
-//                log.error("Failed to load course.config from configService.");
-//            }
-//        } catch (Exception e) {
-//            log.error("Exception while loading course.config: {}", e.getMessage(), e);
-//        }
-//    }
-//
-//    private void loadDomainNames() {
-//        try {
-//            cachedDomainNames = fsCourseDomainNameMapper.selectAllDomainNames();
-//            log.info("Loaded {} domain names for short links.", cachedDomainNames.size());
-//        } catch (Exception e) {
-//            log.error("Failed to load domain names: {}", e.getMessage(), e);
-//        }
-//    }
-//
-//
-//    private void startConsumers() {
-//        qwSopLogsExecutor = Executors.newSingleThreadExecutor(r -> {
-//            Thread t = new Thread(r, "QwSopLogsConsumer");
-//            t.setDaemon(true);
-//            return t;
-//        });
-//        watchLogsExecutor = Executors.newSingleThreadExecutor(r -> {
-//            Thread t = new Thread(r, "WatchLogsConsumer");
-//            t.setDaemon(true);
-//            return t;
-//        });
-//        courseLinkExecutor = Executors.newSingleThreadExecutor(r -> {
-//            Thread t = new Thread(r, "courseLinkConsumer");
-//            t.setDaemon(true);
-//            return t;
-//        });
-//
-//        qwSopLogsExecutor.submit(this::consumeQwSopLogs);
-//        watchLogsExecutor.submit(this::consumeWatchLogs);
-//        courseLinkExecutor.submit(this::consumeCourseLink);
-//    }
-//
-//    // Scheduled tasks to refresh configurations and domain names periodically
-//    @Scheduled(fixedDelay = 60000) // 每60秒刷新一次
-//    public void refreshCourseConfig() {
-//        synchronized(configLock) {
-//            try {
-//                String json = configService.selectConfigByKey("course.config");
-//                CourseConfig newConfig = JSON.parseObject(json, CourseConfig.class);
-//                if (newConfig != null) {
-//                    cachedCourseConfig = newConfig;
-//                    log.info("Refreshed course.config.");
-//                } else {
-//                    log.error("Failed to refresh course.config.");
-//                }
-//            } catch (Exception e) {
-//                log.error("Exception while refreshing course.config: {}", e.getMessage(), e);
-//            }
-//        }
-//    }
-//
-//    @Scheduled(fixedDelay = 60000) // 每60秒刷新一次
-//    public void refreshDomainNames() {
-//        synchronized(domainLock) {
-//            try {
-//                cachedDomainNames = fsCourseDomainNameMapper.selectAllDomainNames();
-//                log.info("Refreshed {} domain names for short links.", cachedDomainNames.size());
-//            } catch (Exception e) {
-//                log.error("Failed to refresh domain names: {}", e.getMessage(), e);
-//            }
-//        }
-//    }
-//
-//
-//    @PreDestroy
-//    public void shutdownConsumers() {
-//        running = false;
-//        qwSopLogsExecutor.shutdown();
-//        watchLogsExecutor.shutdown();
-//        courseLinkExecutor.shutdown();
-//        try {
-//            if (!qwSopLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-//                qwSopLogsExecutor.shutdownNow();
-//            }
-//            if (!watchLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-//                watchLogsExecutor.shutdownNow();
-//            }
-//            if (!courseLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-//                courseLinkExecutor.shutdownNow();
-//            }
-//        } catch (InterruptedException e) {
-//            qwSopLogsExecutor.shutdownNow();
-//            watchLogsExecutor.shutdownNow();
-//            courseLinkExecutor.shutdownNow();
-//            Thread.currentThread().interrupt();
-//        }
-//    }
-//
-//    @Override
-//    public void selectSopUserLogsListByTest() throws Exception {
-//        long startTimeMillis = System.currentTimeMillis();
-//        log.info("====== 开始选择和处理 SOP 用户日志 ======");
-//
-//        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTest();
-//        if (sopUserLogsVos.isEmpty()) {
-//            log.info("没有需要处理的 SOP 用户日志。");
-//            return;
-//        }
-//
-//        Map<String, List<SopUserLogsVo>> sopLogsGroupedById = sopUserLogsVos.stream()
-//                .collect(Collectors.groupingBy(SopUserLogsVo::getSopId));
-//
-//        log.info("共分组 {} 个 SOP ID 进行处理。", sopLogsGroupedById.size());
-//
-//        CountDownLatch sopGroupLatch = new CountDownLatch(sopLogsGroupedById.size());
-//
-//        for (Map.Entry<String, List<SopUserLogsVo>> entry : sopLogsGroupedById.entrySet()) {
-//            String sopId = entry.getKey();
-//            List<SopUserLogsVo> userLogsVos = entry.getValue();
-//            processSopGroupAsync(sopId, userLogsVos, sopGroupLatch);
-//        }
-//
-//        // 等待所有 SOP 分组处理完成
-//        sopGroupLatch.await();
-//
-//        // 触发批量插入(可选,如果需要立即插入队列中的数据)
-//        // batchInsertQwSopLogs();
-//        // batchInsertFsCourseWatchLogs();
-//
-//        long endTimeMillis = System.currentTimeMillis();
-//        log.info("====== SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-//    }
-//
-//    @Async("sopTaskExecutor")
-//    @Retryable(
-//            value = { Exception.class },
-//            maxAttempts = 3,
-//            backoff = @Backoff(delay = 2000)
-//    )
-//    public void processSopGroupAsync(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch) {
-//        try {
-//            processSopGroup(sopId, userLogsVos);
-//        } catch (Exception e) {
-//            log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
-//        } finally {
-//            latch.countDown();
-//        }
-//    }
-//
-//
-//    private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos) throws Exception {
-//        QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
-//
-//        if (ruleTimeVO == null) {
-////            sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
-//            log.info("SOP ID {} 模板已删除或不存在,相关日志已清除。", sopId);
-//            return;
-//        }
-//
-//        if (ruleTimeVO.getStatus() == 0 || "0".equals(ruleTimeVO.getTempStatus())) {
-//            SopUserLogs sopUserLogs = new SopUserLogs();
-//            sopUserLogs.setSopId(sopId);
-//            sopUserLogs.setStatus(2);
-//            sopUserLogsMapper.updateSopUserLogsByStatus(sopUserLogs);
-//            log.info("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
-//            return;
-//        }
-//
-//        if (ruleTimeVO.getTempSetting()==null) {
-//            log.error("SOP ID {} 的 TempSetting 为空,跳过处理。", sopId);
-//            return;
-//        }
-//
-//        //解析模板
-//        String jsonData = ruleTimeVO.getTempSetting();
-//        List<QwSopTempSetting> tempSettings = new ArrayList<>();
-//        if (jsonData.startsWith("[") && jsonData.endsWith("]")) {
-//            // 直接解析 JSON 数组
-//            tempSettings = JSONArray.parseArray(jsonData, QwSopTempSetting.class);
-//        } else {
-//            // 先解包,再解析
-//            String fixedJson = JSON.parseObject(jsonData, String.class);
-//            tempSettings = JSONArray.parseArray(fixedJson, QwSopTempSetting.class);
-//        }
-//
-////        String jsonData = finishTemp.getSetting();
-////
-////        List<QwSopTempSetting> tempSettings = JSON.parseArray(ruleTimeVO.getTempSetting(), QwSopTempSetting.class);
-//        if (tempSettings.isEmpty()) {
-//            log.error("SOP ID {} 的 TempSetting 为空,跳过处理。", sopId);
-//            return;
-//        }
-//
-//        CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
-//        for (SopUserLogsVo logVo : userLogsVos) {
-//            processUserLogAsync(logVo, ruleTimeVO, tempSettings, userLogsLatch);
-//        }
-//
-//        // 等待所有用户日志处理完成
-//        try {
-//            userLogsLatch.await();
-//        } catch (InterruptedException e) {
-//            Thread.currentThread().interrupt();
-//            log.error("等待用户日志处理完成时被中断: {}", e.getMessage(), e);
-//        }
-//        log.info("SOP ID {} 的所有用户日志已处理完毕。", sopId);
-//    }
-//
-//    @Async("sopTaskExecutor")
-//    @Retryable(
-//            value = { Exception.class },
-//            maxAttempts = 3,
-//            backoff = @Backoff(delay = 2000)
-//    )
-//    public void processUserLogAsync(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempSetting> tempSettings, CountDownLatch latch) {
-//        try {
-//            processUserLog(logVo, ruleTimeVO, tempSettings);
-//        } catch (Exception e) {
-//            log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
-//        } finally {
-//            latch.countDown();
-//        }
-//    }
-//
-//
-//    private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempSetting> tempSettings) {
-//        try {
-//            LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
-//            LocalDate currentDate = LocalDate.now();
-////            LocalDate currentDate = LocalDate.parse("2024-12-25", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-//
-//            long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
-//            int tempGap = ruleTimeVO.getTempGap();
-//
-//            if (tempGap <= 0) {
-//                log.error("SOP ID {} 的 TempGap {} 无效,跳过处理。", logVo.getSopId(), tempGap);
-//                return;
-//            }
-//
-//            int intervalDay = (int) (daysBetween / tempGap);
-//            if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
-//                log.info("用户日志 {} 的 intervalDay {} 超出 TempSettings 范围,跳过处理。", logVo.getId(), intervalDay);
-//                return;
-//            }
-//
-//            QwSopTempSetting selectedSetting = tempSettings.get(intervalDay);
-//            List<QwSopTempSetting.Content> contents = selectedSetting.getContent();
-//            if (contents == null || contents.isEmpty()) {
-//                log.error("SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
-//                return;
-//            }
-//
-//
-//            //寻找时间
-////            LocalDateTime currentTime = LocalDateTime.now();
-//            LocalDateTime currentTime = LocalDateTime.of(2025, 2, 11,8 , 0);
-//
-//            // 先算好 60分钟后 ~ 再60分钟后的时间段
-//            LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
-//
-//            // 如果发现已经跨天
-//            if (!startRangeFirst.toLocalDate().equals(currentDate)) {
-//                // 更新 currentDate
-//                currentDate = startRangeFirst.toLocalDate();
-//
-//                // 重新计算 daysBetween
-//                daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
-//                intervalDay = (int) (daysBetween / tempGap);
-//
-//                // 再次验证 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
-//                selectedSetting = tempSettings.get(intervalDay);
-//                contents = selectedSetting.getContent();
-//                if (contents == null || contents.isEmpty()) {
-//                    log.error("跨天-SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
-//                    return;
-//                }
-//            }
-//
-//
-//            // 只有整倍数才做事
-//            if (daysBetween % tempGap != 0) {
-//                log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap,logVo.getSopId());
-//                return;
-//            }
-//
-//
-//            for (QwSopTempSetting.Content content : contents) {
-//                try {
-//
-//                    LocalTime elementLocalTime = LocalTime.parse(content.getTime());
-//                    LocalDateTime elementDateTime = LocalDateTime.of(currentTime.toLocalDate(), elementLocalTime);
-//
-//                    // 动态调整 elementDateTime 的日期
-//                    if (elementLocalTime.isBefore(currentTime.toLocalTime())) {
-//                        elementDateTime = elementDateTime.plusDays(1);
-//                    }
-//
-//                    LocalDateTime startRange = currentTime.plusMinutes(60);
-//                    LocalDateTime endRange = startRange.plusMinutes(60);
-//
-//                    // 跨天逻辑修正:仅当 startRange 的时间晚于 endRange 的时间时调整
-//                    if (startRange.toLocalTime().isAfter(endRange.toLocalTime())
-//                            && startRange.toLocalDate().equals(endRange.toLocalDate())) {
-//                        endRange = endRange.plusDays(1); // 将 endRange 调整为第二天
-//                    }
-//                    if (!elementDateTime.isBefore(startRange) && elementDateTime.isBefore(endRange)) {
-//
-//                        // 如果时间差在目标范围内,更新记录
-//                        // 组合年月日和element的时间
-//                        LocalDate targetDate = startDate.plusDays(intervalDay * tempGap);
-//
-//                        // 将 targetDate 和 elementTime 组合成 LocalDateTime
-//                        LocalDateTime dateTime = LocalDateTime.of(targetDate, elementLocalTime);
-//
-//                        // 将 LocalDateTime 转换为 Date
-//                        Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
-//
-//                        SopUserLogsInfo userLogsInfo=new SopUserLogsInfo();
-//                        userLogsInfo.setSopId(logVo.getSopId());
-//                        userLogsInfo.setUserLogsId(logVo.getId());
-//
-//                        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoList(userLogsInfo);
-//
-//                        insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content);
-//
-//                    }
-//                } catch (Exception e) {
-//                    log.error("解析模板内容 {} 失败: {}", content.getTime(), e.getMessage(), e);
-//                }
-//            }
-//
-//        } catch (Exception e) {
-//            log.error("解析解析模板 {} 失败: {}", logVo.getStartTime(), e.getMessage(), e);
-//        }
-//    }
-//
-//
-//
-//
-//    private void insertSopUserLogs(List<SopUserLogsInfo> sopUserLogsInfos, SopUserLogsVo logVo, Date sendTime,
-//                                   QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content) {
-//        String formattedSendTime = sendTime.toInstant()
-//                .atZone(ZoneId.systemDefault())
-//                .format(DATE_TIME_FORMATTER);
-//        int type = content.getType();
-//        Integer courseId = content.getCourseId();
-//        Integer 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();
-//
-//        //生成语音
-//        List<QwSopTempSetting.Content.Setting> setting = content.getSetting();
-//        for (QwSopTempSetting.Content.Setting st : setting) {
-//            if (st.getContentType().equals("7")){
-//                try {
-//                    AudioVO audioVO = AudioUtils.transferAudioSilkFromText(st.getValue(), Long.valueOf(companyUserId), false);
-//                    st.setVoiceUrl(audioVO.getUrl());
-//                    st.setVoiceDuration(audioVO.getDuration()+"");
-//                }catch (Exception e){
-//                    log.info("音频生成失败-: "+companyUserId);
-//                }
-//            }
-//        }
-//
-//        // 处理每个 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);
-//            } catch (Exception e) {
-//                log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
-//            }
-//        });
-//    }
-//
-//
-//
-//    private QwSopLogs createBaseLog(String formattedSendTime, SopUserLogsVo logVo,
-//                                    QwSopRuleTimeVO ruleTimeVO, String externalId,
-//                                    String externalUserName, Long fsUserId) {
-//        QwSopLogs sopLogs = new QwSopLogs();
-//        sopLogs.setSendTime(formattedSendTime);
-//        sopLogs.setQwUserid(logVo.getQwUserId());
-//        sopLogs.setCorpId(logVo.getCorpId());
-//        sopLogs.setLogType(ruleTimeVO.getType());
-//        sopLogs.setSendType(ruleTimeVO.getSendType());
-//        sopLogs.setSendStatus(3L);
-//        sopLogs.setReceivingStatus(0L);
-//
-//        String[] userKey = logVo.getUserId().split("\\|");
-//        sopLogs.setCompanyId(Long.valueOf(userKey[2].trim()));
-//        sopLogs.setSopId(logVo.getSopId());
-//
-//        sopLogs.setExternalUserId(externalId);
-//        sopLogs.setExternalUserName(externalUserName);
-//        sopLogs.setFsUserId(fsUserId);
-//
-//        return sopLogs;
-//    }
-//
-//    private void handleLogBasedOnType(QwSopLogs sopLogs, QwSopTempSetting.Content content,
-//                                      SopUserLogsVo logVo, Date sendTime, Integer courseId,
-//                                      Integer videoId, int type, String qwUserId,
-//                                      String companyUserId, String companyId, String externalId) {
-//        switch (type) {
-//            case 1:
-//                handleNormalMessage(sopLogs, content,companyUserId);
-//                break;
-//            case 2:
-//                handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
-//                        qwUserId, companyUserId, companyId, externalId);
-//                break;
-//            case 3:
-//                handleOrderMessage(sopLogs, content);
-//                break;
-//            case 4:
-//                handleAIMessage(sopLogs, content);
-//                break;
-//            default:
-//                log.error("未知的消息类型 {},跳过处理。", type);
-//                break;
-//        }
-//    }
-//
-//    private void handleNormalMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,String companyUserId) {
-//
-//        sopLogs.setContentJson(JSON.toJSONString(content));
-//        enqueueQwSopLogs(sopLogs);
-//    }
-//
-//    private void handleAIMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
-//        sopLogs.setContentJson(JSON.toJSONString(content));
-//        sopLogs.setSort(3);
-//        enqueueQwSopLogs(sopLogs);
-//    }
-//
-//    private void handleCourseMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,
-//                                     SopUserLogsVo logVo, Date sendTime, Integer courseId,
-//                                     Integer videoId, String qwUserId, String companyUserId,
-//                                     String companyId, String externalId) {
-//        // 深拷贝 Content 对象,避免使用 JSON
-//        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
-//        if (clonedContent == null) {
-//            log.error("Failed to clone content, skipping handleCourseMessage.");
-//            return;
-//        }
-//
-////
-////        Integer courseType = clonedContent.getCourseType();
-//
-//        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
-//        if (settings == null || settings.isEmpty()) {
-//            log.error("Cloned content settings are empty, skipping.");
-//            return;
-//        }
-//
-//
-//        // 顺序处理每个 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);
-//                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);
-//                        }
-//                    }
-//                } else {
-//                    log.error("生成短链失败,跳过设置 URL。");
-//                }
-//            }
-//        }
-//        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
-//        enqueueQwSopLogs(sopLogs);
-//    }
-//
-//    /**
-//     * 深拷贝 Content 对象,避免使用 JSON 进行序列化和反序列化
-//     */
-//    private QwSopTempSetting.Content deepCopyContent(QwSopTempSetting.Content content) {
-//        if (content == null) {
-//            return null;
-//        }
-//        return content.clone();
-//    }
-//
-//    private void handleOrderMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
-//        sopLogs.setContentJson(JSON.toJSONString(content));
-//        enqueueQwSopLogs(sopLogs);
-//    }
-//
-//
-//
-//
-//    private String generateShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-//                                Integer courseId, Integer videoId, String qwUserId,
-//                                String companyUserId, String companyId, String externalId) {
-//        // 获取缓存的配置
-//        CourseConfig config;
-//        synchronized(configLock) {
-//            config = cachedCourseConfig;
-//        }
-//
-//        if (config == null) {
-//            log.error("CourseConfig is not loaded.");
-//            return "";
-//        }
-//
-//        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
-//        FsCourseLink link = new FsCourseLink();
-//        link.setCompanyId(Long.parseLong(companyId));
-//        link.setQwUserId(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(0); //正常链接
-//
-//        FsCourseRealLink courseMap = new FsCourseRealLink();
-//        courseMap.setCompanyId(link.getCompanyId());
-//        courseMap.setQwUserId(link.getQwUserId());
-//        courseMap.setCompanyUserId(link.getCompanyUserId());
-//        courseMap.setVideoId(link.getVideoId());
-//        courseMap.setCorpId(link.getCorpId());
-//        courseMap.setCourseId(link.getCourseId());
-//        courseMap.setQwExternalId(link.getQwExternalId());
-//        courseMap.setLinkType(0);
-//
-//        String courseJson = JSON.toJSONString(courseMap);
-//        String realLinkFull = REAL_LINK_PREFIX + courseJson;
-//        link.setRealLink(realLinkFull);
-//
-//        String randomString = generateRandomStringWithLock();
-//        link.setLink(randomString);
-//        link.setCreateTime(sendTime);
-//
-//        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
-//                ? config.getVideoLinkExpireDate()
-//                : setting.getExpiresDays();
-//
-//        // 使用 Java 8 时间 API 计算过期时间
-//        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-//        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
-//        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
-//        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
-//        link.setUpdateTime(updateTime);
-//
-//        // 从缓存中随机选择一个域名
-//        FsCourseDomainName fsCourseDomainName;
-//        if (cachedDomainNames == null || cachedDomainNames.isEmpty()) {
-//            log.error("No domain names available for short link generation.");
-//            return "";
-//        }
-//
-//        int randomIndex = ThreadLocalRandom.current().nextInt(cachedDomainNames.size());
-//        fsCourseDomainName = cachedDomainNames.get(randomIndex);
-//
-//        String sortLink = "https://" + fsCourseDomainName.getDomainName() + "/s/" + link.getLink();
-//        enqueueCourseLink(link);
-//        return sortLink;
-//    }
-//
-//
-//    private void addWatchLogIfNeeded(QwSopLogs sopLogs, Integer videoId, Integer courseId,
-//                                     Date sendTime, String qwUserId, String companyUserId,
-//                                     String companyId, String externalId,SopUserLogsVo logsVo) {
-//        FsCourseWatchLog watchLog = new FsCourseWatchLog();
-//        watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
-//        watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
-//        watchLog.setSendType(2);
-//        watchLog.setQwUserId(qwUserId);
-//        watchLog.setSopId(sopLogs.getSopId());
-//        watchLog.setDuration(0L);
-//        watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
-//        watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
-//        watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
-//        watchLog.setCreateTime(convertStringToDate(sopLogs.getSendTime(),"yyyy-MM-dd HH:mm:ss"));
-//        watchLog.setUpdateTime(new Date());
-//        watchLog.setLogType(3);
-//        watchLog.setUserId(sopLogs.getFsUserId());
-//        watchLog.setCampPeriodTime(convertStringToDate(logsVo.getStartTime(),"yyyy-MM-dd"));
-//        enqueueWatchLog(watchLog);
-//    }
-//
-//    /**
-//     * 时间字符串转Date时间
-//     * @param dateString
-//     * @return
-//     */
-//    public static Date convertStringToDate(String dateString,String pattern) {
-//        if (dateString == null || dateString.isEmpty()) {
-//            return null;
-//        }
-//        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
-//        LocalDateTime localDateTime;
-//        LocalDate localDate;
-//        // 先解析成 LocalDate(只含年月日)
-//        if (pattern.equals("yyyy-MM-dd")){
-//            // 先解析成 LocalDate(只含年月日)
-//            localDate = LocalDate.parse(dateString, formatter);
-//            // 将 LocalDate 转为当天 00:00:00 的 LocalDateTime
-//            localDateTime = localDate.atStartOfDay();
-//        }else {
-//            localDateTime = LocalDateTime.parse(dateString, formatter);
-//        }
-//        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
-//    }
-//
-//
-//    /**
-//     * 将 QwSopLogs 放入队列
-//     */
-//    private void enqueueQwSopLogs(QwSopLogs sopLogs) {
-//        try {
-//            boolean offered = qwSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
-//            if (!offered) {
-//                log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
-//                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-//            }
-//        } catch (InterruptedException e) {
-//            Thread.currentThread().interrupt();
-//            log.error("插入 QwSopLogs 队列时被中断: {}", e.getMessage(), e);
-//        }
-//    }
-//
-//    /**
-//     * 将 FsCourseWatchLog 放入队列
-//     */
-//    private void enqueueWatchLog(FsCourseWatchLog watchLog) {
-//        try {
-//            boolean offered = watchLogsQueue.offer(watchLog, 5, TimeUnit.SECONDS);
-//            if (!offered) {
-//                log.error("FsCourseWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(watchLog));
-//                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-//            }
-//        } catch (InterruptedException e) {
-//            Thread.currentThread().interrupt();
-//            log.error("插入 FsCourseWatchLog 队列时被中断: {}", e.getMessage(), e);
-//        }
-//    }
-//
-//    /**
-//     * 将 FsCourseWatchLog 放入队列
-//     */
-//    private void enqueueCourseLink(FsCourseLink courseLink) {
-//        try {
-//            boolean offered = linkQueue.offer(courseLink, 5, TimeUnit.SECONDS);
-//            if (!offered) {
-//                log.error("FsCourseLink 队列已满,无法添加日志: {}", JSON.toJSONString(courseLink));
-//                // 处理队列已满的情况,例如记录到失败队列或持久化存储
-//            }
-//        } catch (InterruptedException e) {
-//            Thread.currentThread().interrupt();
-//            log.error("插入 FsCourseLink 队列时被中断: {}", e.getMessage(), e);
-//        }
-//    }
-//
-//    /**
-//     * 消费 QwSopLogs 队列并进行批量插入
-//     */
-//    private void consumeQwSopLogs() {
-//        List<QwSopLogs> batch = new ArrayList<>(BATCH_SIZE);
-//        while (running || !qwSopLogsQueue.isEmpty()) {
-//            try {
-//                QwSopLogs log = qwSopLogsQueue.poll(1, TimeUnit.SECONDS);
-//                if (log != null) {
-//                    batch.add(log);
-//                }
-//                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && log == null)) {
-//                    if (!batch.isEmpty()) {
-//                        batchInsertQwSopLogs(new ArrayList<>(batch));
-//                        batch.clear();
-//                    }
-//                }
-//            } catch (InterruptedException e) {
-//                Thread.currentThread().interrupt();
-//                log.error("QwSopLogs 消费线程被中断: {}", e.getMessage(), e);
-//            }
-//        }
-//
-//        // 处理剩余的数据
-//        if (!batch.isEmpty()) {
-//            batchInsertQwSopLogs(batch);
-//        }
-//    }
-//
-//    /**
-//     * 消费 FsCourseWatchLog 队列并进行批量插入
-//     */
-//    private void consumeCourseLink() {
-//        List<FsCourseLink> batch = new ArrayList<>(BATCH_SIZE);
-//        while (running || !linkQueue.isEmpty()) {
-//            try {
-//                FsCourseLink courseLink = linkQueue.poll(1, TimeUnit.SECONDS);
-//                if (courseLink != null) {
-//                    batch.add(courseLink);
-//                }
-//                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && courseLink == null)) {
-//                    if (!batch.isEmpty()) {
-//                        batchInsertFsCourseLink(new ArrayList<>(batch));
-//                        batch.clear();
-//                    }
-//                }
-//            } catch (InterruptedException e) {
-//                Thread.currentThread().interrupt();
-//                log.error("FsCourseLink 消费线程被中断: {}", e.getMessage(), e);
-//            }
-//        }
-//
-//        // 处理剩余的数据
-//        if (!batch.isEmpty()) {
-//            batchInsertFsCourseLink(batch);
-//        }
-//    }
-//
-//    /**
-//     * 消费 FsCourseWatchLog 队列并进行批量插入
-//     */
-//    private void consumeWatchLogs() {
-//        List<FsCourseWatchLog> batch = new ArrayList<>(BATCH_SIZE);
-//        while (running || !watchLogsQueue.isEmpty()) {
-//            try {
-//                FsCourseWatchLog watchLog = watchLogsQueue.poll(1, TimeUnit.SECONDS);
-//                if (watchLog != null) {
-//                    batch.add(watchLog);
-//                }
-//                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && watchLog == null)) {
-//                    if (!batch.isEmpty()) {
-//                        batchInsertFsCourseWatchLogs(new ArrayList<>(batch));
-//                        batch.clear();
-//                    }
-//                }
-//            } catch (InterruptedException e) {
-//                Thread.currentThread().interrupt();
-//                log.error("FsCourseWatchLog 消费线程被中断: {}", e.getMessage(), e);
-//            }
-//        }
-//
-//        // 处理剩余的数据
-//        if (!batch.isEmpty()) {
-//            batchInsertFsCourseWatchLogs(batch);
-//        }
-//    }
-//
-//    /**
-//     * 批量插入 QwSopLogs
-//     */
-//    @Transactional
-//    @Retryable(
-//            value = { Exception.class },
-//            maxAttempts = 3,
-//            backoff = @Backoff(delay = 2000)
-//    )
-//    public void batchInsertQwSopLogs(List<QwSopLogs> logsToInsert) {
-//        try {
-//            qwSopLogsService.batchInsertQwSopLogs(logsToInsert);
-//            log.info("批量插入 QwSopLogs 完成,共插入 {} 条记录。", logsToInsert.size());
-//        } catch (Exception e) {
-//            log.error("批量插入 QwSopLogs 失败: {}", e.getMessage(), e);
-//            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-//        }
-//    }
-//
-//    /**
-//     * 批量插入 FsCourseWatchLog
-//     */
-//    @Transactional
-//    @Retryable(
-//            value = { Exception.class },
-//            maxAttempts = 3,
-//            backoff = @Backoff(delay = 2000)
-//    )
-//    public void batchInsertFsCourseWatchLogs(List<FsCourseWatchLog> watchLogsToInsert) {
-//        try {
-//            fsCourseWatchLogMapper.insertFsCourseWatchLogBatch(watchLogsToInsert);
-//            log.info("批量插入 FsCourseWatchLog 完成,共插入 {} 条记录。", watchLogsToInsert.size());
-//        } catch (Exception e) {
-//            log.error("批量插入 FsCourseWatchLog 失败: {}", e.getMessage(), e);
-//            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-//        }
-//    }
-//
-//
-//    /**
-//     * 批量插入 FsCourseLink
-//     */
-//    @Transactional
-//    @Retryable(
-//            value = { Exception.class },
-//            maxAttempts = 3,
-//            backoff = @Backoff(delay = 2000)
-//    )
-//    public void batchInsertFsCourseLink(List<FsCourseLink> courseLinkToInsert) {
-//        try {
-//            fsCourseLinkMapper.insertFsCourseLinkBatch(courseLinkToInsert);
-//            log.info("批量插入 FsCourseLink 完成,共插入 {} 条记录。", courseLinkToInsert.size());
-//        } catch (Exception e) {
-//            log.error("批量插入 FsCourseLink 失败: {}", e.getMessage(), e);
-//            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
-//        }
-//    }
-//
-//}

+ 0 - 262
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopUserLogsInfoByIsDaysNotStudyImpl.java

@@ -1,262 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.SopUserLogsInfoByIsDaysNotStudy;
-import com.fs.course.mapper.FsCourseWatchLogMapper;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.sop.domain.SopUserLogs;
-import com.fs.sop.domain.SopUserLogsInfo;
-import com.fs.sop.params.QwRatingConfig;
-import com.fs.sop.service.ISopUserLogsInfoService;
-import com.fs.sop.service.ISopUserLogsService;
-import com.fs.system.service.ISysConfigService;
-import com.fs.voice.utils.StringUtil;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.*;
-import java.util.stream.Collectors;
-
-@Service
-@Slf4j
-public class SopUserLogsInfoByIsDaysNotStudyImpl implements SopUserLogsInfoByIsDaysNotStudy {
-
-
-    @Autowired
-    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
-
-    @Autowired
-    private ISysConfigService configService;
-
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-
-    @Autowired
-    private ISopUserLogsInfoService iSopUserLogsInfoService;
-
-    @Autowired
-    private ISopUserLogsService iSopUserLogsService;
-
-    @Autowired
-    private ExecutorService sopRatingExecutor;  // 自定义线程池
-
-    // 任务队列
-    private final BlockingQueue<SopUserLogs> taskQueue = new LinkedBlockingQueue<>(10000);
-
-    private volatile boolean running = true;
-    //批量更新队列
-    private final List<CompletableFuture<Void>> updateFutures = Collections.synchronizedList(new ArrayList<>());
-
-    private final Object configLock = new Object();
-
-
-    private  volatile QwRatingConfig qwRatingConfig;
-
-    // 启动时初始化消费者线程
-    @PostConstruct
-    public void init() {
-
-        loadCourseConfig();
-
-        int consumerCount = Runtime.getRuntime().availableProcessors(); // 消费者线程数,默认 CPU 核心数
-        for (int i = 0; i < consumerCount; i++) {
-            sopRatingExecutor.submit(this::consumeTasks); // 提交消费者任务
-        }
-
-    }
-
-    private void loadCourseConfig() {
-        try {
-            String json = configService.selectConfigByKey("qwRating:config");
-            QwRatingConfig config = JSON.parseObject(json, QwRatingConfig.class);
-            if (!StringUtil.strIsNullOrEmpty(json) && config != null) {
-                qwRatingConfig = config;
-                log.info("Loaded qwRating.config successfully.");
-            } else {
-                log.error("Failed to load course.config from configService.");
-            }
-        } catch (Exception e) {
-            log.error("Exception while loading qwRating.config: {}", e.getMessage(), e);
-        }
-    }
-
-
-
-    @Override
-    public void restoreByIsDaysNotStudy() {
-
-        // 分页加载并放入队列
-        int pageSize = 1000;
-        int offset = 0;
-        List<SopUserLogs> sopUserLogs;
-
-        // 获取缓存的配置
-        QwRatingConfig config;
-        synchronized(configLock) {
-            config = qwRatingConfig;
-        }
-
-        do {
-            sopUserLogs = iSopUserLogsService.meetsTherestoreByIsDaysNotStudy(offset, pageSize,config.getNotStudyDays());
-            if (!sopUserLogs.isEmpty()) {
-                sopUserLogs.forEach(item -> {
-                    try {
-                        taskQueue.put(item); // 将任务放入队列
-                    } catch (InterruptedException e) {
-                        log.error("任务放入队列失败,sopId: {}", item.getSopId(), e);
-                        Thread.currentThread().interrupt();
-                    }
-                });
-                offset += pageSize;
-            }
-        } while (!sopUserLogs.isEmpty());
-
-
-        // 等待队列处理完成
-        CompletableFuture.runAsync(() -> {
-            while (!taskQueue.isEmpty()) {
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    log.error("等待队列处理时中断", e);
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }).join(); // 等待任务完成
-
-    }
-
-    private void consumeTasks() {
-        if (!running && taskQueue.isEmpty()) {
-            log.info("没有评级任务需要处理");
-            return; // 如果队列为空且没有正在运行的线程,则直接返回
-        }
-
-        while (running) {
-            try {
-                SopUserLogs item = taskQueue.poll(1, TimeUnit.SECONDS); // 等待 1 秒
-                if (item != null) {
-                    processRestoreByIsDaysNotStudy(item);
-                }
-            } catch (Exception e) {
-                log.error("消费者线程异常", e);
-            }
-        }
-    }
-
-    private void processRestoreByIsDaysNotStudy(SopUserLogs item) {
-
-        // 获取缓存的配置
-        QwRatingConfig config;
-        synchronized(configLock) {
-            config = qwRatingConfig;
-        }
-
-        List<SopUserLogsInfo> infos = iSopUserLogsInfoService.selectRestoreByIsDaysNotStudy(
-                item.getSopId(), item.getId());
-
-        if (infos == null || infos.isEmpty()) {
-            log.error("当前营期没有E级客户-sopId:{},营期id:{}", item.getSopId(), item.getId());
-            return;
-        }
-
-        List<QwExternalContact> contacts = infos.stream()
-                .map(info -> processUserLog(info, config))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-
-        if (!contacts.isEmpty()) {
-            batchUpdateQwExternalContact(contacts);
-        }
-    }
-
-    private void batchUpdateQwExternalContact(List<QwExternalContact> contacts) {
-        // 9. 优化分批逻辑
-        int total = contacts.size();
-        for (int i = 0; i < total; i += 300) {
-            List<QwExternalContact> batch = contacts.subList(i, Math.min(i + 300, total));
-
-            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
-                try {
-                    qwExternalContactMapper.batchUpdateQwExternalByIsDaysNotStudy(batch);
-                    iSopUserLogsInfoService.batchUpdateSopUserLogsInfoByIsDaysNotStudy(batch);
-                } catch (Exception e) {
-                    log.error("批量更新异常, 批次大小: {}", batch.size(), e);
-                }
-            }, sopRatingExecutor);
-
-            updateFutures.add(future);
-        }
-    }
-
-    @PreDestroy
-    public void shutdown() {
-        running = false;  // 标记消费者停止
-        log.info("正在关闭线程池...");
-
-        // **等待任务队列处理完毕**
-        while (!taskQueue.isEmpty()) {
-            try {
-                Thread.sleep(500);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.warn("等待任务队列处理完成时被中断", e);
-            }
-        }
-
-        // **确保所有  的任务完成**
-        log.info("等待所有批量更新任务完成...");
-        CompletableFuture.allOf(updateFutures.toArray(new CompletableFuture[0])).join();
-
-        // 关闭线程池
-        sopRatingExecutor.shutdown();
-        try {
-            if (!sopRatingExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-                List<Runnable> pendingTasks = sopRatingExecutor.shutdownNow();
-                log.warn("强制关闭线程池,未完成任务数: {}", pendingTasks.size());
-            }
-        } catch (InterruptedException e) {
-            sopRatingExecutor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
-        log.info("线程池和消费者已完全关闭");
-    }
-
-    /**
-    * 只计算时长
-    */
-    private QwExternalContact processUserLog(SopUserLogsInfo logsInfo, QwRatingConfig config) {
-        try {
-
-            Long externalId = logsInfo.getExternalId();
-            if (externalId == null) {
-                return null;
-            }
-
-            Integer sumDuration = fsCourseWatchLogMapper.selectFsCourseWatchLogByByIsDaysNotStudy(externalId, config.getNotStudyDays());
-
-            if (sumDuration!=null && sumDuration>0) {
-                QwExternalContact externalContact = new QwExternalContact();
-                externalContact.setId(externalId);
-                externalContact.setIsDaysNotStudy(0);
-                return externalContact;
-            }
-
-            return null;
-
-        } catch (Exception e) {
-            log.error("计算用户积分异常,用户:{}", logsInfo, e);
-            return null;
-        }
-    }
-
-
-}

+ 0 - 127
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsServiceImpl.java

@@ -1,127 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.fs.app.taskService.SopWxLogsService;
-import com.fs.common.utils.PubFun;
-import com.fs.common.utils.StringUtils;
-import com.fs.common.utils.date.DateUtil;
-import com.fs.sop.domain.*;
-import com.fs.sop.mapper.QwSopTempMapper;
-import com.fs.sop.service.*;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.stream.Collectors;
-
-@Service
-@Slf4j
-@AllArgsConstructor
-public class SopWxLogsServiceImpl implements SopWxLogsService {
-
-    private final IQwSopService qwSopService;
-    private final QwSopTempMapper qwSopTempMapper;
-    private final IQwSopTempDayService qwSopTempDayService;
-    private final ISopUserLogsWxService sopUserLogsWxService;
-    private final IQwSopTempRulesService qwSopTempRulesService;
-    private final IQwSopLogsService  qwSopLogsService;
-
-    @Override
-    public void wxSopLogsByTime(LocalDateTime now) throws Exception {
-        long startTimeMillis = System.currentTimeMillis();
-        log.info("====== 开始选择和处理 个微SOP 用户日志 ======");
-
-        List<QwSop> sopList = qwSopService.selectWxSop();
-        if(sopList.isEmpty()){
-            return;
-        }
-        CountDownLatch sopGroupLatch = new CountDownLatch(sopList.size());
-        sopList.forEach(sop -> {
-            processSopGroupAsync(sop, sopGroupLatch, now);
-        });
-        sopGroupLatch.await();
-
-        long endTimeMillis = System.currentTimeMillis();
-        log.info("====== 个微SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
-    }
-
-    private void processSopGroupAsync(QwSop sop, CountDownLatch latch, LocalDateTime now) {
-        try {
-            processSopGroup(sop, now);
-        } catch (Exception e) {
-            log.error("处理个微 SOP ID {} 时发生异常: {}", sop.getId(), e.getMessage(), e);
-        } finally {
-            latch.countDown();
-        }
-    }
-
-    private void processSopGroup(QwSop sop, LocalDateTime now) {
-        // 提前一个小时生成
-        LocalDateTime start = now.plusHours(1);
-        // 当前小时
-        int hour = start.getHour();
-        // 获取模板
-        QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(sop.getTempId());
-        // 如果模板为空
-        if(qwSopTemp == null){
-            log.error("SOP ID:{}没找到模板 {} 跳过生成", sop.getId(), sop.getTempId());
-            return;
-        }
-        // 获取模板下面每一天的数据
-        List<QwSopTempDay> dayList = qwSopTempRulesService.listByTempIdAll(sop.getTempId());
-        // 更具天数分组
-        Map<Long, QwSopTempDay> dayMap = PubFun.listToMapByGroupObject(dayList, e -> e.getDayNum().longValue());
-        // 获取sop下面所有的个微营期数据
-        List<SopUserLogsWx> wxUserList = sopUserLogsWxService.listBySopId(sop.getId());
-        // 循环每一个人的数据,并对数据进行筛选,营期开始时间不能为空,并且要大于当前生成的时间
-        wxUserList.stream().filter(e -> e.getStartTime() != null && !e.getStartTime().isAfter(now.toLocalDate())).forEach(sopUserLogsWx -> {
-            // 计算营期到现在是第几天
-            long until = sopUserLogsWx.getStartTime().until(now.toLocalDate(), ChronoUnit.DAYS) + 1;
-            // 是否存在跨天的现象
-            if(!now.toLocalDate().equals(start.toLocalDate())){
-                until++;
-            }
-            // 根据第几天获取数据
-            QwSopTempDay day = dayMap.get(until);
-            // 筛选规则,获得当前一个小时的所有数据
-            List<QwSopTempRules> rulesList = day.getList().stream().filter(e -> StringUtils.isNotEmpty(e.getTime()) && LocalTime.parse(e.getTime()).getHour() == hour).collect(Collectors.toList());
-            // 创建发送记录
-            List<QwSopLogs> losList = rulesList.stream().map(rules -> {
-                LocalTime time = LocalTime.parse(rules.getTime());
-                return createBaseLog(LocalDateTime.of(start.toLocalDate(), time), sop, sopUserLogsWx, rules);
-            }).collect(Collectors.toList());
-            // 保存记录
-            qwSopLogsService.batchInsertQwSopLogs(losList);
-        });
-
-    }
-
-
-    private QwSopLogs createBaseLog(LocalDateTime sendTime, QwSop sop, SopUserLogsWx wx, QwSopTempRules rules) {
-        QwSopLogs sopLogs = new QwSopLogs();
-        sopLogs.setSendTime(DateUtil.formatLocalDateTime(sendTime));
-        sopLogs.setExpirationTime(DateUtil.formatLocalDateTime(sendTime.plusHours(sop.getExpiryTime())));
-        sopLogs.setQwUserid(wx.getAccountId().toString());
-        sopLogs.setLogType(1);
-        JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(rules));
-        jsonObject.remove("settingList");
-        jsonObject.remove("list");
-        jsonObject.put("setting", rules.getSettingList());
-        sopLogs.setContentJson(jsonObject.toJSONString());
-        sopLogs.setSendType(sop.getSendType());
-        sopLogs.setSendStatus(3L);
-        sopLogs.setReceivingStatus(0L);
-        sopLogs.setSopId(sop.getId());
-        sopLogs.setExternalUserName(wx.getUserWxName());
-        sopLogs.setCorpId(sop.getCorpId());
-        sopLogs.setCompanyId(sop.getCompanyId());
-        return sopLogs;
-    }
-}

+ 0 - 78
fs-wx-task/src/main/java/com/fs/app/taskService/impl/SyncQwExternalContactServiceImpl.java

@@ -1,78 +0,0 @@
-package com.fs.app.taskService.impl;
-
-import com.fs.app.taskService.SyncQwExternalContactService;
-import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.common.utils.StringUtils;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qwApi.domain.QwExternalContactResult;
-import com.fs.qwApi.service.QwApiService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Service
-@Slf4j
-public class SyncQwExternalContactServiceImpl implements SyncQwExternalContactService {
-    @Autowired
-    private RedisCache redisCache;
-    @Autowired
-    private QwExternalContactMapper qwExternalContactMapper;
-    @Autowired
-    private QwApiService qwApiService;
-    @Override
-    public R syncQwExternalContactUnionid() {
-        // 测试环境需要在sql加上:and corp_id='ww51717e2b71d5e2d3'
-        // 查询这次同步的最大id
-        Long maxId = qwExternalContactMapper.selectSyncMaxId();
-        log.info("同步最大id值:"+maxId);
-        if (maxId == null) {
-            return R.ok("无需同步");
-        }
-        Long recordId = 0L;
-        String recordIdStr = redisCache.getCacheObject("syncQwExternalContactUnionId");
-        if (StringUtils.isNotEmpty(recordIdStr)) {
-            try {
-                recordId = Long.parseLong(recordIdStr);
-            } catch (NumberFormatException e) {
-                log.info("Failed to parse recordId from redis: {}", recordIdStr);
-                recordId = 0L;
-            }
-        }
-        log.info("开始同步的recordId值:"+recordId);
-        // 循环同步直到recordId等于maxId
-        while (recordId < maxId) {
-            // 每次查询500条数据
-            List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectSyncData(recordId, maxId);
-            if (qwExternalContacts.isEmpty()) {
-                break;
-            }
-            List<QwExternalContact> batchList = new ArrayList<>();
-            // 调用接口
-            for (QwExternalContact info : qwExternalContacts) {
-                QwExternalContactResult externalcontact = qwApiService.getExternalcontact(info.getExternalUserId(), info.getCorpId());
-                if (null!=externalcontact && null!=externalcontact.getExternal_contact() && null!=externalcontact.getExternal_contact().getUnionid() ) {
-                    info.setUnionid(externalcontact.getExternal_contact().getUnionid());
-                    batchList.add(info);
-                }
-            }
-            if (!batchList.isEmpty()) {
-                for (QwExternalContact qwExternalContact : batchList) {
-                    qwExternalContactMapper.batchUpdateUnionId(qwExternalContact);
-                }
-            }else{
-                log.info("集合为空:{recordId->"+recordId+";syncId->"+qwExternalContacts.get(qwExternalContacts.size() - 1).getId()+"}");
-            }
-            // 更新recordId为本次处理的最后一条记录的id
-            recordId = qwExternalContacts.get(qwExternalContacts.size() - 1).getId();
-            // 更新redis中的记录值
-            redisCache.setCacheObject("syncQwExternalContactUnionId", recordId.toString());
-        }
-        log.info("同步成功,同步完之后的recordId:"+recordId);
-        return R.ok();
-    }
-}

Some files were not shown because too many files changed in this diff