瀏覽代碼

Merge remote-tracking branch 'origin/master'

三七 3 天之前
父節點
當前提交
ba66a1ddd5
共有 100 個文件被更改,包括 7358 次插入381 次删除
  1. 2 1
      .vscode/settings.json
  2. 1 0
      fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java
  3. 67 3
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  4. 12 0
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  5. 27 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  6. 2 2
      fs-admin/src/main/resources/application.yml
  7. 36 0
      fs-common/src/main/java/com/fs/common/core/page/TableDataInfo.java
  8. 0 27
      fs-company/src/main/java/com/fs/company/controller/common/Test.java
  9. 56 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  10. 25 14
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java
  11. 17 3
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  12. 31 0
      fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java
  13. 5 3
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  14. 170 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java
  15. 78 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java
  16. 5 5
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  17. 18 12
      fs-ipad-task/src/main/java/com/fs/app/service/CustomThreadPoolConfig.java
  18. 54 9
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  19. 84 24
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  20. 4 1
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  21. 44 0
      fs-qw-task/src/main/java/com/fs/app/task/wxTask.java
  22. 22 0
      fs-qw-task/src/main/java/com/fs/app/taskService/SopWxLogsTaskService.java
  23. 56 5
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  24. 3431 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsTaskServiceImpl.java
  25. 30 0
      fs-service/src/main/java/com/fs/company/config/AsyncConfig.java
  26. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java
  27. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExecLog.java
  28. 2 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticBusiness.java
  29. 15 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java
  30. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  31. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecLogMapper.java
  32. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  33. 6 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticBusinessMapper.java
  34. 6 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java
  35. 7 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  36. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java
  37. 7 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCalleesMapper.java
  38. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  39. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticWxMapper.java
  40. 7 2
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowEdgeMapper.java
  41. 166 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  42. 6 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java
  43. 5 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogSendmsgService.java
  44. 9 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  45. 16 0
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  46. 15 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java
  47. 11 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java
  48. 363 161
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  49. 4 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  50. 197 0
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  51. 31 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  52. 323 0
      fs-service/src/main/java/com/fs/company/util/CryptoUtil.java
  53. 163 0
      fs-service/src/main/java/com/fs/company/util/PhoneNumberUtil.java
  54. 81 0
      fs-service/src/main/java/com/fs/company/util/RandomNameGeneratorUtil.java
  55. 12 0
      fs-service/src/main/java/com/fs/company/vo/CompanyNodeInfoVo.java
  56. 59 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogAddWxExportVO.java
  57. 16 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogAddwxVO.java
  58. 30 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCount.java
  59. 3 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogSendmsgVO.java
  60. 5 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  61. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java
  62. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  63. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  64. 62 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  65. 9 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java
  66. 24 0
      fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java
  67. 24 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  68. 204 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  69. 43 16
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  70. 28 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  71. 23 0
      fs-service/src/main/java/com/fs/course/vo/CourseProductSalesVO.java
  72. 72 0
      fs-service/src/main/java/com/fs/course/vo/CourseStatisticsDetailVO.java
  73. 34 0
      fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java
  74. 5 0
      fs-service/src/main/java/com/fs/course/vo/FsVideoResourceVO.java
  75. 8 0
      fs-service/src/main/java/com/fs/course/vo/UpdateCourseTimeVo.java
  76. 5 0
      fs-service/src/main/java/com/fs/course/vo/newfs/FsUserCourseVideoDetailsVO.java
  77. 6 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java
  78. 7 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java
  79. 121 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java
  80. 145 0
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  81. 46 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  82. 109 0
      fs-service/src/main/java/com/fs/his/utils/Base62Utils.java
  83. 83 0
      fs-service/src/main/java/com/fs/his/utils/LinkUtil.java
  84. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  85. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  86. 4 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  87. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  88. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  89. 14 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  90. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCompanyMapper.java
  91. 2 0
      fs-service/src/main/java/com/fs/qw/service/IQwCompanyService.java
  92. 6 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCompanyServiceImpl.java
  93. 18 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  94. 3 0
      fs-service/src/main/java/com/fs/sop/mapper/QwSopMapper.java
  95. 4 0
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsInfoMapper.java
  96. 4 0
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java
  97. 1 1
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java
  98. 281 81
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  99. 0 2
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java
  100. 51 0
      fs-service/src/main/java/com/fs/sop/vo/WxSopUserVo.java

+ 2 - 1
.vscode/settings.json

@@ -1,3 +1,4 @@
 {
-    "java.compile.nullAnalysis.mode": "automatic"
+    "java.compile.nullAnalysis.mode": "automatic",
+    "java.configuration.updateBuildConfiguration": "interactive"
 }

+ 1 - 0
fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java

@@ -36,6 +36,7 @@ public class FsVideoResourceBusinessService {
                 .set("file_key", fsVideoResource.getFileKey())
                 .set("file_name", fsVideoResource.getFileName())
                 .set("thumbnail", fsVideoResource.getThumbnail())
+                .set("display_type", fsVideoResource.getDisplayType())
         );
     }
 }

+ 67 - 3
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -9,9 +9,7 @@ import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
-import com.fs.course.vo.FsCourseOverVO;
-import com.fs.course.vo.FsCourseWatchLogListVO;
-import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
+import com.fs.course.vo.*;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
 import com.github.pagehelper.PageHelper;
@@ -25,6 +23,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
@@ -236,4 +235,69 @@ public class FsCourseWatchLogController extends BaseController
         List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
         return getDataTable(list);
     }
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsDetail")
+    public R getCourseStatisticsDetail(@RequestParam("videoId") Long videoId, @RequestParam("periodId") Long periodId)
+    {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(videoId, periodId));
+    }
+
+    /**
+     * 课程小结-用户详情列表(分页)
+     * 根据videoId、periodId查询观看记录,区分首次/第2-n次观看时长,关联订单及销售公司
+     *
+     * @param videoId  视频ID
+     * @param periodId 营期ID
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页用户详情
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsUserDetail")
+    public R getCourseStatisticsUserDetail(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setPageNum(pageNum);
+        param.setPageSize(pageSize);
+        PageHelper.startPage(pageNum, pageSize);
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
+    }
+
+    /**
+     * 课程小结-用户详情导出(按创建时间倒序,最多50000条)
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @Log(title = "课程小结用户详情导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/courseStatisticsUserDetailExport")
+    public AjaxResult courseStatisticsUserDetailExport(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId) {
+        if (videoId == null || periodId == null) {
+            return AjaxResult.error("视频ID和营期ID不能为空");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
+        ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
+        return util.exportExcel(list, "用户看课数据");
+    }
 }

+ 12 - 0
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -107,6 +107,10 @@ public class FsVideoResourceController extends BaseController {
         if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsVideoResource.setUserId(userId);
         }
+        // 视频展示类型:前端未传时默认横屏
+        if (com.fs.common.utils.StringUtils.isBlank(fsVideoResource.getDisplayType())) {
+            fsVideoResource.setDisplayType("landscape");
+        }
 
         fsVideoResource.setCreateTime(LocalDateTime.now());
         boolean save = fsVideoResourceService.save(fsVideoResource);
@@ -128,6 +132,10 @@ public class FsVideoResourceController extends BaseController {
     @Log(title = "视频素材库", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody FsVideoResource fsVideoResource) {
+        // 视频展示类型:前端未传时默认横屏
+        if (com.fs.common.utils.StringUtils.isBlank(fsVideoResource.getDisplayType())) {
+            fsVideoResource.setDisplayType("landscape");
+        }
         if (("今正科技".equals(cloudHostProper.getCompanyName()))) {
             // 同步资源到课程
             videoResourceBusinessService.edit(fsVideoResource);
@@ -203,6 +211,10 @@ public class FsVideoResourceController extends BaseController {
 
         list.forEach(v -> {
             v.setCreateTime(LocalDateTime.now());
+            // 视频展示类型:前端未传时默认横屏
+            if (com.fs.common.utils.StringUtils.isBlank(v.getDisplayType())) {
+                v.setDisplayType("landscape");
+            }
             if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
                 v.setUserId(userId);
             }

+ 27 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -27,6 +27,13 @@ import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.vo.CompanyStoreOrderMoneyLogsVO;
 import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.param.FsCourseWatchLogParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrderQuery;
 import com.fs.erp.dto.ErpOrderQueryRequert;
@@ -151,6 +158,13 @@ public class FsStoreOrderScrmController extends BaseController {
     @Autowired
     private IFsStoreOrderLogsScrmService fsStoreOrderLogsService;
 
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+    @Autowired
+    private IFsUserCoursePeriodService fsUserCoursePeriodService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
+
     @Value("${cloud_host.company_name}")
     private String signProjectName;
 
@@ -672,6 +686,19 @@ public class FsStoreOrderScrmController extends BaseController {
             order.setCompanyName(company.getCompanyName());
         }
 
+        if (order.getOrderType() != null && order.getOrderType() == 3) {
+//            FsCourseWatchLogParam param = new FsCourseWatchLogParam();
+//            param.setVideoId(Long.valueOf(order.getVideoId()));
+//            FsCourseWatchLog log = fsCourseWatchLogService.selectFsCourseWatchLogWithUCCV(order.getUserId(), order.getCompanyUserId(), order.getCourseId(), order.getVideoId());
+            if (order.getPeriodId() != null) {
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(Long.valueOf(order.getPeriodId()));
+                order.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
+            if (order.getVideoId() != null) {
+                FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoService.selectFsUserCourseVideoByVideoId(Long.valueOf(order.getVideoId()));
+                order.setVideoName(fsUserCourseVideo.getTitle());
+            }
+        }
 
         FsStoreOrderItemScrm itemMap = new FsStoreOrderItemScrm();
         itemMap.setOrderId(order.getId());

+ 2 - 2
fs-admin/src/main/resources/application.yml

@@ -4,11 +4,11 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-bjzm-test
+#    active: druid-bjzm-test
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz-test
 #    active: druid-sft
 #    active: druid-fby
-#    active: dev
+    active: dev
 

+ 36 - 0
fs-common/src/main/java/com/fs/common/core/page/TableDataInfo.java

@@ -24,6 +24,12 @@ public class TableDataInfo implements Serializable
     /** 消息内容 */
     private String msg;
 
+    private long totalRecordCount;
+
+    private long successRecordCount;
+
+    private long todayRecordCount;
+
     /**
      * 表格数据对象
      */
@@ -82,4 +88,34 @@ public class TableDataInfo implements Serializable
     {
         this.msg = msg;
     }
+
+    public long getTotalRecordCount()
+    {
+        return totalRecordCount;
+    }
+
+    public void setTotalRecordCount(long totalRecordCount)
+    {
+        this.totalRecordCount = totalRecordCount;
+    }
+
+    public long getSuccessRecordCount()
+    {
+        return successRecordCount;
+    }
+
+    public void setSuccessRecordCount(long successRecordCount)
+    {
+        this.successRecordCount = successRecordCount;
+    }
+
+    public long getTodayRecordCount()
+    {
+        return todayRecordCount;
+    }
+
+    public void setTodayRecordCount(long todayRecordCount)
+    {
+        this.todayRecordCount = todayRecordCount;
+    }
 }

+ 0 - 27
fs-company/src/main/java/com/fs/company/controller/common/Test.java

@@ -1,18 +1,10 @@
 package com.fs.company.controller.common;
 
-import com.alibaba.fastjson.JSON;
-import com.fs.ad.enums.AdUploadType;
-import com.fs.common.annotation.DataSource;
-import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
-import com.fs.common.enums.DataSourceType;
 import com.fs.common.service.impl.SmsServiceImpl;
 import com.fs.company.mapper.*;
 import com.fs.company.service.ICompanyService;
-import com.fs.company.service.ICompanyUserService;
-import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
-import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrderQuery;
@@ -20,31 +12,18 @@ import com.fs.erp.dto.ErpOrderQueryRequert;
 import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
-import com.fs.fastGpt.service.IFastgptEventLogTotalService;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.mapper.*;
 import com.fs.his.service.*;
 import com.fs.his.service.impl.FsPackageOrderServiceImpl;
 import com.fs.his.utils.ConfigUtil;
-import com.fs.his.utils.qrcode.QRCodeUtils;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.im.service.IImService;
-import com.fs.im.service.OpenIMService;
 import com.fs.qw.service.IQwAppContactWayService;
-import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwExternalContactTransferLogService;
-import com.fs.qw.service.IQwUserService;
-import com.fs.qw.vo.AdUploadVo;
 import com.fs.qwApi.service.QwApiService;
-import com.fs.sop.service.IQwSopTempContentService;
-import com.fs.sop.service.IQwSopTempDayService;
-import com.fs.sop.service.IQwSopTempRulesService;
-import com.fs.sop.service.IQwSopTempService;
 import com.fs.system.mapper.SysConfigMapper;
-import com.google.zxing.WriterException;
-import lombok.AllArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -54,12 +33,6 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
 @RestController
 public class Test {
     @Autowired

+ 56 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java

@@ -3,12 +3,15 @@ package com.fs.company.controller.company;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.CompanyWxClient;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import org.springframework.beans.BeanUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -73,7 +76,57 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         }
 
     }
-//
+
+    /**
+     * 查询调用日志_加微信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:list')")
+    @GetMapping("/listAll")
+    public TableDataInfo listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
+        TableDataInfo dataTable = getDataTable(list);
+        Map<String, Long> countMap = companyVoiceRoboticCallLogAddwxService.countListAll(companyVoiceRoboticCallLogAddwx);
+        if (countMap != null) {
+            dataTable.setTotalRecordCount(countMap.getOrDefault("totalRecordCount", 0L));
+            dataTable.setSuccessRecordCount(countMap.getOrDefault("successRecordCount", 0L));
+            dataTable.setTodayRecordCount(countMap.getOrDefault("todayRecordCount", 0L));
+        }
+        return dataTable;
+
+    }
+
+
+    /**
+     * 导出调用日志_加微信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:export')")
+    @Log(title = "调用日志_加微信", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
+        List<CompanyVoiceRoboticCallLogAddWxExportVO> exportList = list.stream().map(item -> {
+            CompanyVoiceRoboticCallLogAddWxExportVO vo = new CompanyVoiceRoboticCallLogAddWxExportVO();
+            BeanUtils.copyProperties(item, vo);
+            return vo;
+        }).collect(Collectors.toList());
+        ExcelUtil<CompanyVoiceRoboticCallLogAddWxExportVO> util = new ExcelUtil<CompanyVoiceRoboticCallLogAddWxExportVO>(CompanyVoiceRoboticCallLogAddWxExportVO.class);
+        return util.exportExcel(exportList, "调用日志_加微信数据");
+    }
+
+    /**
+     * 删除调用日志_加微信
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:remove')")
+    @Log(title = "调用日志_加微信", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(companyVoiceRoboticCallLogAddwxService.deleteCompanyVoiceRoboticCallLogAddwxByLogIds(logIds));
+    }
+
 //    /**
 //     * 导出调用日志_加微信列表
 //     */
@@ -86,6 +139,8 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
 //        ExcelUtil<CompanyVoiceRoboticCallLogAddwx> util = new ExcelUtil<CompanyVoiceRoboticCallLogAddwx>(CompanyVoiceRoboticCallLogAddwx.class);
 //        return util.exportExcel(list, "调用日志_加微信数据");
 //    }
+//
+
 //
 //    /**
 //     * 获取调用日志_加微信详细信息

+ 25 - 14
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java

@@ -4,6 +4,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,14 +41,14 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
     /**
-     * 查询调用日志_发送短信列表
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
      */
     @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
     {
         startPage();
-        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
         return getDataTable(list);
     }
 
@@ -75,21 +76,31 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
             List<CompanyVoiceRoboticCallLogSendmsgVO> list = companyVoiceRoboticCallLogSendmsgService.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
             return getDataTable(list);
         }
+    }
 
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallLogSendMsgCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
-//    /**
-//     * 导出调用日志_发送短信列表
-//     */
-//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:export')")
-//    @Log(title = "调用日志_发送短信", businessType = BusinessType.EXPORT)
-//    @GetMapping("/export")
-//    public AjaxResult export(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
-//    {
-//        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
-//        ExcelUtil<CompanyVoiceRoboticCallLogSendmsg> util = new ExcelUtil<CompanyVoiceRoboticCallLogSendmsg>(CompanyVoiceRoboticCallLogSendmsg.class);
-//        return util.exportExcel(list, "调用日志_发送短信数据");
-//    }
+    /**
+     * 导出调用日志_发送短信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:export')")
+    @Log(title = "调用日志_发送短信", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+    {
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
+        ExcelUtil<CompanyVoiceRoboticCallLogSendmsg> util = new ExcelUtil<CompanyVoiceRoboticCallLogSendmsg>(CompanyVoiceRoboticCallLogSendmsg.class);
+        return util.exportExcel(list, "调用日志_发送短信数据");
+    }
 //
 //    /**
 //     * 获取调用日志_发送短信详细信息

+ 17 - 3
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -272,13 +272,27 @@ public class CompanyVoiceRoboticController extends BaseController
      * 查询任务执行记录
      * 获取每个人的工作流执行状态和节点日志
      */
+//    @GetMapping("/execRecords")
+//    public R getExecRecords(Long roboticId) {
+//        if (roboticId == null) {
+//            return R.error("任务ID不能为空");
+//        }
+//        List<WorkflowExecRecordVo> records = companyVoiceRoboticService.getExecRecords(roboticId);
+//        return R.ok().put("data", records);
+//    }
+
     @GetMapping("/execRecords")
-    public R getExecRecords(Long roboticId) {
+    public R getExecRecords(@RequestParam Long roboticId,
+                            @RequestParam(defaultValue = "1") Integer pageNum,
+                            @RequestParam(defaultValue = "10") Integer pageSize,
+                            @RequestParam(required = false) String customerName,
+                            @RequestParam(required = false) String customerPhone,
+                            @RequestParam Boolean onlyCallNode) {
         if (roboticId == null) {
             return R.error("任务ID不能为空");
         }
-        List<WorkflowExecRecordVo> records = companyVoiceRoboticService.getExecRecords(roboticId);
-        return R.ok().put("data", records);
+        return R.ok(companyVoiceRoboticService.getExecRecords(roboticId, pageNum, pageSize, customerName,
+                 customerPhone,onlyCallNode));
     }
 
     @GetMapping("/getCurrentCompanyId")

+ 31 - 0
fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java

@@ -0,0 +1,31 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.IGeneralCustomerEntryService;
+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;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:01
+ * @description
+ */
+@RestController
+@RequestMapping("/company/general/customer")
+public class GeneralCustomerEntryController {
+
+    @Autowired
+    IGeneralCustomerEntryService iGeneralCustomerEntryService;
+
+    @PostMapping("/entryCustomer")
+    public R entryCustomer(EntryCustomerParam param){
+        iGeneralCustomerEntryService.entryCustomer(param);
+       return R.ok("success");
+    }
+
+
+}

+ 5 - 3
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -242,10 +242,12 @@ public class CrmCustomerController extends BaseController
     ){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
-        customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
         Boolean isReceive=false;
-        if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
-            isReceive=true;
+        if (customer != null){
+            customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+            if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
+                isReceive=true;
+            }
         }
         return R.ok().put("customer",customer).put("isReceive",isReceive);
 

+ 170 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java

@@ -0,0 +1,170 @@
+package com.fs.company.controller.crm;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@Api(tags = "客户属性标签管理")
+@RestController
+@RequestMapping("/crm/customerProperty")
+public class CrmCustomerPropertyController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @ApiOperation("根据客户 ID 查询属性标签列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list/{customerId}")
+    public R listByCustomerId(@PathVariable("customerId") Long customerId) {
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(customerId);
+        return R.ok().put("data", list);
+    }
+
+    @ApiOperation("查询单个属性标签详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerProperty property = crmCustomerPropertyService.selectCrmCustomerPropertyById(id);
+        return R.ok().put("data", property);
+    }
+
+    @ApiOperation("为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setCreateBy(loginUser.getUsername());
+        return toAjax(crmCustomerPropertyService.insertCrmCustomerProperty(property));
+    }
+
+    @ApiOperation("为客户添加或更新属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdateCustomerPropertyWithExtra(
+                property.getCustomerId(),
+                property.getPropertyId(),
+                property.getPropertyName(),
+                property.getPropertyValue(),
+                property.getPropertyValueType(),
+                property.getTradeType(),
+                property.getIntention(),
+                property.getLikeRatio(),
+                loginUser.getUsername()
+        ));
+    }
+
+    @ApiOperation("批量为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAdd/{customerId}")
+    public AjaxResult batchAdd(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody List<CrmCustomerProperty> properties) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        for (CrmCustomerProperty property : properties) {
+            property.setCreateBy(loginUser.getUsername());
+        }
+        return toAjax(crmCustomerPropertyService.batchAddCustomerProperties(customerId, properties));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/addByTemplateId")
+    public AjaxResult addByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addPropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加或更新标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdateByTemplateId")
+    public AjaxResult addOrUpdateByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdatePropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("批量通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAddByTemplateIds/{customerId}")
+    public AjaxResult batchAddByTemplateIds(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody Map<Long, String> propertyMap) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.batchAddPropertiesByTemplateIds(customerId, propertyMap, loginUser.getUsername()));
+    }
+
+    @ApiOperation("修改客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setUpdateBy(loginUser.getUsername());
+        return toAjax(crmCustomerPropertyService.updateCrmCustomerProperty(property));
+    }
+
+    @ApiOperation("删除客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+    }
+
+    @ApiOperation("删除客户单个属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByPropertyId")
+    public AjaxResult deleteByPropertyId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
+        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+                .eq(CrmCustomerProperty::getCustomerId, customerId)
+                .eq(CrmCustomerProperty::getPropertyId, propertyId)
+                .remove());
+    }
+
+    @ApiOperation("导出客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户属性标签", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerProperty crmCustomerProperty) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyList(crmCustomerProperty);
+        ExcelUtil<CrmCustomerProperty> util = new ExcelUtil<CrmCustomerProperty>(CrmCustomerProperty.class);
+        return util.exportExcel(list, "客户属性标签数据");
+    }
+}

+ 78 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java

@@ -0,0 +1,78 @@
+package com.fs.company.controller.crm;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "客户属性标签模板管理")
+@RestController
+@RequestMapping("/crm/customerPropertyTemplate")
+public class CrmCustomerPropertyTemplateController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyTemplateService crmCustomerPropertyTemplateService;
+
+    @ApiOperation("查询客户属性标签模板列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        startPage();
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("查询客户属性标签模板详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerPropertyTemplate template = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateById(id);
+        return R.ok().put("data", template);
+    }
+
+    @ApiOperation("添加客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.insertCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("修改客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.updateCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("删除客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyTemplateService.deleteCrmCustomerPropertyTemplateByIds(ids));
+    }
+
+    @ApiOperation("导出客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        ExcelUtil<CrmCustomerPropertyTemplate> util = new ExcelUtil<CrmCustomerPropertyTemplate>(CrmCustomerPropertyTemplate.class);
+        return util.exportExcel(list, "客户属性标签模板数据");
+    }
+}

+ 5 - 5
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -265,11 +265,11 @@ public class FsStoreOrderScrmController extends BaseController
             order.setCompanyName(company.getCompanyName());
         }
         if (order.getOrderType() != null && order.getOrderType() == 3) {
-            FsCourseWatchLogParam param = new FsCourseWatchLogParam();
-            param.setVideoId(Long.valueOf(order.getVideoId()));
-            FsCourseWatchLog log = fsCourseWatchLogService.selectFsCourseWatchLogWithUCCV(order.getUserId(), order.getCompanyUserId(), order.getCourseId(), order.getVideoId());
-            if (log != null) {
-                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(log.getPeriodId());
+//            FsCourseWatchLogParam param = new FsCourseWatchLogParam();
+//            param.setVideoId(Long.valueOf(order.getVideoId()));
+//            FsCourseWatchLog log = fsCourseWatchLogService.selectFsCourseWatchLogWithUCCV(order.getUserId(), order.getCompanyUserId(), order.getCourseId(), order.getVideoId());
+            if (order.getPeriodId() != null) {
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(Long.valueOf(order.getPeriodId()));
                 order.setPeriodName(fsUserCoursePeriod.getPeriodName());
             }
             if (order.getVideoId() != null) {

+ 18 - 12
fs-ipad-task/src/main/java/com/fs/app/service/CustomThreadPoolConfig.java

@@ -1,34 +1,40 @@
 package com.fs.app.service;
 
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
+import java.util.concurrent.RejectedExecutionHandler;
 import java.util.concurrent.ThreadPoolExecutor;
 
-/**
- * @author MixLiu
- * @date 2025/7/11 上午11:04)
- */
+@Slf4j
 @Configuration
 public class CustomThreadPoolConfig {
+
     @Bean(name = "customThreadPool", destroyMethod = "shutdown")
     public ThreadPoolTaskExecutor customThreadPool() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
-        // 核心线程数
         executor.setCorePoolSize(300);
-        // 最大线程数
         executor.setMaxPoolSize(300);
-        // 线程名前缀
         executor.setThreadNamePrefix("custom-pool-");
-        // 拒绝策略:直接丢弃新任务
-        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
-        // 非核心线程空闲存活时间(秒)
+        executor.setQueueCapacity(1000);
+        executor.setRejectedExecutionHandler(new LoggingCallerRunsPolicy());
         executor.setKeepAliveSeconds(60);
-        // 等待所有任务完成后关闭线程池
         executor.setWaitForTasksToCompleteOnShutdown(true);
-        // 初始化
+        executor.setAwaitTerminationSeconds(300);
         executor.initialize();
         return executor;
     }
+
+    public static class LoggingCallerRunsPolicy implements RejectedExecutionHandler {
+        @Override
+        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+            log.warn("线程池任务被拒绝,将由调用线程执行。活跃线程: {}, 队列大小: {}, 最大线程: {}",
+                    executor.getActiveCount(), executor.getQueue().size(), executor.getMaximumPoolSize());
+            if (!executor.isShutdown()) {
+                r.run();
+            }
+        }
+    }
 }

+ 54 - 9
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -2,6 +2,7 @@ package com.fs.app.service;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.redis.RedisCache;
@@ -22,6 +23,8 @@ import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.utils.LinkUtil;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.ipad.IpadSendUtils;
 import com.fs.ipad.vo.*;
 import com.fs.live.domain.LiveWatchLog;
@@ -278,7 +281,7 @@ public class IpadSendServer {
         }
     }
 
-        public void sendTxtAtMsg(BaseVo vo) {
+    public void sendTxtAtMsg(BaseVo vo) {
         WxSendTextAtMsgTwoDTO dto = new WxSendTextAtMsgTwoDTO();
         List<WxSendTextAtMsgTwoDTO.Contentva> contentvaList = new ArrayList<>();
         WxSendTextAtMsgTwoDTO.Contentva contentva = new WxSendTextAtMsgTwoDTO.Contentva();
@@ -287,10 +290,10 @@ public class IpadSendServer {
         contentvaList.add(contentva);
         dto.setContentva(contentvaList);
         dto.setBase(vo);
-         dto.setUuid(vo.getUuid());
+        dto.setUuid(vo.getUuid());
         dto.setSend_userid(ipadSendUtils.userIds(vo));
         dto.setIsRoom(true);
-       ipadSendUtils.sendTxtAtMsgVo(dto, vo.getServerId());
+        ipadSendUtils.sendTxtAtMsgVo(dto, vo.getServerId());
     }
 
     public void sendVoice(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
@@ -543,12 +546,12 @@ public class IpadSendServer {
         Integer courseType = setting.getCourseType();
         String logId = qwSopLogs.getId();
         if(null != liveWatchLog){
-                    if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
-                        // 作废消息
-                        log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
-                        qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
-                        return false;
-                    }
+            if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
+                // 作废消息
+                log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
+                qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
+                return false;
+            }
         }
         else{
             log.warn("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
@@ -723,6 +726,12 @@ public class IpadSendServer {
                     content.setSendStatus(0);
                     content.setSendRemarks("短信待发送");
                     break;
+                    //跳转APP看课链接
+                case "23":
+                    //跳转APP直播链接
+                case "24":
+                    sendAppShortLink(vo, content, miniMap);
+                    break;
                 case "99":
                     // 群发
                     sendTxtAtMsg(vo);
@@ -739,6 +748,42 @@ public class IpadSendServer {
         }
     }
 
+    private void sendAppShortLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap) {
+//        //发送短链前,先发送一个介绍语
+//        TxtVo introduction = TxtVo.builder().content("请复制以下短链内容").build();
+//        introduction.setBase(vo);
+//        ipadSendUtils.sendTxt(introduction);
+        //发送短链内容
+        String miniProgramPage = content.getMiniprogramPage();
+        String sendShortLink = null;
+        //解析直播短链信息
+        String livePrefix = "/pages_live/livingList?link=";
+        if (miniProgramPage.startsWith(livePrefix)) {
+            JSONObject obj = JSONObject.parseObject(miniProgramPage.substring(livePrefix.length()));
+            sendShortLink = livePrefix + obj.getString("link");
+        }
+        //解析课程短链信息
+        String coursePrefix = "/pages/courseAnswer/index?link=";
+        if (miniProgramPage.startsWith(coursePrefix)) {
+            JSONObject obj = JSONObject.parseObject(miniProgramPage.substring(coursePrefix.length()));
+            sendShortLink = coursePrefix + obj.getString("link");
+        }
+        if (null == sendShortLink) {
+            log.warn("发送链接为空");
+            return;
+        }
+        sendShortLink = sendShortLink.replace(".html","");
+        String InvitationCode = LinkUtil.encryptLink(sendShortLink);
+        TxtVo txtVo = TxtVo.builder().content(InvitationCode).build();
+        txtVo.setBase(vo);
+        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> resp = ipadSendUtils.sendTxt(txtVo);
+        if (resp.getErrcode() != 0) {
+            log.debug("ID:{}-ipad接口请求返回异常:{}", vo.getId(), resp.getErrmsg());
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + resp.getErrmsg());
+        }
+    }
+
     /**
      * 发送直播短链
      * @param vo

+ 84 - 24
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -42,10 +42,8 @@ import org.springframework.util.StringUtils;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
 @Component
@@ -68,7 +66,30 @@ public class SendMsg {
     @Value("${group-no}")
     private String groupNo;
     private final List<QwUser> qwUserList = Collections.synchronizedList(new ArrayList<>());
-    private final Map<Long, Long> qwMap = new ConcurrentHashMap<>();
+    private final Map<Long, TaskContext> qwMap = new ConcurrentHashMap<>();
+    private static final long TASK_TIMEOUT_MS = 3 * 60 * 1000L;
+
+    private static class TaskContext {
+        final long startTime;
+        final AtomicBoolean cancelled;
+
+        TaskContext() {
+            this.startTime = System.currentTimeMillis();
+            this.cancelled = new AtomicBoolean(false);
+        }
+
+        boolean isTimeout() {
+            return System.currentTimeMillis() - startTime > TASK_TIMEOUT_MS;
+        }
+
+        void cancel() {
+            cancelled.set(true);
+        }
+
+        boolean isCancelled() {
+            return cancelled.get();
+        }
+    }
 
     @Autowired
     @Qualifier("customThreadPool")
@@ -140,23 +161,36 @@ public class SendMsg {
         Map<String, FsCoursePlaySourceConfig> miniMap = getMiniMap();
         // 获取 pad 发送的企微
         getQwUserList().forEach(e -> {
-            // 如果没有值就执行后面的方法 并且入值
-            qwMap.computeIfAbsent(e.getId(), k -> {
-                // 线程启动
-                CompletableFuture.runAsync(() -> {
-                    try {
-                        log.info("开始任务:{}", e.getQwUserName());
-                        // 开始任务
-                        processUser(e, delayStart, delayEnd, miniMap);
-                    } catch (Exception exception) {
-                        log.error("发送错误:", exception);
-                    } finally {
-                        log.info("删除任务:{}", e.getQwUserName());
-                        qwMap.remove(e.getId());
-//                        removeQwMap.putIfAbsent(e.getId(), System.currentTimeMillis());
-                    }
-                }, customThreadPool);
-                return System.currentTimeMillis(); // 占位值
+            TaskContext ctx = qwMap.get(e.getId());
+            if (ctx != null) {
+                if (ctx.isTimeout()) {
+                    log.warn("任务超时,标记取消:{}, 已运行: {}ms", e.getQwUserName(), System.currentTimeMillis() - ctx.startTime);
+                    ctx.cancel();
+                } else {
+                    log.debug("任务正在执行中,跳过:{}", e.getQwUserName());
+                    return;
+                }
+            }
+            if (customThreadPool.getActiveCount() >= customThreadPool.getMaxPoolSize()) {
+                log.warn("线程池已满,跳过任务:{}, 活跃线程: {}/{}", e.getQwUserName(), customThreadPool.getActiveCount(), customThreadPool.getMaxPoolSize());
+                return;
+            }
+            TaskContext newCtx = new TaskContext();
+            qwMap.put(e.getId(), newCtx);
+            CompletableFuture.runAsync(() -> {
+                try {
+                    log.info("开始任务:{}", e.getQwUserName());
+                    processUser(e, delayStart, delayEnd, miniMap, newCtx);
+                } catch (Exception exception) {
+                    log.error("发送错误:", exception);
+                } finally {
+                    log.info("删除任务:{}", e.getQwUserName());
+                    qwMap.remove(e.getId());
+                }
+            }, customThreadPool).exceptionally(ex -> {
+                log.error("任务提交失败:{}, 错误: {}", e.getQwUserName(), ex.getMessage());
+                qwMap.remove(e.getId());
+                return null;
             });
         });
     }
@@ -167,8 +201,9 @@ public class SendMsg {
      * @param delayStart 随机延迟 最小值
      * @param delayEnd   随机延迟 最大值
      * @param miniMap    小程序配置
+     * @param ctx        任务上下文(用于取消检查)
      */
-    private void processUser(QwUser qwUser, int delayStart, int delayEnd, Map<String, FsCoursePlaySourceConfig> miniMap) {
+    private void processUser(QwUser qwUser, int delayStart, int delayEnd, Map<String, FsCoursePlaySourceConfig> miniMap, TaskContext ctx) {
         long start1 = System.currentTimeMillis();
         // 获取当前企微待发送记录
         List<QwSopLogs> qwSopLogList = qwSopLogsMapper.selectByQwUserId(qwUser.getId());
@@ -181,15 +216,25 @@ public class SendMsg {
         BaseVo parentVo = new BaseVo();
         parentVo.setCorpCode(qwUser.getCorpId());
         long end1 = System.currentTimeMillis();
+        // 检查是否被取消
+        if (ctx.isCancelled()) {
+            log.info("任务被取消,退出:{}", qwUser.getQwUserName());
+            return;
+        }
         // 判断这个企微是否需要发送
         if (!sendServer.isSend(user, parentVo)) {
             log.info("当前这个企微不需要发送 数据{}",user);
             return;
         }
-        log.info("销售:{}, 消息:{}, 耗时: {}, 时间:{}", user.getQwUserName(), qwSopLogList.size(), end1 - start1, qwMap.get(qwUser.getId()));
+        log.info("销售:{}, 消息:{}, 耗时: {}, 时间:{}", user.getQwUserName(), qwSopLogList.size(), end1 - start1, ctx.startTime);
         long start3 = System.currentTimeMillis();
         // 循环代发送消息
         for (QwSopLogs qwSopLogs : qwSopLogList) {
+            // 检查是否被取消
+            if (ctx.isCancelled()) {
+                log.info("任务被取消,中断发送:{}, 已发送部分消息", qwUser.getQwUserName());
+                return;
+            }
             long start2 = System.currentTimeMillis();
             QwSopCourseFinishTempSetting setting = JSON.parseObject(qwSopLogs.getContentJson(), QwSopCourseFinishTempSetting.class);
             //直播的sendType:20单独走判断 其他的走以前的逻辑
@@ -227,6 +272,11 @@ public class SendMsg {
             Map<Integer, List<QwPushCount>> pushMap = pushCountList.stream().collect(Collectors.groupingBy(QwPushCount::getType));
             // 循环发送消息里面的每一条消息
             for (QwSopCourseFinishTempSetting.Setting content : setting.getSetting()) {
+                // 检查是否被取消
+                if (ctx.isCancelled()) {
+                    log.info("任务被取消,中断发送:{}", qwUser.getQwUserName());
+                    return;
+                }
                 long start4 = System.currentTimeMillis();
                 //判断当前销售推送客户消息限制
                 Long qwUserId = qwUser.getId();//销售的Id
@@ -278,11 +328,16 @@ public class SendMsg {
                         return;
                     }
                     try {
+                        if (ctx.isCancelled()) {
+                            return;
+                        }
                         int delay = ThreadLocalRandom.current().nextInt(300, 1000);
                         log.debug("pad发送消息等待:{}ms", delay);
                         Thread.sleep(delay);
                     } catch (InterruptedException e) {
                         log.error("线程等待错误!");
+                        Thread.currentThread().interrupt();
+                        return;
                     }
                 }
             }
@@ -387,11 +442,16 @@ public class SendMsg {
             int i = qwSopLogsService.updateQwSopLogsSendType(updateQwSop);
             log.info("销售:{}, 修改条数{}, 发送方消息完成:{}, 耗时: {}", user.getQwUserName(), i, qwSopLogs.getId(), end2 - start2);
             try {
+                if (ctx.isCancelled()) {
+                    return;
+                }
                 int delay = ThreadLocalRandom.current().nextInt(delayStart, delayEnd);
                 log.debug("企微发送消息等待:{}ms", delay);
                 Thread.sleep(delay);
             } catch (InterruptedException e) {
                 log.error("线程等待错误!");
+                Thread.currentThread().interrupt();
+                return;
             }
         }
         long end3 = System.currentTimeMillis();

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

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

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

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

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

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

+ 56 - 5
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -426,6 +426,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      Map<String, QwGroupChat> groupChatMap, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                      List<Company> companies) {
         try {
+            log.info("当前线程: {}", Thread.currentThread().getName());
             processSopGroup(sopId, userLogsVos, currentTime, groupChatMap, config, miniMap, companies);
         } catch (Exception e) {
             log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
@@ -436,7 +437,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
     private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos, LocalDateTime currentTime, Map<String,
-            QwGroupChat> groupChatMap, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                         QwGroupChat> groupChatMap, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                  List<Company> companies) throws Exception {
         QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
 
@@ -522,7 +523,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
             if (logVo.getFilterMode()==2 && config != null && config.getRoomLinkAllow() != null && config.getRoomLinkAllow()) {
-                 return;
+                return;
             }
 
             LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
@@ -1360,6 +1361,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         log.error("赋值-小程序封面地址失败-" + e);
                     }
                     break;
+                //录播
+                case "24":
+                    corpId = logVo.getCorpId();
+                    shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(shortH5Link);
+                    break;
                 default:
                     break;
             }
@@ -1370,7 +1382,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     }
 
     private String createRegisteredLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-                                                  String qwUserId,
+                                                 String qwUserId,
                                                  String companyUserId, String companyId, String externalId, Long fsUserId) {
         // 获取缓存的配置
         CourseConfig config;
@@ -1432,7 +1444,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
     public String createRegisteredGroupLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
-                                                      String qwUserId,
+                                                     String qwUserId,
                                                      Long companyUserId, String companyId, String chatId) {
         // 获取缓存的配置
         CourseConfig config;
@@ -1606,6 +1618,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         log.error("赋值-小程序封面地址失败-" + e);
                     }
                     break;
+                //跳转app直播
+                case "24":
+                    corpId = logVo.getCorpId();
+                    shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(shortH5Link);
+                    break;
                 default:
                     break;
             }
@@ -1625,7 +1648,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      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,
+                    Map<Integer, List<CompanyMiniapp>>> miniMap, Integer grade, Integer sendMsgType,
                                      List<Company> companies,Long serverId) {
         QwExternalContact contact = null;
         if (logVo.getExternalId() != null) {
@@ -1954,6 +1977,33 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                         enqueueQwSopSmsLogs(sopSmsLogs);
                     }
+                    break;
+                //跳转app看课
+                case "23":
+                    try {
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo,2);
+
+                        String shortH5link = createH5LinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
+                                qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId());
+
+                        setting.setMiniprogramTitle("邀请链接");
+                        setting.setMiniprogramPage(shortH5link);
+
+                    } catch (Exception e) {
+                        log.error("app看课模板解析失败:" + e);
+                    }
+                    break;
+                //app直播跳转
+                case "24":
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(shortH5Link);
+
                     break;
                 default:
                     break;
@@ -2746,6 +2796,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void enqueueQwSopLogs(QwSopLogs sopLogs) {
         try {
             boolean offered = qwSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
+            System.out.println(sopLogs.getSopId() + "插入队列结果: " + offered + "内容: " + JSON.toJSONString(sopLogs));
             if (!offered) {
                 log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
                 // 处理队列已满的情况,例如记录到失败队列或持久化存储

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

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

+ 30 - 0
fs-service/src/main/java/com/fs/company/config/AsyncConfig.java

@@ -0,0 +1,30 @@
+package com.fs.company.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+    @Bean(name = "calleeTaskExecutor")
+    public Executor calleeTaskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        int cpuCores = Runtime.getRuntime().availableProcessors();
+        executor.setCorePoolSize(cpuCores);
+        executor.setMaxPoolSize(20);
+        executor.setQueueCapacity(1000);
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setKeepAliveSeconds(60);
+        executor.setAllowCoreThreadTimeOut(false);
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(60);
+
+        executor.initialize();
+        return executor;
+    }
+}

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

@@ -94,4 +94,6 @@ public class CompanyAiWorkflowExec {
      */
     private Integer cidGroupNo;
 
+    //是否生成数据(0否,1是)
+    private Integer isGenerate;
 }

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

@@ -78,5 +78,7 @@ public class CompanyAiWorkflowExecLog {
     @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date createdTime;
 
+    //是否生成数据(0否,1是)
+    private Integer isGenerate;
 
 }

+ 2 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticBusiness.java

@@ -45,5 +45,6 @@ public class CompanyVoiceRoboticBusiness extends BaseEntity{
     @Excel(name = "发送短信动作完成,每次加1 初始0")
     private Integer sendMsgDone;
 
-
+    //是否生成数据(0否,1是)
+    private Integer isGenerate;
 }

+ 15 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java

@@ -49,7 +49,7 @@ public class CompanyVoiceRoboticCallLogSendmsg extends BaseEntity{
     private String result;
 
     /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
-    @Excel(name = "执行状态:1、执行中,2、执行成功,3、执行失败")
+    @Excel(name = "执行状态", readConverterExp = "1=执行中,2=执行成功,3=执行失败")
     private Integer status;
 
     /** 公司id */
@@ -80,6 +80,20 @@ public class CompanyVoiceRoboticCallLogSendmsg extends BaseEntity{
 
     private String callbackUuid;
 
+    private String name;
+
+    private Integer totalRecordCount;
+
+    private Integer successCount;
+
+    private Integer failCount;
+
+    private Integer runningCount;
+
+    private String phone;
+
+
+
     public static CompanyVoiceRoboticCallLogSendmsg initCallLog( String runParam, Long keyId, Long taskId,Long companyId,Long companyUserId,Long tempId) {
         CompanyVoiceRoboticCallLogSendmsg log = new CompanyVoiceRoboticCallLogSendmsg();
         log.callerId = keyId;

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java

@@ -64,4 +64,7 @@ public class CompanyVoiceRoboticCallees{
     private String idToString;
 
     private Integer isWeCom;
+
+    //是否生成数据(0否,1是)
+    private Integer isGenerate;
 }

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

@@ -66,4 +66,8 @@ public interface CompanyAiWorkflowExecLogMapper extends BaseMapper<CompanyAiWork
      * @return 执行日志列表
      */
     List<CompanyAiWorkflowExecLog> selectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
+
+    void batchInsert(@Param("list") List<CompanyAiWorkflowExecLog> logList);
+
+    List<CompanyAiWorkflowExecLog> selectByInstanceIds(List<String> instanceIds);
 }

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java

@@ -97,4 +97,18 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
     List<CompanyAiWorkflowExec> selectExecListWithTimeAvailableByStatusAndGroupNo(@Param("status") Integer status, @Param("groupNo") Integer groupNo);
 
     CompanyAiWorkflowExec selectExecWithTimeAvailableByInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
+
+    /**
+     * 批量新增数据
+     * @param list
+     * @return int
+     * **/
+    int insertBatchInfo(@Param("list") List<CompanyAiWorkflowExec> list);
+//    List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(@Param("roboticId") Long roboticId);
+    List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(
+            @Param("roboticId") Long roboticId,
+            @Param("customerName") String customerName,
+            @Param("customerPhone") String customerPhone,
+            @Param(("onlyCallNode")) Boolean onlyCallNode
+    );
 }

+ 6 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticBusinessMapper.java

@@ -87,5 +87,10 @@ public interface CompanyVoiceRoboticBusinessMapper extends BaseMapper<CompanyVoi
 
     Integer selectUnfinishedTaskCountByRoboticId(@Param("roboticId") Long roboticId, @Param("endNodeKey") String endNodeKey);
 
-
+    /**
+     * 批量插入生成业务数据
+     * @param businessList 业务数据列表
+     * @return 影响的行数
+     */
+    int insertBatchGenerateInfo(@Param("list") List<CompanyVoiceRoboticBusiness> businessList);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java

@@ -1,6 +1,7 @@
 package com.fs.company.mapper;
 
 import java.util.List;
+import java.util.Map;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
@@ -64,4 +65,9 @@ public interface CompanyVoiceRoboticCallLogAddwxMapper extends BaseMapper<Compan
     List<CompanyVoiceRoboticCallLogAddwxVO>  listByCustomerIdAndRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
     List<CompanyVoiceRoboticCallLogAddwxVO> listByCustomerId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    List<CompanyVoiceRoboticCallLogAddwxVO> listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
 }

+ 7 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -68,4 +68,11 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
     List<CompanyVoiceRoboticCallLogCallphone>  selectCompanyVoiceRoboticCallLogCallphoneListData(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
     CompanyVoiceRoboticCallLogCallphone selectCallLogByCallbackUuid(@Param("uuid") String uuid);
+
+    /**
+     * 根据业务ID查询当天的通话次数
+     * @param businessId 业务ID (bes.id)
+     * @return 当天通话次数,如果没有记录返回0
+     */
+    int countTodayCallsByBusinessId(@Param("businessId") Long businessId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 
 /**
@@ -28,6 +29,8 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
      */
     List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
+    List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
     /**
      * 新增调用日志_发送短信
      * 
@@ -61,4 +64,6 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
     int deleteCompanyVoiceRoboticCallLogSendmsgByLogIds(Long[] logIds);
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
 }

+ 7 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCalleesMapper.java

@@ -86,4 +86,11 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     List<CompanyVoiceRoboticCallees> selectExcludeList(@Param("list")List<CompanyWxClient> list,@Param("isWeCom") Integer isWeCom);
     List<Long> getNotFinishAddWxRobotic(@Param("roboticIds") Set<Long> roboticIds);
+
+    /**
+     * 批量插入生成数据
+     * @param list 插入数据
+     * @return int
+     * **/
+    int batchInsertGenerateInfo(@Param("list") List<CompanyVoiceRoboticCallees> list);
 }

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

@@ -72,4 +72,6 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
     int finishAddWxRobotic(@Param("collect") List<Long> collect);
 
     List<DictVO> getDictDataList(@Param("dictType") String dictType);
+
+    List<CompanyVoiceRobotic> selectSceneTaskByCompanyIdAndType(@Param("companyId") Long companyId, @Param("sceneType") Integer sceneType);
 }

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

@@ -68,4 +68,6 @@ public interface CompanyVoiceRoboticWxMapper extends BaseMapper<CompanyVoiceRobo
     List<CompanyVoiceRoboticWx> selectByRoboticIdWithGroupBy(@Param("id") Long id);
 
     List<CompanyVoiceRoboticWx> selectByRoboticIdQw(@Param("id") Long id, @Param("intention") String intention);
+
+    CompanyVoiceRoboticWx selectAllocationTargetByTaskId(@Param("roboticId") Long roboticId);
 }

+ 7 - 2
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowEdgeMapper.java

@@ -2,10 +2,9 @@ package com.fs.company.mapper;
 
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyWorkflowEdge;
+import com.fs.company.vo.CompanyNodeInfoVo;
 import org.apache.ibatis.annotations.Param;
-import org.mapstruct.Mapper;
 
 import java.util.List;
 
@@ -32,4 +31,10 @@ public interface CompanyWorkflowEdgeMapper  extends BaseMapper<CompanyWorkflowEd
     Integer deleteCompanyWorkflowEdgeByWorkflowId(Long workflowId);
 
     List<CompanyWorkflowEdge> selectListByWorkflowIdAndNodeKey(@Param("workflowId")Long workflowId, @Param("nodeKey")String nodeKey);
+
+    /**
+     *获取节点信息
+     *
+     * */
+    CompanyNodeInfoVo slectNodeInfoByWorkflowId(@Param("workflowId")Long workflowId, @Param("nodeKey")String nodeKey);
 }

+ 166 - 0
fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java

@@ -0,0 +1,166 @@
+package com.fs.company.param;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:42
+ * @description
+ */
+@Data
+public class EntryCustomerParam {
+
+    /**主键*/
+    private Long customerId;
+
+    /** 组织机构代码 */
+    private String customerCode;
+
+    /** 客户名称 */
+    private String customerName;
+
+    /**
+     * 客户的企微名称
+     */
+    private String qwName;
+
+    /** 手机 */
+    //必传
+    private String mobile;
+
+    /** 流量来源 */
+    private String trafficSource;
+
+    /** 性别 */
+    private Integer sex;
+
+    /** $column.columnComment */
+    private String weixin;
+
+    /** 关联用户ID */
+    private Long userId;
+
+    /** 创建人ID */
+    private Long createUserId;
+
+    /** 当前认领用户 */
+    private Long receiveUserId;
+
+    /** 认领ID */
+    private Long customerUserId;
+
+    /** 省市区 */
+    private String address;
+
+    private String cityIds;
+
+    /** 定位信息 */
+    private String location;
+
+    /** 详细地址 */
+    private String detailAddress;
+
+    /** 地理位置经度 */
+    private String lng;
+
+    /** 地理位置维度 */
+    private String lat;
+
+    /** 客户状态  0锁定 1 正常 */
+    private Integer status;
+
+    /** 1 已认领 0未认领 */
+    private Integer isReceive;
+
+    /** 0 手动导入 1 自动导入 */
+    private Integer importType;
+
+
+
+    /** 所属部门ID */
+    private Long deptId;
+
+    /** 是否删除 */
+    private Integer isDel;
+
+    /** 客户类型 */
+    private Integer customerType;
+
+    /** 最后一次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date receiveTime;
+
+    /** 入公海时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date poolTime;
+
+    /** $column.columnComment */
+    //必传
+    private Long companyId;
+
+    /** 是否为线索客户 */
+    private Integer isLine;
+
+    /** 客户来源 */
+    private String source;
+
+    /** 标签 */
+    private String tags;
+
+    private String extJson;
+    //跟进阶段
+    private Integer visitStatus;
+    //进线日期
+    private String registerDate;
+    //进线客户连接
+    private String registerLinkUrl;
+    //进线客户详情
+    private String registerDesc;
+    //进线客户填写时间
+    private String registerSubmitTime;
+
+    /** 流量平台线索归属账号 */
+    private String thirdAccount;
+
+    /** 流量平台线索Id */
+    private String clueId;
+    //是否在公海
+    private Integer isPool;
+    //进线方式
+    private String registerType;
+    //消费金额
+    private BigDecimal payMoney;
+    //购买次数
+    private Integer buyCount;
+    //来源渠道编码
+    private String sourceCode;
+    //推线时间
+    private String pushTime;
+    //推线编码
+    private String pushCode;
+    private String intention;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date visitTime;
+    // 购买商品
+    private String goodsName;
+    // 购买规格
+    private String goodsSpecification;
+    // 店铺名称
+    private String shopName;
+    // 平台名称
+    private String platformName;
+
+    //场景类型,字典task_scene_type
+    private Integer sceneType;
+    //对话图
+    private String dialogue;
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java

@@ -1,6 +1,7 @@
 package com.fs.company.service;
 
 import java.util.List;
+import java.util.Map;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.domain.CompanyWxClient;
@@ -69,4 +70,9 @@ public interface ICompanyVoiceRoboticCallLogAddwxService extends IService<Compan
     List<CompanyVoiceRoboticCallLogAddwxVO> listByCustomerIdAndRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
     List<CompanyVoiceRoboticCallLogAddwxVO> listByCustomerId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    List<CompanyVoiceRoboticCallLogAddwxVO> listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
 }

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

@@ -3,6 +3,7 @@ package com.fs.company.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 
 /**
@@ -28,6 +29,8 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
      */
     List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
+    List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
     /**
      * 新增调用日志_发送短信
      * 
@@ -61,4 +64,6 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
     int deleteCompanyVoiceRoboticCallLogSendmsgByLogId(Long logId);
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
 }

+ 9 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -8,6 +8,7 @@ import com.fs.company.param.ExecutionContext;
 import com.fs.company.vo.*;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -91,7 +92,14 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      * @param roboticId 任务ID
      * @return 执行记录列表
      */
-    List<WorkflowExecRecordVo> getExecRecords(Long roboticId);
+    Map<String, Object> getExecRecords(Long roboticId, Integer pageNum, Integer pageSize,String customerName,String customerPhone, Boolean onlyCallNode);
 
     void finishAddWxByCallees(Set<Long> roboticIds);
+
+    /**
+     *
+     * @param taskId
+     * @param crmCustomerId
+     */
+    void addNewExec4Task(Long taskId,Long crmCustomerId);
 }

+ 16 - 0
fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java

@@ -0,0 +1,16 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.param.EntryCustomerParam;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:49
+ * @description
+ */
+public interface IGeneralCustomerEntryService {
+
+//    R entryCustomer(String param);
+
+    String entryCustomer(EntryCustomerParam param);
+}

+ 15 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.company.service.impl;
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.domain.CompanyVoiceRoboticCallLog;
@@ -25,7 +26,9 @@ import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
  */
 @Service
 @Slf4j
-public class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
+public
+
+class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
 
 
     @Autowired
@@ -148,4 +151,15 @@ public class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<Comp
     public  List<CompanyWxClient> getWxClientInfoByCustomerId(Long customerId){
         return companyWxClientMapper.getWxClientInfoByCustomerId(customerId);
     }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogAddwxVO> listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx){
+        return baseMapper.listAll(companyVoiceRoboticCallLogAddwx);
+    }
+
+    @Override
+    public Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx){
+        return baseMapper.countListAll(companyVoiceRoboticCallLogAddwx);
+    }
+
 }

+ 11 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java

@@ -6,6 +6,7 @@ import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -39,6 +40,11 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
         return baseMapper.selectCompanyVoiceRoboticCallLogSendmsgByLogId(logId);
     }
 
+    @Override
+    public List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg) {
+        return baseMapper.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
+    }
+
     /**
      * 查询调用日志_发送短信列表
      * 
@@ -119,4 +125,9 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
     public List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg){
         return baseMapper.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
     }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount() {
+        return baseMapper.selectCompanyVoiceRoboticCallLogSendMsgCount();
+    }
 }

File diff suppressed because it is too large
+ 363 - 161
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java


+ 4 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -540,4 +540,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
         }
     }
 
+    public void workFlowAddExec(){
+
+    }
+
 }

+ 197 - 0
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -0,0 +1,197 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceRobotic;
+import com.fs.company.mapper.CompanyVoiceRoboticMapper;
+import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.service.IGeneralCustomerEntryService;
+import com.fs.company.util.CryptoUtil;
+import com.fs.company.util.PhoneNumberUtil;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:49
+ * @description
+ */
+@Service
+@Slf4j
+public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntryService {
+
+    @Autowired
+    @Qualifier("crmCustomerExecutor")
+    Executor customerExecutor;
+    @Autowired
+    CrmCustomerMapper crmCustomerMapper;
+    @Autowired
+    CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+    @Autowired
+    ICompanyVoiceRoboticService companyVoiceRoboticService;
+
+    /**
+     * 录入客户
+     *
+     * @param param
+     * @return
+     */
+    //@Override
+    public R entryCustomer(String param) {
+        try {
+            String decryptParam = CryptoUtil.decrypt(param);
+            if (StringUtils.isBlank(decryptParam)) {
+                return R.error("参数错误");
+            }
+            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
+            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
+        } catch (Exception ex) {
+            log.error("录入客户异常", ex);
+        }
+        return R.ok().put("result", "录入成功");
+    }
+
+    @Override
+    @Async("crmCustomerExecutor")
+    public String entryCustomer(EntryCustomerParam param) {
+        handleData(param);
+        return "success";
+    }
+
+    /**
+     * 处理外部来源客户数据
+     *
+     * @param list
+     */
+    public void handleList(List<EntryCustomerParam> list) {
+        list.forEach(a -> {
+            handleData(a);
+        });
+    }
+
+    /**
+     * 处理单条数据
+     * @param data
+     */
+    public void handleData(EntryCustomerParam data) {
+        //客户数据校验
+        Boolean b = validateCustomerData(data);
+        if (!b) {
+            log.error("客户数据校验失败,{}", data);
+            return;
+        }
+        //客户数据解析,是否包含对话图 对话图解析标签&意向度标识 todo 庄旭组在研发此功能
+        if (StringUtils.isNotBlank(data.getDialogue())) {
+            JSONObject jsonObject = analysisDialogue(data.getDialogue());
+            if (jsonObject != null) {
+                data.setIntention(jsonObject.getString("intention"));
+                data.setTags(jsonObject.getString("tags"));
+            }
+        }
+        //客户数据插入
+        insertCrmCustomer(data);
+        if(null != data.getCompanyId() && null != data.getSceneType()){
+            //公司场景任务读取
+            CompanyVoiceRobotic companySceneTasks = getCompanySceneTask(data);
+            if(null != companySceneTasks){
+                //场景任务存在 加入场景任务队列
+                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId());
+            }
+        }
+    }
+
+    /**
+     * 校验客户数据
+     * @param param
+     * @return
+     */
+    public Boolean validateCustomerData(EntryCustomerParam param) {
+        boolean valid = PhoneNumberUtil.isValid(param.getMobile());
+        if(!valid){
+            log.error("手机号格式错误,{}", param.getMobile());
+            return false;
+        }
+        Long  customerId = crmCustomerMapper.selectCrmCustomerByCrmMobile(param.getMobile());
+        // todo 添加配置是否允许重复客户导入
+        if( null != customerId && true){
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 解析对话图 得到意向度以及标签
+     *
+     * @param dialogue
+     * @return
+     */
+    public JSONObject analysisDialogue(String dialogue) {
+        return null;
+    }
+
+    /**
+     * 插入crmcustomer
+     * @param data
+     * @return
+     */
+    public void insertCrmCustomer(EntryCustomerParam data){
+        CrmCustomer insertObj = new CrmCustomer();
+        BeanUtils.copyProperties(data,insertObj);
+        crmCustomerMapper.insertCrmCustomer(insertObj);
+        data.setCustomerId(insertObj.getCustomerId());
+    }
+
+    /**
+     * 获取公司场景任务
+     * @param data
+     * @return
+     */
+    public CompanyVoiceRobotic getCompanySceneTask(EntryCustomerParam data){
+        List<CompanyVoiceRobotic> companyVoiceRobotics = companyVoiceRoboticMapper.selectSceneTaskByCompanyIdAndType(data.getCompanyId(), data.getSceneType());
+        if(null != companyVoiceRobotics && !companyVoiceRobotics.isEmpty()){
+            List<CompanyVoiceRobotic> resList = companyVoiceRobotics.stream()
+                    .filter(item -> verificationTime(item.getAvailableStartTime(), item.getAvailableEndTime()))
+                    .collect(Collectors.toList());
+            if(null !=  resList && !resList.isEmpty()){
+                return resList.get(0);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 校验当前时间是否在指定时间区间内
+     * 支持跨天凌晨区间(如22:00-06:00)
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     * @return true-在区间内,false-不在区间内
+     */
+    public Boolean verificationTime(LocalTime start, LocalTime end) {
+        if (start == null || end == null) {
+            return false;
+        }
+        LocalTime now = LocalTime.now();
+        if (!start.isAfter(end)) {
+            return !now.isBefore(start) && !now.isAfter(end);
+        }
+        return !now.isBefore(start) || !now.isAfter(end);
+    }
+
+}

+ 31 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -5,11 +5,11 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.ICompanyVoiceRoboticService;
-import com.fs.company.service.IWorkflowNode;
 import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.service.impl.CompanyVoiceRoboticCallLogCallphoneServiceImpl;
 import com.fs.company.vo.AiCallConfigVO;
@@ -21,6 +21,8 @@ import com.fs.company.vo.easycall.EasyCallPhoneItemVO;
 import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.his.config.CidPhoneConfig;
+import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.*;
@@ -46,6 +48,10 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private final String CALL_FROM_CALLBACK = "callBack";
     private final String CALL_FROM_TIMER = "timer";
 
+    private final ISysConfigService configService = SpringUtils.getBean(ISysConfigService.class);
+
+    private final CompanyVoiceRoboticCallLogCallphoneMapper companyVoiceRoboticCallLogCallphoneMapper = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneMapper.class);;
+
     public AiCallTaskNode(String nodeKey, String nodeName, Map<String, Object> properties) {
         super(nodeKey, nodeName, properties);
     }
@@ -166,11 +172,19 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                     super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
                     return ExecutionResult.failure().errorMessage("节点配置解析失败").build();
                 }
+
                 //执行外呼逻辑 需要传入节点信息
                 CompanyVoiceRoboticBusiness bus = super.getRoboticBusiness(context.getWorkflowInstanceId());
                 if (bus == null) {
                     return ExecutionResult.failure().errorMessage("未找到业务数据").build();
                 }
+
+                //进入手机号拨打次数校验
+                if(checkPhoneCallLimit(bus.getId())){
+                    super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
+                    return ExecutionResult.failure().errorMessage("今日拨打次数已达上限!").build();
+                }
+
                 //手动外呼配置 1、人工 2、ai外呼
                 if(Integer.valueOf(1).equals(callConfigVo.getCallMode())){
                     super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.WAITING_DO_CALL);
@@ -316,6 +330,22 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         context.setVariable("easyCallBatchId", batchId);
     }
 
+    private boolean checkPhoneCallLimit(Long businessId){
+        String json = configService.selectConfigByKey("cid.config");
+        if(StringUtils.isNotEmpty(json)){//数据存在
+            //转换数据
+            CidPhoneConfig config =JSONObject.parseObject(json,CidPhoneConfig.class);
+            if(config.getEnablePhoneLimitConfig() != null && config.getEnablePhoneLimitConfig()){//开启了拨打限制按钮
+                //获取当前外呼业务电话拨打信息
+               int num = companyVoiceRoboticCallLogCallphoneMapper.countTodayCallsByBusinessId(businessId);
+               if(num >= config.getNumberCalls()){
+                   return true;
+               }
+            }
+        }
+        return false;
+    }
+
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {
 //        super.postExecute(context, result);

+ 323 - 0
fs-service/src/main/java/com/fs/company/util/CryptoUtil.java

@@ -0,0 +1,323 @@
+package com.fs.company.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * 加密解密工具类
+ * 用于接口入参解析、返回结果加解密处理
+ *
+ */
+public class CryptoUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(CryptoUtil.class);
+
+    public static void main(String[] args) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("name", "MixLiu");
+        jsonObject.put("age", 18);
+        jsonObject.put("sex", "男");
+        jsonObject.put("occupation","engineer");
+        String encrypt = encrypt(jsonObject.toJSONString());
+        System.out.println(encrypt);
+        String decrypt = decrypt(encrypt);
+        System.out.println(decrypt);
+        System.out.println(JSONObject.parse(decrypt));
+    }
+    // ==================== 秘钥配置 ====================
+    /**
+     * AES加密秘钥
+     * 【重要】请根据实际业务需求修改此秘钥
+     * 秘钥长度必须为16位(AES-128)、24位(AES-192)或32位(AES-256)
+     * 当前使用AES-128,秘钥长度为16位
+     */
+    private static final String AES_SECRET_KEY = "FsCmp@nyK3y!2026";
+
+    /**
+     * 备用秘钥(用于秘钥轮换场景)
+     * 【重要】生产环境请修改为不同的秘钥
+     */
+    private static final String AES_BACKUP_KEY = "BkFC0mp@nyK3y!26";
+
+    /**
+     * AES加密算法/模式/填充方式
+     * AES/ECB/PKCS5Padding:AES加密,ECB模式,PKCS5填充
+     */
+    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
+
+    /**
+     * AES算法名称
+     */
+    private static final String AES_KEY_ALGORITHM = "AES";
+
+    // ==================== 加密方法 ====================
+
+    /**
+     * AES加密
+     * 用于加密入参或返回结果
+     *
+     * @param plainText 明文
+     * @return Base64编码的密文,加密失败返回null
+     */
+    public static String encrypt(String plainText) {
+        return encrypt(plainText, AES_SECRET_KEY);
+    }
+
+    /**
+     * AES加密(使用指定秘钥)
+     *
+     * @param plainText 明文
+     * @param secretKey 秘钥
+     * @return Base64编码的密文,加密失败返回null
+     */
+    public static String encrypt(String plainText, String secretKey) {
+        if (plainText == null || secretKey == null) {
+            log.warn("加密参数不能为空");
+            return null;
+        }
+        try {
+            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_KEY_ALGORITHM);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
+            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(encryptedBytes);
+        } catch (Exception e) {
+            log.error("AES加密失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 加密入参
+     * 将原始入参字符串加密后返回
+     *
+     * @param inputParam 原始入参
+     * @return 加密后的入参字符串
+     */
+    public static String encryptInput(String inputParam) {
+        log.debug("加密入参");
+        return encrypt(inputParam);
+    }
+
+    /**
+     * 加密返回结果
+     * 将返回结果对象转换为JSON字符串后加密
+     *
+     * @param result 返回结果对象
+     * @return 加密后的结果字符串
+     */
+    public static String encryptResult(Object result) {
+        if (result == null) {
+            return null;
+        }
+        String jsonStr;
+        if (result instanceof String) {
+            jsonStr = (String) result;
+        } else {
+            jsonStr = JSON.toJSONString(result);
+        }
+        log.debug("加密返回结果");
+        return encrypt(jsonStr);
+    }
+
+    // ==================== 解密方法 ====================
+
+    /**
+     * AES解密
+     * 用于解密入参或返回结果
+     *
+     * @param encryptedText Base64编码的密文
+     * @return 解密后的明文,解密失败返回null
+     */
+    public static String decrypt(String encryptedText) {
+        return decrypt(encryptedText, AES_SECRET_KEY);
+    }
+
+    /**
+     * AES解密(使用指定秘钥)
+     *
+     * @param encryptedText Base64编码的密文
+     * @param secretKey     秘钥
+     * @return 解密后的明文,解密失败返回null
+     */
+    public static String decrypt(String encryptedText, String secretKey) {
+        if (encryptedText == null || secretKey == null) {
+            log.warn("解密参数不能为空");
+            return null;
+        }
+        try {
+            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_KEY_ALGORITHM);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, keySpec);
+            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
+            return new String(decryptedBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            log.error("AES解密失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解密入参
+     * 将加密的入参字符串解密
+     *
+     * @param encryptedInput 加密的入参
+     * @return 解密后的入参字符串
+     */
+    public static String decryptInput(String encryptedInput) {
+        log.debug("解密入参");
+        return decrypt(encryptedInput);
+    }
+
+    /**
+     * 解密返回结果
+     * 将加密的返回结果解密
+     *
+     * @param encryptedResult 加密的返回结果
+     * @return 解密后的结果字符串
+     */
+    public static String decryptResult(String encryptedResult) {
+        log.debug("解密返回结果");
+        return decrypt(encryptedResult);
+    }
+
+    // ==================== 解析方法 ====================
+
+    /**
+     * 解析入参(解密后解析为JSON对象)
+     * 先解密入参,再将解密后的字符串解析为JSONObject
+     *
+     * @param encryptedInput 加密的入参
+     * @return 解析后的JSONObject,解析失败返回null
+     */
+    public static JSONObject parseInput(String encryptedInput) {
+        String decryptedStr = decryptInput(encryptedInput);
+        if (decryptedStr == null) {
+            log.warn("入参解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr);
+        } catch (Exception e) {
+            log.error("入参JSON解析失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析入参(解密后解析为指定类型对象)
+     * 先解密入参,再将解密后的字符串解析为指定类型的对象
+     *
+     * @param encryptedInput 加密的入参
+     * @param clazz          目标类型
+     * @param <T>            泛型类型
+     * @return 解析后的对象,解析失败返回null
+     */
+    public static <T> T parseInput(String encryptedInput, Class<T> clazz) {
+        String decryptedStr = decryptInput(encryptedInput);
+        if (decryptedStr == null) {
+            log.warn("入参解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr, clazz);
+        } catch (Exception e) {
+            log.error("入参解析为{}失败: {}", clazz.getSimpleName(), e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析返回结果(解密后解析为JSON对象)
+     * 先解密返回结果,再将解密后的字符串解析为JSONObject
+     *
+     * @param encryptedResult 加密的返回结果
+     * @return 解析后的JSONObject,解析失败返回null
+     */
+    public static JSONObject parseResult(String encryptedResult) {
+        String decryptedStr = decryptResult(encryptedResult);
+        if (decryptedStr == null) {
+            log.warn("返回结果解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr);
+        } catch (Exception e) {
+            log.error("返回结果JSON解析失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析返回结果(解密后解析为指定类型对象)
+     * 先解密返回结果,再将解密后的字符串解析为指定类型的对象
+     *
+     * @param encryptedResult 加密的返回结果
+     * @param clazz           目标类型
+     * @param <T>             泛型类型
+     * @return 解析后的对象,解析失败返回null
+     */
+    public static <T> T parseResult(String encryptedResult, Class<T> clazz) {
+        String decryptedStr = decryptResult(encryptedResult);
+        if (decryptedStr == null) {
+            log.warn("返回结果解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr, clazz);
+        } catch (Exception e) {
+            log.error("返回结果解析为{}失败: {}", clazz.getSimpleName(), e.getMessage(), e);
+            return null;
+        }
+    }
+
+    // ==================== 秘钥轮换相关 ====================
+
+    /**
+     * 使用备用秘钥解密(用于秘钥轮换场景)
+     * 先尝试主秘钥解密,失败后尝试备用秘钥
+     *
+     * @param encryptedText 加密文本
+     * @return 解密后的明文
+     */
+    public static String decryptWithFallback(String encryptedText) {
+        String result = decrypt(encryptedText, AES_SECRET_KEY);
+        if (result == null) {
+            log.info("主秘钥解密失败,尝试使用备用秘钥");
+            result = decrypt(encryptedText, AES_BACKUP_KEY);
+        }
+        return result;
+    }
+
+    // ==================== 工具方法 ====================
+
+    /**
+     * 验证秘钥长度是否有效
+     *
+     * @param key 秘钥
+     * @return 是否有效
+     */
+    public static boolean isValidKeyLength(String key) {
+        if (key == null) {
+            return false;
+        }
+        int len = key.getBytes(StandardCharsets.UTF_8).length;
+        return len == 16 || len == 24 || len == 32;
+    }
+
+    /**
+     * 获取当前使用的秘钥长度
+     *
+     * @return 秘钥长度
+     */
+    public static int getKeyLength() {
+        return AES_SECRET_KEY.getBytes(StandardCharsets.UTF_8).length;
+    }
+}

+ 163 - 0
fs-service/src/main/java/com/fs/company/util/PhoneNumberUtil.java

@@ -0,0 +1,163 @@
+package com.fs.company.util;
+
+import com.fs.common.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 手机号工具类
+ * 提供手机号格式校验、脱敏等功能
+ *
+ * @author MixLiu
+ * @date 2026/3/17 18:57
+ */
+public class PhoneNumberUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(PhoneNumberUtil.class);
+
+    // ==================== 常量定义 ====================
+
+    /**
+     * 中国大陆手机号正则表达式
+     * 规则说明:
+     * 1. 以1开头
+     * 2. 第二位为3-9之间的数字(目前运营商号段:13x、14x、15x、16x、17x、18x、19x)
+     * 3. 后面跟着9位数字
+     * 4. 总长度为11位
+     */
+    private static final String CHINA_MOBILE_REGEX = "^1[3-9]\\d{9}$";
+
+    /**
+     * 手机号正则编译对象(预编译提高性能)
+     */
+    private static final Pattern MOBILE_PATTERN = Pattern.compile(CHINA_MOBILE_REGEX);
+
+    /**
+     * 手机号长度
+     */
+    private static final int MOBILE_LENGTH = 11;
+
+    // ==================== 校验方法 ====================
+
+    /**
+     * 校验手机号是否合法
+     * <p>
+     * 校验规则:
+     * 1. 手机号不能为空
+     * 2. 手机号长度必须为11位
+     * 3. 手机号必须符合中国大陆手机号格式(以1开头,第二位为3-9,后跟9位数字)
+     * </p>
+     *
+     * @param mobile 待校验的手机号
+     * @return true-手机号合法;false-手机号不合法
+     */
+    public static boolean isValid(String mobile) {
+        // 空值校验
+        if (StringUtils.isBlank(mobile)) {
+            log.warn("手机号校验失败:手机号为空");
+            return false;
+        }
+        // 去除首尾空格
+        String trimmedMobile = mobile.trim();
+        // 长度校验(手机号必须为11位)
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            log.warn("手机号校验失败:手机号长度不正确,当前长度={}", trimmedMobile.length());
+            return false;
+        }
+        // 格式校验(使用正则表达式)
+        boolean isValid = MOBILE_PATTERN.matcher(trimmedMobile).matches();
+        if (!isValid) {
+            log.warn("手机号校验失败:手机号格式不正确,mobile={}", trimmedMobile);
+        }
+        return isValid;
+    }
+
+    /**
+     * 校验手机号是否合法(带详细返回信息)
+     * <p>
+     * 校验规则同 {@link #isValid(String)},但返回详细的校验结果信息
+     * </p>
+     *
+     * @param mobile 待校验的手机号
+     * @return 校验结果对象,包含是否合法及错误信息
+     */
+    public static ValidateResult validate(String mobile) {
+        // 空值校验
+        if (StringUtils.isBlank(mobile)) {
+            return new ValidateResult(false, "手机号不能为空");
+        }
+        // 去除首尾空格
+        String trimmedMobile = mobile.trim();
+        // 长度校验
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            return new ValidateResult(false, "手机号长度必须为11位");
+        }
+        // 格式校验
+        if (!MOBILE_PATTERN.matcher(trimmedMobile).matches()) {
+            return new ValidateResult(false, "手机号格式不正确,请输入有效的中国大陆手机号");
+        }
+        return new ValidateResult(true, "手机号校验通过");
+    }
+
+    // ==================== 脱敏方法 ====================
+
+    /**
+     * 手机号脱敏处理
+     * <p>
+     * 将手机号中间4位替换为*号,例如:13812345678 -> 138****5678
+     * </p>
+     *
+     * @param mobile 原始手机号
+     * @return 脱敏后的手机号,如果手机号为空或格式不正确则返回原始值
+     */
+    public static String mask(String mobile) {
+        if (StringUtils.isBlank(mobile)) {
+            return mobile;
+        }
+        String trimmedMobile = mobile.trim();
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            return mobile;
+        }
+        return trimmedMobile.substring(0, 3) + "****" + trimmedMobile.substring(7);
+    }
+
+    // ==================== 内部类 ====================
+
+    /**
+     * 校验结果类
+     * 用于返回详细的校验结果信息
+     */
+    public static class ValidateResult {
+        /**
+         * 是否校验通过
+         */
+        private boolean valid;
+        /**
+         * 校验结果消息
+         */
+        private String message;
+
+        public ValidateResult(boolean valid, String message) {
+            this.valid = valid;
+            this.message = message;
+        }
+
+        public boolean isValid() {
+            return valid;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        @Override
+        public String toString() {
+            return "ValidateResult{" +
+                    "valid=" + valid +
+                    ", message='" + message + '\'' +
+                    '}';
+        }
+    }
+}

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

@@ -0,0 +1,81 @@
+package com.fs.company.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+//随机生成
+public class RandomNameGeneratorUtil {
+    // 常见姓氏(单姓 + 少量复姓)
+    private static final String[] SURNAMES = {
+            "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈",
+            "褚", "卫", "蒋", "沈", "韩", "杨", "朱", "秦", "尤", "许",
+            "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏",
+            "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章",
+            "云", "苏", "潘", "葛", "奚", "范", "彭", "郎", "鲁", "韦",
+            "昌", "马", "苗", "凤", "花", "方", "俞", "任", "袁", "柳",
+            "鲍", "史", "唐", "费", "廉", "岑", "薛", "雷", "贺", "倪",
+            "汤", "殷", "罗", "毕", "郝", "邬", "安", "常", "乐", "于",
+            "时", "傅", "皮", "卞", "齐", "康", "伍", "余", "元", "卜",
+            "顾", "孟", "平", "黄", "和", "穆", "萧", "尹", "姚", "邵",
+            "湛", "汪", "祁", "毛", "禹", "狄", "米", "贝", "明", "臧",
+            "欧阳", "慕容", "上官", "司马", "夏侯", "诸葛", "东方", "皇甫", "尉迟", "公孙"
+    };
+
+    // 常用名字用字
+    private static final String[] GIVEN_NAMES = {
+            "伟", "强", "军", "勇", "杰", "涛", "斌", "鹏", "宇", "浩",
+            "鑫", "磊", "帅", "超", "俊", "帆", "波", "辉", "刚", "健",
+            "明", "亮", "峰", "松", "林", "森", "荣", "华", "富", "贵",
+            "芳", "娜", "敏", "静", "秀", "娟", "英", "华", "慧", "巧",
+            "美", "颖", "玲", "燕", "红", "丽", "艳", "倩", "婷", "娇",
+            "淑", "贞", "珠", "琴", "雪", "云", "霞", "露", "雯", "姗",
+            "欣", "怡", "晨", "曦", "阳", "光", "天", "然", "乐", "悦",
+            "思", "念", "文", "武", "双", "全", "子", "涵", "泽", "洋",
+            "博", "睿", "智", "远", "达", "通", "道", "德", "仁", "义"
+    };
+
+    // 名字长度范围
+    private static final int MIN_NAME_LENGTH = 2;
+    private static final int MAX_NAME_LENGTH = 3;
+
+    /**
+     * 生成随机姓名
+     * @return 随机姓名
+     */
+    public static String generateOne() {
+        ThreadLocalRandom rand = ThreadLocalRandom.current();
+        // 随机选择姓氏
+        String surname = SURNAMES[rand.nextInt(SURNAMES.length)];
+
+        // 随机决定名字长度
+        int nameLength = rand.nextInt(MIN_NAME_LENGTH, MAX_NAME_LENGTH + 1);
+
+        StringBuilder sb = new StringBuilder(surname);
+        for (int i = 1; i < nameLength; i++) {
+            sb.append(GIVEN_NAMES[rand.nextInt(GIVEN_NAMES.length)]);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 批量生成随机姓名
+     * @param count 生成数量
+     * @return 姓名列表
+     */
+    public static List<String> generateBatch(int count) {
+        List<String> result = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            result.add(generateOne());
+        }
+        return result;
+    }
+
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        List<String> names = generateBatch(100_000);
+        long end = System.currentTimeMillis();
+        System.out.println("生成10万个姓名耗时:" + (end - start) + " ms");
+        names.stream().limit(10).forEach(System.out::println);
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyNodeInfoVo.java

@@ -0,0 +1,12 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class CompanyNodeInfoVo implements Serializable {
+    private String targetNodeKey;
+    private String nodeName;
+    private String nodeType;
+}

+ 59 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogAddWxExportVO.java

@@ -0,0 +1,59 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author ZhuanZ(无密码)
+ */
+@Data
+public class CompanyVoiceRoboticCallLogAddWxExportVO {
+    @Excel(name = "记录id")
+    private Long logId;
+
+    @Excel(name = "任务id")
+    private Long roboticId;
+
+    @Excel(name = "客户昵称")
+    private String wxClientName;
+
+    /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
+    @Excel(name = "执行状态", readConverterExp = "1=执行中,2=执行成功,3=执行失败")
+    private Integer status;
+
+    /** 公司id */
+    @Excel(name = "公司")
+    private String companyName;
+
+    /** 个微账号id */
+    @Excel(name = "个微账号")
+    private String wxAccountName;
+
+    private Long customerId;
+
+    @Excel(name = "加微状态", readConverterExp = "0=失败,1=成功,2=加微中")
+    private Integer isAdd;
+
+    @Excel(name = "电话号码")
+    private String phone;
+
+    @Excel(name = "加微类型", readConverterExp = "1=个人微信,2=企业微信")
+    private Long isWeCom;
+
+    @Excel(name = "客户意向")
+    private String intention;
+
+    @Excel(name = "话术")
+    private String dialogName;
+
+    @Excel(name = "添加时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date addTime;
+
+    /** 记录调用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "调用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date runTime;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogAddwxVO.java

@@ -47,4 +47,20 @@ public class CompanyVoiceRoboticCallLogAddwxVO {
     private String wxAccountName;
 
     private Long customerId;
+
+    private Integer isAdd;
+
+    private String phone;
+
+    private Long isWeCom;
+
+    private String intention;
+
+    private String dialogName;
+
+    private Integer totalRecordCount;
+
+    private Integer successRecordCount;
+
+    private Date addTime;
 }

+ 30 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCount.java

@@ -0,0 +1,30 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author ZhuanZ(无密码)
+ */
+@Data
+public class CompanyVoiceRoboticCallLogCount {
+
+    /**
+     * 总记录数
+     */
+    private Integer recordCount;
+    /**
+     * 发送成功记录数
+     */
+    private Integer successRecordCount;
+    /**
+     * 今日发送数
+     */
+    private Integer todayCount;
+    /**
+     * 今日发送成功数
+     */
+    private Integer todaySuccessCount;
+
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogSendmsgVO.java

@@ -70,4 +70,7 @@ public class CompanyVoiceRoboticCallLogSendmsgVO {
 
     @Excel(name = "短信模板名称")
     private String smsTempName;
+
+    @Excel(name = "发送手机号")
+    private String phone;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java

@@ -119,6 +119,11 @@ public class WorkflowExecRecordVo {
      */
     private List<NodeExecLogVo> nodeLogs;
 
+    /**
+     * 是否在外呼节点等待
+     */
+    private Boolean waitCallNode;
+
     /**
      * 节点执行日志VO
      */

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java

@@ -104,4 +104,9 @@ public class FsVideoResource {
     private String hsyVid;
 
     private String jobId;
+
+    /**
+     * 视频展示类型:landscape-横屏,portrait-竖屏,默认横屏
+     */
+    private String displayType;
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java

@@ -138,4 +138,8 @@ public interface FsCourseAnswerLogsMapper
 
     @Select("select * from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 1 limit 1")
     FsCourseAnswerLogs selectRightLogByCourseVideoIsOpen(@Param("videoId") Long videoId,@Param("userId") Long userId);
+
+    /** 统计指定视频+营期下去重答题人数 */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_answer_logs WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java

@@ -188,6 +188,10 @@ public interface FsCourseRedPacketLogMapper
     @Select("select * from fs_course_red_packet_log where video_id = #{videoId} and user_id = #{userId} and period_id = #{periodId} limit 1")
     FsCourseRedPacketLog selectUserFsCourseRedPacketLog(@Param("videoId") Long videoId, @Param("userId")Long userId, @Param("periodId")Long periodId);
 
+    /** 统计指定视频+营期下去重领红包人数 */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_red_packet_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
     @Select("SELECT * FROM fs_course_red_packet_log \n" +
             "WHERE create_time <= DATE_SUB(NOW(), INTERVAL 10 MINUTE)  -- 10 分钟前或更早\n" +
             "AND create_time >= DATE(NOW())  -- 但必须是今天\n" +

+ 62 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -757,4 +757,66 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
 
     @Select("select * from fs_course_watch_log where user_id=#{userId} and company_user_id=#{companyUserId} and course_id=#{courseId} and video_id=#{videoId} limit 1")
     FsCourseWatchLog selectFsCourseWatchLogWithUCCV(@Param("userId") Long userId,@Param("companyUserId") Long companyUserId,@Param("courseId") Integer courseId,@Param("videoId") Integer videoId);
+
+    /**
+     * 查询视频时长(只返回duration字段)
+     * @param videoId 视频ID
+     * @return 视频时长(秒)
+     */
+    @Select("SELECT duration FROM fs_user_course_video WHERE video_id = #{videoId} AND is_del = 0 LIMIT 1")
+    Long selectVideoDurationByVideoId(@Param("videoId") Long videoId);
+
+    /**
+     * 统计累计观看人数(对userId去重)
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 累计观看人数
+     */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctWatchUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
+    /**
+     * 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 累计完课人数
+     */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId} AND duration >= 1200")
+    Long countDistinctCompleteUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
+    /**
+     * 首次点播数据统计:观看人数、>=20分钟人数、>=30分钟人数
+     * 首次点播窗口 = [营期课程开始时间, 营期课程开始时间+视频时长](由XML内联计算)
+     * 用户观看开始时间 = COALESCE(finish_time - duration, update_time - duration, create_time)
+     *
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return Map: firstWatchCount, firstWatch20MinCount, firstWatch30MinCount
+     */
+    Map<String, Object> selectFirstPlaybackStats(@Param("videoId") Long videoId,
+                                                 @Param("periodId") Long periodId);
+
+    /**
+     * 第2-n次观看数据统计:view_start不在首次点播窗口内的观看记录
+     * 首次点播窗口 = [营期课程开始时间, 营期课程开始时间+视频时长]
+     * 第2-n次:view_start < 窗口开始 或 view_start >= 窗口结束
+     *
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return Map: repeatWatchCount, repeatWatch20MinCount, repeatWatch30MinCount
+     */
+    Map<String, Object> selectRepeatPlaybackStats(@Param("videoId") Long videoId,
+                                                 @Param("periodId") Long periodId);
+
+    /**
+     * 课程小结-用户详情列表(分页):按videoId+periodId查观看记录,区分首次/2-n次时长,关联订单及公司/销售
+     */
+    List<com.fs.course.vo.CourseStatisticsUserDetailVO> selectCourseStatisticsUserDetailList(
+            @Param("param") com.fs.course.param.CourseStatisticsUserDetailParam param);
+
+    /**
+     * 课程小结-用户详情导出:按创建时间倒序,最多50000条
+     */
+    List<com.fs.course.vo.CourseStatisticsUserDetailVO> selectCourseStatisticsUserDetailExportList(
+            @Param("param") com.fs.course.param.CourseStatisticsUserDetailParam param);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java

@@ -133,4 +133,13 @@ public interface FsUserCoursePeriodDaysMapper extends BaseMapper<FsUserCoursePer
     List<Long> selectFsUserCoursePeriodDaysForLastById(FsUserCoursePeriodDays param);
 
     List<FsUserCoursePeriodDays> selectFsUserCoursePeriodDaysByCourseId(@Param("courseId") Long courseId);
+
+    /**
+     * 根据营期ID和视频ID查询营期课程开始时间(首次点播窗口起始)
+     * @param periodId 营期ID
+     * @param videoId 视频ID
+     * @return 开始时间,无则null
+     */
+    @Select("SELECT start_date_time FROM fs_user_course_period_days WHERE period_id = #{periodId} AND video_id = #{videoId} AND del_flag = '0' ORDER BY lesson ASC LIMIT 1")
+    LocalDateTime selectStartDateTimeByPeriodAndVideo(@Param("periodId") Long periodId, @Param("videoId") Long videoId);
 }

+ 24 - 0
fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java

@@ -0,0 +1,24 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 课程小结-用户详情查询参数
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsUserDetailParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 视频ID */
+    private Long videoId;
+    /** 营期ID */
+    private Long periodId;
+    /** 页码 */
+    private Integer pageNum = 1;
+    /** 每页条数 */
+    private Integer pageSize = 10;
+}

+ 24 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -168,7 +168,31 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
 
     R decryptLink(String url);
 
+    R decryptLinkV2(String url);
+
     List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
 
     FsCourseWatchLog selectFsCourseWatchLogWithUCCV(Long userId, Long companyUserId, Integer courseId, Integer videoId);
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    CourseStatisticsDetailVO getCourseStatisticsDetail(Long videoId, Long periodId);
+
+    /**
+     * 查询课程小结-用户详情列表(分页)
+     * @param param 查询参数
+     * @return 用户详情列表
+     */
+    List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailList(CourseStatisticsUserDetailParam param);
+
+    /**
+     * 课程小结-用户详情导出:按创建时间倒序,最多50000条
+     * @param param 查询参数
+     * @return 用户详情列表
+     */
+    List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailExportList(CourseStatisticsUserDetailParam param);
 }

+ 204 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -25,6 +25,12 @@ import com.fs.course.config.RedisKeyScanner;
 import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
 import com.fs.course.param.*;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.dto.FsStoreCartDTO;
+import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
@@ -34,6 +40,7 @@ import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.ConfigUtil;
+import com.fs.his.utils.LinkUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.qw.Bean.MsgBean;
 import com.fs.qw.cache.IQwExternalContactCacheService;
@@ -162,6 +169,24 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
 
+    @Autowired
+    private FsUserCoursePeriodMapper fsUserCoursePeriodMapper;
+
+    @Autowired
+    private FsUserCoursePeriodDaysMapper fsUserCoursePeriodDaysMapper;
+
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+
+    @Autowired
+    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
+
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
+    @Autowired
+    private FsCourseAnswerLogsMapper fsCourseAnswerLogsMapper;
+
     /**
      * 查询短链课程看课记录
      *
@@ -1719,6 +1744,14 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return R.ok().put("data", data);
     }
 
+    @Override
+    public R decryptLinkV2(String url) {
+        String decryptLink = LinkUtil.decryptLink(url);
+        Map<String, Object> data = new HashMap<>();
+        data.put("decryptLink", decryptLink);
+        return R.ok().put("data", data);
+    }
+
     @Override
     public List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact) {
         return fsCourseWatchLogMapper.selectFsUserWatchLogByExtId(qwExternalContact);
@@ -1729,4 +1762,175 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return fsCourseWatchLogMapper.selectFsCourseWatchLogWithUCCV(userId, companyUserId, courseId, videoId);
     }
 
+    @Override
+    public CourseStatisticsDetailVO getCourseStatisticsDetail(Long videoId, Long periodId) {
+        CourseStatisticsDetailVO vo = new CourseStatisticsDetailVO();
+
+        // 总体数据
+        
+        // 1. 查询视频时长(只返回duration字段)
+        FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
+        vo.setVideoDuration(fsUserCourseVideo != null ? fsUserCourseVideo.getDuration() : 0L);
+
+        FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(periodId);
+
+
+        // 2. 统计累计观看人数(对userId去重)
+        Long totalWatchCount = fsCourseWatchLogMapper.countDistinctWatchUsers(videoId, periodId);
+        vo.setTotalWatchCount(totalWatchCount != null ? totalWatchCount : 0L);
+        
+        // 3. 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
+        Long totalCompleteCount = fsCourseWatchLogMapper.countDistinctCompleteUsers(videoId, periodId);
+        vo.setTotalCompleteCount(totalCompleteCount != null ? totalCompleteCount : 0L);
+        
+        // 4. 计算到课完课率 = 累计完课人数 / 累计观看人数
+        BigDecimal completeRate = BigDecimal.ZERO;
+        if (vo.getTotalWatchCount() != null && vo.getTotalWatchCount() > 0) {
+            completeRate = BigDecimal.valueOf(vo.getTotalCompleteCount())
+                    .divide(BigDecimal.valueOf(vo.getTotalWatchCount()), 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100));
+        }
+        vo.setCompleteRate(completeRate);
+
+        // 首次点播数据:营期开始时间+视频时长内的观看记录,view_start=update_time-duration 或 finish_time-duration(SQL内联计算窗口)
+        if (periodId != null && videoId != null) {
+            Map<String, Object> firstStats = fsCourseWatchLogMapper.selectFirstPlaybackStats(videoId, periodId);
+            if (firstStats != null && !firstStats.isEmpty()) {
+                Long firstWatch = getLongFromMap(firstStats, "firstWatchCount");
+                Long first20 = getLongFromMap(firstStats, "firstWatch20MinCount");
+                Long first30 = getLongFromMap(firstStats, "firstWatch30MinCount");
+                vo.setFirstWatchCount(firstWatch != null ? firstWatch : 0L);
+                vo.setFirstWatch20MinCount(first20 != null ? first20 : 0L);
+                vo.setFirstWatch30MinCount(first30 != null ? first30 : 0L);
+                if (firstWatch != null && firstWatch > 0) {
+                    vo.setFirstCompleteRate20Min(BigDecimal.valueOf(first20 != null ? first20 : 0)
+                            .divide(BigDecimal.valueOf(firstWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                    vo.setFirstCompleteRate30Min(BigDecimal.valueOf(first30 != null ? first30 : 0)
+                            .divide(BigDecimal.valueOf(firstWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                }
+            }
+        }
+
+        // 第2-n次观看数据:view_start不在首次点播窗口内的记录(窗口外=首次窗口前或窗口后)
+        if (periodId != null && videoId != null) {
+            Map<String, Object> repeatStats = fsCourseWatchLogMapper.selectRepeatPlaybackStats(videoId, periodId);
+            if (repeatStats != null && !repeatStats.isEmpty()) {
+                Long repeatWatch = getLongFromMap(repeatStats, "repeatWatchCount");
+                Long repeat20 = getLongFromMap(repeatStats, "repeatWatch20MinCount");
+                Long repeat30 = getLongFromMap(repeatStats, "repeatWatch30MinCount");
+                vo.setRepeatWatchCount(repeatWatch != null ? repeatWatch : 0L);
+                vo.setRepeatWatch20MinCount(repeat20 != null ? repeat20 : 0L);
+                vo.setRepeatWatch30MinCount(repeat30 != null ? repeat30 : 0L);
+                if (repeatWatch != null && repeatWatch > 0) {
+                    vo.setRepeatCompleteRate20Min(BigDecimal.valueOf(repeat20 != null ? repeat20 : 0)
+                            .divide(BigDecimal.valueOf(repeatWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                    vo.setRepeatCompleteRate30Min(BigDecimal.valueOf(repeat30 != null ? repeat30 : 0)
+                            .divide(BigDecimal.valueOf(repeatWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                }
+            }
+        }
+
+        // 订单数据:fs_store_order_scrm order_type=3,videoId+periodId 匹配,paid=1
+        if (periodId != null && videoId != null) {
+            FsStoreOrderScrm orderQuery = new FsStoreOrderScrm();
+            orderQuery.setOrderType(3);
+            orderQuery.setVideoId(videoId.intValue());
+            orderQuery.setPeriodId(periodId.intValue());
+            orderQuery.setPaid(1);
+            List<FsStoreOrderScrm> orders = fsStoreOrderScrmMapper.selectFsStoreOrderList(orderQuery);
+            List<FsStoreOrderScrm> paidOrders = orders != null ? orders.stream()
+                    .filter(o -> o.getPaid() != null && o.getPaid() == 1)
+                    .collect(Collectors.toList()) : Collections.emptyList();
+
+            BigDecimal gmv = paidOrders.stream()
+                    .map(FsStoreOrderScrm::getPayPrice)
+                    .filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            vo.setGmv(gmv);
+
+            long paidUserCount = paidOrders.stream()
+                    .filter(o -> o.getUserId() != null)
+                    .map(FsStoreOrderScrm::getUserId)
+                    .distinct()
+                    .count();
+            vo.setPaidUserCount(paidUserCount);
+            vo.setPaidOrderCount((long) paidOrders.size());
+
+            if (vo.getTotalWatchCount() != null && vo.getTotalWatchCount() > 0 && paidUserCount > 0) {
+                vo.setTotalPaidConversionRate(BigDecimal.valueOf(paidUserCount * 100.0 / vo.getTotalWatchCount()).setScale(2, RoundingMode.HALF_UP));
+            }
+            if (vo.getTotalCompleteCount() != null && vo.getTotalCompleteCount() > 0 && paidUserCount > 0) {
+                vo.setPaidConversionRate20Min(BigDecimal.valueOf(paidUserCount * 100.0 / vo.getTotalCompleteCount()).setScale(2, RoundingMode.HALF_UP));
+            }
+            if (vo.getTotalCompleteCount() != null && vo.getTotalCompleteCount() > 0 && gmv != null && gmv.compareTo(BigDecimal.ZERO) > 0) {
+                vo.setCompleteRValue(gmv.divide(BigDecimal.valueOf(vo.getTotalCompleteCount()), 2, RoundingMode.HALF_UP));
+            }
+
+            Long answerCount = fsCourseAnswerLogsMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId);
+            vo.setAnswerUserCount(answerCount != null ? answerCount : 0L);
+
+            Long redCount = fsCourseRedPacketLogMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId);
+            vo.setRedPacketUserCount(redCount != null ? redCount : 0L);
+
+            // 单品销量统计:从订单明细汇总
+            Map<Long, CourseProductSalesVO> productSalesMap = new HashMap<>();
+            for (FsStoreOrderScrm order : paidOrders) {
+                // todo 数据量大的时候需要优化查询 外面批量查询 里面数据过滤
+                List<FsStoreOrderItemVO> items = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+                if (items == null || items.isEmpty()) continue;
+                long totalNum = order.getTotalNum() != null && order.getTotalNum() > 0 ? order.getTotalNum() : 1;
+                BigDecimal orderPayPrice = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+
+                for (FsStoreOrderItemVO item : items) {
+                    FsStoreCartDTO cartDTO = JSONUtil.toBean(item.getJsonInfo(), FsStoreCartDTO.class);
+                    if (item.getProductId() == null) continue;
+                    long itemNum = item.getNum() != null ? item.getNum() : 0;
+                    BigDecimal itemAmount = totalNum > 0 ? orderPayPrice.multiply(BigDecimal.valueOf(itemNum)).divide(BigDecimal.valueOf(totalNum), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+                    CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(item.getProductId(), k -> {
+                        CourseProductSalesVO pvo = new CourseProductSalesVO();
+                        pvo.setProductName(cartDTO.getProductName());
+                        return pvo;
+                    });
+
+                    productSales.setSalesCount(productSales.getSalesCount() + itemNum);
+                    productSales.setSalesAmount(productSales.getSalesAmount().add(itemAmount));
+                }
+            }
+            List<CourseProductSalesVO> productList = new ArrayList<>(productSalesMap.values());
+            productList.sort((a, b) -> b.getSalesAmount().compareTo(a.getSalesAmount()));
+            vo.setProductList(productList);
+        }
+
+        
+        return vo;
+    }
+
+    @Override
+    public List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailList(CourseStatisticsUserDetailParam param) {
+        if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
+            return Collections.emptyList();
+        }
+        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailList(param);
+    }
+
+    @Override
+    public List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailExportList(CourseStatisticsUserDetailParam param) {
+        if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
+            return Collections.emptyList();
+        }
+        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailExportList(param);
+    }
+
+    /**
+     * 从 Map 中安全获取 Long 值,兼容 MyBatis 返回的驼峰/小写键名
+     */
+    private Long getLongFromMap(Map<String, Object> map, String key) {
+        if (map == null || key == null) return null;
+        Object v = map.get(key);
+        if (v == null) v = map.get(key.toLowerCase());
+        if (v == null) return null;
+        if (v instanceof Number) return ((Number) v).longValue();
+        try { return Long.parseLong(String.valueOf(v)); } catch (NumberFormatException e) { return null; }
+    }
+
 }

+ 43 - 16
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -437,23 +437,50 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
 
     @Override
     public R updateCourseDate(UpdateCourseTimeVo vo) {
-        FsUserCoursePeriodDays day = getById(vo.getId());
-        FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(day.getPeriodId());
-        if(!DateUtil.isWithinRangeSafe(vo.getDayDate(), period.getPeriodStartingTime(), period.getPeriodEndTime())) return R.error("时间不在营期范围内");
-        day.setDayDate(vo.getDayDate());
-        day.setStartDateTime(LocalDateTime.of(day.getDayDate(), day.getStartDateTime().toLocalTime()));
-        day.setEndDateTime(LocalDateTime.of(day.getDayDate(), day.getEndDateTime().toLocalTime()));
-        day.setLastJoinTime(LocalDateTime.of(day.getDayDate(), day.getLastJoinTime().toLocalTime()));
-        // 默认开启今天及以后的两天,为进行中
-        LocalDateTime compareDayTime = LocalDateTime.now();
-        if(compareDayTime.isAfter(day.getStartDateTime()) && compareDayTime.isBefore(day.getEndDateTime())){
-            day.setStatus(1);
-        } else if(compareDayTime.isBefore(day.getStartDateTime())){
-            day.setStatus(0);
-        } else {
-            day.setStatus(2);
+        // 确定要更新的id列表:优先使用ids(批量),否则使用id(单个)
+        List<Long> idList = vo.getIds();
+        if (idList == null || idList.isEmpty()) {
+            if (vo.getId() == null) {
+                return R.error("请选择要修改的课程");
+            }
+            idList = java.util.Collections.singletonList(vo.getId());
+        }
+        // 必须提供开始和结束时间,且开始时间不能晚于结束时间
+        LocalDateTime startDateTime = vo.getStartDateTime();
+        LocalDateTime endDateTime = vo.getEndDateTime();
+        if (startDateTime == null || endDateTime == null) {
+            return R.error("请选择营期开始时间和结束时间");
+        }
+        if (!startDateTime.isBefore(endDateTime)) {
+            return R.error("开始时间必须早于结束时间");
+        }
+        LocalDate dayDate = startDateTime.toLocalDate();
+        for (Long dayId : idList) {
+            FsUserCoursePeriodDays day = getById(dayId);
+            if (day == null) {
+                continue;
+            }
+            FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(day.getPeriodId());
+            if (period == null) {
+                continue;
+            }
+            if (!DateUtil.isWithinRangeSafe(dayDate, period.getPeriodStartingTime(), period.getPeriodEndTime())) {
+                return R.error("营期时间不在营期范围内");
+            }
+            day.setDayDate(dayDate);
+            day.setStartDateTime(startDateTime);
+            day.setEndDateTime(endDateTime);
+            day.setLastJoinTime(endDateTime);
+            LocalDateTime compareDayTime = LocalDateTime.now();
+            if (compareDayTime.isAfter(day.getStartDateTime()) && compareDayTime.isBefore(day.getEndDateTime())) {
+                day.setStatus(1);
+            } else if (compareDayTime.isBefore(day.getStartDateTime())) {
+                day.setStatus(0);
+            } else {
+                day.setStatus(2);
+            }
+            updateById(day);
         }
-        updateById(day);
         return R.ok();
     }
 

+ 28 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -2479,6 +2479,19 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUserCourseVideoDetailsVO fsUserCourseVideoDetailsVO = new FsUserCourseVideoDetailsVO();
         BeanUtils.copyProperties(fsUserCourseVideo, fsUserCourseVideoDetailsVO);
 
+        // 默认展示横屏
+        if (StringUtils.isNotEmpty(fsUserCourseVideo.getFileKey())) {
+            FsVideoResource fsVideoResource = fsVideoResourceMapper.selectByFileKey(fsUserCourseVideo.getFileKey());
+            if (fsVideoResource != null) {
+                fsUserCourseVideoDetailsVO.setDisplayType(fsVideoResource.getDisplayType());
+            } else {
+                fsUserCourseVideoDetailsVO.setDisplayType("landscape");
+            }
+        } else {
+            fsUserCourseVideoDetailsVO.setDisplayType("landscape");
+        }
+
+
         //从配置中读取默认线路
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
@@ -4589,13 +4602,26 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     JsonNode originalNode = productIdToJsonNodeMap.get(validProduct.getProductId());
 
                     fsStoreProductScrm.setProductId(validProduct.getProductId());
-                    fsStoreProductScrm.setImages(validProduct.getImages() != null ? validProduct.getImages() :
+                    fsStoreProductScrm.setImages(validProduct.getImage() != null ? validProduct.getImage() :
                             (originalNode != null ? originalNode.path("image").asText() : ""));
-                    fsStoreProductScrm.setImgUrl(validProduct.getImgUrl() != null ? validProduct.getImgUrl() :
+                    fsStoreProductScrm.setImgUrl(validProduct.getSliderImage() != null ? validProduct.getSliderImage() :
                             (originalNode != null ? originalNode.path("imgUrl").asText() : ""));
                     fsStoreProductScrm.setBarCode(validProduct.getBarCode());
                     fsStoreProductScrm.setPrice(validProduct.getPrice());
                     fsStoreProductScrm.setProductName(validProduct.getProductName());
+
+
+
+                    String onShelfTime = originalNode != null ? originalNode.path("onShelfTime").asText("00:00:00") : "00:00:00";
+                    String offShelfTime = originalNode != null ? originalNode.path("offShelfTime").asText("00:00:00") : "00:00:00";
+                    String cardPopupTime = originalNode != null ? originalNode.path("cardPopupTime").asText("00:00:00") : "00:00:00";
+                    String cardCloseTime = originalNode != null ? originalNode.path("cardCloseTime").asText("00:00:00") : "00:00:00";
+                    fsStoreProductScrm.setOnShelfTime(onShelfTime);
+                    fsStoreProductScrm.setOffShelfTime(offShelfTime);
+                    fsStoreProductScrm.setCardPopupTime(cardPopupTime);
+                    fsStoreProductScrm.setCardCloseTime(cardCloseTime);
+
+
                     fsPackageListVOS.add(fsStoreProductScrm);
                 }
                 vo.setFsStoreProductScrms(fsPackageListVOS);

+ 23 - 0
fs-service/src/main/java/com/fs/course/vo/CourseProductSalesVO.java

@@ -0,0 +1,23 @@
+package com.fs.course.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程单品销量统计VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseProductSalesVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 商品名称 */
+    private String productName;
+    /** 商品销量 */
+    private Long salesCount = 0L;
+    /** 商品销售额 */
+    private BigDecimal salesAmount = BigDecimal.ZERO;
+}

+ 72 - 0
fs-service/src/main/java/com/fs/course/vo/CourseStatisticsDetailVO.java

@@ -0,0 +1,72 @@
+package com.fs.course.vo;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程小结详情统计VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsDetailVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 视频时长(秒) */
+    private Long videoDuration;
+
+    /** 累计观看人数 */
+    private Long totalWatchCount;
+
+    /** 累计完课人数 */
+    private Long totalCompleteCount;
+
+    /** 到课完课率(百分比,如:80.50 表示 80.50%) */
+    private BigDecimal completeRate;
+
+    // ========== 首次点播数据 ==========
+    /** 首次点播-观看人数(去重) */
+    private Long firstWatchCount;
+    /** 首次点播->=20分钟人数 */
+    private Long firstWatch20MinCount;
+    /** 首次点播->=30分钟人数 */
+    private Long firstWatch30MinCount;
+    /** 首次点播-到课完课率(>=20分钟) */
+    private BigDecimal firstCompleteRate20Min;
+    /** 首次点播-到课完课率(>=30分钟) */
+    private BigDecimal firstCompleteRate30Min;
+
+    // ========== 第2-n次观看数据 ==========
+    /** 第2-n次-观看人数(去重) */
+    private Long repeatWatchCount;
+    /** 第2-n次->=20分钟人数 */
+    private Long repeatWatch20MinCount;
+    /** 第2-n次->=30分钟人数 */
+    private Long repeatWatch30MinCount;
+    /** 第2-n次-到课完课率(>=20分钟) */
+    private BigDecimal repeatCompleteRate20Min;
+    /** 第2-n次-到课完课率(>=30分钟) */
+    private BigDecimal repeatCompleteRate30Min;
+
+    // ========== 订单数据(fs_store_order_scrm order_type=3,videoId+periodId 匹配) ==========
+    /** GMV=付款订单总金额 */
+    private BigDecimal gmv;
+    /** 付费人数=实际支付下单商品的人数(去重) */
+    private Long paidUserCount;
+    /** 付费单数=付费订单总数 */
+    private Long paidOrderCount;
+    /** 总付费转化率=付费人数/累计观看人数 */
+    private BigDecimal totalPaidConversionRate;
+    /** 20min付费转化率=付费人数/观看时长>=20分钟人数 */
+    private BigDecimal paidConversionRate20Min;
+    /** 完课R值=GMV/完课人数 */
+    private BigDecimal completeRValue;
+    /** 累计答题人数(去重) */
+    private Long answerUserCount;
+    /** 领红包人数(去重) */
+    private Long redPacketUserCount;
+    /** 单品销量统计 */
+    private java.util.List<CourseProductSalesVO> productList;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java

@@ -0,0 +1,34 @@
+package com.fs.course.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程小结-用户详情VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsUserDetailVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 用户ID */
+    private Long userId;
+    @Excel(name = "用户名称")
+    private String userName;
+    @Excel(name = "观看时长(秒)")
+    private Long watchDuration;
+    @Excel(name = "第2-n次观看时长(秒)")
+    private Long repeatWatchDuration;
+    @Excel(name = "订单数")
+    private Long orderCount;
+    @Excel(name = "订单金额")
+    private BigDecimal orderAmount;
+    @Excel(name = "分公司名称")
+    private String companyName;
+    @Excel(name = "销售名称")
+    private String salesName;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/course/vo/FsVideoResourceVO.java

@@ -87,4 +87,9 @@ public class FsVideoResourceVO {
     private String projectIds;
 
     private Integer sort;
+
+    /**
+     * 视频展示类型:landscape-横屏,portrait-竖屏
+     */
+    private String displayType;
 }

+ 8 - 0
fs-service/src/main/java/com/fs/course/vo/UpdateCourseTimeVo.java

@@ -12,6 +12,7 @@ import java.util.List;
 @Data
 public class UpdateCourseTimeVo {
 
+    /** 批量修改营期时间的id列表,与id二选一 */
     private List<Long> ids;
     private Long id;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -22,6 +23,13 @@ public class UpdateCourseTimeVo {
     private LocalDateTime joinTime;
     private LocalDate dayDate;
 
+    /** 修改营期时间:开始时间(与endDateTime成对使用,修改后与结束时间保持一致) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startDateTime;
+    /** 修改营期时间:结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime endDateTime;
+
     // 批量修改开关 0 关闭 1 开启 默认关闭 打开的话修改时间,后续的时间会一起改变
     private Integer batchUpdateSwitch;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/vo/newfs/FsUserCourseVideoDetailsVO.java

@@ -49,4 +49,9 @@ public class FsUserCourseVideoDetailsVO {
      * 是否展示商品 0展示 1不展示
      */
     private String showProduct;
+
+    /**
+     * 视频展示类型:landscape-横屏,portrait-竖屏
+     */
+    private String displayType;
 }

+ 6 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java

@@ -29,4 +29,10 @@ public class CrmCustomerProperty extends BaseEntityTow {
 
     @Excel(name = "内容解析")
     private String aiAnalysis;
+
+    @Excel(name = "意向登记")
+    private String intention;
+
+    @Excel(name = "喜欢占比")
+    private Integer likeRatio;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java

@@ -2,6 +2,7 @@ package com.fs.crm.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.crm.domain.CrmCustomerProperty;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
@@ -11,6 +12,10 @@ public interface CrmCustomerPropertyMapper extends BaseMapper<CrmCustomerPropert
 
     List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty);
 
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+
+    CrmCustomerProperty selectByCustomerIdAndPropertyId(@Param("customerId") Long customerId, @Param("propertyId") Long propertyId);
+
     int insertCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
 
     int updateCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
@@ -18,4 +23,6 @@ public interface CrmCustomerPropertyMapper extends BaseMapper<CrmCustomerPropert
     int deleteCrmCustomerPropertyById(Long id);
 
     int deleteCrmCustomerPropertyByIds(Long[] ids);
+
+    int deleteByCustomerIdAndPropertyId(@Param("customerId") Long customerId, @Param("propertyId") Long propertyId);
 }

+ 121 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java

@@ -4,18 +4,139 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.crm.domain.CrmCustomerProperty;
 
 import java.util.List;
+import java.util.Map;
 
+/**
+ * 客户属性服务接口
+ * @author 
+ * @date 
+ */
 public interface ICrmCustomerPropertyService extends IService<CrmCustomerProperty> {
 
+    /**
+     * 根据ID查询客户属性
+     * @param id 客户属性ID
+     * @return 客户属性对象
+     */
     CrmCustomerProperty selectCrmCustomerPropertyById(Long id);
 
+    /**
+     * 查询客户属性列表
+     * @param crmCustomerProperty 客户属性查询条件
+     * @return 客户属性列表
+     */
     List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty);
 
+    /**
+     * 新增客户属性
+     * @param crmCustomerProperty 客户属性信息
+     * @return 新增结果(1-成功,0-失败)
+     */
     int insertCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
 
+    /**
+     * 修改客户属性
+     * @param crmCustomerProperty 客户属性信息
+     * @return 修改结果(1-成功,0-失败)
+     */
     int updateCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
 
+    /**
+     * 批量删除客户属性
+     * @param ids 客户属性ID数组
+     * @return 删除结果(删除的记录数)
+     */
     int deleteCrmCustomerPropertyByIds(Long[] ids);
 
+    /**
+     * 根据ID删除客户属性
+     * @param id 客户属性ID
+     * @return 删除结果(1-成功,0-失败)
+     */
     int deleteCrmCustomerPropertyById(Long id);
+
+    /**
+     * 根据客户ID查询客户属性列表
+     * @param customerId 客户ID
+     * @return 客户属性列表
+     */
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+
+    /**
+     * 添加客户属性
+     * @param customerId 客户ID
+     * @param propertyId 属性ID
+     * @param propertyName 属性名称
+     * @param propertyValue 属性值
+     * @param propertyValueType 属性值类型
+     * @param tradeType 交易类型
+     * @param createBy 创建人
+     * @return 添加结果(1-成功,0-失败)
+     */
+    int addCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy);
+
+    /**
+     * 添加或更新客户属性
+     * @param customerId 客户ID
+     * @param propertyId 属性ID
+     * @param propertyName 属性名称
+     * @param propertyValue 属性值
+     * @param propertyValueType 属性值类型
+     * @param tradeType 交易类型
+     * @param createBy 创建人
+     * @return 操作结果(1-成功,0-失败)
+     */
+    int addOrUpdateCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy);
+
+    /**
+     * 添加或更新客户属性(带额外信息)
+     * @param customerId 客户ID
+     * @param propertyId 属性ID
+     * @param propertyName 属性名称
+     * @param propertyValue 属性值
+     * @param propertyValueType 属性值类型
+     * @param tradeType 交易类型
+     * @param intention 意向
+     * @param likeRatio 喜欢比例
+     * @param createBy 创建人
+     * @return 操作结果(1-成功,0-失败)
+     */
+    int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy);
+
+    /**
+     * 批量添加客户属性
+     * @param customerId 客户ID
+     * @param properties 客户属性列表
+     * @return 添加结果(添加的记录数)
+     */
+    int batchAddCustomerProperties(Long customerId, List<CrmCustomerProperty> properties);
+
+    /**
+     * 根据属性模板ID添加客户属性
+     * @param customerId 客户ID
+     * @param propertyTemplateId 属性模板ID
+     * @param propertyValue 属性值
+     * @param createBy 创建人
+     * @return 添加结果(1-成功,0-失败)
+     */
+    int addPropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy);
+
+    /**
+     * 根据属性模板ID添加或更新客户属性
+     * @param customerId 客户ID
+     * @param propertyTemplateId 属性模板ID
+     * @param propertyValue 属性值
+     * @param createBy 创建人
+     * @return 操作结果(1-成功,0-失败)
+     */
+    int addOrUpdatePropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy);
+
+    /**
+     * 批量根据属性模板ID添加客户属性
+     * @param customerId 客户ID
+     * @param propertyMap 属性模板ID和属性值的映射
+     * @param createBy 创建人
+     * @return 添加结果(添加的记录数)
+     */
+    int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy);
 }

+ 145 - 0
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -3,15 +3,23 @@ package com.fs.crm.service.impl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.DateUtils;
 import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
 import com.fs.crm.mapper.CrmCustomerPropertyMapper;
 import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
+import java.util.Map;
 
 @Service
 public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPropertyMapper, CrmCustomerProperty> implements ICrmCustomerPropertyService {
 
+    @Autowired
+    private ICrmCustomerPropertyTemplateService propertyTemplateService;
+
     @Override
     public CrmCustomerProperty selectCrmCustomerPropertyById(Long id) {
         return baseMapper.selectCrmCustomerPropertyById(id);
@@ -43,4 +51,141 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
     public int deleteCrmCustomerPropertyById(Long id) {
         return baseMapper.deleteCrmCustomerPropertyById(id);
     }
+
+    @Override
+    public List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId) {
+        return baseMapper.selectCrmCustomerPropertyByCustomerId(customerId);
+    }
+
+    @Override
+    public int addCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy) {
+        CrmCustomerProperty property = new CrmCustomerProperty();
+        property.setCustomerId(customerId);
+        property.setPropertyId(propertyId);
+        property.setPropertyName(propertyName);
+        property.setPropertyValue(propertyValue);
+        property.setPropertyValueType(propertyValueType);
+        property.setTradeType(tradeType);
+        property.setCreateBy(createBy);
+        property.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCrmCustomerProperty(property);
+    }
+
+    @Override
+    public int addOrUpdateCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy) {
+        CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
+        if (existProperty != null) {
+            existProperty.setPropertyValue(propertyValue);
+            existProperty.setPropertyName(propertyName);
+            existProperty.setPropertyValueType(propertyValueType);
+            existProperty.setTradeType(tradeType);
+            existProperty.setUpdateBy(createBy);
+            existProperty.setUpdateTime(DateUtils.getNowDate());
+            return baseMapper.updateCrmCustomerProperty(existProperty);
+        } else {
+            return addCustomerProperty(customerId, propertyId, propertyName, propertyValue, propertyValueType, tradeType, createBy);
+        }
+    }
+
+    public int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy) {
+        String autoIntention = calculateIntentionByLikeRatio(likeRatio, intention);
+        
+        CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
+        if (existProperty != null) {
+            existProperty.setPropertyValue(propertyValue);
+            existProperty.setPropertyName(propertyName);
+            existProperty.setPropertyValueType(propertyValueType);
+            existProperty.setTradeType(tradeType);
+            existProperty.setIntention(autoIntention);
+            existProperty.setLikeRatio(likeRatio);
+            existProperty.setUpdateBy(createBy);
+            existProperty.setUpdateTime(DateUtils.getNowDate());
+            return baseMapper.updateCrmCustomerProperty(existProperty);
+        } else {
+            CrmCustomerProperty property = new CrmCustomerProperty();
+            property.setCustomerId(customerId);
+            property.setPropertyId(propertyId);
+            property.setPropertyName(propertyName);
+            property.setPropertyValue(propertyValue);
+            property.setPropertyValueType(propertyValueType);
+            property.setTradeType(tradeType);
+            property.setIntention(autoIntention);
+            property.setLikeRatio(likeRatio);
+            property.setCreateBy(createBy);
+            property.setCreateTime(DateUtils.getNowDate());
+            return baseMapper.insertCrmCustomerProperty(property);
+        }
+    }
+    
+    private String calculateIntentionByLikeRatio(Integer likeRatio, String intention) {
+        if (likeRatio == null) {
+            return intention;
+        }
+        
+        if (likeRatio >= 80) {
+            return "high";
+        } else if (likeRatio >= 50) {
+            return "medium";
+        } else if (likeRatio >= 20) {
+            return "low";
+        } else {
+            return "none";
+        }
+    }
+
+    @Override
+    public int batchAddCustomerProperties(Long customerId, List<CrmCustomerProperty> properties) {
+        int count = 0;
+        for (CrmCustomerProperty property : properties) {
+            property.setCustomerId(customerId);
+            property.setCreateTime(DateUtils.getNowDate());
+            if (property.getPropertyId() != null) {
+                CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, property.getPropertyId());
+                if (existProperty != null) {
+                    existProperty.setPropertyValue(property.getPropertyValue());
+                    existProperty.setPropertyName(property.getPropertyName());
+                    existProperty.setPropertyValueType(property.getPropertyValueType());
+                    existProperty.setTradeType(property.getTradeType());
+                    existProperty.setUpdateBy(property.getCreateBy());
+                    existProperty.setUpdateTime(DateUtils.getNowDate());
+                    count += baseMapper.updateCrmCustomerProperty(existProperty);
+                    continue;
+                }
+            }
+            count += baseMapper.insertCrmCustomerProperty(property);
+        }
+        return count;
+    }
+
+    @Override
+    public int addPropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy) {
+        CrmCustomerPropertyTemplate template = propertyTemplateService.selectCrmCustomerPropertyTemplateById(propertyTemplateId);
+        if (template == null) {
+            return 0;
+        }
+        return addCustomerProperty(customerId, propertyTemplateId, template.getName(), propertyValue, template.getValueType(), template.getTradeType(), createBy);
+    }
+
+    @Override
+    public int addOrUpdatePropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy) {
+        CrmCustomerPropertyTemplate template = propertyTemplateService.selectCrmCustomerPropertyTemplateById(propertyTemplateId);
+        if (template == null) {
+            return 0;
+        }
+        return addOrUpdateCustomerProperty(customerId, propertyTemplateId, template.getName(), propertyValue, template.getValueType(), template.getTradeType(), createBy);
+    }
+
+    @Override
+    public int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy) {
+        int count = 0;
+        for (Map.Entry<Long, String> entry : propertyMap.entrySet()) {
+            Long templateId = entry.getKey();
+            String propertyValue = entry.getValue();
+            int result = addOrUpdatePropertyByTemplateId(customerId, templateId, propertyValue, createBy);
+            if (result > 0) {
+                count++;
+            }
+        }
+        return count;
+    }
 }

+ 46 - 0
fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java

@@ -0,0 +1,46 @@
+package com.fs.his.config;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * cId手机号生成配置类
+ * **/
+@Data
+public class CidPhoneConfig implements Serializable {
+    /**
+     * 是否开启手机号配置
+     * true: 开启,显示生成条数输入框
+     * false: 关闭,隐藏生成条数输入框
+     */
+    private Boolean enablePhoneConfig;
+
+    /**
+     * 生成条数
+     * 当 enablePhoneConfig 为 true 时有效,表示需要生成的手机号数量
+     */
+    private Integer generateCount;
+
+    /**
+     * 开始位置(从第几位开始生成)
+     */
+    private Integer startIndex;
+
+    /**
+     * 结束位置(到第几位结束)
+     */
+    private Integer endIndex;
+
+    /**
+     * 手机拨打限制开关
+     * true: 开启,显示手机拨打限制输入框
+     * false: 关闭,隐藏手机拨打限制输入框
+     * **/
+    private Boolean enablePhoneLimitConfig;
+
+    /**
+     * 拨打次数
+     * **/
+    private Long numberCalls;
+}

+ 109 - 0
fs-service/src/main/java/com/fs/his/utils/Base62Utils.java

@@ -0,0 +1,109 @@
+package com.fs.his.utils;
+
+public class Base62Utils {
+    private static final String BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+    private static final int BASE62 = 62;
+    
+    /**
+     * 将字节数组转换为 Base62 字符串
+     */
+    public static String bytesToBase62(byte[] data) {
+        if (data == null || data.length == 0) {
+            return "";
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        int idx = 0;
+        
+        while (idx < data.length) {
+            long value = 0;
+            int count = 0;
+            
+            for (int i = 0; i < 7 && idx < data.length; i++, idx++, count++) {
+                value = (value << 8) | (data[idx] & 0xFF);
+            }
+            
+            long[] encoded = encodeBase62(value, count);
+            for (int i = encoded.length - 1; i >= 0; i--) {
+                sb.append(BASE62_CHARS.charAt((int)encoded[i]));
+            }
+        }
+        
+        return sb.toString();
+    }
+    
+    /**
+     * 将 Base62 字符串转换为字节数组
+     */
+    public static byte[] base62ToBytes(String base62String) {
+        if (base62String == null || base62String.isEmpty()) {
+            return new byte[0];
+        }
+        
+        byte[] result = new byte[(base62String.length() * 7 + 7) / 8];
+        int resultIdx = 0;
+        int strIdx = 0;
+        
+        while (strIdx < base62String.length()) {
+            long value = 0;
+            int count = 0;
+            
+            for (int i = 0; i < 11 && strIdx < base62String.length(); i++, strIdx++) {
+                char c = base62String.charAt(strIdx);
+                int digit = decodeBase62Char(c);
+                if (digit == -1) {
+                    throw new IllegalArgumentException("Invalid Base62 character: " + c);
+                }
+                value = value * BASE62 + digit;
+                count++;
+            }
+            
+            int bytesToWrite = (count * 6 + 7) / 8;
+            for (int i = bytesToWrite - 1; i >= 0 && resultIdx < result.length; i--, resultIdx++) {
+                result[resultIdx] = (byte)((value >> (i * 8)) & 0xFF);
+            }
+        }
+        
+        return trimZeroBytes(result);
+    }
+    
+    private static long[] encodeBase62(long value, int byteCount) {
+        int outputSize = (byteCount * 8 + 5) / 6;
+        long[] result = new long[outputSize];
+        int idx = 0;
+        
+        while (value > 0) {
+            result[idx++] = value % BASE62;
+            value /= BASE62;
+        }
+        
+        return java.util.Arrays.copyOf(result, idx);
+    }
+    
+    private static int decodeBase62Char(char c) {
+        if (c >= '0' && c <= '9') return c - '0';
+        if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
+        if (c >= 'a' && c <= 'z') return c - 'a' + 36;
+        return -1;
+    }
+    
+    private static byte[] trimZeroBytes(byte[] data) {
+        int nonZeroCount = 0;
+        for (byte b : data) {
+            if (b != 0) nonZeroCount++;
+        }
+        
+        if (nonZeroCount == data.length) {
+            return data;
+        }
+        
+        byte[] trimmed = new byte[nonZeroCount];
+        int idx = 0;
+        for (byte b : data) {
+            if (b != 0) {
+                trimmed[idx++] = b;
+            }
+        }
+        return trimmed;
+    }
+}

+ 83 - 0
fs-service/src/main/java/com/fs/his/utils/LinkUtil.java

@@ -0,0 +1,83 @@
+package com.fs.his.utils;
+
+import com.fs.common.utils.ParseUtils;
+import org.springframework.util.Base64Utils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+public class LinkUtil {
+    private static final String KEY = "AESAabCdeREssREA";
+    private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
+
+
+    public static String encryptLink(String text) {
+        String encryptedText=null;
+        try {
+            SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+            byte[] encryptedBytes = cipher.doFinal(text.getBytes());
+            encryptedText = Base62Utils.bytesToBase62(encryptedBytes);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return encryptedText;
+    }
+
+    public static String decryptLink(String encryptedText) {
+        String text=null;
+        try {
+            SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.DECRYPT_MODE, secretKey);
+            byte[] decryptedBytes = cipher.doFinal(Base62Utils.base62ToBytes(encryptedText));
+            text = new String(decryptedBytes);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return text;
+    }
+
+    public static String decryptPhoneMk(String encryptedText) {
+        String text=null;
+        try {
+            SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.DECRYPT_MODE, secretKey);
+            byte[] decryptedBytes = cipher.doFinal(Base62Utils.base62ToBytes(encryptedText));
+            text = new String(decryptedBytes);
+            text =text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return text;
+    }
+
+    public static String decryptAutoPhoneMk(String encryptedText) {
+        String text=null;
+        if (encryptedText!=null&&encryptedText!="") {
+            if (encryptedText.length()>11){
+                try {
+                    SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
+                    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+                    cipher.init(Cipher.DECRYPT_MODE, secretKey);
+                    byte[] decryptedBytes = cipher.doFinal(Base62Utils.base62ToBytes(encryptedText));
+                    text = new String(decryptedBytes);
+                    text =text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }else {
+                text =  ParseUtils.parsePhone(encryptedText);
+            }
+        }
+
+        return text;
+    }
+
+    public static void main(String[] args) {
+        System.out.println(decryptLink("43fP1nkB0dzuoa8nyqZ45nbgqeXHtgOeOYbfSS53fsCDaR0Y1F4ySPuMzi8UNrY0VBL21XdcWAY232W9UOkjTTL3O"));
+    }
+}

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -382,4 +382,8 @@ public class FsStoreOrderScrm extends BaseEntity
     private Integer videoId;
     //课程ID
     private Integer courseId;
+    // 项目ID
+    private Integer projectId;
+    // 营期ID
+    private Integer periodId;
 }

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -350,6 +350,18 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "限购数量")
     private Integer purchaseLimit;
 
+    @TableField(exist = false)
+    private String onShelfTime;
+
+    @TableField(exist = false)
+    private String cardPopupTime;
+
+    @TableField(exist = false)
+    private String cardCloseTime;
+
+    @TableField(exist = false)
+    private String offShelfTime;
+
     /** 过滤商品id */
     private Long[] excludeProductIds;
 }

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java

@@ -61,4 +61,8 @@ public class FsStoreOrderCreateParam implements Serializable
     private Integer videoId;
     //课程ID
     private Integer courseId;
+    //项目ID
+    private Integer projectId;
+    //营期ID
+    private Integer periodId;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -912,6 +912,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if ("北京卓美".equals(companyName) && param.getVideoId()!=null){
                 storeOrder.setVideoId(param.getVideoId());
                 storeOrder.setCourseId(param.getCourseId());
+                storeOrder.setPeriodId(param.getPeriodId());
+                storeOrder.setProjectId(param.getProjectId());
             }
             String json = configService.selectConfigByKey("store.config");
             StoreConfig config= JSONUtil.toBean(json, StoreConfig.class);

+ 2 - 0
fs-service/src/main/java/com/fs/live/service/ILiveService.java

@@ -222,6 +222,8 @@ public interface ILiveService
 
     R liveDecryptLink(String url);
 
+    R liveDecryptLinkV2(String url);
+
     R getLiveQwUserInfo(Long qwUserId);
 
     List<Live> selectLiveListNew(Live live);

+ 14 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -23,6 +23,7 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.mapper.FsUserWxMapper;
+import com.fs.his.utils.LinkUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
@@ -222,6 +223,19 @@ public class LiveServiceImpl implements ILiveService
         return R.ok().put("data", data);
     }
 
+    /**
+     * 解密链接
+     * @param url
+     * @return
+     */
+    @Override
+    public R liveDecryptLinkV2(String url) {
+        String decryptLink = LinkUtil.decryptLink(url);
+        Map<String, Object> data = new HashMap<>();
+        data.put("decryptLink", decryptLink);
+        return R.ok().put("data", data);
+    }
+
     @Override
     public R getLiveQwUserInfo(Long qwUserId) {
         QwUser qwUser = qwUserMapper.selectQwUserById(qwUserId);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java

@@ -564,7 +564,7 @@ public class QwSopServiceImpl implements IQwSopService {
                 if (ruleTimeVO.getTempStatus().equals("0")){
                     QwSop qwSop=new QwSop();
                     qwSop.setId(ruleTimeVO.getId());
-                    qwSop.setStatus(0L);
+                    qwSop.setStatus(0L);//模板不运行了,则当前任务也停用
 
                     qwSopMapper.updateQwSop(qwSop);
                     return;

+ 281 - 81
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -99,6 +99,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
     private static final String registeredRealLink = "/pages_course/register.html?link=";
     private static final String h5LiveShortLink = "/pages_course/livingInvite.html?s=";
+    private static final String appLiveShortLink = "/pages_live/livingList?link=";
     private static final String h5miniappLink = "/pages_course/shortLink.html?s=";
 //    private static final String miniappRealLink = "/pages/index/index?course=";
 
@@ -381,64 +382,64 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
         try {
 
-                //用于查询
-                SopUserLogs userLogs=new SopUserLogs();
-                userLogs.setSopId(param.getSopId());
-                userLogs.setQwUserId(param.getQwUserId());
-                userLogs.setCorpId(param.getCorpId());
-                userLogs.setStatus(1);
-                userLogs.setStartTime(param.getParamTime());
-
-                String  unionSopStartId = sopUserLogsService.selectSopUserLogsByUpdate(userLogs);
-
-                 BatchSopUserLogsInfoParamU infoParamU=new BatchSopUserLogsInfoParamU();
-                    infoParamU.setIds(param.getIds());
-                //查询sop任务开始时间营期表 是否有这个营期
-                if (!StringUtil.strIsNullOrEmpty(unionSopStartId)){
-                    infoParamU.setUserLogsId(unionSopStartId);
+            //用于查询
+            SopUserLogs userLogs=new SopUserLogs();
+            userLogs.setSopId(param.getSopId());
+            userLogs.setQwUserId(param.getQwUserId());
+            userLogs.setCorpId(param.getCorpId());
+            userLogs.setStatus(1);
+            userLogs.setStartTime(param.getParamTime());
+
+            String  unionSopStartId = sopUserLogsService.selectSopUserLogsByUpdate(userLogs);
+
+            BatchSopUserLogsInfoParamU infoParamU=new BatchSopUserLogsInfoParamU();
+            infoParamU.setIds(param.getIds());
+            //查询sop任务开始时间营期表 是否有这个营期
+            if (!StringUtil.strIsNullOrEmpty(unionSopStartId)){
+                infoParamU.setUserLogsId(unionSopStartId);
+                if (param.getIds().length>0){
+                    batchUpdateSopUserLogsInfoToTime(infoParamU);
+                }
+
+            }
+            else {
+                //如果查不出来营期 ,则新建营期
+                //查询sop的模板id
+                QwSop qwSop = qwSopService.selectQwSopById(param.getSopId());
+
+                //查询这个sop任务的员工的信息
+                QwUser qwUser = qwUserMapper.selectQwUserByQwUseridAndCorpId(param.getQwUserId(), param.getCorpId());
+
+                SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
+                userLogsParamByDate.setSopId(param.getSopId());
+                userLogsParamByDate.setSopTempId(qwSop.getTempId());
+                userLogsParamByDate.setQwUserId(param.getQwUserId());
+                userLogsParamByDate.setCorpId(param.getCorpId());
+                userLogsParamByDate.setStartTime(param.getParamTime());
+                userLogsParamByDate.setStatus(1);
+                userLogsParamByDate.setUserId(qwUser.getId()+"|"+qwUser.getCompanyUserId()+"|"+qwUser.getCompanyId());
+
+                //如果没有这个营期没有就先插入
+                int i1 = sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+                if (i1>0){
+                    userLogs.setSopTempId(qwSop.getTempId());
+                    //获取新的
+                    String  unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+                    infoParamU.setUserLogsId(unionSopStartIdNew);
+
                     if (param.getIds().length>0){
                         batchUpdateSopUserLogsInfoToTime(infoParamU);
                     }
 
                 }
-                else {
-                    //如果查不出来营期 ,则新建营期
-                    //查询sop的模板id
-                    QwSop qwSop = qwSopService.selectQwSopById(param.getSopId());
-
-                    //查询这个sop任务的员工的信息
-                    QwUser qwUser = qwUserMapper.selectQwUserByQwUseridAndCorpId(param.getQwUserId(), param.getCorpId());
-
-                    SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
-                    userLogsParamByDate.setSopId(param.getSopId());
-                    userLogsParamByDate.setSopTempId(qwSop.getTempId());
-                    userLogsParamByDate.setQwUserId(param.getQwUserId());
-                    userLogsParamByDate.setCorpId(param.getCorpId());
-                    userLogsParamByDate.setStartTime(param.getParamTime());
-                    userLogsParamByDate.setStatus(1);
-                    userLogsParamByDate.setUserId(qwUser.getId()+"|"+qwUser.getCompanyUserId()+"|"+qwUser.getCompanyId());
-
-                    //如果没有这个营期没有就先插入
-                    int i1 = sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
-                    if (i1>0){
-                        userLogs.setSopTempId(qwSop.getTempId());
-                        //获取新的
-                        String  unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
-                        infoParamU.setUserLogsId(unionSopStartIdNew);
-
-                        if (param.getIds().length>0){
-                            batchUpdateSopUserLogsInfoToTime(infoParamU);
-                        }
 
-                    }
 
 
-
-                }
-            } catch (ConstraintViolationException e) {
-                return R.error().put("msg", "修改营期失败:目标营期已经有此客户,请检查客户信息 是否重复,重复请联系超管 删除此营期数据");
-            } catch (Exception e) {
-                return R.error().put("msg", "修改营期失败:" + e.getMessage());
+            }
+        } catch (ConstraintViolationException e) {
+            return R.error().put("msg", "修改营期失败:目标营期已经有此客户,请检查客户信息 是否重复,重复请联系超管 删除此营期数据");
+        } catch (Exception e) {
+            return R.error().put("msg", "修改营期失败:" + e.getMessage());
         }
 
         return R.ok();
@@ -746,7 +747,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 //                                    miniAppId = String.valueOf(luckyBagConfig.get("appId"));
 
 
-                                   if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                                    if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
                                         Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
                                         if (integerListMap != null) {
                                             int listIndexNum = 1;
@@ -856,6 +857,51 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                     log.error("浏览器看课模板解析失败:" + e);
                                 }
                                 break;
+                            //跳转app看课
+                            case "23":
+                                try {
+                                    //生成看课记录
+                                    addWatchLogIfNeeded(
+                                            param.getSopId(),
+                                            param.getVideoId(), param.getCourseId(),
+                                            Long.valueOf(groupUser.getFsUserId()), qwUserId,
+                                            companyUserId, companyId,
+                                            groupUser.getId(), param.getStartTime(),
+                                            createTime, 2
+                                    );
+                                    //生成看课短链
+                                    String shortH5link = createH5LinkByMiniAppV2(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
+                                            String.valueOf(qwUser.getId()), companyUserId, companyId, groupUser.getId(), config);
+                                    st.setMiniprogramPage(shortH5link);
+                                } catch (Exception e) {
+                                    log.error("跳转app看课模板解析失败:" + e);
+                                }
+                                break;
+                            //跳转app直播
+                            case "24":
+                                try{
+                                    sopLogs.setSendType(20);//直播
+                                    qwUserId = String.valueOf(qwUser.getId());
+                                    //生成直播短链
+                                    String shortH5Link = createH5LiveShortLinkV2(st, param.getCorpId(),
+                                            qwUser.getId(), companyUserId, companyId);
+                                    st.setMiniprogramPage(shortH5Link);
+                                    String json0 = configService.selectConfigByKey("his.config");
+                                    FSSysConfig sysConfig0 = JSON.parseObject(json0, FSSysConfig.class);
+                                    //直播观看记录
+                                    createLiveWatchLogAndInsert(
+                                            qwUser.getCompanyId().toString(),
+                                            qwUser.getCompanyUserId().toString(),
+                                            groupUser.getId().toString(),
+                                            Long.valueOf(st.getLiveId()),
+                                            sysConfig0.getAppId(),
+                                            2,
+                                            String.valueOf(qwUser.getId()),
+                                            param.getCorpId());
+                                } catch (Exception e) {
+                                    log.error("跳转app直播模板解析失败:" + e);
+                                }
+                                break;
                             //群公告
                             case "11":
                                 sopLogs.setSendType(21); // 设置为群公告类型
@@ -1043,7 +1089,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 //                                String luckyjson = configService.selectConfigByKey("luckyBag.config");
 //                                Map<String, Object> luckyBagConfig = JSON.parseObject(luckyjson, Map.class);
 //                                miniAppId = String.valueOf(luckyBagConfig.get("appId"));
-                               if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                                if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
                                     Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
                                     if (integerListMap != null) {
                                         int listIndexNum = 1;
@@ -1193,9 +1239,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 sopLogs.setSort(30000000);
                 sopLogs.setSendType(5);
                 sopLogs.setExternalUserName(item.getExternalUserName());
-                   if(sendLiveMsgFinal){
-                       sopLogs.setSendType(20);
-                   }
+                if(sendLiveMsgFinal){
+                    sopLogs.setSendType(20);
+                }
 
                 Long msgNum = Long.valueOf(generateRandomNumberWithLock());
                 sopLogs.setSmsLogsId(msgNum);
@@ -1252,15 +1298,15 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 //                                    log.warn("生成短链失败,跳过设置 URL。");
 //                                }
 //                            }else {
-                                if ("1".equals(st.getContentType())) {
-                                    String defaultName = "同学";
-                                    if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
-                                        defaultName = contact.getName();
-                                    }
-                                    st.setValue(st.getValue()
-                                            .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
-                                            .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
+                            if ("1".equals(st.getContentType())) {
+                                String defaultName = "同学";
+                                if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
+                                    defaultName = contact.getName();
                                 }
+                                st.setValue(st.getValue()
+                                        .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
+                                        .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
+                            }
 //                            }
 
                             break;
@@ -1344,7 +1390,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
                             //发个人看课记录处理
                             try {
-                                    createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUserId,param.getCorpId());
+                                createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUserId,param.getCorpId());
 
                             } catch (Exception e) {
                                 log.error("群聊创建直播看课记录失败!", e);
@@ -1600,6 +1646,49 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 sopSmsLogsList.add(sopSmsLogs);
                             }
                             break;
+                        //跳转app看课
+                        case "23":
+                            try {
+                                //生成看课记录
+                                addWatchLogIfNeeded(
+                                        item.getSopId(),
+                                        param.getVideoId(), param.getCourseId(),
+                                        item.getFsUserId(), String.valueOf(qwUser.getId()),
+                                        companyUserId, companyId,
+                                        item.getExternalId(), item.getStartTime(),
+                                        createTime, 2);
+                                //生成看课短链
+                                String shortH5link = createH5LinkByMiniAppV2(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
+                                        String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config);
+                                st.setMiniprogramPage(shortH5link);
+                            } catch (Exception e) {
+                                log.error("跳转app看课模板解析失败:" + e);
+                            }
+                            break;
+                        //跳转app直播
+                        case "24":
+                            try{
+                                sopLogs.setSendType(20);//直播
+                                //生成直播短链
+                                String shortH5Link = createH5LiveShortLinkV2(st, param.getCorpId(),
+                                        qwUser.getId(), companyUserId, companyId);
+                                st.setMiniprogramPage(shortH5Link);
+                                String json1 = configService.selectConfigByKey("his.config");
+                                FSSysConfig sysConfig0 = JSON.parseObject(json1, FSSysConfig.class);
+                                //直播观看记录
+                                createLiveWatchLogAndInsert(
+                                        qwUser.getCompanyId().toString(),
+                                        qwUser.getCompanyUserId().toString(),
+                                        item.getExternalId().toString(),
+                                        Long.valueOf(st.getLiveId()),
+                                        sysConfig0.getAppId(),
+                                        2,
+                                        String.valueOf(qwUser.getId()),
+                                        param.getCorpId());
+                            } catch (Exception e) {
+                                log.error("跳转app直播模板解析失败:" + e);
+                            }
+                            break;
                         //群公告(仅用于一键群发,个人不应该有群公告)
                         case "11":
                             log.warn("群公告不能发给个人,跳过处理,sopId:{}, externalId:{}", param.getSopId(), item.getExternalId());
@@ -1695,7 +1784,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         param.setIds(ids);
         log.info("一键群发操作日志1:{}", JSON.toJSONString(param));
         processQwSopLogsBySendMsg(param,param.getDraftStrategy());
-        return null;
+        return R.ok();
     }
 
     @Override
@@ -2003,15 +2092,15 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 //                            log.warn("生成短链失败,跳过设置 URL。");
 //                        }
 //                    }else {
-                        if ("1".equals(st.getContentType())) {
-                            String defaultName = "同学";
-                            if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
-                                defaultName = contact.getName();
-                            }
-                            st.setValue(st.getValue()
-                                    .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
-                                    .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
+                    if ("1".equals(st.getContentType())) {
+                        String defaultName = "同学";
+                        if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
+                            defaultName = contact.getName();
                         }
+                        st.setValue(st.getValue()
+                                .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
+                                .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
+                    }
 //                    }
 
                     break;
@@ -2359,12 +2448,55 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         sopSmsLogsList.add(sopSmsLogs);
                     }
                     break;
+                //跳转app看课
+                case "23":
+                    try {
+                        //生成看课记录
+                        addWatchLogIfNeeded(
+                                item.getSopId(),
+                                param.getVideoId(), param.getCourseId(),
+                                item.getFsUserId(), String.valueOf(qwUser.getId()),
+                                companyUserId, companyId,
+                                item.getExternalId(), item.getStartTime(),
+                                dataTime, 2);
+                        //生成看课短链
+                        String shortH5link = createH5LinkByMiniAppV2(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
+                                String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config);
+                        st.setMiniprogramPage(shortH5link);
+                    } catch (Exception e) {
+                        log.error("跳转app看课模板解析失败:" + e);
+                    }
+                    break;
+                //跳转app直播
+                case "24":
+                    try{
+                        sopLogs.setSendType(20);//直播
+                        qwUserId = qwUser.getId();
+                        corpId = param.getCorpId();
+                        //生成直播短链
+                        shortH5Link = createH5LiveShortLinkV2(st, corpId,
+                                qwUserId, companyUserId, companyId);
+                        st.setMiniprogramPage(shortH5Link);
+                        String json0 = configService.selectConfigByKey("his.config");
+                        FSSysConfig sysConfig0 = JSON.parseObject(json0, FSSysConfig.class);
+                        //直播观看记录
+                        createLiveWatchLogAndInsert(
+                                qwUser.getCompanyId().toString(),
+                                qwUser.getCompanyUserId().toString(),
+                                item.getExternalId().toString(),
+                                Long.valueOf(st.getLiveId()),
+                                sysConfig0.getAppId(),
+                                2,
+                                String.valueOf(qwUser.getId()),
+                                param.getCorpId());
+                    } catch (Exception e) {
+                        log.error("跳转app直播模板解析失败:" + e);
+                    }
+                    break;
                 default:
                     break;
-
             }
         }
-
         return  list;
     }
 
@@ -2404,6 +2536,38 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return link.getRealLink();
     }
 
+    /**
+     * 创建看课短链
+     * @param setting
+     * @param corpId
+     * @param sendTime
+     * @param courseId
+     * @param videoId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @param externalId
+     * @param config
+     * @return
+     */
+    String createH5LinkByMiniAppV2(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, Long.valueOf(qwUserId),
+                companyUserId, companyId, externalId, 3, null);
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = appRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+        Date updateTime = createUpdateTime(setting, sendTime, config);
+        link.setUpdateTime(updateTime);
+        link.setCreateTime(new Date());
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink();
+    }
+
     /**
      * 创建直播短链
      * @param setting
@@ -2447,6 +2611,42 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     }
 
 
+    /**
+     * 创建直播短链
+     * @param setting
+     * @param corpId
+     * @param qwUserId
+     * @param companyUserId
+     * @param companyId
+     * @return
+     */
+    private String createH5LiveShortLinkV2(QwSopCourseFinishTempSetting.Setting setting, String corpId, Long qwUserId, String companyUserId, String companyId) {
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setLiveId(Long.valueOf(setting.getLiveId()));
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+        link.setCreateTime(new Date());
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = appLiveShortLink + courseJson;
+        link.setRealLink(realLinkFull);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink();
+    }
+
+
     public String createActivityLinkByMiniApp(QwSopCourseFinishTempSetting.Setting st, QwSopLogs sopLogs, String corpId, Date sendTime, Integer courseId, Integer videoId, String qwUserId, String companyUserId, String companyId, Long externalId, CourseConfig config, String chatId) {
         FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, Long.valueOf(qwUserId),
                 companyUserId, companyId, null, 3, chatId);
@@ -2643,8 +2843,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
     //插入观看记录
     public void addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
-                                     Long fsUserId, String qwUserId, String companyUserId,
-                                     String companyId, Long externalId, String startTime,Date createTime, Integer watchType) {
+                                    Long fsUserId, String qwUserId, String companyUserId,
+                                    String companyId, Long externalId, String startTime,Date createTime, Integer watchType) {
 
         try {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
@@ -2702,11 +2902,11 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     }
 
     public String createLinkByMiniApp(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
-                                     Integer courseId, Integer videoId, Long qwUserId,
-                                     String companyUserId, String companyId, Long externalId,CourseConfig config, String chatId) {
+                                      Integer courseId, Integer videoId, Long qwUserId,
+                                      String companyUserId, String companyId, Long externalId,CourseConfig config, String chatId) {
 
         FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
-                                                companyUserId, companyId, externalId,3,chatId);
+                companyUserId, companyId, externalId,3,chatId);
 
         FsCourseRealLink courseMap = new FsCourseRealLink();
         BeanUtils.copyProperties(link,courseMap);
@@ -2791,7 +2991,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         byAppVO.setSortLink(sortLink);
         byAppVO.setAppMsgLink(appMsgLink);
 
-            //异步生成app链接记录
+        //异步生成app链接记录
         asyncSopTestService.createFsCourseSopAppLink(link.getLink(),sendTime,updateTime,companyId,companyUserId,String.valueOf(qwUserId),
                 qwUserName,corpId,courseId,setting.getLinkTitle(),setting.getLinkImageUrl(),videoId,
                 setting.getLinkDescribe(),appMsgLink,externalId);

+ 0 - 2
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java

@@ -107,8 +107,6 @@ public class SopUserLogsServiceImpl implements ISopUserLogsService {
     private QwGroupChatUserMapper qwGroupChatUserMapper;
     @Autowired
     private QwGroupChatMapper qwGroupChatMapper;
-
-
     @Autowired
     private ISysConfigService configService;
 

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

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

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