Просмотр исходного кода

Merge branch 'master' into 企微聊天

# Conflicts:
#	fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
#	fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
#	fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
ct 1 неделя назад
Родитель
Сommit
191a0912a2
100 измененных файлов с 6148 добавлено и 140 удалено
  1. 41 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java
  2. 7 5
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  3. 3 3
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  4. 1 1
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  5. 35 0
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  6. 14 1
      fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java
  7. 3 0
      fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java
  8. 3 1
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  9. 1 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  10. 106 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java
  11. 111 0
      fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagCollectRecordController.java
  12. 88 0
      fs-company/src/main/java/com/fs/company/controller/qw/LuckyBagController.java
  13. 3 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java
  14. 17 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  15. 12 7
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  16. 1 1
      fs-live-app/src/main/java/com/fs/live/controller/LiveController.java
  17. 108 0
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  18. 112 0
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  19. 312 22
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  20. 13 1
      fs-qw-task/src/main/java/com/fs/app/task/UserCourseWatchCountTask.java
  21. 210 6
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  22. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  23. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRealLink.java
  24. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  25. 69 0
      fs-service/src/main/java/com/fs/course/domain/LuckyBag.java
  26. 151 0
      fs-service/src/main/java/com/fs/course/domain/LuckyBagCollectRecord.java
  27. 1 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  28. 1 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  29. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseAnswerLogsParam.java
  30. 1 0
      fs-service/src/main/java/com/fs/course/param/FsCourseRedPacketLogParam.java
  31. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java
  32. 22 0
      fs-service/src/main/java/com/fs/course/param/LuckyBagActualRewardsParam.java
  33. 0 19
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  34. 3 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  35. 54 0
      fs-service/src/main/java/com/fs/course/vo/FsPeriodCountExportVO.java
  36. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoQVO.java
  37. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java
  38. 8 0
      fs-service/src/main/java/com/fs/his/domain/FsUserIntegralLogs.java
  39. 26 0
      fs-service/src/main/java/com/fs/his/param/FsReceiveLuckyBagParam.java
  40. 1 0
      fs-service/src/main/java/com/fs/his/service/impl/MerchantAppConfigServiceImpl.java
  41. 12 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java
  42. 10 0
      fs-service/src/main/java/com/fs/hisStore/mapper/MergedOrderMapper.java
  43. 31 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesDeliveryParam.java
  44. 43 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesParam.java
  45. 23 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesQueryParam.java
  46. 22 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesRevokeParam.java
  47. 25 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedOrderDeleteParam.java
  48. 56 1
      fs-service/src/main/java/com/fs/hisStore/service/IMergedOrderService.java
  49. 42 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  50. 230 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/MergedOrderServiceImpl.java
  51. 103 0
      fs-service/src/main/java/com/fs/hisStore/vo/MergedAfterSalesVO.java
  52. 6 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  53. 58 0
      fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java
  54. 66 0
      fs-service/src/main/java/com/fs/live/domain/LiveTagConfig.java
  55. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
  56. 89 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java
  57. 53 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  58. 67 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTagConfigMapper.java
  59. 1 1
      fs-service/src/main/java/com/fs/live/mapper/LiveVideoMapper.java
  60. 74 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java
  61. 6 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  62. 59 0
      fs-service/src/main/java/com/fs/live/param/LiveIsAddKfParam.java
  63. 43 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  64. 7 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  65. 61 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchLogService.java
  66. 13 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  67. 328 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  68. 63 7
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  69. 51 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  70. 94 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java
  71. 449 13
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  72. 29 0
      fs-service/src/main/java/com/fs/live/vo/HandleUserTagVO.java
  73. 25 0
      fs-service/src/main/java/com/fs/live/vo/LiveTagItemVO.java
  74. 72 0
      fs-service/src/main/java/com/fs/live/vo/LiveWatchUserEntry.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. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  78. 7 1
      fs-service/src/main/java/com/fs/qw/param/QwExternalContactParam.java
  79. 63 0
      fs-service/src/main/java/com/fs/qw/service/ILuckyBagCollectRecordService.java
  80. 29 0
      fs-service/src/main/java/com/fs/qw/service/ILuckyBagService.java
  81. 59 0
      fs-service/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java
  82. 94 0
      fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagCollectRecordServiceImpl.java
  83. 416 0
      fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagServiceImpl.java
  84. 8 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java
  85. 390 24
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  86. 3 3
      fs-service/src/main/resources/application-config-druid-bjzm-test.yml
  87. 3 3
      fs-service/src/main/resources/application-config-druid-bjzm.yml
  88. 101 0
      fs-service/src/main/resources/application-config-druid-hsyy.yml
  89. 1 1
      fs-service/src/main/resources/application-config-druid-yxj.yml
  90. 4 4
      fs-service/src/main/resources/application-druid-bjzm-test.yml
  91. 172 0
      fs-service/src/main/resources/application-druid-hsyy.yml
  92. 3 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  93. 1 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  94. 81 0
      fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml
  95. 116 0
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  96. 10 7
      fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml
  97. 124 0
      fs-service/src/main/resources/mapper/live/LiveTagConfigMapper.xml
  98. 213 0
      fs-service/src/main/resources/mapper/live/LiveWatchLogMapper.xml
  99. 194 0
      fs-service/src/main/resources/mapper/qw/LuckyBagCollectRecordMapper.xml
  100. 84 0
      fs-service/src/main/resources/mapper/qw/LuckyBagMapper.xml

+ 41 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java

@@ -1,5 +1,6 @@
 package com.fs.course.controller;
 
+import com.fs.baidu.domain.BdAccount;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -15,6 +16,7 @@ import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.service.IFsUserCourseVideoRedPackageService;
 import com.fs.course.vo.*;
+import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.his.vo.OptionsVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -231,6 +234,44 @@ public class FsUserCoursePeriodController extends BaseController {
         return R.ok(result);
     }
 
+    /**
+     * @Description: 会员营期导出课程统计
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/12/12 11:37
+     */
+    @PostMapping("/exportInfo")
+    @ApiOperation("营期统计")
+    public AjaxResult exportInfo(@RequestBody PeriodCountParam param) {
+        List<FsPeriodCountExportVO> exportList = new ArrayList<>();
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsPeriodCountVO> list = fsUserCoursePeriodDaysService.periodCourseCount(param);
+        list.forEach(item -> {
+            FsPeriodCountExportVO exportVO = new FsPeriodCountExportVO();
+            exportVO.setTitle(item.getTitle());
+            exportVO.setDayDate(item.getDayDate());
+            if(item.getCountDetailsVO() != null){
+                FsCourseAnalysisCountVO countDetailsVO =item.getCountDetailsVO();
+                exportVO.setCourseWatchTimes(countDetailsVO.getCourseWatchTimes());
+                exportVO.setCourseCompleteTimes(countDetailsVO.getCourseCompleteTimes());
+                exportVO.setCourseWatchNum(countDetailsVO.getCourseWatchNum());
+                exportVO.setCourseCompleteNum(countDetailsVO.getCourseCompleteNum());
+                exportVO.setCompleteRate(countDetailsVO.getCompleteRate()+"%");
+                exportVO.setAnswerTimes(countDetailsVO.getAnswerTimes());
+                exportVO.setAnswerNum(countDetailsVO.getAnswerNum());
+                exportVO.setAnswerRightNum(countDetailsVO.getAnswerRightNum());
+                exportVO.setAnswerRightRate(countDetailsVO.getAnswerRightRate()+"%");
+                exportVO.setRedPacketNum(countDetailsVO.getRedPacketNum());
+                exportVO.setRedPacketAmount(countDetailsVO.getRedPacketAmount());
+            }
+            exportList.add(exportVO);
+        });
+
+        ExcelUtil<FsPeriodCountExportVO> util = new ExcelUtil<FsPeriodCountExportVO>(FsPeriodCountExportVO.class);
+        return util.exportExcel(exportList, "百度账号数据");
+    }
+
     @GetMapping("/getPeriodListLikeName")
     public R getPeriodListLikeName(@RequestParam(required = false) String name,
                                    @RequestParam(required = false) Long campId,

+ 7 - 5
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -117,8 +117,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                     }
                 }
                 //
-                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") && (vo.getCost() !=null && vo.getTotalNum() != null))) {
-                    vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
+                if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) {
+                    if((vo.getCost() !=null && vo.getTotalNum() != null)){
+                        vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
+                    }
                 } else {
                     vo.setPayPostage(BigDecimal.ZERO);
                     vo.setCost(BigDecimal.ZERO);
@@ -306,7 +308,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                     }
                 }
                 //
-                if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                     vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(BigDecimal.ZERO);
@@ -371,7 +373,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                             } catch (Exception e) {
                             }
                         }
-                        if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                        if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                             vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                         } else {
                             vo.setPayPostage(BigDecimal.ZERO);
@@ -397,7 +399,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                     } catch (Exception e) {
                     }
                 }
-                if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                     vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(BigDecimal.ZERO);

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

@@ -498,7 +498,7 @@ public class FsStoreOrderScrmController extends BaseController {
                     }
                 }
                 //
-                if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                     vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(BigDecimal.ZERO);
@@ -567,7 +567,7 @@ public class FsStoreOrderScrmController extends BaseController {
                         }
                     }
                     //
-                    if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                    if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                         vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                     } else {
                         vo.setPayPostage(BigDecimal.ZERO);
@@ -595,7 +595,7 @@ public class FsStoreOrderScrmController extends BaseController {
                     }
                 }
                 //
-                if (loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
                     vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(BigDecimal.ZERO);

+ 1 - 1
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -172,7 +172,7 @@ public class LiveTask {
 
     // 订单银行回调数据丢失补偿
     public void recoveryBankOrder() {
-        // 查询出来最近三十分钟的订单 待支付 未退款
+        // 查询出来最近15分钟的订单 待支付 未退款
         List<LiveOrder> list = liveOrderService.selectBankOrder();
         if(list == null || list.isEmpty()) return;
         for (LiveOrder liveOrder : list) {

+ 35 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveController.java

@@ -9,12 +9,18 @@ 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.company.vo.CompanyVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.hisStore.task.LiveTask;
 import com.fs.hisStore.task.MallStoreTask;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;
+import com.fs.qw.domain.QwTagGroup;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.service.impl.QwUserServiceImpl;
+import com.fs.qw.vo.QwOptionsVO;
+import com.fs.qw.vo.QwTagGroupListVO;
 import com.hc.openapi.tool.fastjson.JSON;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -41,6 +47,12 @@ public class LiveController extends BaseController {
     @Autowired
     private TokenService tokenService;
 
+    @Autowired
+    QwUserServiceImpl qwUserService;
+
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+
 
     /**
      * 查询直播列表
@@ -186,4 +198,27 @@ public class LiveController extends BaseController {
     }
 
 
+    /**
+     * 获取公司下拉列表
+     * @return
+     */
+    @GetMapping("/getCompanyDropList")
+    public R getCompanyDropList(){
+        List<CompanyVO> companyDropList = liveService.getCompanyDropList();
+        return R.ok().put("data",companyDropList);
+    }
+
+    @GetMapping("/getQwCorpList/{companyId}")
+    public R getQwCorpList(@PathVariable Long companyId){
+        List<QwOptionsVO> qwOptionsVOS = qwUserService.selectQwCompanyListOptionsVOByCompanyId(companyId);
+        return R.ok().put("data",qwOptionsVOS);
+    }
+
+    @GetMapping("/getTagsListByCorpId")
+    public TableDataInfo getTagsListByCorpId(QwTagGroup qwTagGroup){
+        startPage();
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(qwTagGroup);
+        return getDataTable(list);
+    }
+
 }

+ 14 - 1
fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java

@@ -5,14 +5,17 @@ import java.util.Objects;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.core.domain.R;
 import com.fs.common.exception.ServiceException;
 import com.fs.qw.param.QwExternalContactParam;
 import com.fs.qw.param.QwTagSearchParam;
+import com.fs.qw.service.IQwExternalContactInfoService;
 import com.fs.qw.service.IQwTagService;
 import com.fs.qw.vo.QwExternalContactUnionIdExportVO;
 import com.fs.qw.vo.QwExternalContactVO;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
+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.PostMapping;
@@ -33,7 +36,7 @@ import com.fs.common.core.page.TableDataInfo;
 
 /**
  * 企业微信客户Controller
- * 
+ *
  * @author fs
  * @date 2025-06-13
  */
@@ -44,6 +47,9 @@ public class QwExternalContactController extends BaseController
     private IQwExternalContactService qwExternalContactService;
     private IQwTagService iQwTagService;
 
+    @Autowired
+    private IQwExternalContactInfoService qwExternalContactInfoService;
+
     QwExternalContactController(IQwExternalContactService qwExternalContactService,IQwTagService iQwTagService){
         this.qwExternalContactService=qwExternalContactService;
         this.iQwTagService=iQwTagService;
@@ -168,4 +174,11 @@ public class QwExternalContactController extends BaseController
     {
         return toAjax(qwExternalContactService.deleteQwExternalContactByIds(ids));
     }
+
+    @GetMapping(value = "getUserInfo/{id}")
+    public R getUserInfo(@PathVariable("id") Long id)
+    {
+        return R.ok().put("data",qwExternalContactInfoService.selectQwExternalContactInfoByExternalContactId(id));
+    }
+
 }

+ 3 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java

@@ -219,6 +219,9 @@ public class QwSopTempController extends BaseController
     @PreAuthorize("@ss.hasPermi('qw:sopTemp:edit')")
     @PostMapping("/sortDay")
     public AjaxResult sortDay(@RequestBody List<SortDayVo> list){
+        if (list == null || list.isEmpty()){
+            return AjaxResult.error("请先调整排序");
+        }
         qwSopTempService.sortDay(list);
         return toAjax(1);
     }

+ 3 - 1
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -28,7 +28,7 @@ public class LiveKeysConstant {
     public static final String TOP_MSG = "topMsg"; //抽奖记录
 
     public static final String LIVE_FLAG_CACHE = "live:flag:%s"; //直播间直播/回放状态缓存
-    public static final Integer LIVE_FLAG_CACHE_EXPIRE = 300; //直播间状态缓存过期时间(秒)
+    public static final Integer LIVE_FLAG_CACHE_EXPIRE = 60; //直播间状态缓存过期时间(秒)
 
     public static final String LIVE_DATA_CACHE = "live:data:%s"; //直播间数据缓存
     public static final Integer LIVE_DATA_CACHE_EXPIRE = 300; //直播间数据缓存过期时间(秒)
@@ -36,5 +36,7 @@ public class LiveKeysConstant {
     public static final String PRODUCT_DETAIL_CACHE = "product:detail:%s"; //商品详情缓存
     public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
+    public static final String LIVE_TAG_MARK_CACHE = "live:tag:mark:%s"; //直播间打标签缓存,存储直播间ID、开始时间和视频时长
+
 
 }

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -102,7 +102,7 @@ public class LiveDataController extends BaseController
         CompanyUser user = tokenService.getLoginUser(request).getUser();
         List<LiveUserDetailExportVO> list = new ArrayList<>();
         if ("00".equals(user.getUserType())) {
-            liveDataService.getLiveUserDetailListBySql(liveId, user.getCompanyId(), null);
+            list = liveDataService.exportLiveUserDetail(liveId, user.getCompanyId(), null);
         } else {
             list = liveDataService.exportLiveUserDetail(liveId,user.getCompanyId(),user.getUserId());
         }

+ 106 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java

@@ -0,0 +1,106 @@
+package com.fs.company.controller.live;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.service.ILiveWatchLogService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 直播看课记录Controller
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+@RestController
+@RequestMapping("/live/liveWatchLog")
+public class LiveWatchLogController extends BaseController
+{
+    @Autowired
+    private ILiveWatchLogService liveWatchLogService;
+
+    /**
+     * 查询直播看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveWatchLog liveWatchLog)
+    {
+        startPage();
+        List<LiveWatchLog> list = liveWatchLogService.selectLiveWatchLogList(liveWatchLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:export')")
+    @Log(title = "直播看课记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveWatchLog liveWatchLog)
+    {
+        List<LiveWatchLog> list = liveWatchLogService.selectLiveWatchLogList(liveWatchLog);
+        ExcelUtil<LiveWatchLog> util = new ExcelUtil<LiveWatchLog>(LiveWatchLog.class);
+        return util.exportExcel(list, "直播看课记录数据");
+    }
+
+    /**
+     * 获取直播看课记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(liveWatchLogService.selectLiveWatchLogByLogId(logId));
+    }
+
+    /**
+     * 新增直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:add')")
+    @Log(title = "直播看课记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveWatchLog liveWatchLog)
+    {
+        return toAjax(liveWatchLogService.insertLiveWatchLog(liveWatchLog));
+    }
+
+    /**
+     * 修改直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:edit')")
+    @Log(title = "直播看课记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveWatchLog liveWatchLog)
+    {
+        return toAjax(liveWatchLogService.updateLiveWatchLog(liveWatchLog));
+    }
+
+    /**
+     * 删除直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:remove')")
+    @Log(title = "直播看课记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(liveWatchLogService.deleteLiveWatchLogByLogIds(logIds));
+    }
+
+}

+ 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));
+    }
+}

+ 3 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java

@@ -334,6 +334,9 @@ public class QwSopTempController extends BaseController
     @Log(title = "SOP模板天数调整", businessType = BusinessType.UPDATE)
     @PostMapping("/sortDay")
     public AjaxResult sortDay(@RequestBody List<SortDayVo> list){
+        if (list == null || list.isEmpty()){
+            return AjaxResult.error("请先调整排序");
+        }
         qwSopTempService.sortDay(list);
         return toAjax(1);
     }

+ 17 - 0
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -14,7 +14,9 @@ import com.fs.company.service.ICompanyMiniappService;
 import com.fs.config.ai.AiHostProper;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.fastGpt.service.AiHookService;
@@ -83,6 +85,9 @@ public class IpadSendServer {
 
     private final IFsCoursePlaySourceConfigService playSourceConfigService;
     private final FsUserMapper fsUserMapper;
+    private final FsUserCourseVideoMapper fsUserCourseVideoMapper;
+
+
     private static final List<String> PROJECT_NAMES = Arrays.asList("济南联志健康", "北京存在文化","宽益堂");
     private void sendMiniProgram(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap, Long companyId) {
         // 发送参数原本的appid
@@ -483,6 +488,18 @@ public class IpadSendServer {
             return false;
         }
 
+        // 查询视频是否下架
+        if(setting.getVideoId()!= null){
+            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId( setting.getVideoId().longValue());
+            if(video != null){
+                if(video.getIsOnPut() == 1){
+                    log.warn("SOP_LOG_ID:{}, 视频已下架,不发送", qwSopLogs.getId());
+                    qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "视频已下架,不发送");
+                    return false;
+                }
+            }
+        }
+
         if (qwSopLogs.getSendType() != 6 && noSop) {
             // 客户的信息
 //            QwExternalContactHParam contactHParam = new QwExternalContactHParam();

+ 12 - 7
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,18 +273,22 @@ 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文本消息
-                        settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "11".equals(e.getContentType())).collect(Collectors.toList());
+                        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());
+                        if (!settings.isEmpty()) {
+                            asyncSopTestService.asyncSendMsgBySopAppTxtNormalIM(settings, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId());
                         }
                         //app语音消息
-                        settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "12".equals(e.getContentType())).collect(Collectors.toList());
-                        if (!settings.isEmpty()){
-                            asyncSopTestService.asyncSendMsgBySopAppMP3NormalIM(settings, qwSopLogs.getCorpId(),qwUser.getCompanyUserId(),qwSopLogs.getFsUserId());
+                        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);

+ 1 - 1
fs-live-app/src/main/java/com/fs/live/controller/LiveController.java

@@ -126,7 +126,7 @@ public class LiveController {
 
 		LinkedHashMap<String,Object> result = (LinkedHashMap<String,Object>) params.get("WorkflowExecution");
 		String string = result.get("Object").toString();
-		videoService.updateFinishStatus(videoUrl + string.replace(".mp4", "-1080.m3u8"));
+		videoService.updateFinishStatus(string.replace(".mp4", ".m3u8"));
 
 		return R.ok();
 //		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,

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

@@ -0,0 +1,108 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+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.Collection;
+import java.util.List;
+
+/**
+ * 直播完课积分定时任务
+ */
+@Slf4j
+@Component
+public class LiveCompletionPointsTask {
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    /**
+     * 定时检查观看时长并创建完课记录
+     * 每分钟执行一次
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionStatus() {
+        try {
+            // 1. 获取所有观看时长的Redis key
+            Collection<String> keys = redisCache.keys("live:watch:duration:*");
+            
+            if (keys == null || keys.isEmpty()) {
+                return;
+            }
+
+            // 2. 遍历处理每个用户的观看时长
+            for (String key : keys) {
+                try {
+                    String[] parts = key.split(":");
+                    if (parts.length != 5) {
+                        continue;
+                    }
+
+                    Long liveId = Long.parseLong(parts[3]);
+                    Long userId = Long.parseLong(parts[4]);
+
+                    // 3. 获取观看时长(秒)
+                    Object durationObj = redisCache.getCacheObject(key);
+                    if (durationObj == null) {
+                        continue;
+                    }
+
+                    Long watchDuration = Long.parseLong(durationObj.toString());
+
+                    // 4. 检查并创建完课记录
+                    completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, watchDuration);
+
+                    // 5. 检查是否有新的完课记录待领取,推送弹窗消息
+                    sendCompletionNotification(liveId, userId);
+
+                } catch (Exception e) {
+                    log.error("处理观看时长失败, key={}", key, e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("检查完课状态定时任务执行失败", 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);
+        }
+    }
+}

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

@@ -71,6 +71,8 @@ public class Task {
     private ILiveRedConfService liveRedConfService;
     @Autowired
     private ILiveCouponIssueService liveCouponIssueService;
+    @Autowired
+    private ILiveVideoService liveVideoService;
 
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
@@ -163,6 +165,34 @@ public class Task {
                         redisCache.redisTemplate.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
+                
+                // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
+                try {
+                    // 获取视频时长
+                    Long videoDuration = 0L;
+                    List<LiveVideo> videos = liveVideoService.listByLiveId(live.getLiveId(), 1);
+                    if (CollUtil.isNotEmpty(videos)) {
+                        videoDuration = videos.stream()
+                                .filter(v -> v.getDuration() != null)
+                                .mapToLong(LiveVideo::getDuration)
+                                .sum();
+                    }
+                    
+                    // 如果视频时长大于0,将直播间信息存入Redis
+                    if (videoDuration > 0 && live.getStartTime() != null) {
+                        Map<String, Object> tagMarkInfo = new HashMap<>();
+                        tagMarkInfo.put("liveId", live.getLiveId());
+                        tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
+                        tagMarkInfo.put("videoDuration", videoDuration);
+                        
+                        String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                        redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
+                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}", 
+                                live.getLiveId(), live.getStartTime(), videoDuration);
+                    }
+                } catch (Exception e) {
+                    log.error("写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+                }
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
@@ -183,6 +213,15 @@ public class Task {
                     });
                 }
                 webSocketServer.removeLikeCountCache(live.getLiveId());
+                
+                // 删除打标签缓存
+                try {
+                    String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                    redisCache.deleteObject(tagMarkKey);
+                    log.info("直播间结束,已删除打标签缓存: liveId={}", live.getLiveId());
+                } catch (Exception e) {
+                    log.error("删除直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+                }
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
@@ -579,4 +618,77 @@ public class Task {
     public void updateRedQuantityNum() {
         liveRedConfService.updateRedQuantityNum();
     }
+
+    /**
+     * 定时扫描开启的直播间,检查是否到了打标签的时间
+     * 每10秒执行一次
+     */
+    @Scheduled(cron = "0/10 * * * * ?")
+    @DistributeLock(key = "scanLiveTagMark", scene = "task")
+    public void scanLiveTagMark() {
+        try {
+            // 获取所有打标签缓存的key
+            String pattern = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, "*");
+            Set<String> keys = redisCache.redisTemplate.keys(pattern);
+            
+            if (keys == null || keys.isEmpty()) {
+                return;
+            }
+            
+            long currentTimeMillis = System.currentTimeMillis();
+            List<Long> processedLiveIds = new ArrayList<>();
+            
+            for (String key : keys) {
+                try {
+                    // 从Redis获取直播间信息
+                    Object cacheValue = redisCache.getCacheObject(key);
+                    if (cacheValue == null) {
+                        continue;
+                    }
+                    
+                    String jsonStr = cacheValue.toString();
+                    JSONObject tagMarkInfo = JSON.parseObject(jsonStr);
+                    Long liveId = tagMarkInfo.getLong("liveId");
+                    Long startTimeMillis = tagMarkInfo.getLong("startTime");
+                    Long videoDuration = tagMarkInfo.getLong("videoDuration");
+                    
+                    if (liveId == null || startTimeMillis == null || videoDuration == null || videoDuration <= 0) {
+                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}", 
+                                key, liveId, startTimeMillis, videoDuration);
+                        continue;
+                    }
+                    
+                    // 计算结束时间:开始时间 + 视频时长(秒转毫秒)
+                    long endTimeMillis = startTimeMillis + (videoDuration * 1000);
+                    
+                    // 如果当前时间已经超过了结束时间,执行打标签操作
+                    if (currentTimeMillis >= endTimeMillis) {
+                        log.info("直播间视频播放完成,开始打标签: liveId={}, startTime={}, videoDuration={}, endTime={}, currentTime={}", 
+                                liveId, startTimeMillis, videoDuration, endTimeMillis, currentTimeMillis);
+                        
+                        // 调用打标签方法
+                        liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+                        
+                        // 标记为已处理,稍后删除缓存
+                        processedLiveIds.add(liveId);
+                    }
+                } catch (Exception e) {
+                    log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
+                }
+            }
+            
+            // 删除已处理的直播间缓存
+            for (Long liveId : processedLiveIds) {
+                try {
+                    String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, liveId);
+                    redisCache.deleteObject(tagMarkKey);
+                    log.info("已删除已处理的直播间打标签缓存: liveId={}", liveId);
+                } catch (Exception e) {
+                    log.error("删除直播间打标签缓存失败: liveId={}, error={}", liveId, e.getMessage(), e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("扫描直播间打标签任务异常: error={}", e.getMessage(), e);
+        }
+    }
 }

+ 312 - 22
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -11,6 +11,9 @@ import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.config.ProductionWordFilter;
 import com.fs.live.mapper.LiveCouponMapper;
+import com.fs.live.vo.LiveWatchUserEntry;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.domain.LiveVideo;
 import com.fs.live.websocket.auth.WebSocketConfigurator;
 import com.fs.live.websocket.bean.SendMsgVo;
 import com.fs.common.core.domain.R;
@@ -55,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 = 3 * 60 * 1000;
+    private final static long HEARTBEAT_TIMEOUT = 1 * 60 * 1000;
     // admin房间消息发送线程池(单线程,保证串行化)
     private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
 
@@ -72,7 +75,12 @@ public class WebSocketServer {
     private final ILiveUserFirstEntryService liveUserFirstEntryService =  SpringUtils.getBean(ILiveUserFirstEntryService.class);
     private final ILiveCouponIssueService liveCouponIssueService =  SpringUtils.getBean(ILiveCouponIssueService.class);
     private final LiveCouponMapper liveCouponMapper = SpringUtils.getBean(LiveCouponMapper.class);
+    private final ILiveWatchLogService liveWatchLogService = SpringUtils.getBean(ILiveWatchLogService.class);
+    private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private static Random random = new Random();
+    
+    // Redis key 前缀:用户进入直播间时间
+    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -114,6 +122,11 @@ public class WebSocketServer {
 
             LiveWatchUser liveWatchUserVO = liveWatchUserService.join(fsUser,liveId, userId, location);
             room.put(userId, session);
+            
+            // 存储用户进入直播间的时间到 Redis(用于计算在线时长)
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
+            
             // 直播间浏览量 +1
             redisCache.incr(PAGE_VIEWS_KEY + liveId, 1);
 
@@ -161,6 +174,18 @@ public class WebSocketServer {
             }
 
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            // 如果用户连上了 socket,并且公司ID和销售ID大于0,更新 LiveWatchLog 的 logType
+
+            if ((companyId > 0 && companyUserId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
+                // 获取当前直播/回放状态
+                Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                Integer currentLiveFlag = flagMap.get("liveFlag");
+
+                // 如果当前是直播状态(liveFlag = 1),更新 logType
+                if (currentLiveFlag != null && currentLiveFlag == 1) {
+                    updateLiveWatchLogTypeOnConnect(liveId, userId, companyId, companyUserId);
+                }
+            }
             if (liveUserFirstEntry != null) {
                 // 处理第一次自己进入,第二次扫码销售进入
                 if (liveUserFirstEntry.getCompanyUserId() == -1L && companyUserId != -1L) {
@@ -205,6 +230,15 @@ public class WebSocketServer {
     @OnClose
     public void onClose(Session session) {
         Map<String, Object> userProperties = session.getUserProperties();
+        // 获取公司ID和销售ID
+        long companyId = -1L;
+        long companyUserId = -1L;
+        if (!Objects.isNull(userProperties.get("companyId"))) {
+            companyId = (long) userProperties.get("companyId");
+        }
+        if (!Objects.isNull(userProperties.get("companyUserId"))) {
+            companyUserId = (long) userProperties.get("companyUserId");
+        }
 
         long liveId = (long) userProperties.get("liveId");
         long userId = (long) userProperties.get("userId");
@@ -217,6 +251,8 @@ public class WebSocketServer {
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
+            // 计算并更新用户在线时长
+            updateUserOnlineDuration(liveId, userId, companyId, companyUserId);
             room.remove(userId);
             if (room.isEmpty()) {
                 rooms.remove(liveId);
@@ -228,6 +264,7 @@ public class WebSocketServer {
             // 从在线用户Set中移除用户ID
             String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
             redisCache.redisTemplate.opsForSet().remove(onlineUsersSetKey, String.valueOf(userId));
+
             LiveWatchUser liveWatchUserVO = liveWatchUserService.close(fsUser,liveId, userId);
 
 
@@ -278,6 +315,23 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
+                    
+                    // 心跳时同步更新观看时长
+                    long watchUserId = (long) userProperties.get("userId");
+                    String durationKey = "live:watch:duration:" + liveId + ":" + watchUserId;
+                    
+                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+                        try {
+                            Long currentDuration = Long.parseLong(msg.getData());
+                            Object existingDuration = redisCache.getCacheObject(durationKey);
+                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+                                redisCache.setCacheObject(durationKey, currentDuration.toString(), 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":
@@ -565,6 +619,23 @@ public class WebSocketServer {
         sendMsgVo.setData(String.valueOf(scoreAmount));
 
         if(Objects.isNull( session)) return;
+        // 使用带锁的sendMessage方法,保证线程安全
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error("发送积分消息失败: liveId={}, userId={}, error={}", liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 发送完课积分弹窗通知给特定用户
+     */
+    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)));
     }
 
@@ -585,40 +656,56 @@ public class WebSocketServer {
         sendMsgVo.setData(null);
 
         if(Objects.isNull( session)) return;
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        // 使用带锁的sendMessage方法,保证线程安全
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error("发送封禁消息失败: liveId={}, userId={}, error={}", liveId, userId, e.getMessage(), e);
+        }
     }
 
     /**
      * 广播消息
      * @param liveId   直播间ID
      * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     public void broadcastWebMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        
+        if (room.isEmpty()) {
+            return;
+        }
 
-        // 普通用户房间:并行发送
-        room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,1);
+        // 普通用户房间:并行发送(使用快照遍历,避免并发修改)
+        // ConcurrentHashMap 的 entrySet() 是弱一致性的,但为了更安全,我们显式创建快照
+        for (Map.Entry<Long, Session> entry : room.entrySet()) {
+            Session session = entry.getValue();
+            if (session != null && session.isOpen()) {
+                sendWithRetry(session, message, 1);
             }
-        });
+        }
     }
 
     /**
      * 广播消息
      * @param liveId   直播间ID
      * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     public void broadcastMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
-        // 普通用户房间:并行发送
-        room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,1);
+        // 普通用户房间:并行发送(使用快照遍历,避免并发修改)
+        if (!room.isEmpty()) {
+            for (Map.Entry<Long, Session> entry : room.entrySet()) {
+                Session session = entry.getValue();
+                if (session != null && session.isOpen()) {
+                    sendWithRetry(session, message, 1);
+                }
             }
-        });
+        }
 
         // admin房间:串行发送,使用单线程执行器
         if (!adminRoom.isEmpty()) {
@@ -696,23 +783,39 @@ public class WebSocketServer {
         }
     }
 
+
     /**
      * 定时清理无效会话(每分钟执行一次)
      * 检查心跳超时的会话并关闭
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     @Scheduled(fixedRate = 60000) // 每分钟执行一次
     public void cleanInactiveSessions() {
         long currentTime = System.currentTimeMillis();
         int cleanedCount = 0;
 
-        // 遍历所有直播间
+        // 遍历所有直播间(使用快照,避免在遍历过程中被修改影响)
         for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> roomEntry : rooms.entrySet()) {
             Long liveId = roomEntry.getKey();
             ConcurrentHashMap<Long, Session> room = roomEntry.getValue();
+            
+            // 如果房间为空,跳过
+            if (room.isEmpty()) {
+                continue;
+            }
 
-            // 检查普通用户会话
+            // 检查普通用户会话(使用快照遍历,避免并发修改异常)
             List<Long> toRemove = new ArrayList<>();
-            room.forEach((userId, session) -> {
+            // 创建快照,避免在遍历过程中修改原集合
+            for (Map.Entry<Long, Session> userEntry : room.entrySet()) {
+                Long userId = userEntry.getKey();
+                Session session = userEntry.getValue();
+                
+                if (session == null) {
+                    toRemove.add(userId);
+                    continue;
+                }
+                
                 Long lastHeartbeat = heartbeatCache.get(session.getId());
                 if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
                     toRemove.add(userId);
@@ -720,16 +823,35 @@ public class WebSocketServer {
                         if (session.isOpen()) {
                             session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
                         }
+                        
+                        // 计算并更新用户在线时长(心跳超时断开连接)
+                        Map<String, Object> userProperties = session.getUserProperties();
+                        long companyId = -1L;
+                        long companyUserId = -1L;
+                        if (!Objects.isNull(userProperties.get("companyId"))) {
+                            companyId = (long) userProperties.get("companyId");
+                        }
+                        if (!Objects.isNull(userProperties.get("companyUserId"))) {
+                            companyUserId = (long) userProperties.get("companyUserId");
+                        }
+                        updateUserOnlineDuration(liveId, userId, companyId, companyUserId);
                     } catch (Exception e) {
                         log.error("关闭超时会话失败: sessionId={}, liveId={}, userId={}",
                                 session.getId(), liveId, userId, e);
                     }
                 }
-            });
+            }
 
             // 移除超时的会话
-            toRemove.forEach(room::remove);
-            cleanedCount += toRemove.size();
+            if (!toRemove.isEmpty()) {
+                String hashKey = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
+                for (Long userId : toRemove) {
+                    room.remove(userId);
+                    // 从 Redis hash 中删除无效用户
+                    redisCache.hashDelete(hashKey, String.valueOf(userId));
+                }
+                cleanedCount += toRemove.size();
+            }
         }
 
         // 检查admin房间
@@ -770,14 +892,22 @@ public class WebSocketServer {
      * 广播点赞消息
      * @param liveId   直播间ID
      * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     public void broadcastLikeMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,1);
+        
+        if (room.isEmpty()) {
+            return;
+        }
+        
+        // 使用快照遍历,避免并发修改
+        for (Map.Entry<Long, Session> entry : room.entrySet()) {
+            Session session = entry.getValue();
+            if (session != null && session.isOpen()) {
+                sendWithRetry(session, message, 1);
             }
-        });
+        }
     }
 
     private void sendWithRetry(Session session, String message, int maxRetries) {
@@ -919,5 +1049,165 @@ public class WebSocketServer {
         redisCache.redisTemplate.opsForZSet().removeRangeByScore(key + liveId, data, data);
     }
 
+    /**
+     * 计算并更新用户在线时长
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     */
+    private void updateUserOnlineDuration(Long liveId, Long userId, Long companyId, Long companyUserId) {
+        try {
+            // 从 Redis 获取用户进入时间
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            
+            if (entryTime == null) {
+                // 如果没有进入时间记录,可能是旧数据,跳过
+                return;
+            }
+            
+            long currentTimeMillis = System.currentTimeMillis();
+            Date now = new Date();
+            
+            // 计算在线时长(秒)
+            long durationSeconds = (currentTimeMillis - entryTime) / 1000;
+            
+            if (durationSeconds <= 0) {
+                return;
+            }
+            
+            // 获取当前直播/回放状态
+            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+            Integer currentLiveFlag = flagMap.get("liveFlag");
+            Integer currentReplayFlag = flagMap.get("replayFlag");
+            
+            // 查询用户记录
+            LiveWatchUserEntry liveWatchUser = liveWatchUserService.selectLiveWatchAndCompanyUserByFlag(
+                    liveId, userId, currentLiveFlag, currentReplayFlag);
+            
+            if (liveWatchUser != null) {
+                // 累加在线时长
+                Long onlineSeconds = liveWatchUser.getOnlineSeconds();
+                if (onlineSeconds == null) {
+                    onlineSeconds = 0L;
+                }
+                liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
+                liveWatchUser.setUpdateTime(now);
+                
+                // 更新数据库
+                liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
+                
+                // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
+                if (currentLiveFlag != null && currentLiveFlag == 1 
+                        && liveWatchUser.getCompanyId() != null && liveWatchUser.getCompanyId() > 0
+                        && liveWatchUser.getCompanyUserId() != null && liveWatchUser.getCompanyUserId() > 0) {
+                    updateLiveWatchLogTypeByDuration(liveId, userId, 
+                            liveWatchUser.getCompanyId(), liveWatchUser.getCompanyUserId(), 
+                            liveWatchUser.getOnlineSeconds());
+                }
+            }
+            
+            // 删除 Redis 中的进入时间记录
+            redisCache.deleteObject(entryTimeKey);
+        } catch (Exception e) {
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 在连接时更新 LiveWatchLog 的 logType
+     * 如果 logType 类型不是 2,修改 logType 类型为 1(看课中)
+     */
+    private void updateLiveWatchLogTypeOnConnect(Long liveId, Long userId, Long companyId, Long companyUserId) {
+        try {
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setCompanyId(companyId);
+            queryLog.setCompanyUserId(companyUserId);
+            
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs != null && !logs.isEmpty()) {
+                for (LiveWatchLog log : logs) {
+                    // 如果 logType 不是 2(完课),则更新为 1(看课中)
+                    if (log.getLogType() == null || log.getLogType() != 2) {
+                        log.setLogType(1);
+                        liveWatchLogService.updateLiveWatchLog(log);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 根据在线时长更新 LiveWatchLog 的 logType
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     * @param onlineSeconds 在线时长(秒)
+     */
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId, 
+                                                   Long companyUserId, Long onlineSeconds) {
+        try {
+            // 获取直播视频总时长(videoType = 1 的视频)
+            List<LiveVideo> videos = liveVideoService.listByLiveId(liveId, 1);
+            long totalVideoDuration = 0L;
+            if (videos != null && !videos.isEmpty()) {
+                totalVideoDuration = videos.stream()
+                        .filter(v -> v.getDuration() != null)
+                        .mapToLong(LiveVideo::getDuration)
+                        .sum();
+            }
+            
+            // 查询 LiveWatchLog
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setCompanyId(companyId);
+            queryLog.setCompanyUserId(companyUserId);
+            
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs == null || logs.isEmpty()) {
+                return;
+            }
+            
+            for (LiveWatchLog log : logs) {
+                boolean needUpdate = false;
+                Integer newLogType = log.getLogType();
+                
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
+                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+                    newLogType = 4;
+                    needUpdate = true;
+                }
+                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
+                    newLogType = 2;
+                    needUpdate = true;
+                }
+                // 如果直播视频 >= 20分钟且 < 40分钟,在线时长 >= 20分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 1200 && totalVideoDuration < 2400 && onlineSeconds >= 1200) { // 20分钟 = 1200秒
+                    newLogType = 2;
+                    needUpdate = true;
+                }
+                
+                // 如果 logType 已经是 2(完课),不再更新
+                if (needUpdate && (log.getLogType() == null || log.getLogType() != 2)) {
+                    log.setLogType(newLogType);
+                    liveWatchLogService.updateLiveWatchLog(log);
+                }
+            }
+        } catch (Exception e) {
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+
 }
 

+ 13 - 1
fs-qw-task/src/main/java/com/fs/app/task/UserCourseWatchCountTask.java

@@ -6,18 +6,27 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.concurrent.atomic.AtomicBoolean;
+
 @Component
 @Slf4j
 public class UserCourseWatchCountTask {
     @Autowired
     private IFsUserCourseCountService userCourseCountService;
 
+    private final AtomicBoolean isRunning1 = new AtomicBoolean(false);
+
 
     /**
      * 每15分钟执行一次
      */
-    @Scheduled(cron = "0 */10 * * * ?")  // 每10分钟执行一次
+    @Scheduled(cron = "0 */20 * * * ?")  // 每10分钟执行一次
     public void userCourseCountTask() {
+        // 尝试设置标志为 true,表示任务开始执行
+        if (!isRunning1.compareAndSet(false, true)) {
+            log.warn("会员看课统计任务执行 - 上一个任务尚未完成,跳过此次执行");
+            return;
+        }
         try {
             log.info("==============会员看课统计任务执行===============开始");
             long startTime = System.currentTimeMillis();
@@ -29,6 +38,9 @@ public class UserCourseWatchCountTask {
             log.info("会员看课统计任务执行----------执行时长:{}", (endTime - startTime));
         } catch (Exception e) {
             log.error("会员看课统计任务执行----------定时任务执行失败", e);
+        } finally {
+            // 重置标志为 false,表示任务已完成
+            isRunning1.set(false);
         }
 
     }

+ 210 - 6
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -20,6 +20,8 @@ import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.mapper.LiveWatchLogMapper;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
@@ -148,12 +150,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<LiveWatchLog> zmLiveWatchQueue = new LinkedBlockingQueue<>(20000);
 
     // Executors for consumer threads
     private ExecutorService qwSopLogsExecutor;
     private ExecutorService watchLogsExecutor;
     private ExecutorService courseLinkExecutor;
     private ExecutorService courseSopAppLinkExecutor;
+    private ExecutorService zmLiveWatchLogExecutor;
     @Autowired
     private IQwGroupChatService qwGroupChatService;
     @Autowired
@@ -184,6 +188,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     private IQwSopTempVoiceService sopTempVoiceService;
 
+    @Autowired
+    LiveWatchLogMapper liveWatchLogMapper;
+
     @PostConstruct
     public void init() {
         loadCourseConfig();
@@ -230,11 +237,17 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return t;
         });
 
+        zmLiveWatchLogExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "zmLiveWatchLogConsumer");
+            t.setDaemon(true);
+            return t;
+        });
 
         qwSopLogsExecutor.submit(this::consumeQwSopLogs);
         watchLogsExecutor.submit(this::consumeWatchLogs);
         courseLinkExecutor.submit(this::consumeCourseLink);
         courseSopAppLinkExecutor.submit(this::consumeCourseSopAppLink);
+        zmLiveWatchLogExecutor.submit(this::consumeZmLiveWatchQueue);
     }
 
     // Scheduled tasks to refresh configurations and domain names periodically
@@ -265,6 +278,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         watchLogsExecutor.shutdown();
         courseLinkExecutor.shutdown();
         courseSopAppLinkExecutor.shutdown();
+        zmLiveWatchLogExecutor.shutdown();
         try {
             if (!qwSopLogsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
                 qwSopLogsExecutor.shutdownNow();
@@ -278,11 +292,15 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             if (!courseSopAppLinkExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
                 courseSopAppLinkExecutor.shutdownNow();
             }
+            if (!zmLiveWatchLogExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                zmLiveWatchLogExecutor.shutdownNow();
+            }
         } catch (InterruptedException e) {
             qwSopLogsExecutor.shutdownNow();
             watchLogsExecutor.shutdownNow();
             courseLinkExecutor.shutdownNow();
             courseSopAppLinkExecutor.shutdownNow();
+            zmLiveWatchLogExecutor.shutdownNow();
             Thread.currentThread().interrupt();
         }
     }
@@ -873,7 +891,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                       Integer grade, Integer sendMsgType ,List<Company> companies ) {
         switch (type) {
             case 1:
-                handleNormalMessage(sopLogs, content,companyUserId);
+                handleNormalMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo);
                 break;
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
@@ -902,9 +920,73 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         enqueueQwSopLogs(sopLogs);
     }
 
-    private void handleNormalMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content,String companyUserId) {
+    private void handleNormalMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId, String companyId,
+                                     boolean isGroupChat,String qwUserId,QwGroupChat groupChat,String externalId,SopUserLogsVo logVo) {
 
-        sopLogs.setContentJson(JSON.toJSONString(content));
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        if (settings == null || settings.isEmpty()) {
+            log.error("Cloned content settings are empty, skipping.");
+            return;
+        }
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //直播小程序单独
+                case "12":
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 1, qwUserId,logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+        }
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+//        sopLogs.setContentJson(JSON.toJSONString(content));
         enqueueQwSopLogs(sopLogs);
     }
 
@@ -1102,14 +1184,37 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 //直播小程序单独
                 case "12":
                     String sortLiveLink;
-                    sortLiveLink = "/pages_course/living?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId();
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId()+"&corpId=" +logVo.getCorpId()+"&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
+                    if(isGroupChat){
+                        try{
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId,companyUserId,vo.getId().toString(), setting.getLiveId(),sysConfig.getAppId(),2,qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        }catch(Exception e){
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(),e);
+                        }
+                    }else{
+                        try{
+                            createLiveWatchLogAndEnQueue(companyId,companyUserId,externalId, setting.getLiveId(),sysConfig.getAppId(),2,qwUserId,logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        }catch(Exception e){
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(),e);
+                        }
+                    }
 
 
                     String miniprogramLiveTitle = setting.getMiniprogramTitle();
                     int maxLiveLength = 17;
                     setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
-                    String json = configService.selectConfigByKey("his.config");
-                    FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
                     setting.setMiniprogramAppid(sysConfig.getAppId());
                     setting.setMiniprogramPage(sortLiveLink);
                     setting.setContentType("4");
@@ -1500,6 +1605,46 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         enqueueWatchLog(watchLog);
     }
 
+    /**
+     * 直播看课记录处理
+     * @param companyId
+     * @param companyUserId
+     * @param externalId
+     * @param liveId
+     * @param appId
+     * @param logSource
+     * @param qwUserId
+     * @param corpId
+     */
+    public void createLiveWatchLogAndEnQueue(String companyId,String companyUserId,String externalId,Long liveId,String appId,Integer logSource,String qwUserId,String corpId){
+        // 写入对应数据源的记录表
+        LiveWatchLog itemLiveWatchLog = new LiveWatchLog();
+        itemLiveWatchLog.setLiveId(liveId);
+        itemLiveWatchLog.setLogType(3);
+        itemLiveWatchLog.setSopCreateTime(new Date());
+        itemLiveWatchLog.setCompanyId(Long.valueOf(companyId));
+        itemLiveWatchLog.setCompanyUserId(Long.valueOf(companyUserId));
+        itemLiveWatchLog.setSendAppId(appId);
+        itemLiveWatchLog.setLogSource(logSource);
+        itemLiveWatchLog.setQwUserId(qwUserId);
+        itemLiveWatchLog.setExternalContactId(Long.valueOf(externalId));
+        itemLiveWatchLog.setCorpId(corpId);
+        enqueueZmLiveWatchLog(itemLiveWatchLog);
+    }
+
+    private void enqueueZmLiveWatchLog(LiveWatchLog liveWatchLog) {
+        try {
+            boolean offered = zmLiveWatchQueue.offer(liveWatchLog, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("LiveWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(liveWatchLog));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 LiveWatchLog 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
     /**
      * 时间字符串转Date时间
      * @param dateString
@@ -1676,6 +1821,35 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
     }
 
+    /**
+     * 消费 FsCourseSopAppLink 队列并进行批量插入
+     */
+    private void consumeZmLiveWatchQueue() {
+        List<LiveWatchLog> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !zmLiveWatchQueue.isEmpty()) {
+            try {
+                LiveWatchLog livewatchLog = zmLiveWatchQueue.poll(1, TimeUnit.SECONDS);
+                if (livewatchLog != null) {
+                    batch.add(livewatchLog);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && livewatchLog == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertLiveWatchLog(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("zmLiveWatchQueue 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertLiveWatchLog(batch);
+        }
+    }
+
     /**
      * 消费 FsCourseWatchLog 队列并进行批量插入
      */
@@ -1783,6 +1957,36 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
     }
 
+    /**
+     * 批量插入 卓美直播看课记录
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertLiveWatchLog(List<LiveWatchLog> liveWatchLogToInsert) {
+        try {
+            List<LiveWatchLog> lastInsertList = new ArrayList<>();
+            //判断是否存在数据 liveId + his_qw_external_contact_id 唯一
+            for (LiveWatchLog liveWatchLog : liveWatchLogToInsert) {
+                //判断是否存在数据 存在的数据直接更新发送时间
+                if(liveWatchLogMapper.updateLiveWatchLogCondition(liveWatchLog) > 0){
+                    continue;
+                }
+                lastInsertList.add(liveWatchLog);
+            }
+            if(!lastInsertList.isEmpty()){
+                liveWatchLogMapper.insertLiveWatchLogBatch(lastInsertList);
+            }
+//            log.info("批量插入 LiveWatchLog 完成,共插入 {} 条记录。", liveWatchLogToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 LiveWatchLog 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
 
     @Override
     public void updateSopLogsByCancel() {

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

@@ -245,4 +245,9 @@ public interface CompanyMapper
 
     @Select("select company_id from company where live_show=1")
     List<Long> selectLiveShowCompanyId();
+
+    @Select("select company_id,company_name from company where \n" +
+            " `status` != 0   " +
+            " and is_del != 1 ")
+    List<CompanyVO> getCompanyDropList();
 }

+ 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;
 }

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

@@ -121,4 +121,7 @@ public class FsUserCourseVideo extends BaseEntity
     private Long listingEndTime;//商品结束售卖时间
 
     private Integer isSpeed; // 是否启用倍速 0:否 1:是
+
+    // 是否上架 0:上架,1:下架
+    private Integer isOnPut;
 }

+ 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;
+        }
+    }
+
+
+
+}

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

@@ -53,6 +53,7 @@ public interface FsCourseAnswerLogsMapper
             "\tLEFT JOIN company c on cal.company_id=c.company_id " +
             "        <where>  \n" +
             "            <if test=\"map.phone != null \"> and fu.phone = #{map.phone}</if>\n" +
+            "            <if test=\"map.logId != null \"> and cal.log_id = #{map.logId}</if>\n" +
             "            <if test=\"map.courseId != null \"> and uc.course_id = #{map.courseId}</if>\n" +
             "            <if test=\"map.videoId != null \"> and cal.video_id = #{map.videoId}</if>\n" +
             "            <if test=\"map.watchLogId != null \"> and cal.watch_log_id = #{map.watchLogId}</if>\n" +

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

@@ -116,6 +116,7 @@ public interface FsCourseRedPacketLogMapper
             "LEFT JOIN qw_user qu on qu.id= l.qw_user_id  \n" +
             "where 1=1   " +
             "<if test = ' maps.userId !=null '> and l.user_id = #{maps.userId} </if>" +
+            "<if test = ' maps.logId !=null '> and l.log_id = #{maps.logId} </if>" +
             "<if test = ' maps.watchLogId !=null '> and l.watch_log_id = #{maps.watchLogId} </if>" +
             "<if test = ' maps.companyId !=null '> and l.company_id = #{maps.companyId} </if>" +
             "<if test = ' maps.companyUserId !=null '> and l.company_user_id = #{maps.companyUserId} </if>" +

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

@@ -12,6 +12,8 @@ import java.util.Set;
 
 @Data
 public class FsCourseAnswerLogsParam  extends BaseEntity  {
+
+    private Long logId;
     private String phone;
     private String phoneMk;
     private String courseId;

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

@@ -11,6 +11,7 @@ import java.util.stream.Collectors;
 @Data
 public class FsCourseRedPacketLogParam implements Serializable {
 
+    private Long logId;
     private Long userId;
 
     private Long companyId;

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

@@ -11,6 +11,8 @@ import java.util.stream.Collectors;
 @Data
 public class FsCourseWatchLogListParam implements Serializable {
 
+    private Long logId;
+
     private Long userId;
 
     private Long qwUserId;

+ 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;
+
+}

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

@@ -380,7 +380,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
-        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             String[] parts = key.split(":");
@@ -419,17 +418,12 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
-                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
-
-        if(CollectionUtils.isNotEmpty(finishedLogs)){
-            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
-        }
     }
     public Long getFsUserVideoDuration(Long videoId){
         //将视频时长也存到redis
@@ -458,7 +452,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Collection<String> keys = redisCache.keys("h5wxuser:watch:heartbeat:*");
         LocalDateTime now = LocalDateTime.now();
         List<FsCourseWatchLog> logs = new ArrayList<>();
-        List<FsCourseWatchLog> watchingLogs = new ArrayList<>();
         for (String key : keys) {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
             String[] parts = key.split(":");
@@ -483,14 +476,10 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redisCache.deleteObject(key);
             }else {
                 watchLog.setLogType(1);
-                watchingLogs.add(watchLog);
             }
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
-        if(CollectionUtils.isNotEmpty(watchingLogs)){
-            fsTagUpdateService.onCourseWatchingBatch(watchingLogs);
-        }
     }
 
     @Override
@@ -513,7 +502,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
-        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             Long videoId=null;
@@ -562,8 +550,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
-
-                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -571,11 +557,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLogIsOpen(logs,100);
-
-        // 完课打标签
-        if(CollectionUtils.isNotEmpty(finishedLogs)){
-            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
-        }
     }
 
     public void batchUpdateFsCourseWatchLogIsOpen(List<FsCourseWatchLog> logs, int batchSize) {

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -72,6 +72,9 @@ import com.fs.qw.mapper.QwGroupChatUserMapper;
 import com.fs.qw.mapper.QwSessionMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.mapper.*;
+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;

+ 54 - 0
fs-service/src/main/java/com/fs/course/vo/FsPeriodCountExportVO.java

@@ -0,0 +1,54 @@
+package com.fs.course.vo;
+
+import com.fs.common.annotation.Excel;
+import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Data
+@ApiModel
+public class FsPeriodCountExportVO {
+    @Excel(name = "课程名称")
+    private String title;
+
+    @Excel(name = "营期日期")
+    private LocalDate dayDate;
+
+    @Excel(name = "观看次数")
+    private int courseWatchTimes;
+
+    @Excel(name = "完播次数")
+    private int courseCompleteTimes;
+
+    @Excel(name = "观看人数")
+    private int courseWatchNum;
+
+    @Excel(name = "完播人数")
+    private int courseCompleteNum;
+
+    @Excel(name = "完播率")
+    private String completeRate;
+
+    @Excel(name = "答题次数")
+    private int answerTimes;
+
+    @Excel(name = "答题人数")
+    private int answerNum;
+
+    @Excel(name = "正确人数")
+    private int answerRightNum;
+
+    @Excel(name = "正确率")
+    private String answerRightRate;
+
+    @Excel(name = "答题红包个数")
+    private int redPacketNum;
+
+    @Excel(name = "答题红包金额(元)")
+    private BigDecimal redPacketAmount;
+
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoQVO.java

@@ -102,4 +102,7 @@ public class FsUserCourseVideoQVO extends BaseEntity {
      * 课程关联的拍商品
      */
     private List<FsCourseProduct>  courseProducts;
+
+    // 是否上架 0:上架,1:下架
+    private Integer isOnPut;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java

@@ -68,4 +68,7 @@ public class FsUserCourseVideoVO extends BaseEntity {
     private String redPacketMoney;
 
     private String companyRedPacketMoney;
+
+    // 是否上架 0:上架,1:下架
+    private Integer isOnPut;
 }

+ 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;
 }

+ 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;
+
+}

+ 1 - 0
fs-service/src/main/java/com/fs/his/service/impl/MerchantAppConfigServiceImpl.java

@@ -52,6 +52,7 @@ public class MerchantAppConfigServiceImpl extends ServiceImpl<MerchantAppConfigM
                 Thread.sleep(5000);
                 Integer count = baseMapper.checkTableExists();
                 if (ObjectUtil.isNotNull(count)&&count>0) {
+                    log.info("异步初始化商户配置表完成");
                     return;
                 }
                 // 1. 检查并创建表

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java

@@ -147,4 +147,16 @@ public interface FsStoreProductAttrValueScrmMapper
     void updateFsStoreProductAttrValuePrice(List<Long> ids, double v);
 
     List<FsStoreProductAttrValueScrm> getFsStoreProductAttrValueListInProductId(List<Long> productIds);
+
+    @Update({"<script> " +
+            " UPDATE fs_store_product_attr_value_scrm" +
+            " SET stock = stock + CAST(#{totalNum} AS SIGNED)" +
+            " WHERE product_id = #{productId}" +
+            " AND bar_code IN",
+            "<foreach collection='barCodeList' item='barCode' open='(' separator=',' close=')'>" +
+            "#{barCode}" +
+            "</foreach>" +
+            "</script>"
+    })
+    void incStock(@Param("productId") Long productId,@Param("barCodeList") List<String> barCodeList,@Param("totalNum") String totalNum);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/MergedOrderMapper.java

@@ -1,7 +1,9 @@
 package com.fs.hisStore.mapper;
 
 import com.fs.hisStore.param.FsMyStoreOrderQueryParam;
+import com.fs.hisStore.param.MergedAfterSalesQueryParam;
 import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
 import com.fs.live.param.MergedOrderQueryParam;
 import com.fs.live.vo.MergedOrderVO;
 import org.apache.ibatis.annotations.Param;
@@ -25,6 +27,14 @@ public interface MergedOrderMapper
      */
     List<MergedOrderVO> selectMergedOrderList(@Param("maps") MergedOrderQueryParam param);
 
+    /**
+     * 查询合并的售后列表(商城售后+直播售后)
+     *
+     * @param param 查询参数
+     * @return 合并后的售后列表
+     */
+    List<MergedAfterSalesVO> selectMergedAfterSalesList(@Param("maps") MergedAfterSalesQueryParam param);
+
     /**
      * 查询合并的订单列表(商城订单+直播订单)
      *

+ 31 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesDeliveryParam.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 合并售后物流参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesDeliveryParam implements Serializable
+{
+    private Long userId;
+    
+    /** 售后ID */
+    private Long salesId;
+
+    @NotBlank(message = "物流单号不能为空")
+    private String deliverySn;
+
+    @NotBlank(message = "物流公司不能为空")
+    private String deliveryName;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+}
+

+ 43 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesParam.java

@@ -0,0 +1,43 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并售后申请参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesParam implements Serializable
+{
+    /** 订单号 */
+    @NotBlank
+    private String orderCode;
+
+    /** 服务类型 0仅退款1退货退款 */
+    @NotBlank
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @NotBlank
+    private String reasons;
+
+    /** 申请说明 */
+    private String explains;
+
+    /** 申请说明图片 */
+    private String explainImg;
+
+    private BigDecimal refundAmount;
+
+    /** 商品数据 */
+    @NotBlank
+    private List<FsStoreAfterSalesProductParam> productList;
+}
+

+ 23 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesQueryParam.java

@@ -0,0 +1,23 @@
+package com.fs.hisStore.param;
+
+import com.fs.common.param.BaseQueryParam;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 合并售后查询参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesQueryParam extends BaseQueryParam implements Serializable
+{
+    /** 状态 1待处理 2已完成 */
+    private Integer status;
+    
+    /** 用户ID */
+    private Long userId;
+}
+

+ 22 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesRevokeParam.java

@@ -0,0 +1,22 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 合并售后撤销参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesRevokeParam implements Serializable
+{
+    /** 售后ID */
+    private Long salesId;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedOrderDeleteParam.java

@@ -0,0 +1,25 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 合并订单删除参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedOrderDeleteParam implements Serializable
+{
+    /** 订单ID */
+    @NotNull(message = "订单ID不能为空")
+    private Long orderId;
+
+    /** 订单类型 1商城订单 2直播订单 */
+    @NotNull(message = "订单类型不能为空")
+    private Integer orderType;
+}
+

+ 56 - 1
fs-service/src/main/java/com/fs/hisStore/service/IMergedOrderService.java

@@ -1,10 +1,13 @@
 package com.fs.hisStore.service;
 
-import com.fs.hisStore.param.FsMyStoreOrderQueryParam;
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.param.*;
 import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
 import com.fs.live.param.MergedOrderQueryParam;
 import com.fs.live.vo.MergedOrderVO;
 
+import java.text.ParseException;
 import java.util.List;
 
 /**
@@ -24,5 +27,57 @@ public interface IMergedOrderService
     List<FsMergedOrderListQueryVO> selectMergedOrderListVO(FsMyStoreOrderQueryParam param);
 
     List<MergedOrderVO> selectMergedOrderList(MergedOrderQueryParam param);
+
+    /**
+     * 查询合并的售后列表(商城售后+直播售后)
+     *
+     * @param param 查询参数
+     * @return 合并后的售后列表
+     */
+    List<MergedAfterSalesVO> selectMergedAfterSalesList(MergedAfterSalesQueryParam param);
+
+    /**
+     * 申请售后
+     *
+     * @param userId 用户ID
+     * @param param 售后参数
+     * @return 结果
+     */
+    R applyForAfterSales(String userId, MergedAfterSalesParam param);
+
+    /**
+     * 撤销售后
+     *
+     * @param userId 用户ID
+     * @param param 撤销参数
+     * @return 结果
+     */
+    R revokeAfterSales(String userId, MergedAfterSalesRevokeParam param) throws ParseException;
+
+    /**
+     * 提交物流信息
+     *
+     * @param param 物流参数
+     * @return 结果
+     */
+    R addDelivery(MergedAfterSalesDeliveryParam param);
+
+    /**
+     * 查询售后详情
+     *
+     * @param salesId 售后ID
+     * @param afterSalesType 售后类型 1商城售后 2直播售后
+     * @return 售后详情
+     */
+    MergedAfterSalesVO selectMergedAfterSalesById(Long salesId, Integer afterSalesType);
+
+    /**
+     * 删除订单(逻辑删除)
+     *
+     * @param userId 用户ID
+     * @param param 删除参数
+     * @return 结果
+     */
+    R deleteOrder(String userId, MergedOrderDeleteParam param);
 }
 

+ 42 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -756,6 +756,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
         FsStoreOrderPriceDTO priceGroup = this.getOrderPriceGroup(carts, userAddress);
         BigDecimal payPostage = priceGroup.getStorePostage();
+        BigDecimal badCode = BigDecimal.valueOf(-1);
+        // 检查运费计算结果,如果是 -1 表示偏远地区不可购买
+        if (payPostage.compareTo(badCode) == 0) {
+            throw new ServiceException("偏远地区暂不可购买");
+        }
         payPrice = NumberUtil.add(payPrice, payPostage);
 
         FsUserScrm user = userService.selectFsUserById(uid);
@@ -827,7 +832,16 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
         BeanUtils.copyProperties(param, computedParam);
         //计算金额
-        FsStoreOrderComputeDTO dto = this.computedOrder(userId, computedParam);
+        FsStoreOrderComputeDTO dto;
+        try {
+            dto = this.computedOrder(userId, computedParam);
+        } catch (ServiceException e) {
+            // 捕获运费模板检查异常,直接返回错误
+            if ("偏远地区暂不可购买".equals(e.getMessage())) {
+                return R.error("偏远地区暂不可购买");
+            }
+            throw e;
+        }
         String cartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
         Integer payType = redisCache.getCacheObject("createOrderPayType:" + param.getCreateOrderKey());
         if (payType != null) {
@@ -841,7 +855,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             //获取地址
             FsUserAddressScrm address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
             //生成分布式唯一值
-            String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
+
+            String orderSn = OrderCodeUtils.getOrderSn();
             //是否使用积分
             Boolean isIntegral = false;
             //组合数据
@@ -3072,6 +3087,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
      */
     private BigDecimal handlePostage(List<FsStoreCartQueryVO> cartInfo, FsUserAddressScrm userAddress) {
         BigDecimal storePostage = BigDecimal.ZERO;
+        BigDecimal badCode = BigDecimal.valueOf(-1);
         if (userAddress != null) {
             if (userAddress.getCityId() == null) {
                 return storePostage;
@@ -3083,14 +3099,26 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             List<Long> tempIds = cartInfo
                     .stream()
                     .map(FsStoreCartQueryVO::getTempId)
+                    .filter(tempId -> tempId != null)
                     .collect(Collectors.toList());
 
+            if (tempIds.isEmpty()) {
+                return storePostage;
+            }
+
             //获取商品用到的运费模板
             List<FsShippingTemplatesScrm> shippingTemplatesList = shippingTemplatesService.selectFsShippingTemplatesByIds(StringUtils.join(tempIds, ","));
 
             //获取运费模板区域列表按照城市排序
             List<FsShippingTemplatesRegionScrm> shippingTemplatesRegionList = shippingTemplatesRegionService.selectFsShippingTemplatesRegionListByTempIdsAndCityIds(StringUtils.join(tempIds, ","), StringUtils.join(citys, ","));
 
+            // 有运费模板,但当前城市没有匹配的区域
+            if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
+                    && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())) {
+                logger.error("运费模板存在,但城市不在运费模板区域内,cityId: {}", cityId);
+                return badCode;
+            }
+
             //提取运费模板类型
             Map<Long, Integer> shippingTemplatesMap = shippingTemplatesList
                     .stream()
@@ -3107,6 +3135,18 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             Map<Long, TemplateDTO> templateDTOMap = new HashMap<>();
             for (FsStoreCartQueryVO storeCartVO : cartInfo) {
                 Long tempId = storeCartVO.getTempId();
+                if (tempId == null) {
+                    continue;
+                }
+                // 检查该商品的运费模板是否有对应的区域配置
+                FsShippingTemplatesRegionScrm shippingTemplatesRegion = shippingTemplatesRegionMap.get(tempId);
+                // 如果商品有运费模板,但没有找到对应的区域配置,返回错误码
+                if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
+                        && shippingTemplatesList.stream().anyMatch(t -> t.getId().equals(tempId))
+                        && shippingTemplatesRegion == null) {
+                    logger.error("商品运费模板存在,但城市不在运费模板区域内,tempId: {}, cityId: {}", tempId, cityId);
+                    return badCode;
+                }
                 //根据模板类型获取相应的数量
                 double num = 0d;
                 if (ShippingTempEnum.TYPE_1.getValue().equals(shippingTemplatesMap.get(tempId))) {
@@ -3118,8 +3158,6 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     num = NumberUtil.mul(storeCartVO.getCartNum(),
                             storeCartVO.getVolume()).doubleValue();
                 }
-
-                FsShippingTemplatesRegionScrm shippingTemplatesRegion = shippingTemplatesRegionMap.get(tempId);
                 BigDecimal price = NumberUtil.round(NumberUtil.mul(storeCartVO.getCartNum(),
                         storeCartVO.getPrice()), 2);
                 if (!templateDTOMap.containsKey(tempId)) {

+ 230 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/MergedOrderServiceImpl.java

@@ -2,20 +2,37 @@ package com.fs.hisStore.service.impl;
 
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
 import com.fs.common.utils.StringUtils;
+import com.fs.hisStore.domain.FsStoreAfterSalesItemScrm;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
 import com.fs.hisStore.enums.OrderInfoEnum;
 import com.fs.hisStore.mapper.MergedOrderMapper;
-import com.fs.hisStore.param.FsMyStoreOrderQueryParam;
+import com.fs.hisStore.param.*;
+import com.fs.hisStore.service.IFsStoreAfterSalesItemScrmService;
+import com.fs.hisStore.service.IFsStoreAfterSalesScrmService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IMergedOrderService;
 import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
 import com.fs.hisStore.vo.FsStoreOrderItemVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
+import com.fs.live.domain.LiveAfterSales;
+import com.fs.live.domain.LiveAfterSalesItem;
+import com.fs.live.param.LiveAfterSalesDeliveryParam;
+import com.fs.live.param.LiveAfterSalesParam;
+import com.fs.live.param.LiveAfterSalesRevokeParam;
 import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.service.ILiveAfterSalesItemService;
+import com.fs.live.service.ILiveAfterSalesService;
+import com.fs.live.service.ILiveOrderService;
 import com.fs.live.vo.MergedOrderVO;
 import com.fs.store.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.text.ParseException;
 import java.util.*;
 
 /**
@@ -65,6 +82,24 @@ public class MergedOrderServiceImpl implements IMergedOrderService
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private IFsStoreAfterSalesScrmService storeAfterSalesService;
+
+    @Autowired
+    private ILiveAfterSalesService liveAfterSalesService;
+
+    @Autowired
+    private IFsStoreOrderScrmService storeOrderService;
+
+    @Autowired
+    private ILiveOrderService liveOrderService;
+
+    @Autowired
+    private IFsStoreAfterSalesItemScrmService storeAfterSalesItemService;
+
+    @Autowired
+    private ILiveAfterSalesItemService liveAfterSalesItemService;
+
     /*
      * 小程序合并
      * */
@@ -130,5 +165,199 @@ public class MergedOrderServiceImpl implements IMergedOrderService
 
         return list;
     }
+
+    @Override
+    public List<MergedAfterSalesVO> selectMergedAfterSalesList(MergedAfterSalesQueryParam param) {
+        List<MergedAfterSalesVO> list = mergedOrderMapper.selectMergedAfterSalesList(param);
+        
+        // 填充售后商品列表
+        for (MergedAfterSalesVO vo : list) {
+            if (vo.getAfterSalesType() != null && vo.getAfterSalesType() == 1) {
+                // 商城售后
+                FsStoreAfterSalesItemScrm itemParam = new FsStoreAfterSalesItemScrm();
+                itemParam.setStoreAfterSalesId(vo.getId());
+                List<FsStoreAfterSalesItemScrm> items = storeAfterSalesItemService.selectFsStoreAfterSalesItemList(itemParam);
+                vo.setItems(items);
+            } else if (vo.getAfterSalesType() != null && vo.getAfterSalesType() == 2) {
+                // 直播售后
+                List<LiveAfterSalesItem> items = liveAfterSalesItemService.selectLiveAfterSalesItemByAfterId(vo.getId());
+                vo.setItems(items);
+            }
+        }
+        
+        return list;
+    }
+
+    @Override
+    public R applyForAfterSales(String userId, MergedAfterSalesParam param) {
+        // 根据订单号判断是商城订单还是直播订单
+        try {
+            // 先尝试查询商城订单
+            com.fs.hisStore.domain.FsStoreOrderScrm storeOrder = storeOrderService.selectFsStoreOrderByOrderCode(param.getOrderCode());
+            if (storeOrder != null) {
+                // 商城订单,调用商城售后服务
+                FsStoreAfterSalesParam storeParam = new FsStoreAfterSalesParam();
+                BeanUtils.copyProperties(param, storeParam);
+                return storeAfterSalesService.applyForAfterSales(Long.parseLong(userId), storeParam);
+            }
+        } catch (Exception e) {
+            // 商城订单不存在,继续尝试直播订单
+        }
+        
+        // 尝试查询直播订单
+        com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderCode(param.getOrderCode());
+        if (liveOrder != null) {
+            // 直播订单,调用直播售后服务
+            LiveAfterSalesParam liveParam = new LiveAfterSalesParam();
+            liveParam.setOrderCode(param.getOrderCode());
+            liveParam.setServiceType(param.getServiceType());
+            liveParam.setReasons(param.getReasons());
+            liveParam.setExplains(param.getExplains());
+            liveParam.setExplainImg(param.getExplainImg());
+            liveParam.setRefundAmount(param.getRefundAmount());
+            // 转换商品列表
+            if (param.getProductList() != null) {
+                List<com.fs.live.param.LiveAfterSalesProductParam> liveProductList = new ArrayList<>();
+                for (FsStoreAfterSalesProductParam product : param.getProductList()) {
+                    com.fs.live.param.LiveAfterSalesProductParam liveProduct = new com.fs.live.param.LiveAfterSalesProductParam();
+                    liveProduct.setProductId(product.getProductId());
+                    liveProduct.setNum(product.getNum());
+                    liveProductList.add(liveProduct);
+                }
+                liveParam.setProductList(liveProductList);
+            }
+            return liveAfterSalesService.applyForAfterSales(userId, liveParam);
+        }
+        
+        return R.error("订单不存在");
+    }
+
+    @Override
+    public R revokeAfterSales(String userId, MergedAfterSalesRevokeParam param) throws ParseException {
+        if (param.getAfterSalesType() != null && param.getAfterSalesType() == 1) {
+            // 商城售后
+            return storeAfterSalesService.revoke(Long.parseLong(userId), param.getSalesId());
+        } else if (param.getAfterSalesType() != null && param.getAfterSalesType() == 2) {
+            // 直播售后
+            LiveAfterSalesRevokeParam liveParam = new LiveAfterSalesRevokeParam();
+            liveParam.setId(param.getSalesId());
+            return liveAfterSalesService.revoke(userId, liveParam);
+        }
+        return R.error("售后类型错误");
+    }
+
+    @Override
+    public R addDelivery(MergedAfterSalesDeliveryParam param) {
+        if (param.getAfterSalesType() != null && param.getAfterSalesType() == 1) {
+            // 商城售后
+            FsStoreAfterSalesDeliveryParam storeParam = new FsStoreAfterSalesDeliveryParam();
+            storeParam.setUserId(param.getUserId());
+            storeParam.setSalesId(param.getSalesId());
+            storeParam.setDeliverySn(param.getDeliverySn());
+            storeParam.setDeliveryName(param.getDeliveryName());
+            return storeAfterSalesService.addDelivery(storeParam);
+        } else if (param.getAfterSalesType() != null && param.getAfterSalesType() == 2) {
+            // 直播售后
+            LiveAfterSalesDeliveryParam liveParam = new LiveAfterSalesDeliveryParam();
+            liveParam.setUserId(param.getUserId());
+            liveParam.setId(param.getSalesId());
+            liveParam.setDeliverySn(param.getDeliverySn());
+            liveParam.setDeliveryName(param.getDeliveryName());
+            return liveAfterSalesService.addDelivery(liveParam);
+        }
+        return R.error("售后类型错误");
+    }
+
+    @Override
+    public MergedAfterSalesVO selectMergedAfterSalesById(Long salesId, Integer afterSalesType) {
+        MergedAfterSalesVO vo = new MergedAfterSalesVO();
+        
+        if (afterSalesType != null && afterSalesType == 1) {
+            // 商城售后
+            FsStoreAfterSalesScrm storeAfterSales = storeAfterSalesService.selectFsStoreAfterSalesById(salesId);
+            if (storeAfterSales != null) {
+                BeanUtils.copyProperties(storeAfterSales, vo);
+                vo.setAfterSalesType(1);
+                vo.setAfterSalesTypeName("商城售后");
+                vo.setOrderCode(storeAfterSales.getOrderCode());
+                
+                // 填充商品列表
+                FsStoreAfterSalesItemScrm itemParam = new FsStoreAfterSalesItemScrm();
+                itemParam.setStoreAfterSalesId(salesId);
+                List<FsStoreAfterSalesItemScrm> items = storeAfterSalesItemService.selectFsStoreAfterSalesItemList(itemParam);
+                vo.setItems(items);
+            }
+        } else if (afterSalesType != null && afterSalesType == 2) {
+            // 直播售后
+            LiveAfterSales liveAfterSales = liveAfterSalesService.selectLiveAfterSalesById(salesId);
+            if (liveAfterSales != null) {
+                BeanUtils.copyProperties(liveAfterSales, vo);
+                vo.setAfterSalesType(2);
+                vo.setAfterSalesTypeName("直播售后");
+                
+                // 查询订单号
+                com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(String.valueOf(liveAfterSales.getOrderId()));
+                if (liveOrder != null) {
+                    vo.setOrderCode(liveOrder.getOrderCode());
+                }
+                
+                // 填充商品列表
+                List<LiveAfterSalesItem> items = liveAfterSalesItemService.selectLiveAfterSalesItemByAfterId(salesId);
+                vo.setItems(items);
+            }
+        }
+        
+        return vo;
+    }
+
+    @Override
+    public R deleteOrder(String userId, MergedOrderDeleteParam param) {
+        Long orderId = param.getOrderId();
+        Integer orderType = param.getOrderType();
+        
+        if (orderType == null) {
+            return R.error("订单类型不能为空");
+        }
+        
+        if (orderType == 1) {
+            // 商城订单
+            com.fs.hisStore.domain.FsStoreOrderScrm storeOrder = storeOrderService.selectFsStoreOrderById(orderId);
+            if (storeOrder == null) {
+                return R.error("订单不存在");
+            }
+            // 检查订单是否属于当前用户
+            if (!storeOrder.getUserId().equals(Long.parseLong(userId))) {
+                return R.error("无权删除该订单");
+            }
+            // 逻辑删除:设置 isDel = 1
+            storeOrder.setIsDel(1);
+            int result = storeOrderService.updateFsStoreOrder(storeOrder);
+            if (result > 0) {
+                return R.ok("删除成功");
+            } else {
+                return R.error("删除失败");
+            }
+        } else if (orderType == 2) {
+            // 直播订单
+            com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(String.valueOf(orderId));
+            if (liveOrder == null) {
+                return R.error("订单不存在");
+            }
+            // 检查订单是否属于当前用户
+            if (!liveOrder.getUserId().equals(userId)) {
+                return R.error("无权删除该订单");
+            }
+            // 逻辑删除:设置 isDel = "1"
+            liveOrder.setIsDel("1");
+            int result = liveOrderService.updateLiveOrder(liveOrder);
+            if (result > 0) {
+                return R.ok("删除成功");
+            } else {
+                return R.error("删除失败");
+            }
+        } else {
+            return R.error("订单类型错误");
+        }
+    }
 }
 

+ 103 - 0
fs-service/src/main/java/com/fs/hisStore/vo/MergedAfterSalesVO.java

@@ -0,0 +1,103 @@
+package com.fs.hisStore.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并售后VO
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesVO implements Serializable
+{
+    /** 售后ID */
+    @Excel(name = "售后ID")
+    private Long id;
+
+    /** 订单号 */
+    @Excel(name = "订单号")
+    private String orderCode;
+
+    /** 退款金额 */
+    @Excel(name = "退款金额")
+    private BigDecimal refundAmount;
+
+    /** 服务类型0仅退款1退货退款 */
+    @Excel(name = "服务类型0仅退款1退货退款")
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @Excel(name = "申请原因")
+    private String reasons;
+
+    /** 说明 */
+    @Excel(name = "说明")
+    private String explains;
+
+    /** 说明图片->多个用逗号分割 */
+    @Excel(name = "说明图片->多个用逗号分割")
+    private String explainImg;
+
+    /** 物流公司编码 */
+    @Excel(name = "物流公司编码")
+    private String shipperCode;
+
+    /** 物流单号 */
+    @Excel(name = "物流单号")
+    private String deliverySn;
+
+    /** 物流名称 */
+    @Excel(name = "物流名称")
+    private String deliveryName;
+
+    /** 状态 0已提交等待平台审核 1平台已审核 等待用户发货/退款 2 用户已发货 3退款成功 */
+    @Excel(name = "状态 0已提交等待平台审核 1平台已审核 等待用户发货/退款 2 用户已发货 3退款成功")
+    private Integer status;
+
+    /** 售后状态-0正常1用户取消2商家拒绝 */
+    @Excel(name = "售后状态-0正常1用户取消2商家拒绝")
+    private Integer salesStatus;
+
+    @Excel(name = "订单当前状态")
+    private Integer orderStatus;
+
+    /** 逻辑删除 */
+    @Excel(name = "逻辑删除")
+    private Integer isDel;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 商家收货人 */
+    @Excel(name = "商家收货人")
+    private String consignee;
+
+    /** 商家手机号 */
+    @Excel(name = "商家手机号")
+    private String phoneNumber;
+
+    /** 商家地址 */
+    @Excel(name = "商家地址")
+    private String address;
+
+    private Integer isPackage;
+
+    private String packageJson;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+
+    /** 售后类型名称 */
+    private String afterSalesTypeName;
+
+    /** 售后商品列表 */
+    private List<?> items;
+}
+

+ 6 - 0
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -3,14 +3,17 @@ package com.fs.live.domain;
 
 
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.live.vo.LiveTagItemVO;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.time.LocalDateTime;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 直播对象 live
@@ -127,4 +130,7 @@ public class   Live extends BaseEntity {
     private Long videoFileSize;
     private Long videoDuration;
     private Integer globalVisible;
+
+    @TableField(exist = false)
+    private List<LiveTagItemVO> liveTagList;
 }

+ 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;
+}

+ 66 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTagConfig.java

@@ -0,0 +1,66 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播间标签配置对象 live_tag_config
+ *
+ * @author fs
+ * @date 2025-12-13
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTagConfig extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+
+    /** 企微主体id */
+    @Excel(name = "企微主体id")
+    private String corpId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 标记标签行为类型,数据字典live_mark_type */
+    @Excel(name = "标记标签行为类型,数据字典live_mark_type")
+    private Long markType;
+
+    /** 企微标签id */
+    @Excel(name = "企微标签id")
+    private Long qwTagId;
+
+    /** 企微标签真实id */
+    @Excel(name = "企微标签真实id")
+    private String qwTagRealId;
+
+    @Excel(name = "企微标签名称")
+    private String  qwTagName;
+
+    /** 创建人id */
+    @Excel(name = "创建人id")
+    private Long createUserId;
+
+    /** 创建人 */
+    @Excel(name = "创建人")
+    private String createUserName;
+
+    /** 更新人id */
+    @Excel(name = "更新人id")
+    private Long updateUserId;
+
+    /** 更新人 */
+    @Excel(name = "更新人")
+    private String updateUserName;
+
+
+}

+ 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;
 
     /** 观看时长 */

+ 89 - 0
fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java

@@ -0,0 +1,89 @@
+package com.fs.live.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播看课记录对象 live_watch_log
+ *
+ * @author fs
+ * @date 2025-12-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveWatchLog extends BaseEntity{
+
+    /** 日志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;
+
+}

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

@@ -0,0 +1,53 @@
+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("liveId") Long liveId, 
+                                                   @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);
+}

+ 67 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveTagConfigMapper.java

@@ -0,0 +1,67 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.live.domain.LiveTagConfig;
+import com.fs.live.vo.LiveTagItemVO;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 直播间标签配置Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-13
+ */
+public interface LiveTagConfigMapper extends BaseMapper<LiveTagConfig>{
+    /**
+     * 查询直播间标签配置
+     * 
+     * @param id 直播间标签配置主键
+     * @return 直播间标签配置
+     */
+    LiveTagConfig selectLiveTagConfigById(Long id);
+
+    /**
+     * 查询直播间标签配置列表
+     * 
+     * @param liveTagConfig 直播间标签配置
+     * @return 直播间标签配置集合
+     */
+    List<LiveTagConfig> selectLiveTagConfigList(LiveTagConfig liveTagConfig);
+
+    /**
+     * 新增直播间标签配置
+     * 
+     * @param liveTagConfig 直播间标签配置
+     * @return 结果
+     */
+    int insertLiveTagConfig(LiveTagConfig liveTagConfig);
+
+    /**
+     * 修改直播间标签配置
+     * 
+     * @param liveTagConfig 直播间标签配置
+     * @return 结果
+     */
+    int updateLiveTagConfig(LiveTagConfig liveTagConfig);
+
+    /**
+     * 删除直播间标签配置
+     * 
+     * @param id 直播间标签配置主键
+     * @return 结果
+     */
+    int deleteLiveTagConfigById(Long id);
+
+    /**
+     * 批量删除直播间标签配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteLiveTagConfigByIds(Long[] ids);
+
+    int deleteByLiveId(@Param("liveId") Long liveId);
+
+    List<LiveTagItemVO> getLiveTagListByliveId(@Param("liveId") Long liveId);
+}

+ 1 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveVideoMapper.java

@@ -92,6 +92,6 @@ public interface LiveVideoMapper
     List<LiveVideo> selectByAll();
 
 
-    @Update("update live_video set finish_status = 1 where video_url = #{fileName}")
+    @Update("update live_video set finish_status = 1 where video_url like concat('%',#{fileName})")
     void updateFinishStatus(@Param("fileName") String fileName);
 }

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

@@ -0,0 +1,74 @@
+package com.fs.live.mapper;
+
+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 org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播间观看用户Mapper接口
+ * 
+ * @author fs
+ * @date 2025-01-18
+ */
+public interface LiveWatchLogMapper extends BaseMapper<LiveWatchLog> {
+    /**
+     * 查询直播看课记录
+     *
+     * @param logId 直播看课记录主键
+     * @return 直播看课记录
+     */
+    LiveWatchLog selectLiveWatchLogByLogId(Long logId);
+
+    /**
+     * 查询直播看课记录列表
+     *
+     * @param liveWatchLog 直播看课记录
+     * @return 直播看课记录集合
+     */
+    List<LiveWatchLog> selectLiveWatchLogList(LiveWatchLog liveWatchLog);
+
+    /**
+     * 新增直播看课记录
+     *
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    int insertLiveWatchLog(LiveWatchLog liveWatchLog);
+
+    /**
+     * 修改直播看课记录
+     *
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    int updateLiveWatchLog(LiveWatchLog liveWatchLog);
+
+    /**
+     * 删除直播看课记录
+     *
+     * @param logId 直播看课记录主键
+     * @return 结果
+     */
+    int deleteLiveWatchLogByLogId(Long logId);
+
+    /**
+     * 批量删除直播看课记录
+     *
+     * @param logIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteLiveWatchLogByLogIds(Long[] logIds);
+
+    void insertLiveWatchLogBatch(@Param("liveWatchLogs")List<LiveWatchLog> liveWatchLogs);
+
+    int updateLiveWatchLogCondition(@Param("liveWatchLog") LiveWatchLog liveWatchLog);
+
+    LiveWatchLog selectOneLogByLiveIdAndQwUserIdAndExternalId(@Param("liveId")Long liveId,@Param("qwUserId")String qwUserId,@Param("externalContactId")Long externalContactId);
+
+    List<LiveWatchLog> selectLiveWatchLogByLiveId(@Param("liveId")Long liveId);
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java

@@ -3,6 +3,7 @@ package com.fs.live.mapper;
 
 import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.vo.LiveDashBoardDataVo;
+import com.fs.live.vo.LiveWatchUserEntry;
 import com.fs.live.vo.LiveWatchUserStatistics;
 import com.fs.live.vo.LiveWatchUserVO;
 import org.apache.ibatis.annotations.Param;
@@ -143,4 +144,9 @@ public interface LiveWatchUserMapper {
 
     @Select("select * from live_watch_user where live_id = #{liveId}")
     List<LiveWatchUser> selectLiveWatchUserListByLiveId(@Param("liveId") Long liveId);
+
+    @Select("select lufe.company_id,lufe.company_user_id,lwu.* from live_watch_user lwu" +
+            " left join live_user_first_entry lufe on lwu.live_id = lufe.live_id and lwu.user_id = lufe.user_id" +
+            " where lwu.live_id = #{liveId} and lwu.user_id = #{userId} and lwu.live_flag = #{liveFlag} and lwu.replay_flag = #{replayFlag} limit 1 ")
+    LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(@Param("liveId") Long liveId,@Param("userId") Long userId,@Param("liveFlag") Integer liveFlag,@Param("replayFlag") Integer replayFlag);
 }

+ 59 - 0
fs-service/src/main/java/com/fs/live/param/LiveIsAddKfParam.java

@@ -0,0 +1,59 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/12 下午1:32)
+ */
+
+@Data
+public class LiveIsAddKfParam implements Serializable {
+
+    /**
+     * 企微员工 id
+     */
+    @NotNull(message = "企微userId")
+    private String qwUserId;
+
+    /**
+     * 直播id
+     */
+    @NotNull(message = "直播id")
+    private Long liveId;
+
+    /**
+     * 登录的小程序id
+     */
+    private Long userId;
+
+    /**
+     * 企微主体id
+     */
+    private String corpId;
+
+    /**
+     *   companyUserId
+     */
+    @NotNull(message = "客服参数不能为空")
+    private Long companyUserId;
+
+    /**
+     * 公司id
+     */
+    @NotNull(message = "经销商参数参数不能为空")
+    private Long companyId;
+
+    /**
+     * 外部联系人id
+     */
+    private Long qwExternalId;
+
+    /**
+     * 群聊id
+     */
+    private String chatId;
+}

+ 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);
+}

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

@@ -2,6 +2,7 @@ package com.fs.live.service;
 
 
 import com.fs.common.core.page.PageRequest;
+import com.fs.company.vo.CompanyVO;
 import com.fs.live.param.LiveNotifyParam;
 import com.fs.live.vo.LiveVo;
 import com.fs.common.core.domain.R;
@@ -197,4 +198,10 @@ public interface ILiveService
     List<Live> listToLiveNoEnd(Live live);
 
     Live selectLiveDbByLiveId(Long liveId);
+
+    /**
+     * 获取公司下拉列表
+     * @return
+     */
+    List<CompanyVO> getCompanyDropList();
 }

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

@@ -0,0 +1,61 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.live.domain.LiveWatchLog;
+
+/**
+ * 直播看课记录Service接口
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+public interface ILiveWatchLogService extends IService<LiveWatchLog>{
+    /**
+     * 查询直播看课记录
+     * 
+     * @param logId 直播看课记录主键
+     * @return 直播看课记录
+     */
+    LiveWatchLog selectLiveWatchLogByLogId(Long logId);
+
+    /**
+     * 查询直播看课记录列表
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 直播看课记录集合
+     */
+    List<LiveWatchLog> selectLiveWatchLogList(LiveWatchLog liveWatchLog);
+
+    /**
+     * 新增直播看课记录
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    int insertLiveWatchLog(LiveWatchLog liveWatchLog);
+
+    /**
+     * 修改直播看课记录
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    int updateLiveWatchLog(LiveWatchLog liveWatchLog);
+
+    /**
+     * 批量删除直播看课记录
+     * 
+     * @param logIds 需要删除的直播看课记录主键集合
+     * @return 结果
+     */
+    int deleteLiveWatchLogByLogIds(Long[] logIds);
+
+    /**
+     * 删除直播看课记录信息
+     * 
+     * @param logId 直播看课记录主键
+     * @return 结果
+     */
+    int deleteLiveWatchLogByLogId(Long logId);
+}

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

@@ -4,6 +4,8 @@ package com.fs.live.service;
 import com.fs.common.core.domain.R;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.live.domain.LiveWatchUser;
+import com.fs.live.param.LiveIsAddKfParam;
+import com.fs.live.vo.LiveWatchUserEntry;
 import com.fs.live.vo.LiveWatchUserVO;
 
 import java.util.Date;
@@ -126,4 +128,15 @@ public interface ILiveWatchUserService {
     void updateSingleVisible(long liveId, Integer status,long userId);
 
     LiveWatchUser selectLiveWatchUserByFlag(Long liveId, Long userId, Integer liveFlag, Integer replayFlag);
+
+    R liveIsAddKf(LiveIsAddKfParam param);
+
+    LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(Long liveId, Long userId, Integer liveFlag, Integer replayFlag);
+
+    void updateLiveWatchUserEntry(LiveWatchUserEntry liveWatchUser);
+
+    /**
+     * 根据用户直播看课记录来打标签
+     */
+    void qwTagMarkByLiveWatchLog(Long liveId);
 }

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

@@ -0,0 +1,328 @@
+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 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;
+
+
+    /**
+     * 检查并创建完课记录(由定时任务调用)
+     */
+    @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. 获取视频总时长(秒)
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                log.warn("直播间视频时长无效, liveId={}, duration={}", liveId, videoDuration);
+                return;
+            }
+
+            // 4. 计算完课比例
+            BigDecimal watchRate = BigDecimal.valueOf(watchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+
+            // 5. 判断是否达到完课标准
+            if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
+                log.debug("观看时长未达到完课标准, liveId={}, userId={}, watchRate={}%, required={}%",
+                        liveId, userId, watchRate, completionRate);
+                return;
+            }
+
+            // 6. 检查今天是否已有完课记录
+            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(liveId, 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(watchDuration);
+            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={}, continuousDays={}, points={}",
+                    liveId, userId, 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;
+        }
+    }
+}

+ 63 - 7
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -749,6 +749,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             order.setIsPay("1");
             baseMapper.updateLiveOrder(order);
             try {
+                this.updateLiveWatchLog(order);
                 this.createOmsOrderCall(order);
             } catch (Exception e) {
                 log.error("推送erp失败:{}",e.getMessage());
@@ -767,6 +768,38 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
         return "SUCCESS";
     }
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+    @Autowired
+    private ILiveWatchLogService liveWatchLogService;
+
+    private void updateLiveWatchLog(LiveOrder order) {
+        if (order.getCompanyId() != null && order.getCompanyUserId() != null && order.getCompanyId() > 0 && order.getCompanyUserId() > 0) {
+            Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(order.getLiveId());
+            if (liveFlagWithCache != null && liveFlagWithCache.containsKey("liveFlag") && 1 == liveFlagWithCache.get("liveFlag")) {
+                try {
+                    LiveWatchLog queryLog = new LiveWatchLog();
+                    queryLog.setLiveId(order.getLiveId());
+                    queryLog.setUserId(Long.valueOf(order.getUserId()));
+                    queryLog.setCompanyId(order.getCompanyId());
+                    queryLog.setCompanyUserId(order.getCompanyUserId());
+
+                    List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                    if (logs != null && !logs.isEmpty()) {
+                        for (LiveWatchLog log : logs) {
+                            if (log.getLogType() == null || log.getLogType() != 2) {
+                                log.setLiveBuy(1);
+                                liveWatchLogService.updateLiveWatchLog(log);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("更新 updateLiveWatchLog LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}",
+                            order.getLiveId(), order.getUserId(), e.getMessage(), e);
+                }
+            }
+        }
+    }
 
     @Override
     @Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRED)
@@ -1916,12 +1949,23 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         FsStoreProductScrm fsStoreProduct = fsStoreProductService.selectFsStoreProductById(liveOrder.getProductId());
         LiveGoods goods = liveGoodsMapper.selectLiveGoodsByProductId(liveOrder.getLiveId(), liveOrder.getProductId());
         if(goods == null) return R.error("当前商品不存在");
+        FsStoreProductAttrValueScrm attrValue = null;
+        if (!Objects.isNull(liveOrder.getAttrValueId())) {
+            attrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(liveOrder.getAttrValueId());
+        }
+        if (attrValue != null) {
+            attrValue.setStock(attrValue.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
+            attrValue.setSales(attrValue.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
+            fsStoreProductAttrValueMapper.updateFsStoreProductAttrValue(attrValue);
+        }
 
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
+        fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
+        goods.setSales(goods.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
         liveGoodsMapper.updateLiveGoods(goods);
 
         //判断是否是三种特定产品
@@ -1979,7 +2023,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 dto.setImage(fsStoreProduct.getImage());
                 dto.setSku(String.valueOf(fsStoreProduct.getStock()));
                 if (StringUtils.isEmpty(fsStoreProduct.getBarCode())) {
-                    FsStoreProductAttrValueScrm fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attrValue -> StringUtils.isNotEmpty(attrValue.getBarCode())).findFirst().orElse(null);
+                    FsStoreProductAttrValueScrm fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attr -> StringUtils.isNotEmpty(attr.getBarCode())).findFirst().orElse(null);
                     if (fsStoreProductAttrValue != null) {
                         dto.setBarCode(fsStoreProductAttrValue.getBarCode());
                     }
@@ -3516,12 +3560,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             attrValue.setStock(attrValue.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
             attrValue.setSales(attrValue.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
             fsStoreProductAttrValueMapper.updateFsStoreProductAttrValue(attrValue);
-        } else {
-            // 更改店铺库存
-            fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
-            fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
-            fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
         }
+        // 更改店铺库存
+        fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
+        fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
+        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
 
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
@@ -3771,6 +3814,19 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             FsStoreProductScrm fsStoreProduct = fsStoreProductService.selectFsStoreProductById(liveOrder.getProductId());
             LiveGoods goods = liveGoodsMapper.selectLiveGoodsByProductId(liveOrder.getLiveId(), liveOrder.getProductId());
             fsStoreProduct.setStock(fsStoreProduct.getStock()+Integer.parseInt(liveOrder.getTotalNum()));
+            List<LiveOrderItem> liveOrderItems = liveOrderItemMapper.selectCheckedByOrderId(order.getOrderId());
+            List<String> barCodeList = new ArrayList<>();
+            //更新规格库存
+            for (LiveOrderItem item : liveOrderItems) {
+                FsStoreProduct cartDTO = JSONUtil.toBean(item.getJsonInfo(), FsStoreProduct.class);
+                if (StringUtils.isNotEmpty(cartDTO.getBarCode())) {
+                    barCodeList.add(cartDTO.getBarCode());
+                }
+            }
+            if (!barCodeList.isEmpty()) {
+                attrValueScrmMapper.incStock(fsStoreProduct.getProductId(), barCodeList, liveOrder.getTotalNum());
+            }
+
             // 更新商品库存
             fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
             goods.setStock(goods.getStock()+Long.parseLong(liveOrder.getTotalNum()));
@@ -3795,7 +3851,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 
     private void refundCoupon(LiveOrder order) {
         if(order.getCouponUserId()!=null){
-            LiveCouponUser couponUser=liveCouponUserService.selectLiveCouponUserById(order.getCouponUserId());
+            LiveCouponUser couponUser=liveCouponUserService.selectLiveCouponUserById(order.getUserCouponId());
             if(couponUser!=null){
                 couponUser.setStatus(0);
                 couponUser.setUseTime(null);

+ 51 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.page.PageRequest;
 import com.fs.common.exception.base.BaseException;
 import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.vo.CompanyVO;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.his.domain.FsStoreProduct;
 import com.fs.his.domain.FsUser;
@@ -122,6 +123,9 @@ public class LiveServiceImpl implements ILiveService
     private CompanyMapper companyMapper;
     @Autowired
     private LiveCouponMapper liveCouponMapper;
+    
+    @Autowired
+    LiveTagConfigMapper liveTagConfigMapper;
 
     private static String TOKEN_VALID_CODE = "40001";
 
@@ -146,10 +150,25 @@ public class LiveServiceImpl implements ILiveService
             byId.setVideoFileSize(liveVideo.getFileSize());
             byId.setVideoDuration(liveVideo.getDuration());
         }
+        List<LiveTagItemVO> list = liveTagConfigMapper.getLiveTagListByliveId(liveId);
+        if(null != list && !list.isEmpty()){
+            byId.setLiveTagList(list);
+        }else{
+            byId.setLiveTagList(new ArrayList<>());
+        }
 
         return byId;
     }
 
+    /**
+     * 获取公司下拉列表
+     * @return
+     */
+    @Override
+    public  List<CompanyVO> getCompanyDropList(){
+       return  companyMapper.getCompanyDropList();
+    }
+
 
     /**
      * 查询直播
@@ -282,7 +301,7 @@ public class LiveServiceImpl implements ILiveService
     @Override
     public R subNotifyLive(LiveNotifyParam param) {
         LiveMiniprogramSubNotifyTask notifyTask = new LiveMiniprogramSubNotifyTask();
-        notifyTask.setPage("/pages_course/living?liveId=" + param.getLiveId());
+        notifyTask.setPage("/pages_course/living.html?liveId=" + param.getLiveId());
         notifyTask.setTaskName("直播间预约提醒");
         notifyTask.setTemplateId(param.getTemplateId());
         Long userId = param.getUserId();
@@ -391,6 +410,7 @@ public class LiveServiceImpl implements ILiveService
      * @return 结果
      */
     @Override
+    @Transactional
     public int insertLive(Live live){
 
 
@@ -424,6 +444,12 @@ public class LiveServiceImpl implements ILiveService
         liveData.setFavouriteNum(0L);
         liveData.setFollowNum(0L);
         liveDataService.insertLiveData(liveData);
+
+        //处理直播间标签配置
+        if(null != live.getLiveTagList() && !live.getLiveTagList().isEmpty()){
+            insertLiveTagConfig(live.getLiveTagList(),live.getLiveId(),live.getCreateBy());
+        }
+
         return save > 0 ? 1 : 0;
     }
 
@@ -544,6 +570,7 @@ public class LiveServiceImpl implements ILiveService
      * @return 结果
      */
     @Override
+    @Transactional
     public int updateLive(Live live){
         Live exist = baseMapper.selectLiveByLiveId(live.getLiveId());
         if (live.getCompanyId() != null && exist.getCompanyId() != null && !Objects.equals(exist.getCompanyId(), live.getCompanyId())) {
@@ -585,9 +612,32 @@ public class LiveServiceImpl implements ILiveService
         // 清除缓存
         clearLiveCache(live.getLiveId());
 
+        //处理直播间标签配置
+        if(null != live.getLiveTagList() && !live.getLiveTagList().isEmpty()){
+            //删除当前直播间的所有
+            liveTagConfigMapper.deleteByLiveId(live.getLiveId());
+            insertLiveTagConfig(live.getLiveTagList(),live.getLiveId(),live.getCreateBy());
+        }
+        
         return result;
     }
 
+    public void insertLiveTagConfig(List<LiveTagItemVO> list,Long liveId,String createBy){
+        for (LiveTagItemVO liveTagItemVO : list) {
+            LiveTagConfig liveTagConfig = new LiveTagConfig();
+            liveTagConfig.setCompanyId(Long.valueOf(liveTagItemVO.getCompanyId()));
+            liveTagConfig.setLiveId(liveId);
+            liveTagConfig.setCorpId(liveTagItemVO.getCorpId());
+            liveTagConfig.setMarkType(Long.valueOf(liveTagItemVO.getMarkType()));
+            liveTagConfig.setQwTagId(liveTagItemVO.getQwTagId());
+            liveTagConfig.setQwTagName(liveTagItemVO.getQwTagName());
+            liveTagConfig.setQwTagRealId(liveTagItemVO.getQwTagRealId());
+            liveTagConfig.setCreateTime(new Date());
+            liveTagConfig.setCreateBy(createBy);
+            liveTagConfigMapper.insertLiveTagConfig(liveTagConfig);
+        }
+    }
+
     /**
      * 批量删除直播
      *

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

@@ -0,0 +1,94 @@
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.live.mapper.LiveWatchLogMapper;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.service.ILiveWatchLogService;
+
+/**
+ * 直播看课记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+@Service
+public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, LiveWatchLog> implements ILiveWatchLogService {
+
+    /**
+     * 查询直播看课记录
+     * 
+     * @param logId 直播看课记录主键
+     * @return 直播看课记录
+     */
+    @Override
+    public LiveWatchLog selectLiveWatchLogByLogId(Long logId)
+    {
+        return baseMapper.selectLiveWatchLogByLogId(logId);
+    }
+
+    /**
+     * 查询直播看课记录列表
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 直播看课记录
+     */
+    @Override
+    public List<LiveWatchLog> selectLiveWatchLogList(LiveWatchLog liveWatchLog)
+    {
+        return baseMapper.selectLiveWatchLogList(liveWatchLog);
+    }
+
+    /**
+     * 新增直播看课记录
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    @Override
+    public int insertLiveWatchLog(LiveWatchLog liveWatchLog)
+    {
+        liveWatchLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertLiveWatchLog(liveWatchLog);
+    }
+
+    /**
+     * 修改直播看课记录
+     * 
+     * @param liveWatchLog 直播看课记录
+     * @return 结果
+     */
+    @Override
+    public int updateLiveWatchLog(LiveWatchLog liveWatchLog)
+    {
+        liveWatchLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateLiveWatchLog(liveWatchLog);
+    }
+
+    /**
+     * 批量删除直播看课记录
+     * 
+     * @param logIds 需要删除的直播看课记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveWatchLogByLogIds(Long[] logIds)
+    {
+        return baseMapper.deleteLiveWatchLogByLogIds(logIds);
+    }
+
+    /**
+     * 删除直播看课记录信息
+     * 
+     * @param logId 直播看课记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveWatchLogByLogId(Long logId)
+    {
+        return baseMapper.deleteLiveWatchLogByLogId(logId);
+    }
+}

+ 449 - 13
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -5,26 +5,43 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.thread.ThreadUtil;
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.course.domain.FsCourseLink;
+import com.fs.course.service.impl.FsUserCourseVideoServiceImpl;
 import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserService;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveVideo;
+import com.fs.live.domain.LiveWatchLog;
 import com.fs.live.domain.LiveWatchUser;
-import com.fs.live.mapper.LiveWatchUserMapper;
-import com.fs.live.mapper.LiveMapper;
-import com.fs.live.mapper.LiveVideoMapper;
+import com.fs.live.mapper.*;
+import com.fs.live.param.LiveIsAddKfParam;
 import com.fs.live.service.ILiveWatchUserService;
-import com.fs.live.vo.LiveWatchUserStatistics;
-import com.fs.live.vo.LiveWatchUserVO;
+import com.fs.live.vo.*;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.param.QwEditUserTagParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qw.domain.QwGroupChat;
+import com.fs.qw.domain.QwGroupChatUser;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwGroupChatMapper;
+import com.fs.qw.mapper.QwGroupChatUserMapper;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.service.ISopUserLogsInfoService;
 import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
@@ -52,8 +69,27 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     private LiveMapper liveMapper;
     @Autowired
     private LiveVideoMapper liveVideoMapper;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    private QwGroupChatMapper qwGroupChatMapper;
+    @Autowired
+    private QwGroupChatUserMapper qwGroupChatUserMapper;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private LiveWatchLogMapper liveWatchLogMapper;
+    @Autowired
+    private ISopUserLogsInfoService iSopUserLogsInfoService;
+
+    @Autowired
+    LiveTagConfigMapper liveTagConfigMapper;
+    @Autowired
+    private com.fs.qwApi.service.QwApiService qwApiService;
 
 
+    private static final Logger logger = LoggerFactory.getLogger(LiveWatchUserServiceImpl.class);
+
     /**
      * 查询直播间观看用户
      *
@@ -309,14 +345,6 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
         // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
         LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
-        // 设置在线时长
-        try {
-            Long onlineSeconds = liveWatchUser.getOnlineSeconds();
-            if(onlineSeconds == null) onlineSeconds = 0L;
-            liveWatchUser.setOnlineSeconds(onlineSeconds + (System.currentTimeMillis() - liveWatchUser.getUpdateTime().getTime()) / 1000);
-        } catch (Exception e) {
-            log.error("设置在线时长异常:{}", e.getMessage());
-        }
         liveWatchUser.setUpdateTime(DateUtils.getNowDate());
         liveWatchUser.setOnline(1);
         baseMapper.updateLiveWatchUser(liveWatchUser);
@@ -507,4 +535,412 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
     }
 
+    /**
+     * 直播链接打开判定是否添加客户
+     * @param param
+     * @return
+     */
+    @Override
+    public R liveIsAddKf(LiveIsAddKfParam param) {
+
+        logger.info("【直播判断添加客服】:{}", param);
+        //查询用户
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        //用户不存在唤起重新授权
+        if (fsUser == null) {
+            return R.error(401, "未授权");
+        }
+        if (fsUser.getStatus() == 0) {
+            return R.error("会员被停用,无权限,请联系客服!");
+        }
+        //未注册提示
+        String noRegisterMsg = "由于您还未完成注册,请联系伴学助手完成注册即可观看!";
+        //非独属链接提示
+        String noMemberMsg = "此链接已被绑定,请联系伴学助手领取您的专属链接,专属链接请勿分享哦!";
+
+        if (StringUtils.isNotBlank(param.getChatId())) {
+            return handleLiveChat(param,fsUser, noMemberMsg, noRegisterMsg);
+        } else if (null != param.getQwExternalId()) {
+            return handleLivePerson(param,fsUser, noMemberMsg, noRegisterMsg);
+        } else {
+            return R.error("直播参数错误");
+        }
+
+    }
+
+    @Override
+    public LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(Long liveId, Long userId, Integer liveFlag, Integer replayFlag) {
+        return baseMapper.selectLiveWatchAndCompanyUserByFlag(liveId,userId,liveFlag,replayFlag);
+    }
+
+    @Override
+    public void updateLiveWatchUserEntry(LiveWatchUserEntry liveWatchUser) {
+        LiveWatchUser updateEntity = new LiveWatchUser();
+        BeanUtil.copyProperties(updateEntity, liveWatchUser);
+        baseMapper.updateLiveWatchUser(updateEntity);
+    }
+
+    /**
+     * 处理发送群聊逻辑
+     * @param param
+     * @param user
+     * @param noMemberMsg
+     * @param noRegisterMsg
+     * @return
+     */
+    public R handleLiveChat(LiveIsAddKfParam param,FsUser user, String noMemberMsg,String noRegisterMsg){
+
+        QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(param.getChatId());
+        if (qwGroupChat == null) {
+            return R.error("直播群参数异常");
+        }
+        SopUserLogsInfo sopUserLogsInfo = new SopUserLogsInfo();
+        sopUserLogsInfo.setChatId(param.getChatId());
+        List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);
+        if (qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()) {
+            return R.error("直播群参数异常");
+        }
+
+        QwExternalContact qwExternalContact = null;
+        if (null != param.getUserId() && null == qwExternalContact) {
+            try {
+                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));
+            } catch (Exception e) {
+                log.error("直播群聊用户id匹配异常,参数user_id:{},fs_user_id:{},corp_id:{}", qwGroupChat.getOwner(), param.getUserId(), param.getCorpId(), e);
+            }
+        }
+        if (StringUtils.isNotBlank(param.getChatId()) && null == qwExternalContact) {
+            List<QwExternalContact> groupChatUserByChatIdAndUserName = qwExternalContactMapper.getGroupChatUserByChatIdAndUserName(qwGroupChat.getOwner(), user.getNickName(), param.getCorpId(), param.getChatId());
+            log.info("直播群聊用户查询结果,参数user_id:{},name:{},corp_id:{},chatId:{},groupChatUserByChatIdAndUserName:{}", qwGroupChat.getOwner(), user.getNickName(), param.getCorpId(), param.getChatId(), groupChatUserByChatIdAndUserName);
+            //没找到用户 || 找到的用户数量大于1 使用userid查询匹配
+            if (null == groupChatUserByChatIdAndUserName || groupChatUserByChatIdAndUserName.isEmpty() || groupChatUserByChatIdAndUserName.size() > 1) {
+                log.error("直播群聊用户昵称匹配异常,参数user_id:{},name:{},corp_id:{},chatId:{}", qwGroupChat.getOwner(), user.getNickName(), param.getCorpId(), param.getChatId());
+            } else {
+                qwExternalContact = groupChatUserByChatIdAndUserName.get(0);
+            }
+        }
+        if(qwExternalContact==null){
+            return R.error(noRegisterMsg);
+        }
+        QwExternalContact finalQwExternalContact = qwExternalContact;
+        if (qwGroupChatUsers.stream().noneMatch(e -> e.getUserId().equals(finalQwExternalContact.getExternalUserId()))) {
+            log.error("直播客户不在群:{},里面:{}", qwGroupChat.getChatId(), qwExternalContact.getExternalUserId());
+            return R.error(noRegisterMsg);
+        }
+        Long qwExternalId = qwExternalContact.getId();
+
+        LiveWatchLog liveWatchLog = liveWatchLogMapper.selectOneLogByLiveIdAndQwUserIdAndExternalId(param.getLiveId(), param.getQwUserId(),qwExternalId);
+        if (liveWatchLog==null ){
+            return R.error(noRegisterMsg);
+        }
+        //判断外部联系人有没有绑定userId
+        if (qwExternalContact.getFsUserId() != null) {
+            //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
+            if (!qwExternalContact.getFsUserId().equals(param.getUserId())) {
+                return R.error(noRegisterMsg);
+            }
+            List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());
+            //匹配客户公司id
+            if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
+                return R.error(noRegisterMsg);
+            }
+
+            //看课记录中userId为0绑定userId
+            if (liveWatchLog.getUserId() == null || liveWatchLog.getUserId().equals(0L) || !liveWatchLog.getUserId().equals(param.getUserId())) {
+                liveWatchLog.setUserId(param.getUserId());
+            }
+
+            liveWatchLog.setUpdateTime(new Date());
+
+            liveWatchLogMapper.updateLiveWatchLog(liveWatchLog);
+            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());
+
+
+        } else {
+            //没绑定fsUser直接绑定fsUser
+            QwExternalContact contact = new QwExternalContact();
+            contact.setId(qwExternalId);
+            contact.setFsUserId(param.getUserId());
+            qwExternalContactMapper.updateQwExternalContact(contact);
+            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());
+
+            FsUser fsUser = new FsUser();
+            fsUser.setUserId(user.getUserId());
+            fsUser.setIsAddQw(1);
+            fsUserMapper.updateFsUser(fsUser);
+            //绑定上之后 更新观看记录
+            //看课记录中userId为0绑定userId
+            liveWatchLog.setUserId(param.getUserId());
+            liveWatchLog.setUpdateTime(new Date());
+            liveWatchLogMapper.updateLiveWatchLog(liveWatchLog);
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 处理发送个人逻辑
+     * @param param
+     * @param noMemberMsg
+     * @param noRegisterMsg
+     * @return
+     */
+    public R handleLivePerson(LiveIsAddKfParam param,FsUser user,String noMemberMsg,String noRegisterMsg){
+
+        Long qwExternalId = param.getQwExternalId();
+
+        LiveWatchLog liveWatchLog = liveWatchLogMapper.selectOneLogByLiveIdAndQwUserIdAndExternalId(param.getLiveId(), param.getQwUserId(),qwExternalId);
+
+        if (liveWatchLog==null ){
+            return R.error(noMemberMsg);
+        }
+        //查询是否有添加客服
+        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(qwExternalId);
+
+        //用小程序id查询外部联系人
+        List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());
+
+        //判断外部联系人有没有绑定userId
+        if (externalContact.getFsUserId() != null) {
+            //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
+            if (!externalContact.getFsUserId().equals(param.getUserId())) {
+                return R.error(noMemberMsg);
+            }
+            //匹配客户公司id
+            if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
+                return R.error(noMemberMsg);
+            }
+
+            //看课记录中userId为0绑定userId
+            if (liveWatchLog.getUserId() == null || liveWatchLog.getUserId().equals(0L) || !liveWatchLog.getUserId().equals(param.getUserId())) {
+                liveWatchLog.setUserId(param.getUserId());
+            }
+
+            liveWatchLog.setUpdateTime(new Date());
+
+            liveWatchLogMapper.updateLiveWatchLog(liveWatchLog);
+            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());
+
+        } else { //没绑定fsUser直接绑定fsUser
+            QwExternalContact contact = new QwExternalContact();
+            contact.setId(qwExternalId);
+            contact.setFsUserId(param.getUserId());
+            qwExternalContactMapper.updateQwExternalContact(contact);
+            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());
+
+            FsUser fsUser = new FsUser();
+            fsUser.setUserId(user.getUserId());
+            fsUser.setIsAddQw(1);
+            fsUserMapper.updateFsUser(fsUser);
+            //绑定上之后 更新观看记录
+            //看课记录中userId为0绑定userId
+            liveWatchLog.setUserId(param.getUserId());
+            liveWatchLog.setUpdateTime(new Date());
+            liveWatchLogMapper.updateLiveWatchLog(liveWatchLog);
+        }
+
+        return R.ok();
+
+    }
+
+    /**
+     * 根据用户直播看课记录来打标签
+     */
+    @Override
+    @Async
+    public void qwTagMarkByLiveWatchLog(Long liveId) {
+        //查询直播间的标签配置
+        List<LiveTagItemVO> liveTagConfig = liveTagConfigMapper.getLiveTagListByliveId(liveId);
+
+        /**
+         * 8	回放已下单
+         * 7	直播已下单
+         * 6	回放已完课
+         * 5	直播已完课
+         * 4	回放到课未完课
+         * 3	直播到课未完课
+         * 2	回放未到课
+         * 1	直播未到课
+         */
+        Map<Integer, LiveTagItemVO> liveTagMp = liveTagConfig.stream()
+                .collect(Collectors.toMap(
+                        LiveTagItemVO::getMarkType,
+                        Function.identity(),
+                        (existing, replacement) -> existing
+                ));
+        //查询直播间的看课记录
+        List<LiveWatchLog> liveWatchLogs = liveWatchLogMapper.selectLiveWatchLogByLiveId(liveId);
+
+        //根据配置给每位用户打上标签
+        List<HandleUserTagVO> handleUserTagVOS = new ArrayList<>();
+        liveWatchLogs.forEach(liveLog -> {
+            HandleUserTagVO addItem = new HandleUserTagVO();
+            addItem.setLiveId(liveId);
+            addItem.setExternalId(liveLog.getExternalContactId());
+            LiveTagItemVO liveTagItemVO = null;
+            List<String> tags = new ArrayList<>();
+            switch (liveLog.getLogType()) {
+                //1看课中
+                case 1:
+                    //打标签 直播到课未完课
+                    liveTagItemVO = liveTagMp.get(3);
+                    if (null != liveTagItemVO) {
+                        tags.add(liveTagItemVO.getQwTagRealId());
+                    }
+                    break;
+                //2完课
+                case 2:
+                    //打标签 直播已完课
+                    liveTagItemVO = liveTagMp.get(5);
+                    if (null != liveTagItemVO) {
+                        tags.add(liveTagItemVO.getQwTagRealId());
+                    }
+                    break;
+                //3待看课
+                case 3:
+
+                    //打标签 直播未到课
+                    liveTagItemVO = liveTagMp.get(1);
+                    if (null != liveTagItemVO) {
+                        tags.add(liveTagItemVO.getQwTagRealId());
+                    }
+                    break;
+                //4看课中断
+                case 4:
+                    //打标签 直播未到课
+                    liveTagItemVO = liveTagMp.get(3);
+                    if (null != liveTagItemVO) {
+                        tags.add(liveTagItemVO.getQwTagRealId());
+                    }
+                    break;
+                default:
+                    break;
+            }
+            if (null != liveLog.getLiveBuy() && liveLog.getLiveBuy().equals(1)) {
+                liveTagItemVO = liveTagMp.get(7);
+                if (null != liveTagItemVO) {
+                    tags.add(liveTagItemVO.getQwTagRealId());
+                }
+            }
+            handleUserTagVOS.add(addItem);
+        });
+        handleUserTags2Qw(handleUserTagVOS);
+    }
+
+    /**
+     * 对企微用户打标签
+     * @param userTagVOS 用户标签列表,包含外部联系人ID和要添加的标签列表
+     */
+    private void handleUserTags2Qw(List<HandleUserTagVO> userTagVOS) {
+        if (CollUtil.isEmpty(userTagVOS)) {
+            log.warn("用户标签列表为空,跳过打标签操作");
+            return;
+        }
+
+        int successCount = 0;
+        int failCount = 0;
+
+        for (HandleUserTagVO userTagVO : userTagVOS) {
+            try {
+                // 参数校验
+                if (userTagVO.getExternalId() == null) {
+                    log.warn("外部联系人ID为空,跳过该用户");
+                    failCount++;
+                    continue;
+                }
+
+                if (CollUtil.isEmpty(userTagVO.getTags())) {
+                    log.warn("标签列表为空,跳过该用户: externalId={}", userTagVO.getExternalId());
+                    failCount++;
+                    continue;
+                }
+
+                // 根据外部联系人ID查询企微外部联系人信息
+                QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(userTagVO.getExternalId());
+                if (qwExternalContact == null) {
+                    log.warn("未找到企微外部联系人: externalId={}", userTagVO.getExternalId());
+                    failCount++;
+                    continue;
+                }
+
+                // 校验必要字段
+                if (StringUtils.isEmpty(qwExternalContact.getUserId())
+                        || StringUtils.isEmpty(qwExternalContact.getExternalUserId())
+                        || StringUtils.isEmpty(qwExternalContact.getCorpId())) {
+                    log.warn("企微外部联系人信息不完整: externalId={}, userId={}, externalUserId={}, corpId={}",
+                            userTagVO.getExternalId(),
+                            qwExternalContact.getUserId(),
+                            qwExternalContact.getExternalUserId(),
+                            qwExternalContact.getCorpId());
+                    failCount++;
+                    continue;
+                }
+
+                // 构建打标签参数
+                QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+                qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
+                qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+                qwEditUserTagParam.setAdd_tag(userTagVO.getTags());
+
+                // 调用企微API打标签
+                QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, qwExternalContact.getCorpId());
+
+                if (qwResult != null && qwResult.getErrcode() == 0) {
+                    // 打标签成功,更新数据库中的标签信息
+                    String existingTagIds = qwExternalContact.getTagIds();
+                    Set<String> uniqueTagIds = new HashSet<>();
+
+                    // 合并现有标签
+                    if (StringUtils.isNotEmpty(existingTagIds)) {
+                        try {
+                            List<String> parsedTags = JSON.parseArray(existingTagIds, String.class);
+                            if (CollUtil.isNotEmpty(parsedTags)) {
+                                uniqueTagIds.addAll(parsedTags);
+                            }
+                        } catch (Exception e) {
+                            log.warn("解析现有标签失败: externalId={}, tagIds={}, error={}",
+                                    userTagVO.getExternalId(), existingTagIds, e.getMessage());
+                        }
+                    }
+
+                    // 添加新标签
+                    uniqueTagIds.addAll(userTagVO.getTags());
+
+                    // 更新数据库
+                    QwExternalContact updateContact = new QwExternalContact();
+                    updateContact.setId(qwExternalContact.getId());
+                    updateContact.setTagIds(JSON.toJSONString(new ArrayList<>(uniqueTagIds)));
+                    qwExternalContactMapper.updateQwExternalContact(updateContact);
+
+                    successCount++;
+                    log.info("成功为用户打标签: externalId={}, userId={}, externalUserId={}, tags={}",
+                            userTagVO.getExternalId(),
+                            qwExternalContact.getUserId(),
+                            qwExternalContact.getExternalUserId(),
+                            userTagVO.getTags());
+                } else {
+                    // 打标签失败
+                    failCount++;
+                    String errorMsg = qwResult != null ? qwResult.getErrmsg() : "未知错误";
+                    log.error("为用户打标签失败: externalId={}, userId={}, externalUserId={}, error={}",
+                            userTagVO.getExternalId(),
+                            qwExternalContact.getUserId(),
+                            qwExternalContact.getExternalUserId(),
+                            errorMsg);
+                }
+            } catch (Exception e) {
+                failCount++;
+                log.error("为用户打标签异常: externalId={}, error={}",
+                        userTagVO.getExternalId(), e.getMessage(), e);
+            }
+        }
+
+        log.info("打标签操作完成: 总数={}, 成功={}, 失败={}",
+                userTagVOS.size(), successCount, failCount);
+    }
+
 }

+ 29 - 0
fs-service/src/main/java/com/fs/live/vo/HandleUserTagVO.java

@@ -0,0 +1,29 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/13 下午6:40)
+ */
+
+@Data
+public class HandleUserTagVO {
+
+    /**
+     * 直播间id
+     */
+    private Long liveId;
+
+    /**
+     * 外部联系人id
+     */
+    private Long externalId;
+
+    /**
+     * 打标签列表
+     */
+    private List<String> tags;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/live/vo/LiveTagItemVO.java

@@ -0,0 +1,25 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/13 下午4:43)
+ */
+@Data
+public class LiveTagItemVO {
+
+   private Long id;
+    //公司id
+   private Integer companyId;
+   //主体id
+   private String  corpId;
+   //标记类型
+   private Integer markType;
+   //标签id
+   private Long qwTagId;
+   //企微标签id
+   private String qwTagRealId;
+   //标签名称
+   private String qwTagName;
+}

+ 72 - 0
fs-service/src/main/java/com/fs/live/vo/LiveWatchUserEntry.java

@@ -0,0 +1,72 @@
+package com.fs.live.vo;
+
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播间观看用户对象 live_watch_user
+ *
+ * @author fs
+ * @date 2025-01-18
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveWatchUserEntry extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /** 直播ID */
+    @Excel(name = "直播ID")
+    private Long liveId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    @Excel(name = "用户头像")
+    private String avatar;
+
+    /** 消息状态;0正常1禁言 */
+    @Excel(name = "消息状态;0正常1禁言")
+    private Integer msgStatus;
+
+    /** 在线状态;0在线1离线 */
+    @Excel(name = "在线状态;0在线1离线")
+    private Integer online = 0;
+    /** 全局用户自见 */
+    private Integer globalVisible = 0;
+    /** 用户自见 */
+    private Integer singleVisible = 0;
+
+    private Long onlineSeconds;
+
+    /** 用户名字 */
+
+    private String nickName;
+    private String tabName;
+
+    /** 直播进入标记:0-否 1-是 */
+    @Excel(name = "直播进入标记")
+    private Integer liveFlag = 0;
+
+    /** 回放进入标记:0-否 1-是 */
+    @Excel(name = "回放进入标记")
+    private Integer replayFlag = 0;
+
+    /** 用户所在位置 */
+    @Excel(name = "用户所在位置")
+    private String location;
+
+
+    private Integer pageNum;
+    private Integer pageSize;
+
+    private Long companyId;
+    private Long companyUserId;
+
+}

+ 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);
+}

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

@@ -511,4 +511,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);
 }

+ 7 - 1
fs-service/src/main/java/com/fs/qw/param/QwExternalContactParam.java

@@ -10,8 +10,12 @@ import java.util.List;
 
 @Data
 public class QwExternalContactParam {
+
     private Long id;
 
+    @TableField(exist = false)
+    private Long extId;
+
     /** 属于用户id */
     @Excel(name = "属于用户id")
     private String userId;
@@ -24,10 +28,12 @@ public class QwExternalContactParam {
 
     /** 外部联系人id */
     @Excel(name = "外部联系人id")
-    private Long externalUserId;
+    private String externalUserId;
 
     private Long companyUserId;
 
+    private Long fsUserId;
+
     /**
      * 销售员工信息(昵称,手机号)
      */

+ 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);
+}

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

@@ -601,4 +601,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());
+    }
+
+}

+ 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 {

+ 390 - 24
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,10 @@ 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.*;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwExtCourseSopWatchLog;
@@ -59,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;
@@ -89,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
@@ -170,6 +176,18 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     @Autowired
     private IQwSopTempVoiceService sopTempVoiceService;
 
+    @Autowired
+    LiveWatchLogMapper liveWatchLogMapper;
+
+    @Autowired
+    private LuckyBagMapper luckyBagMapper;
+
+    @Autowired
+    private LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
 
 
     @Override
@@ -511,20 +529,25 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     e.setUserList(userMap.getOrDefault(e.getUserId(), Collections.emptyList()));
                 });
             }
-            try {
-                groupList.forEach(groupChat -> {
-                    QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText2(groupChat.getOwner(), groupChat.getCorpId());
-                    groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
-                        Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
-                        GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
-                        if (vo != null && vo.getId() != null) {
-                            addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), param.getStartTime(), createTime,2);
-                        }
+
+            //没有传值课程和课节 是直播的数据
+            if(null != param.getCourseId() && null !=param.getVideoId()){
+                try {
+                    groupList.forEach(groupChat -> {
+                        QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText2(groupChat.getOwner(), groupChat.getCorpId());
+                        groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                            Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                            GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                            if (vo != null && vo.getId() != null) {
+                                addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), param.getStartTime(), createTime,2);
+                            }
+                        });
                     });
-                });
-            } catch (Exception e) {
-                log.error("群聊创建看课记录失败!", e);
+                } catch (Exception e) {
+                    log.error("群聊创建看课记录失败!", e);
+                }
             }
+
             if (param.getSendType() != null && param.getSendType() == 2) {
                 sopLogsList = groupUserList.stream().map(groupUser -> {
                     QwGroupChat qwGroupChat = groupMap.get(groupUser.getChatId());
@@ -666,10 +689,19 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 break;
                             //直播小程序单独
                             case "12":
-                                String sortLiveLink = "/pages_course/living?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId();
+                                String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + vo.getId().toString();
                                 st.setContentType("4");
                                 String js = configService.selectConfigByKey("his.config");
                                 FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
+                                //todo 发个人看课记录处理
+                                try {
+                                    if (vo != null && vo.getId() != null) {
+                                        createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),vo.getId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUser.getId().toString(),param.getCorpId());
+                                    }
+                                } catch (Exception e) {
+                                    log.error("群聊创建直播看课记录失败!", e);
+                                }
+//                                createLiveWatchLogAndInsert();
                                 st.setMiniprogramAppid(sysConfig.getAppId());
                                 st.setMiniprogramPage(sortLiveLink);
                                 break;
@@ -809,10 +841,25 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 break;
                             //直播小程序单独
                             case "12":
-                                String sortLiveLink = "/pages_course/living?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId();
+                                String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" +param.getCorpId()+"&qwUserId=" + qwUser.getId() + "&chatId=" + groupChat.getChatId();
                                 st.setContentType("4");
                                 String js = configService.selectConfigByKey("his.config");
                                 FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
+                                //发群处理看课记录
+//                                createLiveWatchLogAndInsert();
+                                try {
+                                    groupList.forEach(gc -> {
+                                        gc.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                            Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                            GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                            if (vo != null && vo.getId() != null) {
+                                                createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),vo.getId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUser.getId().toString(),param.getCorpId());
+                                            }
+                                        });
+                                    });
+                                } catch (Exception e) {
+                                    log.error("群聊创建直播看课记录失败!", e);
+                                }
                                 st.setMiniprogramAppid(sysConfig.getAppId());
                                 st.setMiniprogramPage(sortLiveLink);
                                 break;
@@ -1013,10 +1060,17 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             break;
                         //直播小程序单独
                         case "12":
-                            String sortLiveLink = "/pages_course/living?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId();
+                            String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUserId +"&externalId=" + item.getExternalId().toString();
                             st.setContentType("4");
                             String js = configService.selectConfigByKey("his.config");
                             FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
+                            //todo 发个人看课记录处理
+                            try {
+                                    createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUserId,param.getCorpId());
+
+                            } catch (Exception e) {
+                                log.error("群聊创建直播看课记录失败!", e);
+                            }
                             st.setMiniprogramAppid(sysConfig.getAppId());
                             st.setMiniprogramPage(sortLiveLink);
                             break;
@@ -1280,7 +1334,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:
@@ -1344,7 +1398,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) {
@@ -1471,17 +1525,25 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     }
 
                     break;
+
                 //直播小程序单独
                 case "12":
                     String sortLiveLink;
-                    sortLiveLink = "/pages_course/living?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId();
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + item.getExternalId().toString();
 
 
                     String miniprogramLiveTitle = st.getMiniprogramTitle();
                     int maxLiveLength = 17;
                     st.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+
                     String json = configService.selectConfigByKey("his.config");
                     FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
+                    //todo 发个人看课记录处理
+                    try {
+                        createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, String.valueOf(qwUser.getId()),param.getCorpId());
+                    } catch (Exception e) {
+                        log.error("群聊创建直播看课记录失败!", e);
+                    }
                     st.setMiniprogramAppid(sysConfig.getAppId());
                     st.setMiniprogramPage(sortLiveLink);
                     st.setContentType("4");
@@ -1492,7 +1554,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     }
 
                     break;
-                case "14":
+                case "15":
                     //app语音
                     try {
                         qwSop = qwSopMapper.selectQwSopById(param.getSopId());
@@ -1504,7 +1566,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         throw new RuntimeException(e);
                     }
                     break;
-                case "15":
+                case "16":
                     //app文本
                     String txt = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                     st.setValue(st.getValue()
@@ -1517,6 +1579,99 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         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;
 
@@ -1526,6 +1681,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 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) {
@@ -1783,5 +2066,88 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
     }
 
+    /**
+     * 直播看课记录处理
+     * @param companyId
+     * @param companyUserId
+     * @param externalId
+     * @param liveId
+     * @param appId
+     * @param logSource
+     * @param qwUserId
+     * @param corpId
+     */
+    public void createLiveWatchLogAndInsert(String companyId,String companyUserId,String externalId,Long liveId,String appId,Integer logSource,String qwUserId,String corpId){
+        try{
+            // 写入对应数据源的记录表
+            LiveWatchLog itemLiveWatchLog = new LiveWatchLog();
+            itemLiveWatchLog.setLiveId(liveId);
+            itemLiveWatchLog.setLogType(3);
+            itemLiveWatchLog.setSopCreateTime(new Date());
+            itemLiveWatchLog.setCompanyId(Long.valueOf(companyId));
+            itemLiveWatchLog.setCompanyUserId(Long.valueOf(companyUserId));
+            itemLiveWatchLog.setSendAppId(appId);
+            itemLiveWatchLog.setLogSource(logSource);
+            itemLiveWatchLog.setQwUserId(qwUserId);
+            itemLiveWatchLog.setExternalContactId(Long.valueOf(externalId));
+            itemLiveWatchLog.setCorpId(corpId);
+            if(liveWatchLogMapper.updateLiveWatchLogCondition(itemLiveWatchLog) > 0){
+
+            }else{
+                List<LiveWatchLog> handleList= new ArrayList<>();
+                handleList.add(itemLiveWatchLog);
+                liveWatchLogMapper.insertLiveWatchLogBatch(handleList);
+            }
+        }catch(Exception e){
+            log.error("创建直播看课记录失败:{}",e.getMessage());
+        }
+
+
+    }
+
+    /**
+     * 构建福袋记录对象
+     */
+    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;
+    }
 
 }

+ 3 - 3
fs-service/src/main/resources/application-config-druid-bjzm-test.yml

@@ -10,9 +10,9 @@ logging:
 wx:
   miniapp:
     configs:
-      - appid: wx94951f52d3ac5e25   #北京存在文化
-        secret: bfe27b20c6e3c4232a1d4ef36228e84b #北京存在文化
-        token: Ncbnd7lJvkripxxna6NAWCxCrvC
+      - appid: wx44beed5640bcb1ba   #北京卓美
+        secret: 1bfcfa420f741801575a74d94752d014 #北京卓美
+        token: cbnd7lJvkripVOpyTFAna6NAWCxCrvC
         aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
         msgDataFormat: JSON
   cp:

+ 3 - 3
fs-service/src/main/resources/application-config-druid-bjzm.yml

@@ -10,9 +10,9 @@ logging:
 wx:
   miniapp:
     configs:
-      - appid: wx94951f52d3ac5e25   #北京卓美
-        secret: bfe27b20c6e3c4232a1d4ef36228e84b #北京卓美
-        token: Ncbnd7lJvkripxxna6NAWCxCrvC
+      - appid: wx44beed5640bcb1ba   #北京卓美
+        secret: 1bfcfa420f741801575a74d94752d014 #北京卓美
+        token: cbnd7lJvkripVOpyTFAna6NAWCxCrvC
         aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
         msgDataFormat: JSON
   cp:

+ 101 - 0
fs-service/src/main/resources/application-config-druid-hsyy.yml

@@ -0,0 +1,101 @@
+baidu:
+  token: 1
+  back-domain: https://www.xxxx.com
+#配置
+logging:
+  level:
+    org.springframework.web: INFO
+    com.github.binarywang.demo.wx.cp: DEBUG
+    me.chanjar.weixin: DEBUG
+wx:
+  miniapp:
+    configs:
+      - appid: w   #中康智慧
+        secret: 5
+        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+      - appid: w   #中康未来智慧药房
+        secret: 9
+        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+  cp:
+    corpId: wwb
+    appConfigs:
+      - agentId: 100005
+        secret: ec7okROXJqkNafq66aKNv0asTzQIG0CYrj3vyBbo
+        token: PPKOdAloMO
+        aesKey: PKvaxtpSvNGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+  pay:
+    appId: wx #微信公众号或者小程序等的appid
+    mchId: 1611045 #微信支付商户号
+    mchKey: 8cab128997a3547c10898b877f38 #微信支付商户密钥
+    subAppId:  #服务商模式下的子商户公众账号ID
+    subMchId:  #服务商模式下的子商户号
+    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl: https://usepp.his.runtzh.com/app/wxpay/wxPayNotify
+  mp:
+    useRedis: false
+    redisConfig:
+      host: 127.0.0.1
+      port: 6379
+      timeout: 2000
+    configs:
+      - appId: wx17f36a56c701bdea # 第一个公众号的appid   //公众号名称
+        secret: 185030bbe7f8d7a0c16b94dd9d4ea542 # 公众号的appsecret
+        token: PPKOdAlCoMO # 接口配置里的Token值
+        aesKey: Eswa6VjwtVcw03qZy6Wllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+aifabu:  #爱链接
+  appKey: 7b471be905ab17ef358c610dd117601d008
+watch:
+  watchUrl: watch.ylrzcloud.com/prod-api
+#  account: tcloud
+#  password: mdf-m2h_6yw2$hq
+  account1: ccif #866655060138751
+  password1: cp-t5or_6xw7$mt
+  account2: tcloud #rt500台
+  password2: mdf-m2h_6yw2$hq
+  account3: whr
+  password3: v9xsKuqn_$d2y
+
+fs :
+  commonApi: http://172.16.16.47:7771
+  h5CommonApi: http://172.16.16.47:7771
+  jwt:
+    # 加密秘钥
+    secret: f4h2s52034348y86y67cde581c0f9eb5
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
+nuonuo:
+  key: 10924508
+  secret: A2EB20764D304D16
+# 存储捅配置
+tencent_cloud_config:
+  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+  bucket: heshanyy-1323137866
+  app_id: 1323137866
+  region: ap-chongqing
+  proxy: heshanyy
+cloud_host:
+  company_name: 河山医院
+  projectCode: heshanyy
+#看课授权时显示的头像
+headerImg:
+  imgUrl: https://hsyy-1348049832.cos.ap-chongqing.myqcloud.com/hsyy.jpg
+ipad:
+  ipadUrl: http://ipad.hshsyy.com
+  aiApi: http://49.
+  voiceApi:
+  commonApi:
+wx_miniapp_temp:
+  pay_order_temp_id:
+  inquiry_temp_id:
+# 聚水潭API配置
+jst:
+  app_key: 5dea3c46f0214985bd6428b2b12 #聚水潭2025-09-03
+  app_secret: 3f382758a4f4470e8912be0ce #聚水潭2025-09-0
+  authorization_code: 999999
+  shop_code: "188784"

+ 1 - 1
fs-service/src/main/resources/application-config-druid-yxj.yml

@@ -86,7 +86,7 @@ cloud_host:
 headerImg:
   imgUrl: https://yxj-1323137866.cos.ap-chongqing.myqcloud.com/app/yxj.jpg
 ipad:
-  ipadUrl: http://.top
+  ipadUrl: http://yxjipad.ylrztop.com
   aiApi: http://49/api
   voiceApi:
   commonApi:

+ 4 - 4
fs-service/src/main/resources/application-druid-bjzm-test.yml

@@ -45,10 +45,10 @@ spring:
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
-                    enabled: false
-                    url:
-                    username:
-                    password:
+                    enabled: true
+                    url: jdbc:mysql://gz-cdb-ofgnuz1n.sql.tencentcdb.com:26872/fs_his?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_1q2w3e4r5t6y
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 172 - 0
fs-service/src/main/resources/application-druid-hsyy.yml

@@ -0,0 +1,172 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-druid-hsyy,common
+    # redis 配置
+    redis:
+        host: 172.16.16.46
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password: Ylrz_tM8
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        #        clickhouse:
+        #            type: com.alibaba.druid.pool.DruidDataSource
+        #            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
+        #            url: jdbc:clickhouse://1.14.104.71:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
+        #            username: rt_2024
+        #            password: Yzx_19860213
+        #            initialSize: 10
+        #            maxActive: 100
+        #            minIdle: 10
+        #            maxWait: 6000
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://172.16.16.37:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_tM818782145I@
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    url: jdbc:mysql://172.16.16.37:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_tM818782145I@
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 2000
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+        sop:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://172.16.16.37:3306/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_tM818782145I@
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 200
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+rocketmq:
+    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    consumer:
+        group: voice-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+custom:
+    token: "1o62d3YxvdHd4LEUiltnu7sK"
+    encoding-aes-key: "UJfTQ5qKTKlegjkXtp1YuzJzxeHlUKvq5GyFbERN1iU"
+    corp-id: "ww51717e2b71d5e2d3"
+    secret: "6ODAmw-8W4t6h9mdzHh2Z4Apwj8mnsyRnjEDZOHdA7k"
+    private-key-path: "privatekey.pem"
+    webhook-url: "https://your-server.com/wecom/archive"
+# token配置
+token:
+    # 令牌自定义标识
+    header: Authorization
+    # 令牌密钥
+    secret: abcdefghijklmnopqrstuvwxyz
+    # 令牌有效期(默认30分钟)
+    expireTime: 180
+openIM:
+    secret: openIM123
+    userID: imAdmin
+    url: https://web.im.ysya.top/api
+#是否使用新im
+im:
+    type: OPENIM
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: true
+enableRedPackAccount: 0

+ 3 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -86,6 +86,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test ='maps.userId !=null'>
                 and l.user_id = #{maps.userId}
             </if>
+            <if test ='maps.logId !=null'>
+                and l.log_id = #{maps.logId}
+            </if>
             <if test ='maps.project !=null'>
                 and l.project = #{maps.project}
             </if>

+ 1 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml

@@ -239,6 +239,7 @@
             <if test="projectId != null">project_id = #{projectId},</if>
             <if test="isFirst != null">is_first = #{isFirst},</if>
             <if test="isSpeed != null">is_speed = #{isSpeed},</if>
+            <if test="isOnPut != null">is_on_put = #{isOnPut},</if>
         </trim>
         where video_id = #{videoId}
     </update>

+ 81 - 0
fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml

@@ -399,5 +399,86 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY create_time DESC
     </select>
 
+    <!-- 查询合并的售后列表(商城售后+直播售后) -->
+    <select id="selectMergedAfterSalesList" parameterType="com.fs.hisStore.param.MergedAfterSalesQueryParam" resultType="com.fs.hisStore.vo.MergedAfterSalesVO">
+        SELECT * FROM (
+            -- 商城售后
+            SELECT
+                s.id,
+                s.order_code AS orderCode,
+                s.refund_amount AS refundAmount,
+                s.service_type AS serviceType,
+                s.reasons,
+                s.explains,
+                s.explain_img AS explainImg,
+                s.shipper_code AS shipperCode,
+                s.delivery_sn AS deliverySn,
+                s.delivery_name AS deliveryName,
+                s.status,
+                s.sales_status AS salesStatus,
+                s.order_status AS orderStatus,
+                s.is_del AS isDel,
+                s.user_id AS userId,
+                s.consignee,
+                s.phone_number AS phoneNumber,
+                s.address,
+                s.is_package AS isPackage,
+                s.package_json AS packageJson,
+                1 AS afterSalesType,
+                '商城售后' AS afterSalesTypeName,
+                s.create_time AS createTime
+            FROM fs_store_after_sales_scrm s
+            WHERE s.is_del = 0
+            <if test="maps.status != null and maps.status == 1">
+                AND s.sales_status = 0
+            </if>
+            <if test="maps.status != null and maps.status == 2">
+                AND s.sales_status = 3
+            </if>
+            <if test="maps.userId != null">
+                AND s.user_id = #{maps.userId}
+            </if>
+            UNION ALL
+            -- 直播售后
+            SELECT
+                s.id,
+                o.order_code AS orderCode,
+                s.refund_amount AS refundAmount,
+                s.refund_type AS serviceType,
+                s.reasons,
+                s.explains,
+                s.explain_img AS explainImg,
+                s.delivery_code AS shipperCode,
+                s.delivery_sn AS deliverySn,
+                s.delivery_name AS deliveryName,
+                s.status,
+                s.sales_status AS salesStatus,
+                s.order_status AS orderStatus,
+                s.is_del AS isDel,
+                s.user_id AS userId,
+                s.consignee,
+                s.phone_number AS phoneNumber,
+                s.address,
+                NULL AS isPackage,
+                NULL AS packageJson,
+                2 AS afterSalesType,
+                '直播售后' AS afterSalesTypeName,
+                s.create_time AS createTime
+            FROM live_after_sales s
+            LEFT JOIN live_order o ON o.order_id = s.order_id
+            WHERE s.is_del = 0
+            <if test="maps.status != null and maps.status == 1">
+                AND s.sales_status = 0
+            </if>
+            <if test="maps.status != null and maps.status == 2">
+                AND s.sales_status = 3
+            </if>
+            <if test="maps.userId != null">
+                AND s.user_id = #{maps.userId}
+            </if>
+        ) AS merged_after_sales
+        ORDER BY createTime DESC
+    </select>
+
 </mapper>
 

+ 116 - 0
fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml

@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveCompletionPointsRecordMapper">
+
+    <resultMap type="com.fs.live.domain.LiveCompletionPointsRecord" id="LiveCompletionPointsRecordResult">
+        <id     property="id"                      column="id"    />
+        <result property="liveId"                  column="live_id"    />
+        <result property="userId"                  column="user_id"    />
+        <result property="watchDuration"           column="watch_duration"    />
+        <result property="videoDuration"           column="video_duration"    />
+        <result property="completionRate"          column="completion_rate"    />
+        <result property="continuousDays"          column="continuous_days"    />
+        <result property="pointsAwarded"           column="points_awarded"    />
+        <result property="lastCompletionDate"      column="last_completion_date"    />
+        <result property="currentCompletionDate"   column="current_completion_date"    />
+        <result property="receiveStatus"           column="receive_status"    />
+        <result property="receiveTime"             column="receive_time"    />
+        <result property="createTime"              column="create_time"    />
+        <result property="updateTime"              column="update_time"    />
+    </resultMap>
+
+    <!-- 插入完课积分记录 -->
+    <insert id="insertRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO live_completion_points_record (
+            live_id,
+            user_id,
+            watch_duration,
+            video_duration,
+            completion_rate,
+            continuous_days,
+            points_awarded,
+            last_completion_date,
+            current_completion_date,
+            receive_status,
+            receive_time,
+            create_time,
+            update_time
+        ) VALUES (
+            #{liveId},
+            #{userId},
+            #{watchDuration},
+            #{videoDuration},
+            #{completionRate},
+            #{continuousDays},
+            #{pointsAwarded},
+            #{lastCompletionDate},
+            #{currentCompletionDate},
+            #{receiveStatus},
+            #{receiveTime},
+            NOW(),
+            NOW()
+        )
+    </insert>
+
+    <!-- 更新完课积分记录 -->
+    <update id="updateRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord">
+        UPDATE live_completion_points_record
+        <set>
+            <if test="watchDuration != null">watch_duration = #{watchDuration},</if>
+            <if test="videoDuration != null">video_duration = #{videoDuration},</if>
+            <if test="completionRate != null">completion_rate = #{completionRate},</if>
+            <if test="continuousDays != null">continuous_days = #{continuousDays},</if>
+            <if test="pointsAwarded != null">points_awarded = #{pointsAwarded},</if>
+            <if test="lastCompletionDate != null">last_completion_date = #{lastCompletionDate},</if>
+            <if test="currentCompletionDate != null">current_completion_date = #{currentCompletionDate},</if>
+            <if test="receiveStatus != null">receive_status = #{receiveStatus},</if>
+            <if test="receiveTime != null">receive_time = #{receiveTime},</if>
+            update_time = NOW()
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <!-- 查询用户某天的完课记录 -->
+    <select id="selectByUserAndDate" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND current_completion_date = #{currentDate}
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户最近一次完课记录 -->
+    <select id="selectLatestByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户未领取的完课记录列表 -->
+    <select id="selectUnreceivedByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND receive_status = 0
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 查询用户的完课积分领取记录列表 -->
+    <select id="selectRecordsByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 根据ID查询 -->
+    <select id="selectById" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE id = #{id}
+    </select>
+
+</mapper>

+ 10 - 7
fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml

@@ -1133,15 +1133,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 left join fs_store_product_category_scrm fspcs on fspcs.cate_id = fsps.cate_id
                 left join company t3 on t1.company_id = t3.company_id
                 left join company_user  t4 on t4.user_id = t1.company_user_id
-                LEFT JOIN (
-                SELECT
-                    t5.*,
-                    ROW_NUMBER() OVER (PARTITION BY t5.business_code ORDER BY t5.status desc,t5.create_time DESC) as rn
-                FROM live_order_payment t5
-                WHERE t5.business_id IS NOT NULL
-            ) lop ON lop.business_id = t1.order_id AND lop.rn = 1
+        LEFT JOIN (
+        SELECT
+        sp.*,
+        ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn
+        FROM live_order_payment sp
+        WHERE sp.business_code IS NOT NULL
+        ) lop ON lop.business_code = t1.order_code AND lop.rn = 1
         <where>
             t1.is_del = 0 and fsps.product_id IS NOT NULL
+            <if test="bankTransactionId != null and  bankTransactionId !=''">
+                and lop.bank_transaction_id like CONCAT('%',#{bankTransactionId},'%')
+            </if>
             <if test="orderId != null">
                 AND t1.order_id = #{orderId}
             </if>

+ 124 - 0
fs-service/src/main/resources/mapper/live/LiveTagConfigMapper.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveTagConfigMapper">
+    
+    <resultMap type="LiveTagConfig" id="LiveTagConfigResult">
+        <result property="id"    column="id"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="markType"    column="mark_type"    />
+        <result property="qwTagId"    column="qw_tag_id"    />
+        <result property="qwTagName"    column="qw_tag_name"    />
+        <result property="qwTagRealId"    column="qw_tag_real_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="createUserId"    column="create_user_id"    />
+        <result property="createUserName"    column="create_user_name"    />
+        <result property="updateUserId"    column="update_user_id"    />
+        <result property="updateUserName"    column="update_user_name"    />
+    </resultMap>
+
+    <sql id="selectLiveTagConfigVo">
+        select id, live_id, corp_id, company_id, mark_type, qw_tag_id, qw_tag_name, qw_tag_real_id, create_time, update_time, create_user_id, create_user_name, update_user_id, update_user_name from live_tag_config
+    </sql>
+
+    <select id="selectLiveTagConfigList" parameterType="LiveTagConfig" resultMap="LiveTagConfigResult">
+        <include refid="selectLiveTagConfigVo"/>
+        <where>  
+            <if test="liveId != null "> and live_id = #{liveId}</if>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="markType != null "> and mark_type = #{markType}</if>
+            <if test="qwTagId != null "> and qw_tag_id = #{qwTagId}</if>
+            <if test="qwTagName != null "> and qw_tag_name = #{qwTagName}</if>
+            <if test="qwTagRealId != null "> and qw_tag_real_id = #{qwTagRealId}</if>
+            <if test="createUserId != null "> and create_user_id = #{createUserId}</if>
+            <if test="createUserName != null  and createUserName != ''"> and create_user_name like concat('%', #{createUserName}, '%')</if>
+            <if test="updateUserId != null "> and update_user_id = #{updateUserId}</if>
+            <if test="updateUserName != null  and updateUserName != ''"> and update_user_name like concat('%', #{updateUserName}, '%')</if>
+        </where>
+    </select>
+    
+    <select id="selectLiveTagConfigById" parameterType="Long" resultMap="LiveTagConfigResult">
+        <include refid="selectLiveTagConfigVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertLiveTagConfig" parameterType="LiveTagConfig">
+        insert into live_tag_config
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="liveId != null">live_id,</if>
+            <if test="corpId != null">corp_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="markType != null">mark_type,</if>
+            <if test="qwTagId != null">qw_tag_id,</if>
+            <if test="qwTagName != null">qw_tag_name,</if>
+            <if test="qwTagRealId != null">qw_tag_real_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="createUserId != null">create_user_id,</if>
+            <if test="createUserName != null">create_user_name,</if>
+            <if test="updateUserId != null">update_user_id,</if>
+            <if test="updateUserName != null">update_user_name,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="liveId != null">#{liveId},</if>
+            <if test="corpId != null">#{corpId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="markType != null">#{markType},</if>
+            <if test="qwTagId != null">#{qwTagId},</if>
+            <if test="qwTagName != null">#{qwTagName},</if>
+            <if test="qwTagRealId != null">#{qwTagRealId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="createUserId != null">#{createUserId},</if>
+            <if test="createUserName != null">#{createUserName},</if>
+            <if test="updateUserId != null">#{updateUserId},</if>
+            <if test="updateUserName != null">#{updateUserName},</if>
+         </trim>
+    </insert>
+
+    <update id="updateLiveTagConfig" parameterType="LiveTagConfig">
+        update live_tag_config
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="liveId != null">live_id = #{liveId},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="markType != null">mark_type = #{markType},</if>
+            <if test="qwTagId != null">qw_tag_id = #{qwTagId},</if>
+            <if test="qwTagName != null">qw_tag_name = #{qwTagName},</if>
+            <if test="qwTagRealId != null">qw_tag_real_id = #{qwTagRealId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="createUserId != null">create_user_id = #{createUserId},</if>
+            <if test="createUserName != null">create_user_name = #{createUserName},</if>
+            <if test="updateUserId != null">update_user_id = #{updateUserId},</if>
+            <if test="updateUserName != null">update_user_name = #{updateUserName},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLiveTagConfigById" parameterType="Long">
+        delete from live_tag_config where id = #{id}
+    </delete>
+
+    <delete id="deleteLiveTagConfigByIds" parameterType="String">
+        delete from live_tag_config where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteByLiveId" parameterType="Long">
+        delete from live_tag_config where live_id = #{liveId}
+    </delete>
+
+    <select id="getLiveTagListByliveId" parameterType="Long" resultType="com.fs.live.vo.LiveTagItemVO">
+        select * from live_tag_config where live_id = #{liveId}
+    </select>
+</mapper>

+ 213 - 0
fs-service/src/main/resources/mapper/live/LiveWatchLogMapper.xml

@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveWatchLogMapper">
+
+
+    <resultMap type="LiveWatchLog" id="LiveWatchLogResult">
+        <result property="logId"    column="log_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="logType"    column="log_type"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="externalContactId"    column="external_contact_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="finishTime"    column="finish_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="sopCreateTime"    column="sop_create_time"    />
+        <result property="sendAppId"    column="send_app_id"    />
+        <result property="logSource"    column="log_source"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="watchType"    column="watch_type"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="liveBuy"    column="live_buy"    />
+        <result property="replayBuy"    column="replay_buy"    />
+    </resultMap>
+
+    <sql id="selectLiveWatchLogVo">
+        select log_id, user_id, live_id, log_type, create_time, update_time, external_contact_id, company_user_id, company_id, finish_time, create_by, sop_create_time,live_buy,replay_buy,
+               send_app_id, log_source, qw_user_id,watch_type,corp_id from live_watch_log
+    </sql>
+
+    <select id="selectLiveWatchLogList" parameterType="LiveWatchLog" resultMap="LiveWatchLogResult">
+        <include refid="selectLiveWatchLogVo"/>
+        <where>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="liveId != null "> and live_id = #{liveId}</if>
+            <if test="logType != null "> and log_type = #{logType}</if>
+            <if test="externalContactId != null "> and external_contact_id = #{externalContactId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="finishTime != null "> and finish_time = #{finishTime}</if>
+            <if test="sopCreateTime != null "> and sop_create_time = #{sopCreateTime}</if>
+            <if test="sendAppId != null  and sendAppId != ''"> and send_app_id = #{sendAppId}</if>
+            <if test="logSource != null "> and log_source = #{logSource}</if>
+            <if test="qwUserId != null  and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
+            <if test="watchType != null">and watch_type = #{watchType} </if>
+            <if test="corpId != null">and corp_id = #{corpId} </if>
+            <if test="liveBuy != null">and live_buy = #{liveBuy} </if>
+            <if test="replayBuy != null">and replay_buy = #{replayBuy} </if>
+        </where>
+    </select>
+
+    <select id="selectLiveWatchLogByLogId" parameterType="Long" resultMap="LiveWatchLogResult">
+        <include refid="selectLiveWatchLogVo"/>
+        where log_id = #{logId}
+    </select>
+
+    <insert id="insertLiveWatchLog" parameterType="LiveWatchLog" useGeneratedKeys="true" keyProperty="logId">
+        insert into live_watch_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="liveId != null">live_id,</if>
+            <if test="logType != null">log_type,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="externalContactId != null">external_contact_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="finishTime != null">finish_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="sopCreateTime != null">sop_create_time,</if>
+            <if test="sendAppId != null">send_app_id,</if>
+            <if test="logSource != null">log_source,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="watchType != null">watch_type,</if>
+            <if test="corpId != null">corp_id,</if>
+            <if test="liveBuy != null">live_buy,</if>
+            <if test="replayBuy != null">replay_buy,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="liveId != null">#{liveId},</if>
+            <if test="logType != null">#{logType},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="externalContactId != null">#{externalContactId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="finishTime != null">#{finishTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="sopCreateTime != null">#{sopCreateTime},</if>
+            <if test="sendAppId != null">#{sendAppId},</if>
+            <if test="logSource != null">#{logSource},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="watchType != null">#{watchType},</if>
+            <if test="corpId != null">#{corpId},</if>
+            <if test="liveBuy != null">#{liveBuy},</if>
+            <if test="replayBuy != null">#{replayBuy},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLiveWatchLog" parameterType="LiveWatchLog">
+        update live_watch_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="liveId != null">live_id = #{liveId},</if>
+            <if test="logType != null">log_type = #{logType},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="externalContactId != null">external_contact_id = #{externalContactId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="finishTime != null">finish_time = #{finishTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="sopCreateTime != null">sop_create_time = #{sopCreateTime},</if>
+            <if test="sendAppId != null">send_app_id = #{sendAppId},</if>
+            <if test="logSource != null">log_source = #{logSource},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="watchType != null">watch_type = #{watchType},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+            <if test="liveBuy != null">live_buy = #{liveBuy},</if>
+            <if test="replayBuy != null">replay_buy = #{replayBuy},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+
+    <delete id="deleteLiveWatchLogByLogId" parameterType="Long">
+        delete from live_watch_log where log_id = #{logId}
+    </delete>
+
+    <delete id="deleteLiveWatchLogByLogIds" parameterType="String">
+        delete from live_watch_log where log_id in
+        <foreach item="logId" collection="array" open="(" separator="," close=")">
+            #{logId}
+        </foreach>
+    </delete>
+
+    <insert id="insertLiveWatchLogBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="logId">
+        INSERT INTO live_watch_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="liveWatchLogs != null and liveWatchLogs.size() &gt; 0">
+                <foreach collection="liveWatchLogs" item="item" index="index" open="" close="" separator="">
+                    <if test="index == 0">
+                        <if test="item.userId != null">user_id,</if>
+                        <if test="item.liveId != null">live_id,</if>
+                        <if test="item.logType != null">log_type,</if>
+                        <if test="item.createTime != null">create_time,</if>
+                        <if test="item.updateTime != null">update_time,</if>
+                        <if test="item.externalContactId != null">external_contact_id,</if>
+                        <if test="item.companyUserId != null">company_user_id,</if>
+                        <if test="item.companyId != null">company_id,</if>
+                        <if test="item.finishTime != null">finish_time,</if>
+                        <if test="item.createBy != null">create_by,</if>
+                        <if test="item.sopCreateTime != null">sop_create_time,</if>
+                        <if test="item.sendAppId != null">send_app_id,</if>
+                        <if test="item.logSource != null">log_source,</if>
+                        <if test="item.qwUserId != null">qw_user_id,</if>
+                        <if test="item.watchType != null">watch_type,</if>
+                        <if test="item.corpId != null">corp_id,</if>
+                        <if test="item.liveBuy != null">live_buy,</if>
+                        <if test="item.replayBuy != null">replay_buy,</if>
+                    </if>
+                </foreach>
+            </if>
+        </trim>
+        <trim prefix="VALUES">
+            <foreach collection="liveWatchLogs" item="item" separator=",">
+                (<trim suffixOverrides=",">
+                <if test="item.userId != null">#{item.userId},</if>
+                <if test="item.liveId != null">#{item.liveId},</if>
+                <if test="item.logType != null">#{item.logType},</if>
+                <if test="item.createTime != null">#{item.createTime},</if>
+                <if test="item.updateTime != null">#{item.updateTime},</if>
+                <if test="item.externalContactId != null">#{item.externalContactId},</if>
+                <if test="item.companyUserId != null">#{item.companyUserId},</if>
+                <if test="item.companyId != null">#{item.companyId},</if>
+                <if test="item.finishTime != null">#{item.finishTime},</if>
+                <if test="item.createBy != null">#{item.createBy},</if>
+                <if test="item.sopCreateTime != null">#{item.sopCreateTime},</if>
+                <if test="item.sendAppId != null">#{item.sendAppId},</if>
+                <if test="item.logSource != null">#{item.logSource},</if>
+                <if test="item.qwUserId != null">#{item.qwUserId},</if>
+                <if test="item.watchType != null">#{item.watchType},</if>
+                <if test="item.corpId != null">#{item.corpId},</if>
+                <if test="item.liveBuy != null">#{item.liveBuy},</if>
+                <if test="item.replayBuy != null">#{item.replayBuy},</if>
+            </trim>)
+            </foreach>
+        </trim>
+    </insert>
+
+    <update id="updateLiveWatchLogCondition" parameterType="com.fs.live.domain.LiveWatchLog" >
+        update live_watch_log
+        set update_time = NOW(),
+            sop_create_time = NOW(),
+            send_app_id = #{liveWatchLog.sendAppId},
+            log_source = #{liveWatchLog.logSource}
+        where external_contact_id = #{liveWatchLog.externalContactId}
+            and live_id = #{liveWatchLog.liveId}
+            and qw_user_id = #{liveWatchLog.qwUserId}
+    </update>
+
+    <select id="selectOneLogByLiveIdAndQwUserIdAndExternalId"  resultType="com.fs.live.domain.LiveWatchLog">
+        select * from live_watch_log where live_id = #{liveId} and qw_user_id = #{qwUserId} and external_contact_id = #{externalContactId}
+    </select>
+<!--    todo lmx-->
+    <select id="selectLiveWatchLogByLiveId" resultType="com.fs.live.domain.LiveWatchLog">
+        select * from live_watch_log where live_id = #{liveId}
+    </select>
+</mapper>

+ 194 - 0
fs-service/src/main/resources/mapper/qw/LuckyBagCollectRecordMapper.xml

@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.LuckyBagCollectRecordMapper">
+
+    <resultMap type="LuckyBagCollectRecord" id="LuckyBagCollectRecordResult">
+        <result property="id"    column="id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="userName"    column="user_name"    />
+        <result property="rewardType"    column="reward_type"    />
+        <result property="luckyBagId"    column="lucky_bag_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="coinAmount"    column="coin_amount"    />
+        <result property="sendTime"    column="send_time"    />
+        <result property="collectTime"    column="collect_time"    />
+        <result property="expiryTime"    column="expiry_time"    />
+        <result property="collectType"    column="collect_type"    />
+        <result property="externalUserName"    column="external_user_name"    />
+        <result property="chatId"    column="chat_id"    />
+        <result property="relationId"    column="relation_id"    />
+        <result property="sendLink"    column="send_link"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="qwUserName"    column="qw_user_name"    />
+    </resultMap>
+
+    <sql id="selectLuckyBagCollectRecordVo">
+        select id, user_id, user_name, reward_type, lucky_bag_id, company_user_id, company_id, company_name, coin_amount, send_time, collect_time, expiry_time, collect_type, external_user_name, chat_id, relation_id, send_link, update_time, create_time,qw_user_id, qw_user_name from lucky_bag_collect_record
+    </sql>
+
+    <select id="selectLuckyBagCollectRecordList" parameterType="LuckyBagCollectRecord" resultMap="LuckyBagCollectRecordResult">
+        select
+        r.id,r.user_id,r.user_name,r.reward_type,r.lucky_bag_id,r.company_user_id,r.company_id,r.company_name,r.coin_amount,r.send_time,r.collect_time,r.expiry_time,r.collect_type,r.external_user_name,r.chat_id,r.relation_id,r.send_link,r.update_time,r.create_time,r.qw_user_id,r.qw_user_name,
+        cu.user_name as companyUserName,
+        b.name as luckyBagName
+        from lucky_bag_collect_record r
+        left join lucky_bag b
+        on r.lucky_bag_id = b.id
+        left join company_user cu
+        on r.company_user_id = cu.user_id
+        <where>
+            <if test="userId != null "> and r.user_id = #{userId}</if>
+            <if test="userName != null  and userName != ''"> and r.user_name like concat('%', #{userName}, '%')</if>
+            <if test="rewardType != null "> and r.reward_type = #{rewardType}</if>
+            <if test="luckyBagId != null "> and r.lucky_bag_id = #{luckyBagId}</if>
+            <if test="companyUserId != null "> and r.company_user_id = #{companyUserId}</if>
+            <if test="companyId != null "> and r.company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and r.company_name like concat('%', #{companyName}, '%')</if>
+            <if test="coinAmount != null "> and r.coin_amount = #{coinAmount}</if>
+             <if test="params.beginSendTime != null and params.endSendTime != null">and r.send_time between concat(#{params.beginSendTime}, ' 00:00:00') and concat(#{params.endSendTime}, ' 23:59:59')</if>
+            <if test="params.beginCollectTime != null and params.endCollectTime != null">and r.collect_time between concat(#{params.beginCollectTime}, ' 00:00:00') and concat(#{params.endCollectTime}, ' 23:59:59')</if>
+            <if test="params.beginExpiryTime != null and params.endExpiryTime != null">and r.expiry_time between concat(#{params.beginExpiryTime}, ' 00:00:00') and concat(#{params.endExpiryTime}, ' 23:59:59')</if>
+            <if test="collectType != null  and collectType != ''"> and r.collect_type = #{collectType}</if>
+            <if test="externalUserName != null  and externalUserName != ''"> and r.external_user_name like concat('%', #{externalUserName}, '%')</if>
+            <if test="chatId != null  and chatId != ''"> and r.chat_id = #{chatId}</if>
+            <if test="relationId != null "> and r.relation_id = #{relationId}</if>
+            <if test="sendLink != null  and sendLink != ''"> and r.send_link = #{sendLink}</if>
+            <if test="qwUserName != null  and qwUserName != ''"> and r.qw_user_name like concat('%', #{qwUserName}, '%')</if>
+        </where>
+        order by r.send_time desc,r.lucky_bag_id desc
+    </select>
+
+    <select id="selectLuckyBagCollectRecordById" parameterType="Long" resultMap="LuckyBagCollectRecordResult">
+        <include refid="selectLuckyBagCollectRecordVo"/>
+        where id = #{id}
+    </select>
+    <select id="selectLuckyBagCollectRecordByRelationId"
+            resultType="com.fs.course.domain.LuckyBagCollectRecord">
+        <include refid="selectLuckyBagCollectRecordVo"/>
+        where relation_id = #{relationId} and user_id = #{userId} limit 1
+    </select>
+
+
+    <insert id="insertBagCollectRecord" parameterType="FsAppContactWay" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO lucky_bag_collect_record
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="luckyBagId != null">lucky_bag_id,</if>
+            <if test="sendTime != null">send_time,</if>
+            <if test="collectTime != null">collect_time,</if>
+            <if test="expiryTime != null">expiry_time,</if>
+            <if test="collectType != null and collectType != ''">collect_type,</if>
+            <if test="externalUserName != null and externalUserName != ''">external_user_name,</if>
+            <if test="chatId != null and chatId != ''">chat_id,</if>
+            <if test="sendLink != null and sendLink != ''">send_link,</if>
+        </trim>
+        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
+            <if test="luckyBagId != null">#{luckyBagId},</if>
+            <if test="sendTime != null">#{sendTime},</if>
+            <if test="collectTime != null">#{collectTime},</if>
+            <if test="expiryTime != null">#{expiryTime},</if>
+            <if test="collectType != null and collectType != ''">#{collectType},</if>
+            <if test="externalUserName != null and externalUserName != ''">#{externalUserName},</if>
+            <if test="chatId != null and chatId != ''">#{chatId},</if>
+            <if test="sendLink != null and sendLink != ''">#{sendLink},</if>
+        </trim>
+    </insert>
+
+
+    <insert id="insertLuckyBagCollectRecord" parameterType="LuckyBagCollectRecord" useGeneratedKeys="true" keyProperty="id">
+        insert into lucky_bag_collect_record
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="userName != null">user_name,</if>
+            <if test="rewardType != null">reward_type,</if>
+            <if test="luckyBagId != null">lucky_bag_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="coinAmount != null">coin_amount,</if>
+            <if test="sendTime != null">send_time,</if>
+            <if test="collectTime != null">collect_time,</if>
+            <if test="expiryTime != null">expiry_time,</if>
+            <if test="collectType != null">collect_type,</if>
+            <if test="externalUserName != null">external_user_name,</if>
+            <if test="chatId != null">chat_id,</if>
+            <if test="relationId != null">relation_id,</if>
+            <if test="sendLink != null">send_link,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="qwUserName != null">qw_user_name,</if>
+
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="userName != null">#{userName},</if>
+            <if test="rewardType != null">#{rewardType},</if>
+            <if test="luckyBagId != null">#{luckyBagId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="coinAmount != null">#{coinAmount},</if>
+            <if test="sendTime != null">#{sendTime},</if>
+            <if test="collectTime != null">#{collectTime},</if>
+            <if test="expiryTime != null">#{expiryTime},</if>
+            <if test="collectType != null">#{collectType},</if>
+            <if test="externalUserName != null">#{externalUserName},</if>
+            <if test="chatId != null">#{chatId},</if>
+            <if test="relationId != null">#{relationId},</if>
+            <if test="sendLink != null">#{sendLink},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="qwUserName != null">#{qwUserName},</if>
+         </trim>
+    </insert>
+
+    <update id="updateLuckyBagCollectRecord" parameterType="LuckyBagCollectRecord">
+        update lucky_bag_collect_record
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="userName != null">user_name = #{userName},</if>
+            <if test="rewardType != null">reward_type = #{rewardType},</if>
+            <if test="luckyBagId != null">lucky_bag_id = #{luckyBagId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="coinAmount != null">coin_amount = #{coinAmount},</if>
+            <if test="sendTime != null">send_time = #{sendTime},</if>
+            <if test="collectTime != null">collect_time = #{collectTime},</if>
+            <if test="expiryTime != null">expiry_time = #{expiryTime},</if>
+            <if test="collectType != null">collect_type = #{collectType},</if>
+            <if test="externalUserName != null">external_user_name = #{externalUserName},</if>
+            <if test="chatId != null">chat_id = #{chatId},</if>
+            <if test="relationId != null">relation_id = #{relationId},</if>
+            <if test="sendLink != null">send_link = #{sendLink},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="qwUserName != null">qw_user_name = #{qwUserName},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLuckyBagCollectRecordById" parameterType="Long">
+        delete from lucky_bag_collect_record where id = #{id}
+    </delete>
+
+    <delete id="deleteLuckyBagCollectRecordByIds" parameterType="String">
+        delete from lucky_bag_collect_record where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <update id="updateLuckyBagExpiryStatus">
+        update lucky_bag_collect_record set collect_type = 2 where expiry_time &lt;= NOW()  and collect_type != 1
+    </update>
+
+</mapper>

+ 84 - 0
fs-service/src/main/resources/mapper/qw/LuckyBagMapper.xml

@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.LuckyBagMapper">
+
+    <insert id="insertLuckyBag" parameterType="LuckyBag" useGeneratedKeys="true" keyProperty="id">
+        insert into lucky_bag
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="name != null and name != ''">name,</if>
+            <if test="type != null">type,</if>
+            <if test="status != null">status,</if>
+            <if test="createId != null">create_id,</if>
+            <if test="updateId != null">update_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="actualRewards != null and actualRewards != ''">actual_rewards,</if>
+            <if test="createName != null and createName != ''">create_name,</if>
+            <if test="companyId != null and companyId != ''">company_id,</if>
+            <if test="amount != null and amount != ''">amount,</if>
+            <if test="dataStatus != null and dataStatus != ''">data_status,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="name != null and name != ''">#{name},</if>
+            <if test="type != null">#{type},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createId != null">#{createId},</if>
+            <if test="updateId != null">#{updateId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="actualRewards != null and actualRewards != ''">#{actualRewards},</if>
+            <if test="createName != null and createName != ''">#{createName},</if>
+            <if test="companyId != null and companyId != ''">#{companyId},</if>
+            <if test="amount != null and amount != ''">#{amount},</if>
+            <if test="dataStatus != null and dataStatus != ''">#{dataStatus},</if>
+        </trim>
+    </insert>
+    <update id="updateLuckyBag" parameterType="LuckyBag">
+        UPDATE lucky_bag
+        <set>
+            <if test="name != null and name != ''">name = #{name},</if>
+            <if test="type != null">type = #{type},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createId != null">create_id = #{createId},</if>
+            <if test="updateId != null">update_id = #{updateId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="actualRewards != null and actualRewards != ''">actual_rewards = #{actualRewards},</if>
+            <if test="createName != null and createName != ''">create_name = #{createName},</if>
+            <if test="companyId != null and companyId != ''">company_id = #{companyId},</if>
+            <if test="amount != null">amount = #{amount},</if>
+            <if test="dataStatus != null and dataStatus != ''">data_status=#{dataStatus},</if>
+        </set>
+        WHERE id = #{id}
+    </update>
+    <update id="deleteLuckyBagByIds">
+        UPDATE lucky_bag SET status = '0' WHERE id IN
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+    <sql id="selectLuckyBagVo">
+        select id, name, type,status, create_id,update_id, create_time, update_time, actual_rewards,company_id,amount,data_status from lucky_bag
+    </sql>
+    <select id="selectLuckyBagList" resultType="com.fs.course.domain.LuckyBag">
+        <include refid="selectLuckyBagVo"/>
+        <where>
+            and status='1'
+            <if test="companyId != null and companyId != ''">and company_id = #{companyId}</if>
+            <if test="id != null "> and id = #{id}</if>
+            <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="createId != null "> and create_id = #{createId}</if>
+            <if test="dataStatus != null and dataStatus != ''">and data_status=#{dataStatus}</if>
+        </where>
+        order by id desc
+    </select>
+    <select id="selectLuckyBagById" resultType="com.fs.course.domain.LuckyBag">
+        <include refid="selectLuckyBagVo"/>
+        where id = #{id}
+    </select>
+
+
+
+</mapper>

Некоторые файлы не были показаны из-за большого количества измененных файлов