Ver Fonte

Merge remote-tracking branch 'origin/master' into Payment-Configuration

yfh há 1 semana atrás
pai
commit
b27e35a519
100 ficheiros alterados com 3613 adições e 115 exclusões
  1. 6 6
      fs-admin/src/main/java/com/fs/his/controller/FsAdvController.java
  2. 1 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  3. 2 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  4. 5 0
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  5. 33 0
      fs-admin/src/main/java/com/fs/qw/controller/QwFriendWelcomeController.java
  6. 7 0
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  7. 3 3
      fs-admin/src/main/resources/logback.xml
  8. 3 3
      fs-common-api/src/main/resources/logback.xml
  9. 3 3
      fs-company-app/src/main/resources/logback.xml
  10. 4 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  11. 13 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java
  12. 111 0
      fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagCollectRecordController.java
  13. 88 0
      fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagController.java
  14. 2 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwFriendWelcomeController.java
  15. 3 3
      fs-company/src/main/resources/logback.xml
  16. 20 5
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  17. 18 1
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  18. 159 0
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  19. 71 0
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  20. 40 1
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  21. 3 3
      fs-qw-api/src/main/resources/logback.xml
  22. 27 11
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  23. 3 3
      fs-qw-task/src/main/resources/logback.xml
  24. 3 3
      fs-qwhook-msg/src/main/resources/logback.xml
  25. 3 3
      fs-qwhook-sop/src/main/resources/logback.xml
  26. 3 3
      fs-qwhook/src/main/resources/logback.xml
  27. 8 0
      fs-service/pom.xml
  28. 8 0
      fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java
  29. 30 0
      fs-service/src/main/java/com/fs/core/config/VolcEngineConfiguration.java
  30. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRealLink.java
  31. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseWatchLog.java
  32. 4 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  33. 69 0
      fs-service/src/main/java/com/fs/course/domain/LuckyBag.java
  34. 151 0
      fs-service/src/main/java/com/fs/course/domain/LuckyBagCollectRecord.java
  35. 2 2
      fs-service/src/main/java/com/fs/course/mapper/FsCourseFinishTempMapper.java
  36. 3 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  37. 10 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  38. 1 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java
  39. 22 0
      fs-service/src/main/java/com/fs/course/param/LuckyBagActualRewardsParam.java
  40. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseRedPacketLogService.java
  41. 18 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  42. 24 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseRedPacketLogServiceImpl.java
  43. 265 6
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  44. 1 1
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  45. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  46. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java
  47. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiServiceImpl.java
  48. 8 0
      fs-service/src/main/java/com/fs/his/domain/FsUserIntegralLogs.java
  49. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserInformationCollectionMapper.java
  50. 26 0
      fs-service/src/main/java/com/fs/his/param/FsReceiveLuckyBagParam.java
  51. 8 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  52. 10 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreAfterSalesScrm.java
  53. 7 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  54. 16 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  55. 17 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  56. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java
  57. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java
  58. 2 0
      fs-service/src/main/java/com/fs/im/dto/OpenImMsgDTO.java
  59. 7 1
      fs-service/src/main/java/com/fs/ipad/IpadSendUtils.java
  60. 31 0
      fs-service/src/main/java/com/fs/ipad/vo/WxSendTextAtMsgVo.java
  61. 58 0
      fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java
  62. 4 0
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  63. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
  64. 52 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  65. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java
  66. 43 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  67. 8 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchLogService.java
  68. 18 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  69. 354 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  70. 16 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  71. 10 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java
  72. 69 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  73. 10 0
      fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  74. 118 0
      fs-service/src/main/java/com/fs/live/vo/LiveWatchLogListVO.java
  75. 71 0
      fs-service/src/main/java/com/fs/qw/mapper/LuckyBagCollectRecordMapper.java
  76. 25 0
      fs-service/src/main/java/com/fs/qw/mapper/LuckyBagMapper.java
  77. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwFriendWelcomeMapper.java
  78. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  79. 3 2
      fs-service/src/main/java/com/fs/qw/service/AsyncQwAiChatSopService.java
  80. 63 0
      fs-service/src/main/java/com/fs/qw/service/ILuckyBagCollectRecordService.java
  81. 29 0
      fs-service/src/main/java/com/fs/qw/service/ILuckyBagService.java
  82. 1 1
      fs-service/src/main/java/com/fs/qw/service/IQwContactWayService.java
  83. 1 1
      fs-service/src/main/java/com/fs/qw/service/IQwFriendWelcomeService.java
  84. 63 0
      fs-service/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java
  85. 94 0
      fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagCollectRecordServiceImpl.java
  86. 416 0
      fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagServiceImpl.java
  87. 4 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwContactWayServiceImpl.java
  88. 4 3
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  89. 34 5
      fs-service/src/main/java/com/fs/qw/service/impl/QwFriendWelcomeServiceImpl.java
  90. 3 0
      fs-service/src/main/java/com/fs/qw/vo/QwFriendWelcomeVO.java
  91. 8 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java
  92. 9 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopTempSetting.java
  93. 3 0
      fs-service/src/main/java/com/fs/sop/domain/QwSopTemp.java
  94. 3 0
      fs-service/src/main/java/com/fs/sop/domain/QwSopTempRules.java
  95. 6 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempServiceImpl.java
  96. 535 13
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  97. 2 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java
  98. 46 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxSendTextAtMsgTwoDTO.java
  99. 7 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceNew.java
  100. 5 0
      fs-service/src/main/resources/application-common.yml

+ 6 - 6
fs-admin/src/main/java/com/fs/his/controller/FsAdvController.java

@@ -40,7 +40,7 @@ public class FsAdvController extends BaseController
     /**
      * 查询广告列表
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:list')")
+    @PreAuthorize("@ss.hasPermi('store:adv:list') or @ss.hasPermi('his:adv:list')")
     @GetMapping("/list")
     public TableDataInfo list(FsAdv fsAdv)
     {
@@ -52,7 +52,7 @@ public class FsAdvController extends BaseController
     /**
      * 导出广告列表
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:export')")
+    @PreAuthorize("@ss.hasPermi('store:adv:export') or @ss.hasPermi('his:adv:export')")
     @Log(title = "广告", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(FsAdv fsAdv)
@@ -65,7 +65,7 @@ public class FsAdvController extends BaseController
     /**
      * 获取广告详细信息
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:query')")
+    @PreAuthorize("@ss.hasPermi('store:adv:query') or @ss.hasPermi('his:adv:query')")
     @GetMapping(value = "/{advId}")
     public AjaxResult getInfo(@PathVariable("advId") String advId)
     {
@@ -76,7 +76,7 @@ public class FsAdvController extends BaseController
     /**
      * 新增广告
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:add')")
+    @PreAuthorize("@ss.hasPermi('store:adv:add') or @ss.hasPermi('his:adv:add')")
     @Log(title = "广告", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody FsAdv fsAdv)
@@ -91,7 +91,7 @@ public class FsAdvController extends BaseController
     /**
      * 修改广告
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:edit')")
+    @PreAuthorize("@ss.hasPermi('store:adv:edit') or @ss.hasPermi('his:adv:edit')")
     @Log(title = "广告", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody FsAdv fsAdv)
@@ -105,7 +105,7 @@ public class FsAdvController extends BaseController
     /**
      * 删除广告
      */
-    @PreAuthorize("@ss.hasPermi('store:adv:remove')")
+    @PreAuthorize("@ss.hasPermi('store:adv:remove') or @ss.hasPermi('his:adv:remove')")
     @Log(title = "广告", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{advIds}")
     public AjaxResult remove(@PathVariable String[] advIds)

+ 1 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -106,6 +106,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
                     .map(vo -> {
                         FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
                         try {
+                            zmvo.setPayCode(vo.getPayCode());
                             zmvo.setOrderCode(vo.getOrderCode());
                             zmvo.setStatus(vo.getOrderStatus().toString());
                             zmvo.setUserId(vo.getUserId());

+ 2 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -26,6 +26,7 @@ import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.param.FsStoreOrderParam;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
+import com.github.pagehelper.PageHelper;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -82,7 +83,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
 //    @PreAuthorize("@ss.hasPermi('store:healthStoreOrder:list')")
       @PostMapping("/healthList")
       public TableDataInfo healthStoreList(@RequestBody FsStoreOrderParam param) {
-        startPage();
+          PageHelper.startPage(param.getPageNum(), param.getPageSize());
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCreateTimeList(param.getCreateTimeRange().split("--"));
         }

+ 5 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -93,6 +93,10 @@ public class LiveAfterSalesController extends BaseController
     public TableDataInfo list(LiveAfterSalesVo liveAfterSales)
     {
         startPage();
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));
@@ -116,6 +120,7 @@ public class LiveAfterSalesController extends BaseController
                     .map(vo -> {
                         FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
                         try {
+                            zmvo.setPayCode(vo.getPayCode());
                             zmvo.setOrderCode(vo.getOrderCode());
                             zmvo.setStatus(vo.getOrderStatus().toString());
                             zmvo.setUserId(vo.getUserId());

+ 33 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwFriendWelcomeController.java

@@ -0,0 +1,33 @@
+package com.fs.qw.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.param.QwFriendWelcomeParam;
+import com.fs.qw.service.IQwFriendWelcomeService;
+import com.fs.qw.vo.QwFriendWelcomeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/qw/friendWelcome")
+public class QwFriendWelcomeController extends BaseController {
+
+    @Autowired
+    private IQwFriendWelcomeService qwFriendWelcomeService;
+
+    /**
+     * 查询好友欢迎语列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:friendWelcome:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwFriendWelcomeParam qwFriendWelcomeParam) {
+        startPage();
+        List<QwFriendWelcomeVO> list = qwFriendWelcomeService.selectQwFriendWelcomeList(qwFriendWelcomeParam);
+        return getDataTable(list);
+    }
+}

+ 7 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -354,4 +354,11 @@ public class QwUserController extends BaseController {
         List<QwUserVO> list = qwUserService.selectAllQwUserListVO(qwUser);
         return getDataTable(list);
     }
+
+    @GetMapping("/getQwAllUserList")
+    public R getQwAllUserList(@RequestParam String corpId, @RequestParam Long companyId)
+    {
+        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId, companyId);
+        return  R.ok().put("data",list);
+    }
 }

+ 3 - 3
fs-admin/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 3 - 3
fs-common-api/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 3 - 3
fs-company-app/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 4 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java

@@ -59,6 +59,10 @@ public class LiveAfterSalesController extends BaseController
         startPage();
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
         liveAfterSales.setCompanyId(user.getCompanyId());
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));

+ 13 - 2
fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java

@@ -3,6 +3,10 @@ package com.fs.company.controller.live;
 import java.util.List;
 
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.live.vo.LiveWatchLogListVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -34,7 +38,8 @@ public class LiveWatchLogController extends BaseController
 {
     @Autowired
     private ILiveWatchLogService liveWatchLogService;
-
+    @Autowired
+    private TokenService tokenService;
     /**
      * 查询直播看课记录列表
      */
@@ -43,7 +48,13 @@ public class LiveWatchLogController extends BaseController
     public TableDataInfo list(LiveWatchLog liveWatchLog)
     {
         startPage();
-        List<LiveWatchLog> list = liveWatchLogService.selectLiveWatchLogList(liveWatchLog);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(null == loginUser) {
+           throw new RuntimeException("用户信息错误");
+        }
+        Long companyId = loginUser.getCompany().getCompanyId();
+        liveWatchLog.setCompanyId(companyId);
+        List<LiveWatchLogListVO> list = liveWatchLogService.selectLiveWatchLogListInfo(liveWatchLog);
         return getDataTable(list);
     }
 

+ 111 - 0
fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagCollectRecordController.java

@@ -0,0 +1,111 @@
+package com.fs.company.controller.qw;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.LuckyBagCollectRecord;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.service.ILuckyBagCollectRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 福袋发放及领取记录Controller
+ *
+ * @author fs
+ * @date 2025-11-24
+ */
+@RestController
+@RequestMapping("/qw/luckyBagCollectRecord")
+public class LuckyBagCollectRecordController extends BaseController
+{
+    @Autowired
+    private ILuckyBagCollectRecordService luckyBagCollectRecordService;
+    @Autowired
+    private TokenService tokenService;
+    /**
+     * 查询福袋发放及领取记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        luckyBagCollectRecord.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<LuckyBagCollectRecord> list = luckyBagCollectRecordService.selectLuckyBagCollectRecordList(luckyBagCollectRecord);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出福袋发放及领取记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:export')")
+    @Log(title = "福袋发放及领取记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        luckyBagCollectRecord.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<LuckyBagCollectRecord> list = luckyBagCollectRecordService.selectLuckyBagCollectRecordList(luckyBagCollectRecord);
+        // 数据转换处理
+        list.forEach(item -> {
+            item.setRewardTypeName(item.getRewardTypeName());
+            item.setCollectTypeName(item.getCollectTypeName());
+        });
+
+        ExcelUtil<LuckyBagCollectRecord> util = new ExcelUtil<LuckyBagCollectRecord>(LuckyBagCollectRecord.class);
+        return util.exportExcel(list, "福袋发放及领取记录数据");
+    }
+
+    /**
+     * 获取福袋发放及领取记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(luckyBagCollectRecordService.selectLuckyBagCollectRecordById(id));
+    }
+
+    /**
+     * 新增福袋发放及领取记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:add')")
+    @Log(title = "福袋发放及领取记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        return toAjax(luckyBagCollectRecordService.insertLuckyBagCollectRecord(luckyBagCollectRecord));
+    }
+
+    /**
+     * 修改福袋发放及领取记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:edit')")
+    @Log(title = "福袋发放及领取记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        return toAjax(luckyBagCollectRecordService.updateLuckyBagCollectRecord(luckyBagCollectRecord));
+    }
+
+    /**
+     * 删除福袋发放及领取记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:luckyBagCollectRecord:remove')")
+    @Log(title = "福袋发放及领取记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(luckyBagCollectRecordService.deleteLuckyBagCollectRecordByIds(ids));
+    }
+}

+ 88 - 0
fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagController.java

@@ -0,0 +1,88 @@
+package com.fs.company.controller.qw;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.course.domain.LuckyBag;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.service.ILuckyBagService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 福袋管理
+ *
+ * @author fs
+ * @date 2024-12-02
+ */
+@RestController
+@RequestMapping("/qw/luckyBag")
+public class LuckyBagController extends BaseController
+{
+    @Autowired
+    private ILuckyBagService luckyBagService;
+    @Autowired
+    private TokenService tokenService;
+
+//    @PreAuthorize("@ss.hasPermi('course:reward:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LuckyBag reward)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        reward.setCompanyId(String.valueOf(loginUser.getCompany().getCompanyId()));
+        List<LuckyBag> list = luckyBagService.selectLuckyBagList(reward);
+        return getDataTable(list);
+    }
+
+
+//    @PreAuthorize("@ss.hasPermi('luckybag:reward:add')")
+    @Log(title = "奖励配置", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody LuckyBag reward) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        reward.setCreateName(loginUser.getUser().getNickName());
+        reward.setCompanyId(String.valueOf(loginUser.getCompany().getCompanyId()));
+        reward.setUpdateBy(String.valueOf(loginUser.getUser().getUserId()));
+        reward.setCreateId(loginUser.getUser().getUserId());
+        reward.setCreateTime(new Date());
+        reward.setUpdateId(loginUser.getUser().getUserId());
+        reward.setUpdateTime(new Date());
+       return toAjax(luckyBagService.add(reward));
+    }
+
+
+//    @PreAuthorize("@ss.hasPermi('course:reward:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        LuckyBag reward = new LuckyBag();
+        reward.setId(id);
+        return AjaxResult.success(luckyBagService.selectLuckyBagList(reward));
+    }
+
+//    @PreAuthorize("@ss.hasPermi('course:reward:edit')")
+    @Log(title = "修改福袋配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LuckyBag reward) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        reward.setUpdateId(loginUser.getUser().getUserId());
+        reward.setUpdateTime(new Date());
+        return toAjax(luckyBagService.updateLuckyBag(reward));
+    }
+
+//    @PreAuthorize("@ss.hasPermi('course:reward:remove')")
+    @Log(title = "奖励配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(luckyBagService.deleteLuckyBagByIds(ids));
+    }
+}

+ 2 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwFriendWelcomeController.java

@@ -17,6 +17,7 @@ import com.fs.qw.domain.QwFriendWelcome;
 import com.fs.qw.param.QwFriendWelcomeParam;
 import com.fs.qw.service.IQwFriendWelcomeService;
 import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.QwFriendWelcomeVO;
 import com.fs.qw.vo.QwOptionsVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -59,7 +60,7 @@ public class QwFriendWelcomeController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         qwFriendWelcomeParam.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
-        List<QwFriendWelcome> list = qwFriendWelcomeService.selectQwFriendWelcomeList(qwFriendWelcomeParam);
+        List<QwFriendWelcomeVO> list = qwFriendWelcomeService.selectQwFriendWelcomeList(qwFriendWelcomeParam);
         return getDataTable(list);
     }
 

+ 3 - 3
fs-company/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 20 - 5
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -40,10 +40,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -248,6 +245,21 @@ public class IpadSendServer {
         }
     }
 
+        public void sendTxtAtMsg(BaseVo vo) {
+        WxSendTextAtMsgTwoDTO dto = new WxSendTextAtMsgTwoDTO();
+        List<WxSendTextAtMsgTwoDTO.Contentva> contentvaList = new ArrayList<>();
+        WxSendTextAtMsgTwoDTO.Contentva contentva = new WxSendTextAtMsgTwoDTO.Contentva();
+        contentva.setMsgtype(5);
+        contentva.setVid(0);
+        contentvaList.add(contentva);
+        dto.setContentva(contentvaList);
+        dto.setBase(vo);
+         dto.setUuid(vo.getUuid());
+        dto.setSend_userid(ipadSendUtils.userIds(vo));
+        dto.setIsRoom(true);
+       ipadSendUtils.sendTxtAtMsgVo(dto, vo.getServerId());
+    }
+    
     public void sendVoice(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
         if (StringUtils.isEmpty(content.getVoiceUrl()) || StringUtils.isEmpty(content.getVoiceDuration())) {
             log.debug("语音未生成无法发送,转文字发送:{}", vo);
@@ -376,7 +388,7 @@ public class IpadSendServer {
         if(setting.getVideoId()!= null){
             FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId( setting.getVideoId().longValue());
             if(video != null){
-                if(video.getIsOnPut() == 1){
+                if(video.getIsOnPut()!=null && video.getIsOnPut() == 1){
                     log.warn("SOP_LOG_ID:{}, 视频已下架,不发送", qwSopLogs.getId());
                     qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "视频已下架,不发送");
                     return false;
@@ -465,6 +477,9 @@ public class IpadSendServer {
                     // 语音
                     sendWxVideo(vo, content);
                     break;
+                case "99":
+                    // 群发
+                    sendTxtAtMsg(vo);
                 default:
                     // 未知类型,记录警告
                     log.error("SOP_LOG_ID:{}错误的发送类型: {}", qwSopLogs.getId(), content.getContentType());

+ 18 - 1
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -2,6 +2,7 @@ package com.fs.app.task;
 
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.app.service.IpadSendServer;
 import com.fs.common.core.redis.RedisCacheT;
@@ -272,7 +273,23 @@ public class SendMsg {
                 new Thread(() -> {
                     try {
                         List<QwSopTempSetting.Content.Setting> settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "9".equals(e.getContentType())).collect(Collectors.toList());
-                        asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(settings, qwSopLogs.getCorpId(), user.getCompanyUserId(), qwSopLogs.getFsUserId());
+                        if (!settings.isEmpty()) {
+                            asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(settings, qwSopLogs.getCorpId(), user.getCompanyUserId(), qwSopLogs.getFsUserId());
+                        }
+
+                        //app文本消息
+                        log.info("开始发送app文本消息消息开始,消息{},用户{}", JSONObject.toJSONString(settings), user.getQwUserName());
+                        settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "15".equals(e.getContentType())).collect(Collectors.toList());
+
+                        if (!settings.isEmpty()) {
+                            asyncSopTestService.asyncSendMsgBySopAppTxtNormalIM(settings, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId());
+                        }
+                        //app语音消息
+                        log.info("开始发送app语音消息消息开始,消息{},用户{}", JSONObject.toJSONString(settings), user.getQwUserName());
+                        settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "16".equals(e.getContentType())).collect(Collectors.toList());
+                        if (!settings.isEmpty()) {
+                            asyncSopTestService.asyncSendMsgBySopAppMP3NormalIM(settings, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId());
+                        }
                     } catch (Exception e) {
                         log.error("推送APP失败", e);
                     }

+ 159 - 0
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -0,0 +1,159 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 直播完课积分定时任务
+ */
+@Slf4j
+@Component
+public class LiveCompletionPointsTask {
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    @Autowired
+    private ILiveService liveService;
+
+    /**
+     * 定时检查观看时长并创建完课记录
+     * 每分钟执行一次
+     * 优化:使用Hash结构 + 防重复推送
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionStatus() {
+        try {
+            List<Live> activeLives = liveService.selectNoEndLiveList();
+            
+            if (activeLives == null || activeLives.isEmpty()) {
+                return;
+            }
+
+            for (Live live : activeLives) {
+                try {
+                    Long liveId = live.getLiveId();
+                    
+                    // 使用Hash结构获取该直播间所有用户的观看时长
+                    String hashKey = "live:watch:duration:hash:" + liveId;
+                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
+                    
+                    if (userDurations == null || userDurations.isEmpty()) {
+                        continue;
+                    }
+                    
+                    // 3. 逐个用户处理
+                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                        try {
+                            Long userId = Long.parseLong(entry.getKey().toString());
+                            
+                            // 4. 检查并创建完课记录(传null,自动累计直播+回放时长)
+                            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, null);
+
+                            // 5. 检查是否有新的完课记录待领取,推送弹窗消息(防重复)
+                            sendCompletionNotificationOnce(liveId, userId);
+
+                        } catch (Exception e) {
+                            log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
+                        }
+                    }
+                    
+                } catch (Exception e) {
+                    log.error("处理直播间完课状态失败, liveId={}", live.getLiveId(), e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("检查完课状态定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 发送完课通知(通过WebSocket推送弹窗) - 防重复版本
+     */
+    private void sendCompletionNotificationOnce(Long liveId, Long userId) {
+        try {
+            // 1. 检查 Redis 是否已推送过(防止每分钟都推送)
+            String notifyKey = "live:completion:notified:" + liveId + ":" + userId;
+            Boolean hasNotified = redisCache.hasKey(notifyKey);
+            
+            if (Boolean.TRUE.equals(hasNotified)) {
+                return;  // 已经推送过,不再重复推送
+            }
+            
+            // 2. 查询未领取的完课记录
+            List<LiveCompletionPointsRecord> unreceivedRecords = 
+                completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+            
+            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
+                // 3. 构造弹窗消息
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setCmd("completionPoints");
+                sendMsgVo.setMsg("完成任务!");
+                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+
+                // 4. 通过WebSocket发送给特定用户
+                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+                
+                // 5. 记录已推送,24小时后过期(第二天可以再次推送)
+                redisCache.setCacheObject(notifyKey, "1", 24, TimeUnit.HOURS);
+                
+                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}, points={}", 
+                        liveId, userId, unreceivedRecords.get(0).getPointsAwarded());
+            }
+        } catch (Exception e) {
+            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
+    /**
+     * 发送完课通知(通过WebSocket推送弹窗) - 旧版本(保留)
+     */
+    private void sendCompletionNotification(Long liveId, Long userId) {
+        try {
+            // 查询未领取的完课记录
+            List<LiveCompletionPointsRecord> unreceivedRecords = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+            
+            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
+                // 构造弹窗消息
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setCmd("completionPoints");
+                sendMsgVo.setMsg("完成任务!");
+                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+
+                // 通过WebSocket发送给特定用户(调用已有的发送方法)
+                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+                
+                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}", liveId, userId);
+            }
+        } catch (Exception e) {
+            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+}

+ 71 - 0
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -691,4 +691,75 @@ public class Task {
             log.error("扫描直播间打标签任务异常: error={}", e.getMessage(), e);
         }
     }
+
+    /**
+     * 批量同步Redis中的观看时长到数据库
+     * 每2分钟执行一次,减少数据库压力
+     */
+    @Scheduled(cron = "0 0/2 * * * ?")
+    @DistributeLock(key = "batchSyncWatchDuration", scene = "task")
+    public void batchSyncWatchDuration() {
+        try {
+            log.info("开始批量同步观看时长到数据库");
+            
+            // 优化:从所有直播间的Hash中批量获取数据
+            List<Live> activeLives = liveService.selectNoEndLiveList();
+            
+            if (activeLives == null || activeLives.isEmpty()) {
+                log.debug("当前没有活跃的直播间");
+                return;
+            }
+            
+            int totalCount = 0;
+            int successCount = 0;
+            int failCount = 0;
+            
+            // 逐个直播间处理
+            for (Live live : activeLives) {
+                try {
+                    Long liveId = live.getLiveId();
+                    
+                    // 使用Hash结构存储每个直播间的观看时长
+                    String hashKey = "live:watch:duration:hash:" + liveId;
+                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
+                    
+                    if (userDurations == null || userDurations.isEmpty()) {
+                        continue;
+                    }
+                    
+                    // 获取直播/回放标记(一次查询,所有用户复用)
+                    Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                    Integer liveFlag = flagMap.get("liveFlag");
+                    Integer replayFlag = flagMap.get("replayFlag");
+                    
+                    // 批量处理该直播间的所有用户
+                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                        try {
+                            Long userId = Long.parseLong(entry.getKey().toString());
+                            Long duration = Long.parseLong(entry.getValue().toString());
+                            
+                            totalCount++;
+                            
+                            // 异步更新数据库
+                            liveWatchUserService.updateWatchDuration(liveId, userId, liveFlag, replayFlag, duration);
+                            successCount++;
+                            
+                        } catch (Exception e) {
+                            failCount++;
+                            log.error("同步用户观看时长失败: liveId={}, userId={}, error={}", 
+                                    liveId, entry.getKey(), e.getMessage());
+                        }
+                    }
+                    
+                } catch (Exception e) {
+                    log.error("处理直播间观看时长失败: liveId={}, error={}", live.getLiveId(), e.getMessage());
+                }
+            }
+            
+            log.info("批量同步观看时长完成: 总数={}, 成功={}, 失败={}", totalCount, successCount, failCount);
+            
+        } catch (Exception e) {
+            log.error("批量同步观看时长任务异常", e);
+        }
+    }
 }

+ 40 - 1
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -58,7 +58,7 @@ public class WebSocketServer {
     // 心跳超时缓存:key=sessionId,value=最后心跳时间戳
     private final static ConcurrentHashMap<String, Long> heartbeatCache = new ConcurrentHashMap<>();
     // 心跳超时时间(毫秒):3分钟无心跳则认为超时
-    private final static long HEARTBEAT_TIMEOUT = 1 * 60 * 1000;
+    private final static long HEARTBEAT_TIMEOUT = 2 * 60 * 1000;
     // admin房间消息发送线程池(单线程,保证串行化)
     private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
 
@@ -315,6 +315,33 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
+
+                    // 心跳时同步更新观看时长到Redis Hash
+                    long watchUserId = (long) userProperties.get("userId");
+                    
+                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+                        try {
+                            Long currentDuration = Long.parseLong(msg.getData());
+                            
+                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
+                            String hashKey = "live:watch:duration:hash:" + liveId;
+                            String userIdField = String.valueOf(watchUserId);
+                            
+                            // 获取现有时长
+                            Object existingDuration = redisCache.redisTemplate.opsForHash().get(hashKey, userIdField);
+                            
+                            // 只有当新的时长更大时才更新(避免时间倒退)
+                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+                                // 更新Hash中的用户时长
+                                redisCache.redisTemplate.opsForHash().put(hashKey, userIdField, currentDuration.toString());
+                                // 设置过期时间(2小时)
+                                redisCache.redisTemplate.expire(hashKey, 2, TimeUnit.HOURS);
+                            }
+                        } catch (Exception e) {
+                            log.error("心跳更新观看时长失败, liveId={}, userId={}", liveId, watchUserId, e);
+                        }
+                    }
+                    
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -610,6 +637,18 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 发送完课积分弹窗通知给特定用户
+     */
+    public void sendCompletionPointsMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
     private void sendBlockMessage(Long liveId, Long userId) {
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);

+ 3 - 3
fs-qw-api/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 27 - 11
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -676,6 +676,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             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());
     }
@@ -700,7 +701,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         if(content.getSetting() == null){
             return;
         }
-        List<QwSopTempSetting.Content.Setting> setting = content.getSetting().stream().filter(e -> "7".equals(e.getContentType())).collect(Collectors.toList());
+        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()) {
@@ -708,13 +709,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
                     if (voiceList != null && !voiceList.isEmpty()) {
                         Map<String, QwSopTempVoice> collect = voiceList.stream().collect(Collectors.toMap(QwSopTempVoice::getVoiceTxt, e -> e));
-                        setting.parallelStream().filter(e -> "7".equals(e.getContentType())).forEach(st -> {
+
+                        setting.parallelStream().forEach(st -> {
                             QwSopTempVoice voice = collect.get(st.getValue());
-                            if (voice.getVoiceUrl() == null) {
+                            if (voice == null || voice.getVoiceUrl() == null) {
                                 return;
                             }
-                            st.setVoiceUrl(voice.getVoiceUrl());
-                            st.setVoiceDuration(voice.getDuration() + "");
+                            // 企微语音
+                            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) {
@@ -1023,7 +1033,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             log.error("Cloned content settings are empty, skipping.");
             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()) {
@@ -1104,14 +1119,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                 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);
+                                    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);
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,2);
                     }
 
                     String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
@@ -1148,7 +1163,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //app
                 case "9":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,1);
 
                     QwCreateLinkByAppVO linkByApp = createLinkByApp(setting, logVo, sendTime, courseId, videoId,
                             qwUserId, companyUserId, companyId, externalId,sopLogs.getCorpId(),qwUserName);
@@ -1161,7 +1176,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //自定义小程序
                 case "10":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,2);
 
                     Optional<Company> matchedCompany = companies.stream()
                             .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
@@ -1585,7 +1600,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     private void addWatchLogIfNeeded(QwSopLogs sopLogs, Long videoId, Long courseId,
                                      Date sendTime, String qwUserId, String companyUserId,
-                                     String companyId, String externalId,SopUserLogsVo logsVo) {
+                                     String companyId, String externalId,SopUserLogsVo logsVo,Integer watchType) {
         FsCourseWatchLog watchLog = new FsCourseWatchLog();
         watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
         watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
@@ -1600,6 +1615,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         watchLog.setUpdateTime(new Date());
         watchLog.setLogType(3);
         watchLog.setUserId(sopLogs.getFsUserId());
+        watchLog.setWatchType(watchType);
         watchLog.setCampPeriodTime(convertStringToDate(logsVo.getStartTime(),"yyyy-MM-dd"));
         enqueueWatchLog(watchLog);
     }

+ 3 - 3
fs-qw-task/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 3 - 3
fs-qwhook-msg/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 3 - 3
fs-qwhook-sop/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 3 - 3
fs-qwhook/src/main/resources/logback.xml

@@ -20,7 +20,7 @@
             <!-- 日志文件名格式 -->
 			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
@@ -42,7 +42,7 @@
             <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 			<!-- 日志最大的历史 30 -->
-			<maxHistory>30</maxHistory>
+			<maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
@@ -64,7 +64,7 @@
             <!-- 按天回滚 daily -->
             <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
             <!-- 日志最大的历史 30 -->
-            <maxHistory>30</maxHistory>
+            <maxHistory>7</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>

+ 8 - 0
fs-service/pom.xml

@@ -291,6 +291,14 @@
 <!--            <version>1.1.26</version>-->
 <!--        </dependency>-->
 
+        <!--火山云sdk-->
+        <dependency>
+            <groupId>com.volcengine</groupId>
+            <artifactId>volc-sdk-java</artifactId>
+            <version>1.0.250</version>
+        </dependency>
+
+
     </dependencies>
 
 </project>

+ 8 - 0
fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java

@@ -15,4 +15,12 @@ public class CloudHostProper {
 
     @Value("${cloud_host.projectCode}")
     private String projectCode;
+
+    //火山云空间名称
+    @Value("${cloud_host.spaceName}")
+    public String spaceName;
+
+    //火山云空间绑定域名
+    @Value("${cloud_host.volcengineUrl}")
+    public String volcengineUrl;
 }

+ 30 - 0
fs-service/src/main/java/com/fs/core/config/VolcEngineConfiguration.java

@@ -0,0 +1,30 @@
+package com.fs.core.config;
+
+import com.volcengine.service.vod.IVodService;
+import com.volcengine.service.vod.impl.VodServiceImpl;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class VolcEngineConfiguration {
+    @Value("${hsy.access_key:''}")
+    private String access_key;
+
+    @Value("${hsy.secret_key:''}")
+    private String secret_key;
+    @Value("${hsy.region:''}")
+    private String region;
+    @Bean
+    public IVodService vodService() throws Exception {
+        // 根据区域获取火山云点播服务实例
+        IVodService vodService = VodServiceImpl.getInstance(region);
+
+        // 设置 AccessKey 和 SecretKey
+        vodService.setAccessKey(access_key);
+        vodService.setSecretKey(secret_key);
+
+        return vodService;
+    }
+}
+

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

@@ -48,4 +48,9 @@ public class FsCourseRealLink implements Serializable
 
     @ApiModelProperty(value = "项目唯一标识(PS:MYHK)")
     private String projectCode;
+
+    /**
+     * 业务id
+     */
+    private String businessId;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseWatchLog.java

@@ -91,4 +91,6 @@ public class FsCourseWatchLog extends BaseEntity
     /** im发送消息详情id */
     private Long imMsgSendDetailId;
 
+    private Integer watchType;//看课方式:1 app  2 小程序
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java

@@ -124,4 +124,8 @@ public class FsUserCourseVideo extends BaseEntity
 
     // 是否上架 0:上架,1:下架
     private Integer isOnPut;
+
+    private String jobId;
+
+    private String vid;
 }

+ 69 - 0
fs-service/src/main/java/com/fs/course/domain/LuckyBag.java

@@ -0,0 +1,69 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 奖励配置对象 fs_course_reward
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LuckyBag extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 福袋名称 */
+    @Excel(name = "福袋名称")
+    private String name;
+
+    /** 奖励类型 (1:定值, 2:宝箱) */
+    @Excel(name = "奖励类型 (1:定值, 2:宝箱)")
+    private String type;
+
+    /**
+     * 是否删除 0-正常 1-删除
+     */
+    @Excel(name = "状态 (0:删除, 1:正常)")
+    private Long status;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 实际获得的奖励内容 */
+    @Excel(name = "实际获得的奖励内容")
+    private String actualRewards;
+
+    /** 实际获得的奖励内容 */
+    @Excel(name = "创建人")
+    private String createName;
+
+    @Excel(name = "所属公司")
+    private String companyId;
+
+    /** 创建人ID */
+    @Excel(name = "修改人ID")
+    private Long updateId;
+
+    /**
+     * 定额金额
+     */
+    private BigDecimal amount;
+
+    /** 状态 (0:禁用, 1:启用) */
+    private String dataStatus;
+
+    /**
+     * 奖励类型 1-定额 2-随机
+     */
+    private String rewardType;
+
+}

+ 151 - 0
fs-service/src/main/java/com/fs/course/domain/LuckyBagCollectRecord.java

@@ -0,0 +1,151 @@
+package com.fs.course.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 福袋发放及领取记录对象 lucky_bag_collect_record
+ *
+ * @author fs
+ * @date 2025-11-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LuckyBagCollectRecord extends BaseEntity{
+
+
+
+    /** 领取用户ID */
+    @Excel(name = "领取用户ID")
+    private Long userId;
+
+    /** 客户名称 */
+    @Excel(name = "客户名称")
+    private String userName;
+
+    // 福袋名称
+    @Excel(name = "福袋名称")
+    private String luckyBagName;
+
+    /** 业务类型:1-群福袋 2-个人福袋 */
+//    @Excel(name = "业务类型:1-群福袋 2-个人福袋")
+    private Long rewardType;
+
+    @Excel(name = "业务类型")
+    private String rewardTypeName;
+
+    /** 福袋表主键ID */
+//    @Excel(name = "福袋表主键ID")
+    private Long luckyBagId;
+
+    /** 销售ID */
+//    @Excel(name = "销售ID")
+    private Long companyUserId;
+
+    /** 客服名称 */
+    @Excel(name = "客服名称")
+    private String companyUserName;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "公司名称")
+    private String companyName;
+
+    /** 芳华币数量 */
+    @Excel(name = "芳华币数量")
+    private BigDecimal coinAmount;
+
+    /** 发放时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "发放时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date sendTime;
+
+    /** 领取时间 精确到时分秒*/
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "领取时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date collectTime;
+
+    /** 失效时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd  HH:mm:ss")
+    @Excel(name = "失效时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date expiryTime;
+
+    /** 领取状态(0-已发放 1-已领取 2-已失效) */
+//    @Excel(name = "领取状态(0-已发放 1-已领取 2-已失效,3-待发放)")
+    private String collectType;
+
+    @Excel(name = "领取状态")
+    private String collectTypeName;
+
+    /** 群聊名称 */
+    @Excel(name = "群聊名称")
+    private String externalUserName;
+
+    // 企微用户id
+    private String qwUserId;
+
+    /** 企微用户名称 */
+    @Excel(name = "企微员工名称")
+    private String qwUserName;
+
+
+    /** 群聊会话id */
+//    @Excel(name = "群聊会话id")
+    private String chatId;
+
+    /** 主键ID */
+    @Excel(name = "领取Id")
+    private Long id;
+
+    /** 关联id 关联群发记录id */
+//    @Excel(name = "关联id 关联群发记录id")
+    @Excel(name = "群领取Id")
+    private Long relationId;
+
+    /** 发送链接 */
+//    @Excel(name = "发送链接")
+    private String sendLink;
+
+    // 更新时间
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    // 在 getter 中进行转换
+    public String getRewardTypeName() {
+        if (this.rewardType == null) return "";
+        switch (this.rewardType.intValue()) {
+            case 1: return "群福袋";
+            case 2: return "个人福袋";
+            default: return "";
+        }
+    }
+
+    public String getCollectTypeName() {
+        if (this.collectType == null) {
+            return "";
+        }
+        switch (this.collectType) {
+            case "0":
+                return "已发放";
+            case "1":
+                return "已领取";
+            case "2":
+                return "已失效";
+            default:
+                return this.collectType;
+        }
+    }
+
+
+
+}

+ 2 - 2
fs-service/src/main/java/com/fs/course/mapper/FsCourseFinishTempMapper.java

@@ -87,9 +87,9 @@ public interface FsCourseFinishTempMapper
             "<if test = ' maps.status !=null '> " +
             "and t.status = #{maps.status} " +
             "</if>" +
-            "            <if test=\"userIds != null and !userIds.isEmpty()\">\n" +
+            "            <if test=\" maps.userIds != null and !maps.userIds.isEmpty()\">\n" +
             "                AND create_by IN\n" +
-            "                <foreach collection='userIds' item='item' open='(' separator=',' close=')'>\n" +
+            "                <foreach collection=' maps.userIds' item='item' open='(' separator=',' close=')'>\n" +
             "                    #{item}\n" +
             "                </foreach>\n" +
             "            </if>" +

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

@@ -75,6 +75,9 @@ public interface FsCourseRedPacketLogMapper
     @Select("select * from fs_course_red_packet_log where out_batch_no = #{outBatchNo}")
     FsCourseRedPacketLog selectFsCourseRedPacketLogByBatchNo(@Param("outBatchNo") String outBatchNo);
 
+    @Select("select * from fs_course_red_packet_log where batch_id = #{batchId}")
+    FsCourseRedPacketLog selectFsCourseRedPacketLogByBatchId(@Param("batchId") String batchId);
+
 
     @Select({"<script> " +
             "select o.*,v.title as video_name,u.phone as user_name  from fs_course_red_packet_log l " +

+ 10 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -209,7 +209,7 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
             "        <if test=\"data.keyword != null and data.keyword !='' \">\n" +
             "            AND v.title LIKE concat('%',#{data.keyword},'%')\n" +
             "        </if>" +
-            "order by v.video_id asc " +
+            "order by v.course_sort asc, v.video_id asc " +
             "</script>")
     List<FsCourseVideoListBySidebarVO> getFsCourseVideoListBySidebar(@Param("data") FsCourseListBySidebarParam param);
 
@@ -265,4 +265,13 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     @Select("select video_id,is_first,course_sort,tg_id,watching_tg_id,watched_tg_id,watching_tag_id,watched_tag_id,tag_group_id from fs_user_course_video")
     @MapKey("videoId")
     Map<Long, FsUserCourseVideo> selectAllMap();
+
+    @Select("select * from fs_user_course_video where line_two is not null and job_id is null")
+    List<FsUserCourseVideo> selectVideoByHuaWei();
+
+    @Select("select * from fs_user_course_video where job_id is not null and vid is null")
+    List<FsUserCourseVideo> selectVideoByJobId();
+
+    @Select("select * from fs_user_course_video where vid is not null")
+    List<FsUserCourseVideo> selectVideoByVid();
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java

@@ -114,6 +114,7 @@ public class FsCourseWatchLogListParam implements Serializable {
     private Integer pageNum;
     private Integer pageSize;
     private List<String> userIds;
+    private Integer watchType;
 
     public List<String> getUserIds() {
         if (userIds == null || userIds.isEmpty()) {

+ 22 - 0
fs-service/src/main/java/com/fs/course/param/LuckyBagActualRewardsParam.java

@@ -0,0 +1,22 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@Data
+public class LuckyBagActualRewardsParam {
+
+    @JsonProperty("type")
+    private Integer type;
+    @JsonProperty("name")
+    private String name;
+    @JsonProperty("amount")
+    private String amount;
+    @JsonProperty("probability")
+    private String probability;
+    @JsonProperty("code")
+    private String code;
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseRedPacketLogService.java

@@ -87,4 +87,6 @@ public interface IFsCourseRedPacketLogService
     void sendRedPacketBf();
 
     void queryRedPacketResult(String startTime, String endTime);
+
+    R getBillsByTransferBillNo(String batchId);
 }

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

@@ -222,4 +222,22 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
     R sendAppReward(FsCourseSendRewardUParam param);
 
     R isSaveKf(FsUserCourseVideoAddKfUParam param);
+
+    /**
+     * 上传视频到火山云通过URL(把华为云的视频传到火山去并且存储JOBID)
+     * @return
+     */
+    R uploadVideoToHuoShanByUrl();
+
+    /**
+     * 通过jobId拿vid
+     * @return
+     */
+    R getVidByJob();
+
+    /**
+     * 通过vid获取视频路径并且组装地址存到替换线路二
+     * @return
+     */
+    R getVideoInfoByVid();
 }

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

@@ -476,4 +476,28 @@ public class FsCourseRedPacketLogServiceImpl implements IFsCourseRedPacketLogSer
                 company.getCompanyId(), moneyLog.getMoney());
     }
 
+    @Override
+    public R getBillsByTransferBillNo(String batchId) {
+        FsCourseRedPacketLog log = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogByBatchId(batchId);
+        if (log ==null){
+            return R.error("未查询到红包记录!");
+        }
+        String json = configService.selectConfigByKey("redPacket.config");
+        RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+        //创建微信订单
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config,payConfig);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService=wxPayService.getTransferService();
+
+        try {
+            TransferBillsGetResult queryRedPacketResult = transferService.getBillsByTransferBillNo(batchId);
+            logger.info("FsCourseRedPacketLog-log_id:{},【红包处理】查询批次结果:{}",log.getLogId(),queryRedPacketResult);
+            return R.ok(queryRedPacketResult.toString());
+        } catch (WxPayException e) {
+            logger.error(e.getMessage());
+            return R.error(e.getMessage());
+        }
+    }
 }

+ 265 - 6
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -62,6 +62,9 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.domain.*;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwGroupChatMapper;
 import com.fs.qw.mapper.QwGroupChatUserMapper;
@@ -79,6 +82,14 @@ import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import com.volcengine.service.vod.IVodService;
+import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
+import com.volcengine.service.vod.model.request.VodGetMediaInfosRequest;
+import com.volcengine.service.vod.model.request.VodQueryUploadTaskInfoRequest;
+import com.volcengine.service.vod.model.request.VodUrlUploadRequest;
+import com.volcengine.service.vod.model.response.VodGetMediaInfosResponse;
+import com.volcengine.service.vod.model.response.VodQueryUploadTaskInfoResponse;
+import com.volcengine.service.vod.model.response.VodUrlUploadResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.redisson.api.RLock;
@@ -94,11 +105,12 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 
@@ -999,6 +1011,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         log.setQwUserId(Long.valueOf(param.getQwUserId()));
         log.setCreateTime(new Date());
         log.setLogType(3);
+        log.setWatchType(2);
         logger.info("【群聊生成看课记录】:{}", param);
         courseWatchLogMapper.insertFsCourseWatchLog(log);
     }
@@ -3012,7 +3025,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
 
         //看课记录
-        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId());
+        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId(),2);
 
         //生成小程序链接
         String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(), 2, null, 0);
@@ -3055,7 +3068,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             domainName = config.getRealLinkDomainName();
         }
 
-        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId());
+        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId(),2);
 
         String linkByCartLink = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(), 1, domainName, 0);
 
@@ -3073,7 +3086,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
     //插入观看记录
     private void addWatchLogIfNeeded(Long videoId, Long courseId,
-                                     Long fsUserId, QwUser qwUser, Long externalId) {
+                                     Long fsUserId, QwUser qwUser, Long externalId,Integer watchType) {
 
         try {
 
@@ -3089,7 +3102,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             watchLog.setCreateTime(new Date());
             watchLog.setUpdateTime(new Date());
             watchLog.setLogType(3);
-
+            watchLog.setWatchType(watchType);
             if (fsUserId == null) {
                 fsUserId = 0L;
             }
@@ -4030,5 +4043,251 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
 
+    @Override
+    public R uploadVideoToHuoShanByUrl() {
+        List <FsUserCourseVideo> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
+        for (FsUserCourseVideo video : videos){
+            uploadVideoByUrl(video);
+        }
+        return R.ok();
+    }
+
+    @Autowired
+    private IVodService vodService;
+
+    //通过Url上传视频
+    public void uploadVideoByUrl(FsUserCourseVideo courseVideo) {
+
+        try {
+            VodUrlUploadRequest.Builder reqBuilder = VodUrlUploadRequest.newBuilder();
+            //空间名称
+            reqBuilder.setSpaceName(cloudHostProper.getSpaceName());
+            VodUrlUploadURLSet.Builder uRLSetsBuilder = VodUrlUploadURLSet.newBuilder();
+            //源文件 URL
+            uRLSetsBuilder.setSourceUrl(courseVideo.getLineTwo());//华为云
+            //存储类型。默认为 1。取值如下:
+            //1:标准存储。
+            //2:归档存储。
+            //3:低频存储。
+            uRLSetsBuilder.setStorageClass(1);
+            //文件后缀,即点播存储中文件的类型。
+            uRLSetsBuilder.setFileExtension(".mp4");
+            //用户额外信息,最大长度 512 字节。
+            uRLSetsBuilder.setCallbackArgs("");
+            // 火山云存储路径(文件上传后在火山云的路径)
+//            String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
+//            String fileName = System.currentTimeMillis() + ".mp4";
+//            String remoteFileName = "fs/" + datePath + "/" + fileName;
+
+            uRLSetsBuilder.setFileName(courseVideo.getFileKey());
+            reqBuilder.addURLSets(uRLSetsBuilder);
+
+            VodUrlUploadResponse resp = vodService.uploadMediaByUrl(reqBuilder.build());
+
+            if (resp.getResponseMetadata().hasError()) {
+                log.info("上传返回异常:{}",resp.getResponseMetadata().getError());
+                System.exit(-1);
+            }else {
+                FsUserCourseVideo video = new FsUserCourseVideo();
+                video.setVideoId(courseVideo.getVideoId());
+                video.setJobId(resp.getResult().getData(0).getJobId());
+                //更新JobId
+                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+            }
+            log.info("上传返回参数:{}",resp);
+        } catch (Exception e) {
+            throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public R getVidByJob() {
+        // 查询有JobId的视频
+        List<FsUserCourseVideo> list = fsUserCourseVideoMapper.selectVideoByJobId();
+        if (list.isEmpty()) {
+            log.info("没有待上传的视频任务");
+            return R.error();
+        }
+        // 按五百一批切割
+        List<List<FsUserCourseVideo>> batches = splitList(list, 500);
+        log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
+
+        int batchIndex = 1;
+
+        // 批次顺序执行,每批内部多线程并发执行
+        for (List<FsUserCourseVideo> batch : batches) {
+
+            log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
+
+            CountDownLatch latch = new CountDownLatch(batch.size());
+
+            for (FsUserCourseVideo video : batch) {
+                uploadExecutor.submit(() -> {
+                    try {
+                        uploadSingleTaskWithRetry(video,1);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // 等待这一批全部完成
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                log.error("批次等待异常", e);
+            }
+
+            log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
+            batchIndex++;
+        }
+
+        log.info("全部批次执行完成");
+        return R.ok();
+    }
+
+    @Override
+    public R getVideoInfoByVid() {
+        // 查询有JobId的视频
+        List<FsUserCourseVideo> list = fsUserCourseVideoMapper.selectVideoByVid();
+        if (list.isEmpty()) {
+            log.info("没有包含vid的视频");
+            return R.error();
+        }
+        // 按五百一批切割
+        List<List<FsUserCourseVideo>> batches = splitList(list, 500);
+        log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
+
+        int batchIndex = 1;
+
+        // 批次顺序执行,每批内部多线程并发执行
+        for (List<FsUserCourseVideo> batch : batches) {
+
+            log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
+
+            CountDownLatch latch = new CountDownLatch(batch.size());
+
+            for (FsUserCourseVideo video : batch) {
+                uploadExecutor.submit(() -> {
+                    try {
+                        uploadSingleTaskWithRetry(video,2);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // 等待这一批全部完成
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                log.error("批次等待异常", e);
+            }
+
+            log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
+            batchIndex++;
+        }
+
+        log.info("全部批次执行完成");
+        return R.ok();
+    }
+
+    private final ExecutorService uploadExecutor = new ThreadPoolExecutor(
+            8,  // core
+            16, // max
+            60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(2000),
+            new ThreadFactory() {
+                private final AtomicInteger index = new AtomicInteger(1);
+
+                @Override
+                public Thread newThread(Runnable r) {
+                    return new Thread(r, "video-upload-" + index.getAndIncrement());
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
+    public <T> List<List<T>> splitList(List<T> list, int batchSize) {
+        List<List<T>> result = new ArrayList<>();
+        int total = list.size();
+        for (int i = 0; i < total; i += batchSize) {
+            result.add(list.subList(i, Math.min(total, i + batchSize)));
+        }
+        return result;
+    }
+
+    //根据jobid查询上传视频的vid
+    public void getVidByJobId(FsUserCourseVideo courseVideo){
+        try {
+            VodQueryUploadTaskInfoRequest.Builder reqBuilder = VodQueryUploadTaskInfoRequest.newBuilder();
+            reqBuilder.setJobIds(courseVideo.getJobId());
+
+            VodQueryUploadTaskInfoResponse resp = vodService.queryUploadTaskInfo(reqBuilder.build());
+            if (resp.getResponseMetadata().hasError()) {
+                System.out.println(resp.getResponseMetadata().getError());
+                System.exit(-1);
+            }else {
+                FsUserCourseVideo video = new FsUserCourseVideo();
+                video.setVideoId(courseVideo.getVideoId());
+                video.setVid(resp.getResult().getData().getMediaInfoList(0).getVid());
+                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+            }
+            System.out.println(resp);
+        } catch (Exception e) {
+            throw new RuntimeException("查询 URL 批量上传任务状态: " + e.getMessage(), e);
+        }
+    }
+
+
+    public void getVideoInfoByVid(FsUserCourseVideo courseVideo) {
+        try {
+            VodGetMediaInfosRequest.Builder reqBuilder = VodGetMediaInfosRequest.newBuilder();
+            reqBuilder.setVids(courseVideo.getVid());
+
+            VodGetMediaInfosResponse resp = vodService.getMediaInfos20230701(reqBuilder.build());
+            if (resp.getResponseMetadata().hasError()) {
+                System.out.println(resp.getResponseMetadata().getError());
+                System.exit(-1);
+            }else {
+                //更新小节
+                FsUserCourseVideo video = new FsUserCourseVideo();
+                video.setVideoId(courseVideo.getVideoId());
+                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+                video.setLineTwo(url);
+                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+                //更新视频资源
+                FsVideoResource videoResource = fsVideoResourceMapper.selectByFileKey(courseVideo.getFileKey());
+                videoResource.setLine2(url);
+                fsVideoResourceMapper.updateById(videoResource);
+            }
+            System.out.println(resp);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    public void uploadSingleTaskWithRetry(FsUserCourseVideo courseVideo,Integer type) {
+        int maxRetry = 3;
+        for (int i = 1; i <= maxRetry; i++) {
+            try {
+                if (type == 1){
+                    //获取上传成功的视频vid,同步到数据库
+                    getVidByJobId(courseVideo);
+                }else if (type == 2){
+                    //获取视频地址同步到线路二
+                    getVideoInfoByVid(courseVideo);
+                }
+                return;
+            } catch (Exception e) {
+                log.error("视频 {} 上传失败,第 {} 次重试,原因:{}",
+                        courseVideo.getVideoId(), i, e.getMessage());
+                if (i == maxRetry) {
+                    log.error("视频 {} 上传最终失败!", courseVideo.getVideoId());
+                }
+            }
+        }
+    }
 }
 

+ 1 - 1
fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -844,7 +844,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         log.info("订单号: {},发货状态: {},是否发货后: {}",fsStoreOrder.getOrderCode(),fsStoreOrder.getStatus(),ObjectUtils.equals(fsStoreOrder.getStatus(),2));
 
         // 发货后退款
-        if(ObjectUtils.equals(param.getOrderStatus(),2)){
+        if(ObjectUtils.equals(param.getOrderStatus(),2) || ObjectUtils.equals(param.getOrderStatus(),3)){
 
             FsJstAftersalePush fsJstAftersalePush = new FsJstAftersalePush();
             fsJstAftersalePush.setOrderId(fsStoreOrder.getOrderCode());

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -1394,7 +1394,7 @@ public class AiHookServiceImpl implements AiHookService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     // sendWebSocketMsg(s,msgVo,user,session);
@@ -1425,7 +1425,7 @@ public class AiHookServiceImpl implements AiHookService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             // sendWebSocketMsg(s,msgVo,user,session);
@@ -1935,7 +1935,7 @@ public class AiHookServiceImpl implements AiHookService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1966,7 +1966,7 @@ public class AiHookServiceImpl implements AiHookService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java

@@ -552,7 +552,7 @@ public class AiNewServiceImpl implements AiNewService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebSocketMsg(s,msgVo,user,session);
@@ -583,7 +583,7 @@ public class AiNewServiceImpl implements AiNewService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             sendWebSocketMsg(s,msgVo,user,session);
@@ -1070,7 +1070,7 @@ public class AiNewServiceImpl implements AiNewService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1101,7 +1101,7 @@ public class AiNewServiceImpl implements AiNewService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiServiceImpl.java

@@ -530,7 +530,7 @@ public class AiServiceImpl implements AiService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebSocketMsg(s,msgVo,user,session);
@@ -561,7 +561,7 @@ public class AiServiceImpl implements AiService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             sendWebSocketMsg(s,msgVo,user,session);
@@ -1048,7 +1048,7 @@ public class AiServiceImpl implements AiService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1079,7 +1079,7 @@ public class AiServiceImpl implements AiService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 8 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserIntegralLogs.java

@@ -5,6 +5,8 @@ import com.fs.common.core.domain.BaseEntity;
 import io.swagger.models.auth.In;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 积分记录对象 fs_user_integral_logs
  *
@@ -42,4 +44,10 @@ public class FsUserIntegralLogs extends BaseEntity
     private Integer businessType;
 
     private Integer status;
+
+    private String nickName;
+
+    private String phone;
+
+    private BigDecimal commission;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserInformationCollectionMapper.java

@@ -100,4 +100,6 @@ public interface FsUserInformationCollectionMapper extends BaseMapper<FsUserInfo
     List<FsUserInformationCollection>selectFsUserInformationCollectionByDoctorType2(@Param("maps") UserInformationDoctorType2Param userInformationDoctorType2Param);
     List<FsUserInformationCollection>selectFsUserInformationCollectionByDoctorType1(@Param("maps") UserInformationDoctorType2Param userInformationDoctorType2Param);
     FsUserInformationCollection selectFsUserInformationCollectionByOrderCode(String orderCode);
+
+    List<FsUserInformationCollection> selectListByIsPayAndConfirmStatus();
 }

+ 26 - 0
fs-service/src/main/java/com/fs/his/param/FsReceiveLuckyBagParam.java

@@ -0,0 +1,26 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+/**
+ * @description: TODO
+ * @author: Xgb
+ * @createDate: 2025/11/21
+ * @version: 1.0
+ */
+@Data
+public class FsReceiveLuckyBagParam {
+
+    // 记录id
+    private Long recordId;
+
+    // 用户id
+    private Long userId;
+
+    // 企微员工id
+    private String userName;
+
+    // 企微主体id
+    private String corpId;
+
+}

+ 8 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -638,6 +638,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                     break;
                 case 2:
                     json = companyConfigService.selectRedPacketConfigByKey(param.getCompanyId());
+                    //如果分公司配置为空就走总后台的配置
+                    if (StringUtils.isEmpty(json)){
+                        json = configService.selectConfigByKey("redPacket.config");
+                    }
                     config = JSONUtil.toBean(json, RedPacketConfig.class);
                     break;
                 default:
@@ -763,6 +767,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                     break;
                 case 2:
                     json = companyConfigService.selectRedPacketConfigByKey(param.getCompanyId());
+                    //如果分公司配置为空就走总后台的配置
+                    if (StringUtils.isEmpty(json)){
+                        json = configService.selectConfigByKey("redPacket.config");
+                    }
                     config = JSONUtil.toBean(json, RedPacketConfig.class);
                     break;
                 default:

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreAfterSalesScrm.java

@@ -140,4 +140,14 @@ public class FsStoreAfterSalesScrm extends BaseEntity
 
     private String remark;
 
+    /** 产品名称查询参数(用于搜索) */
+    @TableField(exist = false)
+    private String productName;
+
+    /**
+     * 用于查询汇付订单号
+     */
+    @TableField(exist = false)
+    private String hfOrderCode;
+
 }

+ 7 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -91,7 +91,7 @@ public interface FsStoreAfterSalesScrmMapper
             "cu.phonenumber as company_usere_phonenumber,o.pay_money,o.id as orderId,o.create_time as orderCreateTime,o.user_phone," +
             "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage," +
             "fsps.bank_serial_no,fsps.bank_transaction_id,o.delivery_id as orderDeliveryId,o.delivery_name as orderDeliveryName,o.delivery_sn as orderDeliverySn," +
-            "o.status as orderStatus " +
+            "o.status as orderStatus,fsps.pay_code as payCode " +
             " from fs_store_after_sales_scrm s " +
             " INNER join fs_store_order_scrm o on o.order_code=s.order_code " +
             " left join fs_user u on s.user_id=u.user_id " +
@@ -99,6 +99,9 @@ public interface FsStoreAfterSalesScrmMapper
             " left join company_user cu on cu.user_id=s.company_user_id " +
             " left join fs_store_payment_scrm fsps on fsps.business_order_id = o.id and fsps.status in (-1,1) " +
             " where 1=1 " +
+            "<if test =\"maps.hfOrderCode != null and  maps.hfOrderCode!='' \"> " +
+              "and fsps.pay_code = #{maps.hfOrderCode} " +
+            "</if>" +
             "<if test = 'maps.status != null    '> " +
             "and s.status = #{maps.status} " +
             "</if>" +
@@ -140,6 +143,9 @@ public interface FsStoreAfterSalesScrmMapper
             "<if test = 'maps.consigneePhone != null and  maps.consigneePhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.consigneePhone},'%') " +
             "</if>" +
+            "<if test = 'maps.productName != null and  maps.productName != \"\" '> " +
+            "and EXISTS (SELECT 1 FROM fs_store_order_item_scrm oi WHERE oi.order_id = o.id AND JSON_UNQUOTE(JSON_EXTRACT(oi.json_info, '$.productName')) LIKE CONCAT('%', #{maps.productName}, '%')) " +
+            "</if>" +
             "<if test = 'maps.deptId != null    '> " +
             "  AND (o.dept_id = #{maps.deptId} OR o.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors) )) " +
             "</if>" +

+ 16 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -87,7 +87,17 @@ public interface FsStoreOrderItemScrmMapper
             " left join company_tcm_schedule cts on cts.id = o.schedule_id " +
             " left join fs_store_product_scrm psps on i.product_id=psps.product_id " +
             " left join fs_store_product_category_scrm fspcs on fspcs.cate_id=psps.cate_id " +
+            "            LEFT JOIN (\n" +
+            "            SELECT\n" +
+            "            sp.*,\n" +
+            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
+            "            FROM fs_store_payment_scrm sp\n" +
+            "            WHERE sp.business_code IS NOT NULL\n" +
+            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
             " where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
@@ -162,7 +172,7 @@ public interface FsStoreOrderItemScrmMapper
             "left join company_user cu on cu.user_id=o.company_user_id " +
             "left join company_tcm_schedule cts on cts.id = o.schedule_id " +
             "LEFT JOIN fs_store_order_df df on df.order_id=o.id\n" +
-            "        <if test=\"maps.appId != null and maps.appId != ''\">\n" +
+            "        <if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">\n" +
             "            LEFT JOIN (\n" +
             "            SELECT\n" +
             "            sp.*,\n" +
@@ -170,9 +180,14 @@ public interface FsStoreOrderItemScrmMapper
             "            FROM fs_store_payment_scrm sp\n" +
             "            WHERE sp.business_code IS NOT NULL\n" +
             "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
+                        "<if test=\"maps.appId != null and maps.appId != ''\">" +
             "            LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id\n" +
+                        "</if>" +
             "        </if>" +
             "where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            "and sp_latest.bank_transaction_id = #{maps.bankTransactionId}\n" +
+            "</if>" +
             "<if test=\"maps.appId != null and maps.appId != ''\">\n" +
             "   and csc.appid = #{maps.appId}\n" +
             " </if>\n" +

+ 17 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -648,9 +648,24 @@ public interface FsStoreOrderScrmMapper
 
     @Select({"<script> " +
             "select o.*,cts.name as scheduleName,u.nickname,u.phone,cc.push_code,cc.create_time as customer_create_time,cc.source,cc.customer_code, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber ,p.title as package_title ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload  " +
-            " from fs_store_order_scrm o  left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id left join fs_user u on o.user_id=u.user_id  " +
-            " left join company c on c.company_id=o.company_id left join company_user cu on cu.user_id=o.company_user_id left join crm_customer cc on cc.customer_id=o.customer_id left join company_tcm_schedule cts on cts.id = o.schedule_id " +
+            " from fs_store_order_scrm o  " +
+            " left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id " +
+            " left join fs_user u on o.user_id=u.user_id  " +
+            " left join company c on c.company_id=o.company_id " +
+            " left join company_user cu on cu.user_id=o.company_user_id " +
+            " left join crm_customer cc on cc.customer_id=o.customer_id " +
+            " left join company_tcm_schedule cts on cts.id = o.schedule_id " +
+            "            LEFT JOIN (\n" +
+            "            SELECT\n" +
+            "            sp.*,\n" +
+            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
+            "            FROM fs_store_payment_scrm sp\n" +
+            "            WHERE sp.business_code IS NOT NULL\n" +
+            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
             "where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java

@@ -143,4 +143,6 @@ public class FsStoreAfterSalesVO implements Serializable
     private String orderDeliverySn;
     private String orderDeliveryName;
     private String orderDeliveryId;
+
+    private String payCode;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java

@@ -23,6 +23,9 @@ public class FsStoreOrderItemExportRefundZMVO implements Serializable  {
     @Excel(name = "订单号",sort = 1)
     private String orderCode;
 
+    @Excel(name = "支付单号",sort = 2)
+    private String payCode;
+
     @Excel(name = "订单状态", dictType = "store_order_status",sort = 10)
     private String status;
 

+ 2 - 0
fs-service/src/main/java/com/fs/im/dto/OpenImMsgDTO.java

@@ -30,6 +30,8 @@ public class OpenImMsgDTO {
         private String data;
         private String description;
         private String extension;
+        private String sourceUrl;
+        private Integer duration;
     }
     @Data
     public static class ImData{

+ 7 - 1
fs-service/src/main/java/com/fs/ipad/IpadSendUtils.java

@@ -221,7 +221,7 @@ public class IpadSendUtils {
      * @param vo 调用接口参数
      * @return 返回的userid
      */
-    private Long userIds(BaseVo vo){
+    public Long userIds(BaseVo vo){
         if(vo.isRoom()){
             return chatIds(vo);
         }
@@ -562,4 +562,10 @@ public class IpadSendUtils {
         }
         return data.stream().map(WxWorkVid2UserIdRespDTO::getUser_id).collect(Collectors.toList());
     }
+    
+    public void sendTxtAtMsgVo(WxSendTextAtMsgTwoDTO dto, Long serverId){
+        WxWorkResponseDTO<WxSendTextAtMsgVo> result = wxWorkService.sendTextAtMsgTwo(dto, serverId);
+        log.info("发送@所有人返回数据:{}", result);
+        if(result.getErrcode() != 0) throw new BaseException("发送@所有人消息错误:" + result.getErrmsg());
+    }
 }

+ 31 - 0
fs-service/src/main/java/com/fs/ipad/vo/WxSendTextAtMsgVo.java

@@ -0,0 +1,31 @@
+package com.fs.ipad.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+
+@Data
+public class WxSendTextAtMsgVo {
+
+    private Long receiver;
+    private Long sender;
+    private List<atList> at_list;
+    private String sender_name;
+    private Long room_conversation_id;
+    private Integer is_room;
+    private Integer sendtime;
+    private Long msg_id;
+    private Long server_id;
+    private Long msgtype;
+    private String content;
+
+    @Data
+    public class atList {
+        private Long user_id;
+        private String nikeName;
+
+    }
+}
+
+

+ 58 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java

@@ -0,0 +1,58 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 直播完课积分领取记录对象 live_completion_points_record
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCompletionPointsRecord extends BaseEntity {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 直播ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 观看时长(秒) */
+    private Long watchDuration;
+
+    /** 视频总时长(秒) */
+    private Long videoDuration;
+
+    /** 完课比例(%) */
+    private BigDecimal completionRate;
+
+    /** 连续完课天数 */
+    private Integer continuousDays;
+
+    /** 获得积分 */
+    private Integer pointsAwarded;
+
+    /** 上次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date lastCompletionDate;
+
+    /** 本次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date currentCompletionDate;
+
+    /** 领取状态 0-未领取 1-已领取 */
+    private Integer receiveStatus;
+
+    /** 领取时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/live/domain/LiveOrder.java

@@ -352,4 +352,8 @@ public class LiveOrder extends BaseEntity {
     @TableField(exist = false)
     private Long attrValueId;
 
+    /** 小程序AppId */
+    @Excel(name = "小程序AppId")
+    private String appId;
+
 }

+ 1 - 1
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -27,7 +27,7 @@ public class LiveWatchConfig extends BaseEntity{
     private Boolean enabled;
 
     /** 参与条件 1达到指定观看时长 */
-    @Excel(name = "参与条件 1达到指定观看时长")
+    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分")
     private Long participateCondition;
 
     /** 观看时长 */

+ 52 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -0,0 +1,52 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Mapper接口
+ */
+public interface LiveCompletionPointsRecordMapper {
+
+    /**
+     * 插入完课积分记录
+     */
+    int insertRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 更新完课积分记录
+     */
+    int updateRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 查询用户某天的完课记录
+     */
+    LiveCompletionPointsRecord selectByUserAndDate(@Param("liveId") Long liveId, 
+                                                     @Param("userId") Long userId, 
+                                                     @Param("currentDate") Date currentDate);
+
+    /**
+     * 查询用户最近一次完课记录(不限直播间,用于计算连续天数)
+     */
+    LiveCompletionPointsRecord selectLatestByUser(@Param("userId") Long userId);
+
+    /**
+     * 查询用户未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> selectUnreceivedByUser(@Param("liveId") Long liveId, 
+                                                             @Param("userId") Long userId);
+
+    /**
+     * 查询用户的完课积分领取记录列表
+     */
+    List<LiveCompletionPointsRecord> selectRecordsByUser(@Param("liveId") Long liveId, 
+                                                          @Param("userId") Long userId);
+
+    /**
+     * 根据ID查询
+     */
+    LiveCompletionPointsRecord selectById(@Param("id") Long id);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.vo.LiveWatchLogListVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -71,4 +72,6 @@ public interface LiveWatchLogMapper extends BaseMapper<LiveWatchLog> {
 
     List<LiveWatchLog> selectLiveWatchLogByLiveId(@Param("liveId")Long liveId);
 
+    List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog);
+
 }

+ 43 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java

@@ -0,0 +1,43 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service接口
+ */
+public interface ILiveCompletionPointsRecordService {
+
+    /**
+     * 检查并创建完课记录(定时任务调用)
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @param watchDuration 观看时长(秒)
+     */
+    void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 用户领取完课积分
+     * @param recordId 完课记录ID
+     * @param userId 用户ID
+     * @return 领取结果
+     */
+    LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId);
+
+    /**
+     * 获取用户完课状态
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId);
+
+    /**
+     * 查询用户积分领取记录
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId);
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/service/ILiveWatchLogService.java

@@ -3,6 +3,7 @@ package com.fs.live.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.vo.LiveWatchLogListVO;
 
 /**
  * 直播看课记录Service接口
@@ -58,4 +59,11 @@ public interface ILiveWatchLogService extends IService<LiveWatchLog>{
      * @return 结果
      */
     int deleteLiveWatchLogByLogId(Long logId);
+
+    /**
+     * 查询列表信息
+     * @param liveWatchLog
+     * @return
+     */
+    List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog);
 }

+ 18 - 0
fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java

@@ -139,4 +139,22 @@ public interface ILiveWatchUserService {
      * 根据用户直播看课记录来打标签
      */
     void qwTagMarkByLiveWatchLog(Long liveId);
+
+    /**
+     * 更新用户观看时长(心跳时调用)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param liveFlag 直播标记
+     * @param replayFlag 回放标记
+     * @param duration 观看时长(秒)
+     */
+    void updateWatchDuration(Long liveId, Long userId, Integer liveFlag, Integer replayFlag, Long duration);
+
+    /**
+     * 获取用户在某直播间的总观看时长(直播 + 回放)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @return 总观看时长(秒)
+     */
+    Long getTotalWatchDuration(Long liveId, Long userId);
 }

+ 354 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -0,0 +1,354 @@
+package com.fs.live.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.base.BaseException;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveWatchUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service业务层处理
+ */
+@Slf4j
+@Service
+public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPointsRecordService {
+
+    @Autowired
+    private LiveCompletionPointsRecordMapper recordMapper;
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+
+    /**
+     * 检查并创建完课记录(由定时任务调用)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param watchDuration 观看时长(可为null,为null时从数据库自动累计直播+回放时长)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration) {
+        try {
+            // 1. 获取直播信息和配置
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                log.warn("直播间不存在, liveId={}", liveId);
+                return;
+            }
+
+            // 2. 从数据库获取完课积分配置
+            CompletionPointsConfig config = getCompletionPointsConfig(live);
+            
+            // 检查是否开启完课积分功能
+            if (!config.isEnabled()) {
+                log.debug("直播间未开启完课积分功能, liveId={}", liveId);
+                return;
+            }
+            
+            // 检查配置完整性
+            Integer completionRate = config.getCompletionRate();
+            int[] pointsConfig = config.getPointsConfig();
+            
+            if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
+                log.warn("完课积分配置不完整, liveId={}, completionRate={}, pointsConfig={}", 
+                        liveId, completionRate, pointsConfig);
+                return;
+            }
+
+            // 3. 获取观看时长(如果为null,则从数据库累计直播+回放时长)
+            Long actualWatchDuration = watchDuration;
+            if (actualWatchDuration == null) {
+                // 自动累加直播和回放的观看时长
+                actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
+                log.debug("自动累计观看时长: liveId={}, userId={}, totalDuration={}",
+                        liveId, userId, actualWatchDuration);
+            }
+
+            if (actualWatchDuration == null || actualWatchDuration <= 0) {
+                log.debug("观看时长为0, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            // 4. 获取视频总时长(秒)
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                log.warn("直播间视频时长无效, liveId={}, duration={}", liveId, videoDuration);
+                return;
+            }
+
+            // 5. 计算完课比例
+            BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+            
+            // 限制完课比例最大值为100.00%(防止数据库字段溢出)
+            if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                watchRate = BigDecimal.valueOf(100);
+            }
+
+            // 6. 判断是否达到完课标准
+            if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
+                log.debug("观看时长未达到完课标准, liveId={}, userId={}, watchDuration={}, videoDuration={}, watchRate={}%, required={}%",
+                        liveId, userId, actualWatchDuration, videoDuration, watchRate, completionRate);
+                return;
+            }
+
+            // 7. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+            if (todayRecord != null) {
+                log.debug("今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            // 7. 查询最近一次完课记录(不限直播间),计算连续天数
+            LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
+            int continuousDays = 1;
+
+            if (latestRecord != null) {
+                LocalDate lastDate = latestRecord.getCurrentCompletionDate()
+                        .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+                long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
+
+                if (daysBetween == 1) {
+                    // 昨天完课了,连续天数+1
+                    continuousDays = latestRecord.getContinuousDays() + 1;
+                } else if (daysBetween > 1) {
+                    // 中断了,重新开始
+                    continuousDays = 1;
+                } else {
+                    // daysBetween == 0 说明今天已经有记录了(理论上不会进入这里,因为前面已经检查过)
+                    log.warn("异常情况: 今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                    return;
+                }
+            }
+
+            // 8. 计算积分
+            int points = calculatePoints(continuousDays, pointsConfig);
+
+            // 9. 创建完课记录
+            LiveCompletionPointsRecord record = new LiveCompletionPointsRecord();
+            record.setLiveId(liveId);
+            record.setUserId(userId);
+            record.setWatchDuration(actualWatchDuration);
+            record.setVideoDuration(videoDuration);
+            record.setCompletionRate(watchRate);
+            record.setContinuousDays(continuousDays);
+            record.setPointsAwarded(points);
+            record.setCurrentCompletionDate(currentDate);
+            record.setReceiveStatus(0); // 未领取
+
+            if (latestRecord != null) {
+                record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
+            }
+
+            recordMapper.insertRecord(record);
+
+            log.info("创建完课记录成功, liveId={}, userId={}, watchDuration={}, videoDuration={}, watchRate={}%, continuousDays={}, points={}",
+                    liveId, userId, actualWatchDuration, videoDuration, watchRate, continuousDays, points);
+
+        } catch (Exception e) {
+            log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 用户领取完课积分
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId) {
+        // 1. 查询完课记录
+        LiveCompletionPointsRecord record = recordMapper.selectById(recordId);
+        if (record == null) {
+            throw new BaseException("完课记录不存在");
+        }
+
+        // 2. 校验用户
+        if (!record.getUserId().equals(userId)) {
+            throw new BaseException("无权领取该完课积分");
+        }
+
+        // 3. 校验领取状态
+        if (record.getReceiveStatus() == 1) {
+            throw new BaseException("该完课积分已领取");
+        }
+
+        // 4. 更新用户积分
+        FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+        if (user == null) {
+            throw new BaseException("用户不存在");
+        }
+
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + record.getPointsAwarded();
+
+        FsUser updateUser = new FsUser();
+        updateUser.setUserId(userId);
+        updateUser.setIntegral(newIntegral);
+        fsUserMapper.updateFsUser(updateUser);
+
+        // 5. 记录积分变动日志
+        FsUserIntegralLogs integralLog = new FsUserIntegralLogs();
+        integralLog.setUserId(userId);
+        integralLog.setIntegral(Long.valueOf(record.getPointsAwarded()));
+        integralLog.setBalance(newIntegral);
+        integralLog.setLogType(5); // 5-直播完课积分
+        integralLog.setBusinessId("live_completion_" + recordId); // 业务ID:直播完课记录ID
+        integralLog.setBusinessType(5); // 5-直播完课
+        integralLog.setStatus(1);
+        integralLog.setCreateTime(new Date());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog);
+
+        // 6. 更新完课记录状态
+        LiveCompletionPointsRecord updateRecord = new LiveCompletionPointsRecord();
+        updateRecord.setId(recordId);
+        updateRecord.setReceiveStatus(1);
+        updateRecord.setReceiveTime(new Date());
+        recordMapper.updateRecord(updateRecord);
+
+        // 7. 返回更新后的记录
+        record.setReceiveStatus(1);
+        record.setReceiveTime(new Date());
+
+        log.info("用户领取完课积分成功, userId={}, recordId={}, points={}", userId, recordId, record.getPointsAwarded());
+
+        return record;
+    }
+
+    /**
+     * 获取用户未领取的完课记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId) {
+        return recordMapper.selectUnreceivedByUser(liveId, userId);
+    }
+
+    /**
+     * 查询用户积分领取记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId) {
+        return recordMapper.selectRecordsByUser(liveId, userId);
+    }
+
+    /**
+     * 从直播配置中获取完课积分配置
+     */
+    private CompletionPointsConfig getCompletionPointsConfig(Live live) {
+        CompletionPointsConfig config = new CompletionPointsConfig();
+        config.setEnabled(false);
+        config.setCompletionRate(null);
+        config.setPointsConfig(null);
+        
+        String configJson = live.getConfigJson();
+        if (configJson == null || configJson.isEmpty()) {
+            return config;
+        }
+        
+        try {
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+
+            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+
+            Integer rate = jsonConfig.getInteger("completionRate");
+            if (rate != null && rate > 0 && rate <= 100) {
+                config.setCompletionRate(rate);
+            }
+
+            List<Integer> pointsList = jsonConfig.getObject("pointsConfig", List.class);
+            if (pointsList != null && !pointsList.isEmpty()) {
+                config.setPointsConfig(pointsList.stream().mapToInt(Integer::intValue).toArray());
+            }
+        } catch (Exception e) {
+            log.warn("解析完课积分配置失败, liveId={}, 配置未生效", live.getLiveId(), e);
+        }
+        
+        return config;
+    }
+    
+    /**
+     * 计算积分
+     * 根据连续天数和积分配置计算应得积分
+     * @param continuousDays 连续完课天数
+     * @param pointsConfig 积分配置数组
+     * @return 应得积分
+     */
+    private int calculatePoints(int continuousDays, int[] pointsConfig) {
+        if (continuousDays <= 0) {
+            return pointsConfig[0];
+        }
+        if (continuousDays > pointsConfig.length) {
+            // 超过配置天数,使用最后一天的积分
+            return pointsConfig[pointsConfig.length - 1];
+        }
+        return pointsConfig[continuousDays - 1];
+    }
+    
+    /**
+     * 完课积分配置内部类
+     */
+    private static class CompletionPointsConfig {
+        private boolean enabled;
+        private Integer completionRate;
+        private int[] pointsConfig;
+        
+        public boolean isEnabled() {
+            return enabled;
+        }
+        
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+        
+        public Integer getCompletionRate() {
+            return completionRate;
+        }
+        
+        public void setCompletionRate(Integer completionRate) {
+            this.completionRate = completionRate;
+        }
+        
+        public int[] getPointsConfig() {
+            return pointsConfig;
+        }
+        
+        public void setPointsConfig(int[] pointsConfig) {
+            this.pointsConfig = pointsConfig;
+        }
+    }
+}

+ 16 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -191,6 +191,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private FsStoreProductAttrValueScrmMapper fsStoreProductAttrValueMapper;
 
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
     @Autowired
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
 
@@ -1962,7 +1965,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
-        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf("-" + liveOrder.getTotalNum()),fsStoreProduct.getProductId());
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         goods.setSales(goods.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
@@ -2946,12 +2949,18 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     public R payConfirmReward(LiveOrder liveOrder) {
         Long orderId = liveOrder.getOrderId();
         if(orderId==null) return R.error("订单ID不存在");
+        // 保存传入的appId
+        String appId = liveOrder.getAppId();
         Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
         try {
             liveOrder = baseMapper.selectLiveOrderByOrderId(String.valueOf(orderId));
             if(liveOrder==null || !liveOrder.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())){
                 throw new CustomException("当前订单未找到或者订单状态不为待支付! orderId:" + orderId);
             }
+            // 设置appId
+            if (StringUtils.isNotEmpty(appId)) {
+                liveOrder.setAppId(appId);
+            }
             FsUserScrm user = userMapper.selectFsUserById(Long.valueOf(liveOrder.getUserId()));
             if(user == null) return R.error("用户不存在");
 //            String json = configService.selectConfigByKey("store.pay");
@@ -3107,6 +3116,10 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     order.setPayDelivery(order.getPayPrice().subtract(payMoney) );
 //                    order.setPayMoney(BigDecimal.ZERO);
                 }
+                // 保存appId到订单
+                if (StringUtils.isNotEmpty(param.getAppId())) {
+                    order.setAppId(param.getAppId());
+                }
                 baseMapper.updateLiveOrder(order);
             }
             String payCode = OrderCodeUtils.getOrderSn();
@@ -3568,7 +3581,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
-        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf("-" + liveOrder.getTotalNum()),fsStoreProduct.getProductId());
 
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
@@ -3832,7 +3845,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             }
 
             // 更新商品库存
-            fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+            fsStoreProductScrmMapper.incStockDecSales(Long.valueOf(liveOrder.getTotalNum()),fsStoreProduct.getProductId());
             goods.setStock(goods.getStock()+Long.parseLong(liveOrder.getTotalNum()));
             // 更新商品库存
             liveGoodsMapper.updateLiveGoods(goods);

+ 10 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.live.service.impl;
 import java.util.List;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.live.vo.LiveWatchLogListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.live.mapper.LiveWatchLogMapper;
@@ -18,6 +19,9 @@ import com.fs.live.service.ILiveWatchLogService;
 @Service
 public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, LiveWatchLog> implements ILiveWatchLogService {
 
+    @Autowired
+    private LiveWatchLogMapper liveWatchLogMapper;
+
     /**
      * 查询直播看课记录
      * 
@@ -42,6 +46,12 @@ public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, Liv
         return baseMapper.selectLiveWatchLogList(liveWatchLog);
     }
 
+    @Override
+    public List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog){
+
+        return liveWatchLogMapper.selectLiveWatchLogListInfo(liveWatchLog);
+    }
+
     /**
      * 新增直播看课记录
      * 

+ 69 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -943,4 +943,73 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 userTagVOS.size(), successCount, failCount);
     }
 
+    /**
+     * 更新用户观看时长(心跳时调用)- 异步执行
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param liveFlag 直播标记
+     * @param replayFlag 回放标记
+     * @param duration 观看时长(秒)
+     */
+    @Override
+    @Async
+    public void updateWatchDuration(Long liveId, Long userId, Integer liveFlag, Integer replayFlag, Long duration) {
+        try {
+
+            LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
+            
+            if (liveWatchUser != null) {
+                if (liveWatchUser.getOnlineSeconds() == null || duration > liveWatchUser.getOnlineSeconds()) {
+                    liveWatchUser.setOnlineSeconds(duration);
+                    liveWatchUser.setUpdateTime(DateUtils.getNowDate());
+                    baseMapper.updateLiveWatchUser(liveWatchUser);
+                    log.debug("更新观看时长成功: liveId={}, userId={}, liveFlag={}, replayFlag={}, duration={}",
+                            liveId, userId, liveFlag, replayFlag, duration);
+                }
+            } else {
+                log.warn("未找到观看记录,无法更新时长: liveId={}, userId={}, liveFlag={}, replayFlag={}",
+                        liveId, userId, liveFlag, replayFlag);
+            }
+        } catch (Exception e) {
+            log.error("更新观看时长失败: liveId={}, userId={}, liveFlag={}, replayFlag={}, duration={}",
+                    liveId, userId, liveFlag, replayFlag, duration, e);
+        }
+    }
+
+    /**
+     * 获取用户在某直播间的总观看时长(直播 + 回放)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @return 总观看时长(秒)
+     */
+    @Override
+    public Long getTotalWatchDuration(Long liveId, Long userId) {
+        try {
+            long totalDuration = 0L;
+            
+            // 1. 查询直播观看记录(liveFlag=1, replayFlag=0)
+            LiveWatchUser liveRecord = baseMapper.selectByUniqueIndex(liveId, userId, 1, 0);
+            if (liveRecord != null && liveRecord.getOnlineSeconds() != null) {
+                totalDuration += liveRecord.getOnlineSeconds();
+            }
+            
+            // 2. 查询回放观看记录(liveFlag=0, replayFlag=1)
+            LiveWatchUser replayRecord = baseMapper.selectByUniqueIndex(liveId, userId, 0, 1);
+            if (replayRecord != null && replayRecord.getOnlineSeconds() != null) {
+                totalDuration += replayRecord.getOnlineSeconds();
+            }
+            
+            log.debug("查询总观看时长: liveId={}, userId={}, liveDuration={}, replayDuration={}, total={}",
+                    liveId, userId,
+                    liveRecord != null ? liveRecord.getOnlineSeconds() : 0,
+                    replayRecord != null ? replayRecord.getOnlineSeconds() : 0,
+                    totalDuration);
+            
+            return totalDuration;
+        } catch (Exception e) {
+            log.error("查询总观看时长失败: liveId={}, userId={}", liveId, userId, e);
+            return 0L;
+        }
+    }
+
 }

+ 10 - 0
fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java

@@ -1,5 +1,6 @@
 package com.fs.live.vo;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
@@ -151,6 +152,10 @@ public class LiveAfterSalesVo {
 
     @Excel(name ="产品名称")
     private String productName;
+    
+    /** 产品名称查询参数(用于搜索) */
+    private String productNameQuery;
+    
     @Excel(name ="产品编码")
     private String productBarCode;
     @Excel(name ="规格")
@@ -185,6 +190,11 @@ public class LiveAfterSalesVo {
     private String orderDeliverySn;
     private String orderDeliveryName;
     private String orderDeliveryId;
+    /**
+     * 用于查询汇付订单号
+     */
+    private String hfOrderCode;
 
+    private String payCode;
 
 }

+ 118 - 0
fs-service/src/main/java/com/fs/live/vo/LiveWatchLogListVO.java

@@ -0,0 +1,118 @@
+package com.fs.live.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/15 下午3:40)
+ */
+
+@Data
+public class LiveWatchLogListVO {
+
+    /** 日志id */
+    private Long logId;
+
+    /** 用户userId */
+    @Excel(name = "用户userId")
+    private Long userId;
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+
+    /** 记录类型 1看课中 2完课 3待看课 4看课中断 */
+    @Excel(name = "记录类型 1看课中 2完课 3待看课 4看课中断")
+    private Integer logType;
+
+    /** 外部联系人id */
+    @Excel(name = "外部联系人id")
+    private Long externalContactId;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 完课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "完课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date finishTime;
+
+    /** sop最后创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "sop最后创建时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date sopCreateTime;
+
+    /** 发送小程序appid */
+    @Excel(name = "发送小程序appid")
+    private String sendAppId;
+
+    /** 日志创建来源:1、个人sop,2、群聊sop,3、一键群发 */
+    @Excel(name = "日志创建来源:1、个人sop,2、群聊sop,3、一键群发")
+    private Integer logSource;
+
+    /** 分享人企微id */
+    @Excel(name = "分享人企微id")
+    private String qwUserId;
+    /**
+     * 查看直播类型:1、直播,2、回放
+     */
+    private Integer watchType;
+
+    /**
+     * 企微主体id
+     */
+    private String corpId;
+
+    /**
+     * 直播购买
+     */
+    private Integer liveBuy;
+
+    /**
+     * 回放购买
+     */
+    private Integer replayBuy;
+
+    /**
+     * 会员昵称
+     */
+    private String userName;
+    /**
+     * 会员头像
+     */
+    private String userAvatar;
+
+    /**
+     * 外部联系人名称
+     */
+    private String qwExternalName;
+
+    /**
+     * 外部联系人头像
+     */
+    private String qwExternalAvatar;
+
+    /**
+     * 直播间名称
+     */
+    private String liveName;
+
+    /**
+     * 所属销售
+     */
+    private String companyUserName;
+
+    /**
+     * 企微用户
+     */
+    private String qwUserName;
+}

+ 71 - 0
fs-service/src/main/java/com/fs/qw/mapper/LuckyBagCollectRecordMapper.java

@@ -0,0 +1,71 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.LuckyBagCollectRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+
+/**
+ * 福袋发放及领取记录Mapper接口
+ *
+ * @author fs
+ * @date 2025-11-20
+ */
+public interface LuckyBagCollectRecordMapper extends BaseMapper<LuckyBagCollectRecord>{
+
+    int insertBagCollectRecord(LuckyBagCollectRecord reward);
+
+    /**
+     * 查询福袋发放及领取记录
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 福袋发放及领取记录
+     */
+    LuckyBagCollectRecord selectLuckyBagCollectRecordById(Long id);
+
+    /**
+     * 查询福袋发放及领取记录列表
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 福袋发放及领取记录集合
+     */
+    List<LuckyBagCollectRecord> selectLuckyBagCollectRecordList(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 新增福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    int insertLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 修改福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    int updateLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 删除福袋发放及领取记录
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 结果
+     */
+    int deleteLuckyBagCollectRecordById(Long id);
+
+    /**
+     * 批量删除福袋发放及领取记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteLuckyBagCollectRecordByIds(Long[] ids);
+
+    LuckyBagCollectRecord selectLuckyBagCollectRecordByRelationId(@Param("relationId") Long relationId,@Param("userId") Long userId);
+
+    int updateLuckyBagExpiryStatus();
+}

+ 25 - 0
fs-service/src/main/java/com/fs/qw/mapper/LuckyBagMapper.java

@@ -0,0 +1,25 @@
+package com.fs.qw.mapper;
+
+import com.fs.course.domain.LuckyBag;
+
+import java.util.List;
+
+/**
+ * app客服活码上架Mapper接口
+ *
+ * @author fs
+ * @date 2024-12-02
+ */
+public interface LuckyBagMapper
+{
+
+    int insertLuckyBag(LuckyBag reward);
+
+    List<LuckyBag> selectLuckyBagList(LuckyBag reward);
+
+    int updateLuckyBag(LuckyBag reward);
+
+    int deleteLuckyBagByIds(Long[] ids);
+
+    LuckyBag selectLuckyBagById(Long id);
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwFriendWelcomeMapper.java

@@ -161,5 +161,5 @@ public interface QwFriendWelcomeMapper
      * @param qwFriendWelcomeParam 参数
      * @return  list
      */
-    List<QwFriendWelcome> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam);
+    List<QwFriendWelcomeVO> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam);
 }

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

@@ -503,4 +503,7 @@ public interface QwUserMapper extends BaseMapper<QwUser>
     List<QwUser> selectQwUserByServerIds(@Param("serverIds")List<String> serverIds);
 
     int batchUpdateUnbind(@Param("ids")List<Long> ids);
+
+    @Select("select * from qw_user where qw_user_id=#{qwUserId} and corp_id =#{corpId} limit 1")
+    QwUser selectQwUserEntityByQwUserIdAndCorId(@Param("qwUserId")String qwUserId,@Param("corpId") String corpId);
 }

+ 3 - 2
fs-service/src/main/java/com/fs/qw/service/AsyncQwAiChatSopService.java

@@ -187,7 +187,7 @@ public class AsyncQwAiChatSopService {
                         //小程序单独
                         case "4":
                             addWatchLogIfNeededByNewChat(item.getId(), content.getVideoId(), content.getCourseId(), fsUserId,qwUser.getId(), qwUser.getCompanyUserId(), qwUser.getCompanyId(),
-                                    externalId, sendTime, expirySendTime);
+                                    externalId, sendTime, expirySendTime,2);
 
                             String linkByMiniApp = createLinkByMiniAppByNewChat(setting.getExpiresDays(), qwUser.getCorpId(), expirySendTime, content.getCourseId(), content.getVideoId(),
                                     qwUser.getId(), String.valueOf(qwUser.getCompanyUserId()),String.valueOf(qwUser.getCompanyId()), externalId, config);
@@ -255,7 +255,7 @@ public class AsyncQwAiChatSopService {
     //插入观看记录
     private Long addWatchLogIfNeededByNewChat(String sopId, Integer videoId, Integer courseId,
                                      Long fsUserId, Long qwUserId, Long companyUserId,
-                                     Long companyId, Long externalId, String startTime, Date createTime) {
+                                     Long companyId, Long externalId, String startTime, Date createTime,Integer watchType) {
 
         try {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
@@ -272,6 +272,7 @@ public class AsyncQwAiChatSopService {
             watchLog.setUpdateTime(createTime);
             watchLog.setLogType(3);
             watchLog.setUserId(fsUserId);
+            watchLog.setWatchType(watchType);
             watchLog.setCampPeriodTime(sopUserLogsInfoService.convertStringToDate(startTime, "yyyy-MM-dd HH:mm:ss"));
 
             //存看课记录

+ 63 - 0
fs-service/src/main/java/com/fs/qw/service/ILuckyBagCollectRecordService.java

@@ -0,0 +1,63 @@
+package com.fs.qw.service;
+
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.course.domain.LuckyBagCollectRecord;
+
+import java.util.List;
+
+/**
+ * 福袋发放及领取记录Service接口
+ *
+ * @author fs
+ * @date 2025-11-24
+ */
+public interface ILuckyBagCollectRecordService extends IService<LuckyBagCollectRecord> {
+    /**
+     * 查询福袋发放及领取记录
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 福袋发放及领取记录
+     */
+    LuckyBagCollectRecord selectLuckyBagCollectRecordById(Long id);
+
+    /**
+     * 查询福袋发放及领取记录列表
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 福袋发放及领取记录集合
+     */
+    List<LuckyBagCollectRecord> selectLuckyBagCollectRecordList(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 新增福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    int insertLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 修改福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    int updateLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord);
+
+    /**
+     * 批量删除福袋发放及领取记录
+     *
+     * @param ids 需要删除的福袋发放及领取记录主键集合
+     * @return 结果
+     */
+    int deleteLuckyBagCollectRecordByIds(Long[] ids);
+
+    /**
+     * 删除福袋发放及领取记录信息
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 结果
+     */
+    int deleteLuckyBagCollectRecordById(Long id);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/qw/service/ILuckyBagService.java

@@ -0,0 +1,29 @@
+package com.fs.qw.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.course.domain.LuckyBag;
+import com.fs.his.domain.FsUser;
+import com.fs.his.param.FsReceiveLuckyBagParam;
+
+import java.util.List;
+
+
+
+public interface ILuckyBagService
+{
+
+
+    int add(LuckyBag reward);
+
+    List<LuckyBag> selectLuckyBagList(LuckyBag reward);
+
+    int updateLuckyBag(LuckyBag reward);
+
+    int deleteLuckyBagByIds(Long[] ids);
+
+    LuckyBag getLuckyBagInfo(Long luckyBagId);
+
+    R receiveLuckyBag(FsReceiveLuckyBagParam param, FsUser user);
+
+    R getLuckyBagInfoByRecordId(Long recordId);
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/IQwContactWayService.java

@@ -82,5 +82,5 @@ public interface IQwContactWayService
 
     void addWatchLogIfNeeded(Integer videoId, Integer courseId,
                                      String qwUserId, String companyUserId,
-                                     String companyId, String externalId);
+                                     String companyId, String externalId,Integer watchType);
 }

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/IQwFriendWelcomeService.java

@@ -70,5 +70,5 @@ public interface IQwFriendWelcomeService
      * @param qwFriendWelcomeParam 参数
      * @return list
      */
-    List<QwFriendWelcome> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam);
+    List<QwFriendWelcomeVO> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam);
 }

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

@@ -1,5 +1,6 @@
 package com.fs.qw.service.impl;
 
+import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.common.utils.PubFun;
@@ -7,6 +8,8 @@ import com.fs.course.domain.FsCourseSopAppLink;
 import com.fs.course.mapper.FsCourseSopAppLinkMapper;
 import com.fs.gtPush.service.uniPush2Service;
 import com.fs.his.mapper.FsUserMapper;
+import com.fs.im.dto.OpenImMsgDTO;
+import com.fs.im.service.OpenIMService;
 import com.fs.qw.domain.QwSopUpdateStatus;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -29,6 +32,7 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
@@ -539,4 +543,63 @@ public class AsyncSopTestService {
 
     }
 
+    @Autowired
+    private OpenIMService openIMService;
+    @Async("scheduledExecutorService")
+    public void  asyncSendMsgBySopAppTxtNormalIM(List<QwSopTempSetting.Content.Setting> setting,String cropId,Long companyUserId,Long fsUserId){
+
+        setting.forEach(item->{
+            try {
+                log.info("执行发送app文本消息:{}",item);
+                OpenImMsgDTO openImMsgDTO = new OpenImMsgDTO();
+                openImMsgDTO.setSendID("C"+companyUserId);
+                openImMsgDTO.setRecvID("U"+fsUserId);
+                openImMsgDTO.setContentType(101);
+                openImMsgDTO.setSessionType(1);
+                OpenImMsgDTO.Content imContent = new OpenImMsgDTO.Content();
+                imContent.setContent(item.getValue());
+                openImMsgDTO.setContent(imContent);
+                openIMService.openIMSendMsg(openImMsgDTO);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+
+    }
+
+    @Async("scheduledExecutorService")
+    public void  asyncSendMsgBySopAppMP3NormalIM(List<QwSopTempSetting.Content.Setting> setting,String cropId,Long companyUserId,Long fsUserId){
+
+        setting.forEach(item->{
+            try {
+                if(StrUtil.isEmpty(item.getVoiceUrl())){
+                    log.info("执行发送app文本消息:{}",item);
+                    OpenImMsgDTO openImMsgDTO = new OpenImMsgDTO();
+                    openImMsgDTO.setSendID("C"+companyUserId);
+                    openImMsgDTO.setRecvID("U"+fsUserId);
+                    openImMsgDTO.setContentType(101);
+                    openImMsgDTO.setSessionType(1);
+                    OpenImMsgDTO.Content imContent = new OpenImMsgDTO.Content();
+                    imContent.setContent(item.getValue());
+                    openImMsgDTO.setContent(imContent);
+                    openIMService.openIMSendMsg(openImMsgDTO);
+                }else {
+                    log.info("执行发送app语音消息:{}",item);
+                    OpenImMsgDTO openImMsgDTO = new OpenImMsgDTO();
+                    openImMsgDTO.setSendID("C"+companyUserId);
+                    openImMsgDTO.setRecvID("U"+fsUserId);
+                    openImMsgDTO.setContentType(103);
+                    openImMsgDTO.setSessionType(1);
+                    OpenImMsgDTO.Content imContent = new OpenImMsgDTO.Content();
+                    imContent.setSourceUrl(item.getVoiceUrl());
+                    imContent.setDuration(Integer.parseInt(item.getVoiceDuration()));
+                    openImMsgDTO.setContent(imContent);
+                    openIMService.openIMSendMsg(openImMsgDTO);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+
+    }
 }

+ 94 - 0
fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagCollectRecordServiceImpl.java

@@ -0,0 +1,94 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.LuckyBagCollectRecord;
+import com.fs.qw.mapper.LuckyBagCollectRecordMapper;
+import com.fs.qw.service.ILuckyBagCollectRecordService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 福袋发放及领取记录Service业务层处理
+ *
+ * @author fs
+ * @date 2025-11-24
+ */
+@Service
+public class LuckyBagCollectRecordServiceImpl extends ServiceImpl<LuckyBagCollectRecordMapper, LuckyBagCollectRecord> implements ILuckyBagCollectRecordService {
+
+    /**
+     * 查询福袋发放及领取记录
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 福袋发放及领取记录
+     */
+    @Override
+    public LuckyBagCollectRecord selectLuckyBagCollectRecordById(Long id)
+    {
+        return baseMapper.selectLuckyBagCollectRecordById(id);
+    }
+
+    /**
+     * 查询福袋发放及领取记录列表
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 福袋发放及领取记录
+     */
+    @Override
+    public List<LuckyBagCollectRecord> selectLuckyBagCollectRecordList(LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        return baseMapper.selectLuckyBagCollectRecordList(luckyBagCollectRecord);
+    }
+
+    /**
+     * 新增福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    @Override
+    public int insertLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        luckyBagCollectRecord.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertLuckyBagCollectRecord(luckyBagCollectRecord);
+    }
+
+    /**
+     * 修改福袋发放及领取记录
+     *
+     * @param luckyBagCollectRecord 福袋发放及领取记录
+     * @return 结果
+     */
+    @Override
+    public int updateLuckyBagCollectRecord(LuckyBagCollectRecord luckyBagCollectRecord)
+    {
+        luckyBagCollectRecord.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateLuckyBagCollectRecord(luckyBagCollectRecord);
+    }
+
+    /**
+     * 批量删除福袋发放及领取记录
+     *
+     * @param ids 需要删除的福袋发放及领取记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLuckyBagCollectRecordByIds(Long[] ids)
+    {
+        return baseMapper.deleteLuckyBagCollectRecordByIds(ids);
+    }
+
+    /**
+     * 删除福袋发放及领取记录信息
+     *
+     * @param id 福袋发放及领取记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLuckyBagCollectRecordById(Long id)
+    {
+        return baseMapper.deleteLuckyBagCollectRecordById(id);
+    }
+}

+ 416 - 0
fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagServiceImpl.java

@@ -0,0 +1,416 @@
+package com.fs.qw.service.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.LuckyBag;
+import com.fs.course.domain.LuckyBagCollectRecord;
+import com.fs.course.param.LuckyBagActualRewardsParam;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.param.FsReceiveLuckyBagParam;
+import com.fs.his.service.impl.FsUserIntegralLogsServiceImpl;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwGroupChat;
+import com.fs.qw.domain.QwGroupChatUser;
+import com.fs.qw.mapper.*;
+import com.fs.qw.service.ILuckyBagService;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.impl.SysConfigServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.*;
+
+@Slf4j
+@Service
+public class LuckyBagServiceImpl implements ILuckyBagService
+{
+
+    @Autowired
+    private LuckyBagMapper luckyBagMapper;
+
+    @Autowired
+    private LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;
+
+    @Autowired
+    private FsUserIntegralLogsServiceImpl fsUserIntegralLogsService;
+
+    @Autowired
+    private QwGroupChatMapper qwGroupChatMapper;
+    @Autowired
+    private QwGroupChatUserMapper qwGroupChatUserMapper;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private FsUserMapper userMapper;
+    @Autowired
+    private SysConfigServiceImpl sysConfigService;
+
+    @Override
+    public int add(LuckyBag reward) {
+
+       checkParam(reward);
+        return luckyBagMapper.insertLuckyBag(reward);
+    }
+
+    @Override
+    public List<LuckyBag> selectLuckyBagList(LuckyBag reward) {
+        return luckyBagMapper.selectLuckyBagList(reward);
+    }
+
+    @Override
+    public int updateLuckyBag(LuckyBag reward) {
+        checkParam(reward);
+        return luckyBagMapper.updateLuckyBag(reward);
+    }
+
+    @Override
+    public int deleteLuckyBagByIds(Long[] ids) {
+        return luckyBagMapper.deleteLuckyBagByIds(ids);
+    }
+
+    @Override
+    public LuckyBag getLuckyBagInfo(Long luckyBagId) {
+        return luckyBagMapper.selectLuckyBagById(luckyBagId);
+    }
+
+    /**
+     * @Description: 领取福袋奖励
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/20 17:27
+     */
+    @Override
+    @Transactional
+    public R receiveLuckyBag(FsReceiveLuckyBagParam param, FsUser user) {
+
+        Long userId = param.getUserId();
+        LuckyBagCollectRecord record = luckyBagCollectRecordMapper.selectLuckyBagCollectRecordById(param.getRecordId());
+        // 校验福袋 用户信息
+        LuckyBagCollectRecord info;
+        R r=checkRecordParam(record, param);
+        if(!r.get("code").equals(200)){
+            return r;
+        }else {
+            info=(LuckyBagCollectRecord)r.get("info");
+        }
+        // 查询福袋信息
+        LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(record.getLuckyBagId());
+        if(luckyBag==null){
+            log.warn("未找到对应的福袋信息 [luckyBagId:{}]", record.getLuckyBagId());
+            return R.error().put("msg","未找到对应的福袋信息");
+        }
+        if(luckyBag.getStatus()==0){
+            log.warn("该福袋已下架 [luckyBagId:{}]", record.getLuckyBagId());
+            return R.error().put("msg","该福袋已下架");
+        }
+        if(luckyBag.getDataStatus().equals("0")){ // 禁用
+            log.warn("该福袋已禁用 [luckyBagId:{}]", record.getLuckyBagId());
+            return R.error().put("msg","该福袋已禁用");
+        }
+
+        // 获取方法币数量
+        Long coinAmount ;
+        if("1".equals(luckyBag.getType())){ // 定值
+            coinAmount= luckyBag.getAmount().longValue();
+        }else {
+            coinAmount =findIntegral(luckyBag.getActualRewards());
+        }
+
+        // 登记日志 有ClickHouse 不支持回滚
+        Long balance = user.getIntegral()+coinAmount;
+
+        FsUserIntegralLogs fsUserIntegralLogs = new FsUserIntegralLogs();
+        fsUserIntegralLogs.setUserId(userId);
+        fsUserIntegralLogs.setLogType(30); // 福袋获取获得芳华币
+        fsUserIntegralLogs.setIntegral(coinAmount);
+        fsUserIntegralLogs.setPhone(user.getPhone());
+        fsUserIntegralLogs.setBalance(balance);
+        fsUserIntegralLogs.setCreateTime(new Date());
+        fsUserIntegralLogs.setNickName(user.getNickName());
+        //写入积分日志
+        fsUserIntegralLogsService.insertFsUserIntegralLogs(fsUserIntegralLogs);
+
+        //给用户增加积分
+        FsUser updateUser = new FsUser();
+        updateUser.setUserId(userId);
+        updateUser.setIntegral(balance);
+        userMapper.updateFsUser(updateUser);
+
+        // 更新状态
+        LuckyBagCollectRecord recordUpdate = new LuckyBagCollectRecord();
+        recordUpdate.setId(info.getId());
+        recordUpdate.setCollectType("1");
+        recordUpdate.setCollectTime(new Date());
+        recordUpdate.setUpdateTime(new Date());
+        recordUpdate.setCoinAmount(BigDecimal.valueOf(coinAmount));
+        if(StringUtils.isBlank(info.getUserName())){
+            recordUpdate.setUserName(user.getNickName());
+        }
+        if(info.getUserId()==null){
+            recordUpdate.setUserId(userId);
+            recordUpdate.setUserName(user.getNickName());
+        }else if(info.getUserId()==0){// 有用户id为0的数据需要处理
+            recordUpdate.setUserId(userId);
+        }
+        // 个人领取
+        luckyBagCollectRecordMapper.updateLuckyBagCollectRecord(recordUpdate);
+        if(record.getRewardType()==1L){ // 群福袋 两条记录
+            recordUpdate.setId(record.getId()); // 群福袋
+            recordUpdate.setCollectType(""); // 状态不更新
+            recordUpdate.setUserId(null);
+            recordUpdate.setUserName("");
+            luckyBagCollectRecordMapper.updateLuckyBagCollectRecord(recordUpdate);
+        }
+
+        Map<String,Object> map = new HashMap<>();
+        map.put("balance",balance);
+        map.put("coinAmount",coinAmount);
+        return R.ok().put("data",map);
+    }
+
+    /**
+     * @Description: 是否领取成功
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/21 13:58
+     */
+    @Override
+    public R getLuckyBagInfoByRecordId(Long recordId) {
+
+        LuckyBagCollectRecord record = luckyBagCollectRecordMapper.selectLuckyBagCollectRecordById(recordId);
+        if(record==null){
+            return R.error().put("msg","未找到该福袋信息");
+        }
+        Map<String,Object> map = new HashMap<>();
+        map.put("collectType",record.getCollectType());// 领取状态(0-已发放 1-已领取 2-已失效)
+        if(record.getExpiryTime().before(new Date())){
+            map.put("isExpiry",false); // 失效
+        }else {
+            map.put("isExpiry",true);
+        }
+        return R.ok().put("data",map);
+    }
+
+    /**
+     * @Description: 校验信息,判断是否可以领取
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/21 9:09
+     */
+    private R checkRecordParam(LuckyBagCollectRecord record,FsReceiveLuckyBagParam param) {
+
+        //未注册提示
+//        String noRegisterMsg = "由于您还未完成注册,请联系伴学助手完成注册即可观看!";
+        // 校验用户信息
+        if(record==null){
+            log.error("未找到该福袋信息");
+            return R.error().put("msg","未找到该福袋信息");
+        }
+
+        LuckyBagCollectRecord info;
+        if(record.getRewardType()==1L){// 群福袋
+            // 根据 关联id 和 userId 查询领取记录
+            info = luckyBagCollectRecordMapper.selectLuckyBagCollectRecordByRelationId(record.getId(), param.getUserId());
+            // 判断是否群成员 是的话生成一条记录
+            if(info==null){
+                if(record.getExpiryTime().before(DateUtils.getNowDate())){
+                    log.error("该福袋已失效");
+                    return R.error().put("msg","该福袋已失效");
+                }
+                QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(record.getChatId());
+                if(qwGroupChat == null){
+                    log.error("未查询到该群信息");
+                    return R.error().put("msg","未查询到该群信息");
+                }
+                SopUserLogsInfo sopUserLogsInfo =  new SopUserLogsInfo();
+                sopUserLogsInfo.setChatId(record.getChatId());
+                List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);
+                if(qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()){
+                    log.error("SOP任务群参数异常");
+                    return R.error().put("msg","SOP任务群参数异常");
+                }
+                QwExternalContact qwExternalContact =
+                        qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
+                                .eq("user_id", qwGroupChat.getOwner())
+                                .eq("fs_user_id", param.getUserId())
+                                .eq("corp_id", param.getCorpId())
+                                .eq("status",0));
+                if(qwExternalContact==null){
+                    log.error("客户企微QwExternalContact未绑定fsUserId,{}",param.getUserId());
+                    return R.error().put("msg","客户未注册");
+                }
+                if(qwGroupChatUsers.stream().noneMatch(e -> e.getUserId().equals(qwExternalContact.getExternalUserId()))){
+                    log.error("客户不在群:{},里面:{}", qwGroupChat.getChatId(), qwExternalContact.getExternalUserId());
+                    return  R.error().put("msg","客户不在群");
+                }
+
+                info= BeanCopyUtils.copy(record, LuckyBagCollectRecord.class);
+                assert info != null;
+                info.setId(null);
+                info.setUserId(param.getUserId());
+                info.setUserName(param.getUserName());
+                info.setRelationId(param.getRecordId());
+                luckyBagCollectRecordMapper.insertLuckyBagCollectRecord(info);
+            }else {
+                log.error("该用户已领取过福袋");
+                return R.error().put("msg","该用户已领取过福袋");
+            }
+
+        }else {// 个人福袋
+            // 校验用户信息 没有领取过的不校验,谁领取到福袋就是谁的
+            if(record.getUserId()!=null && record.getUserId()!=0L && !Objects.equals(record.getUserId(), param.getUserId())){
+                log.error("专属福袋,用户id不一致record.getUserId:{},param.getUserId:{}", record.getUserId(), param.getUserId());
+                return R.error().put("msg","专属福袋,用户id不一致");
+            }
+            info=record;
+        }
+
+        if("1".equals(info.getCollectType())){
+            log.error("该用户已领取过福袋");
+            return R.error().put("msg","该用户已领取过福袋");
+        }
+
+        // 判断福袋是否失效
+        if("2".equals(info.getCollectType())){
+            log.error("该福袋已失效");
+            return  R.error().put("msg","该福袋已失效");
+        }
+        if(record.getExpiryTime().before(DateUtils.getNowDate())){
+            // 更新福袋状态失效
+            LuckyBagCollectRecord updateRecord = new LuckyBagCollectRecord();
+            updateRecord.setId(info.getId());
+            updateRecord.setCollectType("2"); // 已失效
+            luckyBagCollectRecordMapper.updateLuckyBagCollectRecord(updateRecord);
+            if(record.getRewardType()==1L){// 群福袋
+                updateRecord.setId(info.getRelationId());
+                luckyBagCollectRecordMapper.updateLuckyBagCollectRecord(updateRecord);
+            }
+            log.error("该福袋已失效");
+            return R.error().put("msg","该福袋已失效");
+        }
+
+        // 检查次数限制
+        // 动态计算时间范围
+        LuckyBagCollectRecord luckyBagCollectRecord = new LuckyBagCollectRecord();
+        luckyBagCollectRecord.setUserId(param.getUserId());
+        luckyBagCollectRecord.setCollectType("1");// 已领取
+        luckyBagCollectRecord.setCompanyUserId(record.getCompanyUserId());
+        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);
+        List<LuckyBagCollectRecord> luckyBagCollectRecords= luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(luckyBagCollectRecord);
+        int recordCount = luckyBagCollectRecords != null ? luckyBagCollectRecords.size() : 0;
+
+        Integer maxCount;
+        // 查询福袋领取数量
+        // 查询福袋配置项
+        SysConfig config = sysConfigService.selectConfigByConfigKey("luckyBag.config");
+        if (config == null || StringUtils.isEmpty(config.getConfigValue())) {
+            log.warn("未找到有效的系统配置 [configKey: luckyBag.config]");
+            throw new ServiceException("系统配置不存在或无效");
+        }
+        try {
+            Map<String, Object> configMap = JSONObject.parseObject(config.getConfigValue(), Map.class);
+            Object maxCountObj = configMap.get("weekLimit");
+            if (maxCountObj == null) {
+                log.warn("系统配置中缺少 weekLimit 参数");
+                throw new ServiceException("系统配置缺失 weekLimit 参数");
+            }
+            maxCount = Integer.parseInt(maxCountObj.toString());
+            // 继续后续逻辑处理...
+        } catch (NumberFormatException e) {
+            log.error("解析 weekLimit 配置失败: {}", e.getMessage());
+            throw new ServiceException("系统配置 weekLimit 格式错误");
+        }
+        if(recordCount>=maxCount){
+            log.error("单个客服每周(7天)给同客户发送数量超过次数限制");
+            return R.error().put("msg","单个客服每周(7天)给同客户发送数量超过次数限制");
+        }
+
+        return R.ok().put("info", info);
+    }
+
+    private void checkParam(LuckyBag reward) {
+        String type = reward.getType();
+        if ("1".equals(type)) {
+            BigDecimal amount = reward.getAmount();
+            if (amount == null && amount.compareTo(BigDecimal.ZERO) == 0) {
+                // amount不为null且不为0的逻辑处理
+                throw new ServiceException("定值不能为空");
+            }
+        }
+        if ("2".equals(type)) {
+            List<LuckyBagActualRewardsParam> actualRewardsParams = JSONArray.parseArray(reward.getActualRewards(), LuckyBagActualRewardsParam.class);
+            if (actualRewardsParams.isEmpty()) {
+                throw new ServiceException("配置不能为空");
+            }
+        }
+    }
+
+    /**
+     * @Description: 算法规则 copy from 天降宝箱规则
+     * @Param:
+     * @Return:
+     * @Author yfh
+     * @Date 2025/11/20 17:05
+     */
+    private Long findIntegral(String listString) {
+        List<Map> items = JSONObject.parseArray(listString, Map.class);
+
+        // 根据probability概率随机选择一个项
+        Map<String, Object> selectedItem = new HashMap<>();
+
+        // 1. 提取并转换概率值
+        List<Double> probabilities = new ArrayList<>();
+        double totalProbability = 0.0;
+
+        for (Map item : items) {
+            String probStr = (String) item.get("probability");
+            // 移除百分号并转换为小数
+            double prob = Double.parseDouble(probStr.replace("%", "")) / 100.0;
+            probabilities.add(prob);
+            totalProbability += prob;
+        }
+
+        // 2. 验证概率总和(应该是1.0,即100%)
+        if (Math.abs(totalProbability - 1.0) > 0.0001) {
+            for (int i = 0; i < probabilities.size(); i++) {
+                probabilities.set(i, probabilities.get(i) / totalProbability);
+            }
+        }
+
+        // 3. 生成随机数并选择
+        double random = Math.random();
+        double cumulativeProbability = 0.0;
+
+        for (int i = 0; i < probabilities.size(); i++) {
+            cumulativeProbability += probabilities.get(i);
+            if (random <= cumulativeProbability) {
+                selectedItem = items.get(i);
+                break;
+            }
+        }
+
+        return Long.parseLong(selectedItem.get("amount").toString());
+    }
+
+}

+ 4 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwContactWayServiceImpl.java

@@ -490,7 +490,8 @@ public class QwContactWayServiceImpl implements IQwContactWayService
                                         String.valueOf(qwUser.getId()),
                                         String.valueOf(qwUser.getCompanyUserId()),
                                         String.valueOf(qwUser.getCompanyId()),
-                                        String.valueOf(qwExternalId));
+                                        String.valueOf(qwExternalId),
+                                        2);
                 }  catch (Exception e) {
                     log.error("欢迎语生成短链失败:", e);
                 }
@@ -503,7 +504,7 @@ public class QwContactWayServiceImpl implements IQwContactWayService
     //添加watchLog记录
     public   void addWatchLogIfNeeded(Integer videoId, Integer courseId,
                                      String qwUserId, String companyUserId,
-                                     String companyId, String externalId) {
+                                     String companyId, String externalId,Integer watchType) {
         FsCourseWatchLog watchLog = new FsCourseWatchLog();
         watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
         watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
@@ -517,6 +518,7 @@ public class QwContactWayServiceImpl implements IQwContactWayService
         watchLog.setUpdateTime(new Date());
         watchLog.setLogType(3);
         watchLog.setUserId(0L);
+        watchLog.setWatchType(watchType);
         watchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
     }
 

+ 4 - 3
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -3818,7 +3818,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                             String.valueOf(qwUser.getId()),
                             String.valueOf(qwUser.getCompanyUserId()),
                             String.valueOf(qwUser.getCompanyId()),
-                            String.valueOf(qwExternalId));
+                            String.valueOf(qwExternalId),2);
 
                 }  catch (Exception e) {
                     logger.error("欢迎语生成短链失败:", e);
@@ -3870,7 +3870,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                                         String.valueOf(qwUser.getId()),
                                         String.valueOf(qwUser.getCompanyUserId()),
                                         String.valueOf(qwUser.getCompanyId()),
-                                        String.valueOf(qwExternalId));
+                                        String.valueOf(qwExternalId),2);
                             }else {
                                 logger.error("查到课程相关信息:"+corpId+":"+att.getMiniprogram().getCourseId());
                             }
@@ -3892,7 +3892,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     //添加watchLog记录
     private void addWatchLogIfNeeded(Integer videoId, Integer courseId,
                                      String qwUserId, String companyUserId,
-                                     String companyId, String externalId) {
+                                     String companyId, String externalId,Integer watchType) {
         FsCourseWatchLog watchLog = new FsCourseWatchLog();
         watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
         watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
@@ -3906,6 +3906,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         watchLog.setUpdateTime(new Date());
         watchLog.setLogType(3);
         watchLog.setUserId(0L);
+        watchLog.setWatchType(watchType);
         watchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
     }
 

+ 34 - 5
fs-service/src/main/java/com/fs/qw/service/impl/QwFriendWelcomeServiceImpl.java

@@ -5,7 +5,12 @@ import com.fs.common.constant.FsConstants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwFriendWelcome;
+import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwFriendWelcomeItemMapper;
 import com.fs.qw.mapper.QwFriendWelcomeMapper;
 import com.fs.qw.mapper.QwUserMapper;
@@ -28,10 +33,10 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.net.URL;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 好友欢迎语Service业务层处理
@@ -55,6 +60,10 @@ public class QwFriendWelcomeServiceImpl implements IQwFriendWelcomeService {
 
     @Autowired
     private RedisCacheT<Long> redisCache;
+    @Autowired
+    private CompanyMapper companyMapper;
+    @Autowired
+    private QwCompanyMapper qwCompanyMapper;
 
     /**
      * 查询好友欢迎语
@@ -389,8 +398,28 @@ public class QwFriendWelcomeServiceImpl implements IQwFriendWelcomeService {
      * @return list
      */
     @Override
-    public List<QwFriendWelcome> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam) {
-        return qwFriendWelcomeMapper.selectQwFriendWelcomeList(qwFriendWelcomeParam);
+    public List<QwFriendWelcomeVO> selectQwFriendWelcomeList(QwFriendWelcomeParam qwFriendWelcomeParam) {
+        List<QwFriendWelcomeVO> qwFriendWelcomes = qwFriendWelcomeMapper.selectQwFriendWelcomeList(qwFriendWelcomeParam);
+
+        if (qwFriendWelcomes.isEmpty()) {
+            return qwFriendWelcomes;
+        }
+
+        List<Long> companyIds = qwFriendWelcomes.stream().map(QwFriendWelcomeVO::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        List<String> corpIds = qwFriendWelcomes.stream().map(QwFriendWelcomeVO::getCorpId).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
+
+        Map<Long, String> companyMap = companyIds.isEmpty()
+                ? Collections.emptyMap()
+                : companyMapper.selectCompanyByIds(companyIds).stream().collect(Collectors.toMap(Company::getCompanyId, Company::getCompanyName, (a, b) -> a));
+        Map<String, String> corpMap = corpIds.isEmpty()
+                ? Collections.emptyMap()
+                : qwCompanyMapper.selectByCorpIds(corpIds).stream().collect(Collectors.toMap(QwCompany::getCorpId, QwCompany::getCorpName, (a, b) -> a
+        ));
+        qwFriendWelcomes.forEach(qwFriendWelcome -> {
+            qwFriendWelcome.setCompanyName(companyMap.getOrDefault(qwFriendWelcome.getCompanyId(), ""));
+            qwFriendWelcome.setCorpName(corpMap.getOrDefault(qwFriendWelcome.getCorpId(), ""));
+        });
+        return qwFriendWelcomes;
     }
 
     /**

+ 3 - 0
fs-service/src/main/java/com/fs/qw/vo/QwFriendWelcomeVO.java

@@ -67,4 +67,7 @@ public class QwFriendWelcomeVO extends BaseEntity
 
     public List<QwUserVO> userSelectList;
 
+    private String companyName;
+    private String corpName;
+
 }

+ 8 - 0
fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java

@@ -125,6 +125,14 @@ public class QwSopCourseFinishTempSetting implements Serializable,Cloneable{
 
         //app显示标题 app用的参数
         private String title;
+
+        //福袋id
+        private Long luckyBagId;
+
+        /**
+         * 业务id
+         */
+        private String businessId;
         @Override
         public Setting clone() {
             try {

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

@@ -44,6 +44,8 @@ public class QwSopTempSetting implements Serializable{
         private String addTag;
 
         private String delTag;
+        
+        private Integer isAtAll;
 
         @Override
         public Content clone() {
@@ -151,6 +153,13 @@ public class QwSopTempSetting implements Serializable{
             //app显示标题 app用的参数
             private String title;
 
+            //福袋id
+            private Long luckyBagId;
+            /**
+             * 业务id
+             */
+            private String businessId;
+
 
             @Override
             public Setting clone() {

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

@@ -150,4 +150,7 @@ public class QwSopTemp implements Serializable
 
     @TableField(exist = false)
     private String createByDeptName;
+    
+    @TableField(exist = false)
+    private String openIsAtAll;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/sop/domain/QwSopTempRules.java

@@ -83,4 +83,7 @@ public class QwSopTempRules{
     private Integer dayNum;
     @TableField(exist = false)
     private FsUserCourseVideoRedPackage red;
+    
+     /**是否@所有人  1是0否**/
+    private Integer isAtAll;
 }

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

@@ -467,6 +467,12 @@ public class QwSopTempServiceImpl implements IQwSopTempService {
                     rules.setTime(time);
                 }
 
+                if (temp.getOpenIsAtAll() != null && temp.getOpenIsAtAll().equals("1")){
+                    rules.setIsAtAll(1);
+                }else {
+                    rules.setIsAtAll(0);
+                }
+
                 rules.setContentType(2);
                 rules.setType(2);
                 rules.setCourseType(0);

+ 535 - 13
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.core.domain.R;
@@ -18,10 +19,7 @@ import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.service.ICompanyMiniappService;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
-import com.fs.course.domain.FsCourseDomainName;
-import com.fs.course.domain.FsCourseLink;
-import com.fs.course.domain.FsCourseRealLink;
-import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.*;
 import com.fs.course.mapper.FsCourseDomainNameMapper;
 import com.fs.course.mapper.FsCourseLinkMapper;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
@@ -30,6 +28,8 @@ import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
+import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.live.domain.LiveWatchLog;
 import com.fs.live.mapper.LiveWatchLogMapper;
 import com.fs.qw.domain.*;
@@ -61,6 +61,8 @@ import com.fs.sop.vo.ExtCourseSopWatchLogVO;
 import com.fs.sop.vo.QwCreateLinkByAppVO;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
 import com.fs.sop.vo.SopUserLogsVo;
+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 org.slf4j.Logger;
@@ -91,7 +93,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     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 appActivitlLink = "/pages_course/activity.html?link=";
     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 miniappRealLink = "/pages/index/index?course=";
 
     @Autowired
@@ -175,6 +179,15 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     @Autowired
     LiveWatchLogMapper liveWatchLogMapper;
 
+    @Autowired
+    private LuckyBagMapper luckyBagMapper;
+
+    @Autowired
+    private LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
 
 
     @Override
@@ -526,7 +539,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
                             GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
                             if (vo != null && vo.getId() != null) {
-                                addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), param.getStartTime(), createTime);
+                                addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), param.getStartTime(), createTime,2);
                             }
                         });
                     });
@@ -572,6 +585,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),groupUser.getChatId());
                     //域名
                     String companyUserId = qwUser.getCompanyUserId().toString();
+                    String companyId = String.valueOf(qwUser.getCompanyId()).trim();
                     String domainName = companyUserMapper.selectDomainByUserId(Long.parseLong(companyUserId));
                     if (StringUtils.isEmpty(domainName)) {
                         domainName = config.getRealLinkDomainName();
@@ -692,6 +706,45 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 st.setMiniprogramAppid(sysConfig.getAppId());
                                 st.setMiniprogramPage(sortLiveLink);
                                 break;
+                            case "14":
+
+                                linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, qwGroupChat.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
+                                        String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),null ,config, groupUser.getChatId());
+
+                                miniAppId = null;
+                                if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                                    Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
+                                    if (integerListMap != null) {
+                                        int listIndexTemp = 1;
+                                        List<CompanyMiniapp> miniapps = integerListMap.get(listIndexTemp);
+                                        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("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                                }
+
+                                st.setMiniprogramTitle("福袋发放");
+
+                                st.setMiniprogramPage(linkByMiniApp);
+                                break;
+                            case "16":
+                                createVoiceUrl(st, companyUserId, qwSop);
+                                break;
+                            default:
+                                break;
                         }
                     }
                     setting.setSetting(list);
@@ -850,6 +903,60 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 st.setMiniprogramAppid(sysConfig.getAppId());
                                 st.setMiniprogramPage(sortLiveLink);
                                 break;
+                            case "14":
+                                String companyId = String.valueOf(qwUser.getCompanyId()).trim();
+                                linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, qwUser.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
+                                        String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),null, config, groupChat.getChatId());
+                                miniAppId = null;
+                                if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                                    Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
+                                    if (integerListMap != null) {
+                                        int listIndexTemp = 1;
+                                        List<CompanyMiniapp> miniapps = integerListMap.get(listIndexTemp);
+                                        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("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                                }
+
+                                st.setMiniprogramTitle("福袋发放");
+
+                                st.setMiniprogramPage(linkByMiniApp);
+                                break;
+                            case "15":
+                                String txt2 = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
+                                st.setValue(st.getValue().replaceAll("#客服称呼#", txt2).replaceAll("#销售称呼#", txt2));
+                                try {
+                                    replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
+                                } catch (Exception e) {
+                                    throw new RuntimeException(e);
+                                }
+                                break;
+                            case "16":
+                                if (qwUser.getCompanyUserId() != null) {
+                                    createVoiceUrl(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
+                                }
+                                try {
+                                    if (qwUser.getCompanyUserId() != null) {
+                                        createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
+                                    }
+                                } catch (Exception e) {
+                                    throw new RuntimeException(e);
+                                }
+                                break;
                         }
                     }
                     setting.setSetting(list);
@@ -977,7 +1084,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             break;
                         //小程序单独
                         case "4":
-                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime );
+                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,2 );
 
                             String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
                                     Long.valueOf(qwUserId), companyUserId, companyId, item.getExternalId(), config, null);
@@ -1020,7 +1127,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             break;
                         //app
                         case "9":
-                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime );
+                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,1 );
 
                             QwCreateLinkByAppVO linkByApp = createLinkByApp(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
                                     Long.valueOf(qwUserId), companyUserId, companyId, item.getExternalId(), config,qwUser.getQwUserName(),contact.getFsUserId());
@@ -1030,7 +1137,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             break;
                         //自定义小程序
                         case "10":
-                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime );
+                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,2 );
                             if (company!=null){
 
                                 String customMiniAppId = company.getCustomMiniAppId();
@@ -1061,6 +1168,119 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             st.setMiniprogramAppid(sysConfig.getAppId());
                             st.setMiniprogramPage(sortLiveLink);
                             break;
+                        case "14":
+                            LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(st.getLuckyBagId());
+                            if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
+                                sopLogs.setSendStatus(5L);
+                                sopLogs.setReceivingStatus(0L);
+                                sopLogs.setRemark("福袋配置被禁用");
+                            }
+                            else if (ObjectUtil.isNotEmpty(sopLogs.getFsUserId())){
+                                //获取配置并校验
+                                SysConfig luckyBagConfig = configService.selectConfigByConfigKey("luckyBag.config");
+                                if (ObjectUtil.isEmpty(luckyBagConfig)) {
+                                    sopLogs.setSendStatus(5L);
+                                    sopLogs.setReceivingStatus(0L);
+                                    sopLogs.setRemark("福袋配置不存在");
+                                }
+// 2. 解析配置值
+                                JSONObject jsonObject;
+                                try {
+                                    jsonObject = JSON.parseObject(luckyBagConfig.getConfigValue());
+                                    Integer count = jsonObject.getInteger("weekLimit");
+
+                                    // 查询用户记录并校验次数
+                                    LuckyBagCollectRecord queryRecord = new LuckyBagCollectRecord();
+                                    queryRecord.setUserId(sopLogs.getFsUserId());
+                                    queryRecord.setCollectType("1");
+                                    // 动态计算时间范围
+                                    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());
+                                    queryRecord.setParams(params);
+                                    List<LuckyBagCollectRecord> luckyBagCollectRecords =
+                                            luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(queryRecord);
+
+                                    // 判断是否超过限制
+                                    if (luckyBagCollectRecords.size() >= count) {
+                                        sopLogs.setSendStatus(5L);
+                                        sopLogs.setReceivingStatus(0L);
+                                        sopLogs.setRemark("超过福袋发放次数");
+                                    }
+
+                                } catch (Exception e) {
+                                    // 处理配置解析异常
+                                    sopLogs.setSendStatus(5L);
+                                    sopLogs.setReceivingStatus(0L);
+                                    sopLogs.setRemark("福袋配置解析失败");
+                                }
+                            }
+                            linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
+                                    qwUserId, companyUserId, companyId, item.getExternalId(), config,null);
+
+                            miniAppId = null;
+
+                            if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                                Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
+                                if (integerListMap != null) {
+                                    int effectiveGradeTemp = (item.getGrade() == null) ? 5 : item.getGrade();
+                                    int listIndexTemp = (effectiveGradeTemp == 1 || effectiveGradeTemp == 2) ? 0 : 1;
+
+                                    //评级是6 S级,则走A类小程序
+                                    if (effectiveGradeTemp==6){
+                                        listIndexTemp=2;
+                                    }
+
+                                    List<CompanyMiniapp> miniapps = integerListMap.get(listIndexTemp);
+
+                                    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("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                            }
+
+                            st.setMiniprogramTitle("福袋发放");
+
+                            st.setMiniprogramPage(linkByMiniApp);
+                            break;
+                        case "15":
+                            String txt = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
+                            st.setValue(st.getValue()
+                                    .replaceAll("#客服称呼#", txt)
+                                    .replaceAll("#销售称呼#", txt)
+                                    .replaceAll("#客户称呼#", StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus()));
+                            try {
+                                replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                            break;
+                        case "16":
+                            createVoiceUrl(st, companyUserId, qwSop);
+                            try {
+                                if (qwUser.getCompanyUserId() != null) {
+                                    createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
+                                }
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                            break;
                         default:
                             break;
 
@@ -1321,7 +1541,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             switch (finalSendType){
                 case 5:
                     List<QwSopCourseFinishTempSetting.Setting> list = processSetting(item,qwUser, param, words, config, qwCompany,companyUserId,companyId,
-                            contact,dataTime, finalDomainName,miniMap,companies);
+                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs);
                     setting.setSetting(list);
                     break;
                 case 9:
@@ -1385,7 +1605,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                                                       CourseConfig config,QwCompany qwCompany,String companyUserId, String companyId,
                                                                       QwExternalContact contact,Date dataTime,String domainName,
                                                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                                                      List<Company> companies ){
+                                                                      List<Company> companies,QwSopLogs sopLogs ){
         List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
 
         for (QwSopCourseFinishTempSetting.Setting st : list) {
@@ -1446,7 +1666,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 case "4":
 
                     addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
-                            item.getExternalId(),item.getStartTime(),dataTime );
+                            item.getExternalId(),item.getStartTime(),dataTime,2 );
 
                     String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
                             qwUser.getId(), companyUserId, companyId, item.getExternalId(), config, null);
@@ -1487,12 +1707,22 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     QwSop qwSop = qwSopMapper.selectQwSopById(param.getSopId());
                     createVoiceUrl(st, companyUserId, qwSop);
                     break;
+                //app
+                case "9":
+                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(), item.getFsUserId(), String.valueOf(qwUser.getId()),
+                            companyUserId, companyId, item.getExternalId(), item.getStartTime(), dataTime,1);
+
+                    QwCreateLinkByAppVO linkByApp = createLinkByApp(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
+                            qwUser.getId(), companyUserId, companyId, item.getExternalId(), config, qwUser.getQwUserName(), contact.getFsUserId());
+                    st.setLinkUrl(linkByApp.getSortLink());
+                    st.setAppLinkUrl(linkByApp.getAppMsgLink());
+                    break;
 
                 //自定义小程序
                 case "10":
 
                     addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
-                            item.getExternalId(),item.getStartTime(),dataTime );
+                            item.getExternalId(),item.getStartTime(),dataTime,2 );
 
                     Optional<Company> matchedCompany = companies.stream()
                             .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
@@ -1512,6 +1742,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     }
 
                     break;
+
                 //直播小程序单独
                 case "12":
                     String sortLiveLink;
@@ -1540,6 +1771,123 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     }
 
                     break;
+                case "15":
+                    //app文本
+                    try {
+                        qwSop = qwSopMapper.selectQwSopById(param.getSopId());
+                        createVoiceUrl(st, companyUserId, qwSop);
+                        if (qwUser.getCompanyUserId() != null) {
+                            createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
+                        }
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                    break;
+                case "16":
+                    //app语音
+                    try {
+                        qwSop = qwSopMapper.selectQwSopById(param.getSopId());
+                        createVoiceUrl(st, companyUserId, qwSop);
+                        if (qwUser.getCompanyUserId() != null) {
+                            createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
+                        }
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                    break;
+                case "14":
+                    LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(st.getLuckyBagId());
+                    if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
+                        sopLogs.setSendStatus(5L);
+                        sopLogs.setReceivingStatus(0L);
+                        sopLogs.setRemark("福袋配置被禁用");
+                    }else
+                    if (ObjectUtil.isNotEmpty(sopLogs.getFsUserId())){
+                        //获取配置并校验
+                        SysConfig luckBagConfig = configService.selectConfigByConfigKey("luckyBag.config");
+                        if (ObjectUtil.isEmpty(luckBagConfig)) {
+                            sopLogs.setSendStatus(5L);
+                            sopLogs.setReceivingStatus(0L);
+                            sopLogs.setRemark("福袋配置不存在");
+                        }
+                        // 2. 解析配置值
+                        JSONObject jsonObject;
+                        try {
+                            jsonObject = JSON.parseObject(luckBagConfig.getConfigValue());
+                            Integer count = jsonObject.getInteger("weekLimit");
+
+                            // 查询用户记录并校验次数
+                            LuckyBagCollectRecord queryRecord = new LuckyBagCollectRecord();
+                            queryRecord.setUserId(sopLogs.getFsUserId());
+                            queryRecord.setCollectType("1");
+                            // 动态计算时间范围
+                            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());
+                            queryRecord.setParams(params);
+                            List<LuckyBagCollectRecord> luckyBagCollectRecords =
+                                    luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(queryRecord);
+
+                            // 判断是否超过限制
+                            if (luckyBagCollectRecords.size() >= count) {
+                                sopLogs.setSendStatus(5L);
+                                sopLogs.setReceivingStatus(0L);
+                                sopLogs.setRemark("超过福袋发放次数");
+                            }
+
+                        } catch (Exception e) {
+                            // 处理配置解析异常
+                            sopLogs.setSendStatus(5L);
+                            sopLogs.setReceivingStatus(0L);
+                            sopLogs.setRemark("福袋配置解析失败");
+                        }
+                    }
+
+                    linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
+                            String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config,null);
+
+                    miniAppId = null;
+
+                    if (!miniMap.isEmpty() && qwUser.getSendMsgType() == 1) {
+                        Map<Integer, List<CompanyMiniapp>> integerListMap = miniMap.get(Long.valueOf(companyId));
+                        if (integerListMap != null) {
+                            effectiveGrade = (item.getGrade() == null) ? 5 : item.getGrade();
+                            listIndex = (effectiveGrade == 1 || effectiveGrade == 2) ? 0 : 1;
+
+                            //评级是6 S级,则走A类小程序
+                            if (effectiveGrade==6){
+                                listIndex=2;
+                            }
+
+                            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("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                    }
+
+                    st.setMiniprogramTitle("福袋发放");
+
+                    st.setMiniprogramPage(linkByMiniApp);
+                    break;
+
                 default:
                     break;
 
@@ -1549,6 +1897,134 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return  list;
     }
 
+    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);
+        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));
+        st.setExternalUserId(sopLogs.getExternalUserId());
+        String json = configService.selectConfigByKey("luckyBag.config");
+        Map<String, Object> luckyBagConfig = JSON.parseObject(json, Map.class);
+        Object miniprogramPicUrl = luckyBagConfig.get("miniprogramPicUrl");
+        if(miniprogramPicUrl != null){
+            st.setMiniprogramPicUrl(miniprogramPicUrl.toString());
+        }
+        courseMap.setQwExternalId(sopLogs.getExternalId());
+        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(QwSopCourseFinishTempSetting.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 void createVoiceUrlToIm(QwSopCourseFinishTempSetting.Setting st, String companyUserId, QwSop qwSop) {
+        QwSopTempVoice qwSopTempVoice = sopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(Long.valueOf(companyUserId), st.getValue());
+        if (qwSopTempVoice != null && qwSopTempVoice.getVoiceUrl() != null && qwSopTempVoice.getRecordType() == 1) {
+            st.setVoiceUrl(qwSopTempVoice.getUserVoiceUrl());
+            st.setVoiceDuration(String.valueOf(qwSopTempVoice.getDuration()));
+        } else if (qwSopTempVoice == null) {
+            if(st.getValue() != null){
+                qwSopTempVoice = new QwSopTempVoice();
+                qwSopTempVoice.setCompanyUserId(Long.valueOf(companyUserId));
+                qwSopTempVoice.setVoiceTxt(st.getValue());
+                qwSopTempVoice.setRecordType(0);
+                sopTempVoiceService.insertQwSopTempVoice(qwSopTempVoice);
+            }
+        }
+    }
+
     private List<QwSopCourseFinishTempSetting.Setting> parseSettings(String jsonData) {
         try {
             if (jsonData.startsWith("[") && jsonData.endsWith("]")) {
@@ -1597,7 +2073,7 @@ 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) {
+                                     String companyId, Long externalId, String startTime,Date createTime, Integer watchType) {
 
         try {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
@@ -1614,6 +2090,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             watchLog.setUpdateTime(createTime);
             watchLog.setLogType(3);
             watchLog.setUserId(fsUserId);
+            watchLog.setWatchType(watchType);
             watchLog.setCampPeriodTime(convertStringToDate(startTime,"yyyy-MM-dd"));
 
             //存看课记录
@@ -1828,4 +2305,49 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
     }
 
+    /**
+     * 构建福袋记录对象
+     */
+    private LuckyBagCollectRecord buildLuckyBagRecord(QwSopCourseFinishTempSetting.Setting content,
+                                                      QwSopLogs qwSopLogs,
+                                                      Date sendTime,
+                                                      String companyUserId,
+                                                      String companyId,
+                                                      String chatId,
+                                                      Company company,
+                                                      LuckyBag luckyBag) {
+        LuckyBagCollectRecord record = new LuckyBagCollectRecord();
+        QwUser qwUser = qwUserMapper.selectQwUserEntityByQwUserIdAndCorId(qwSopLogs.getQwUserid(),qwSopLogs.getCorpId());
+        record.setQwUserId(qwUser.getQwUserId());
+        record.setQwUserName(qwUser.getQwUserName());
+        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;
+    }
+
 }

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

@@ -190,7 +190,7 @@ public class SopUserLogsServiceImpl implements ISopUserLogsService {
                         Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
                         GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
                         if (vo != null && vo.getId() != null) {
-                            sopUserLogsInfoService.addWatchLogIfNeeded(sopId, rules.getVideoId().intValue(), rules.getCourseId().intValue(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), sopUserLogs.getStartTime(), new Date());
+                            sopUserLogsInfoService.addWatchLogIfNeeded(sopId, rules.getVideoId().intValue(), rules.getCourseId().intValue(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), sopUserLogs.getStartTime(), new Date(),2);
                         }
                     });
                 } catch (Exception e) {
@@ -596,6 +596,7 @@ public class SopUserLogsServiceImpl implements ISopUserLogsService {
 //                                if (fsCourseWatchLogs.isEmpty()) {
 //                                    watchLog.setLogType(3);
 //                                    watchLog.setCreateTime(new Date());
+//                                    watchLog.setWatchType(2);
 //                                    fsCourseWatchLogMapper.insertFsCourseWatchLog(watchLog);
 //                                }
 //

+ 46 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxSendTextAtMsgTwoDTO.java

@@ -0,0 +1,46 @@
+package com.fs.wxwork.dto;
+
+import com.fs.ipad.vo.BaseVo;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class WxSendTextAtMsgTwoDTO extends BaseVo {
+    /**
+     * 消息的唯一标识符 (UUID)
+     */
+    private String uuid;
+
+    /**
+     * 要发送的人或群id
+     */
+    private Long send_userid;
+
+    /**
+     * 发送者用户 ID
+     */
+    private List<Contentva> contentva;
+    /**
+     * 是否为群组消息
+     * <p>
+     * true: 群组消息; false: 单聊消息
+     * </p>
+     */
+    private Boolean isRoom;
+
+    @Data
+    public static class Contentva {
+        private Integer msgtype;
+        private String content;
+        /**
+         * "msgtype":5,//@类型
+         * "vid":0 //填0就是所有人
+         * <p>
+         * "msgtype":5, //就是@人的类型
+         * "vid":788130xx38 //@人id
+         */
+        private Integer vid;
+    }
+
+}

+ 7 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceNew.java

@@ -9,6 +9,7 @@ import com.fs.ipad.param.WxSendAtMsgParam;
 import com.fs.ipad.vo.WxGetSessionRoomListVo;
 import com.fs.ipad.vo.WxRoomUserListVo;
 import com.fs.ipad.vo.WxSendAtMsgVo;
+import com.fs.ipad.vo.WxSendTextAtMsgVo;
 import com.fs.qw.domain.QwIpadServer;
 import com.fs.qw.service.IQwIpadServerService;
 import com.fs.wxwork.dto.*;
@@ -232,4 +233,10 @@ public class WxWorkServiceNew {
         String url = getUrl(serverId) + "/RoomIdToChatId";
         return WxWorkHttpUtilNew.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkChatIdToRoomIdResp>>() {}, serverId);
     }
+    
+    
+    public WxWorkResponseDTO<WxSendTextAtMsgVo> sendTextAtMsgTwo(WxSendTextAtMsgTwoDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/SendTextAtMsgTwo";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxSendTextAtMsgVo>>() {});
+    }
 }

+ 5 - 0
fs-service/src/main/resources/application-common.yml

@@ -143,3 +143,8 @@ wechat:
   api:
     base-url: https://api.weixin.qq.com
     upload-shipping-info: /wxa/sec/order/upload_shipping_info
+hsy:
+  access_key: AKLTZTc4YTE4ZjI2OWViNDNjZGI2NjhiYTI5Njc5ZjA1Mzk
+  secret_key: WXpjelpUYzFOakF5TUdObE5EZGtNR0ZsWXpKaU1tTmtZakk1WXpObE4yRQ==
+  region: cn-north-1
+

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff