Ver código fonte

Merge remote-tracking branch 'origin/master'

吴树波 22 horas atrás
pai
commit
289b33f406
100 arquivos alterados com 1797 adições e 405 exclusões
  1. 1 1
      fs-ad-new-api/Dockerfile
  2. 3 4
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  3. 14 10
      fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java
  4. 26 2
      fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java
  5. 6 4
      fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java
  6. 7 3
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  7. 54 41
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  8. 3 1
      fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java
  9. 2 14
      fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java
  10. 54 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/RocketMQTraceIdAspect.java
  11. 1 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  12. 2 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  13. 3 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  14. 1 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java
  15. 2 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java
  16. 1 0
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  17. 38 0
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  18. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  19. 2 0
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  20. 2 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  21. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  22. 89 5
      fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java
  23. 13 1
      fs-company/src/main/java/com/fs/company/controller/course/FsUserCoursePeriodController.java
  24. 6 5
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  25. 28 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java
  26. 4 1
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  27. 6 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  28. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  29. 3 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  30. 2 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java
  31. 25 4
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  32. 5 0
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  33. 83 11
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  34. 154 52
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  35. 1 1
      fs-qw-api/Dockerfile
  36. 2 19
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  37. 89 7
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  38. 1 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  39. 2 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  40. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java
  41. 1 1
      fs-service/src/main/java/com/fs/course/service/IFsUserCoursePeriodService.java
  42. 30 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java
  43. 41 25
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  44. 2 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCoursePeriodVO.java
  45. 54 0
      fs-service/src/main/java/com/fs/enums/ExceptionCodeEnum.java
  46. 161 5
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  47. 26 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  48. 2 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  49. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  50. 5 2
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  51. 39 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  52. 6 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreAfterSalesScrm.java
  53. 71 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  54. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponUserScrmMapper.java
  55. 7 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  56. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  57. 9 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java
  58. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsMyStoreOrderQueryParam.java
  59. 1 1
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  60. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductPackageQueryParam.java
  61. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreAfterSalesScrmService.java
  62. 3 3
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  63. 41 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  64. 22 8
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  65. 8 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  66. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java
  67. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java
  68. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java
  69. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderPromotionExportVO.java
  70. 2 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderStatisticsVO.java
  71. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderVO.java
  72. 1 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductExportVO.java
  73. 12 0
      fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayQueryRequest.java
  74. 13 1
      fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java
  75. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  76. 17 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java
  77. 2 0
      fs-service/src/main/java/com/fs/live/mapper/LiveAfterSalesMapper.java
  78. 6 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  79. 1 1
      fs-service/src/main/java/com/fs/live/param/LiveOrderSearchParam.java
  80. 2 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  81. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  82. 10 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  83. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveDataService.java
  84. 197 113
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  85. 10 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  86. 30 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  87. 17 12
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  88. 36 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
  89. 39 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  90. 39 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  91. 8 0
      fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  92. 19 0
      fs-service/src/main/java/com/fs/live/vo/LiveDataListVo.java
  93. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveOrderListVo.java
  94. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveOrderVoZm.java
  95. 2 1
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  96. 1 1
      fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java
  97. 1 0
      fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java
  98. 16 7
      fs-service/src/main/java/com/fs/newAdv/domain/Lead.java
  99. 12 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/QwExternalIdBindTrackReq.java
  100. 10 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/TraceIdDto.java

+ 1 - 1
fs-ad-new-api/Dockerfile

@@ -1,4 +1,4 @@
-FROM openjdk:8-jre
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
 # java版本,最好使用openjdk,而不是类似于Java:1.8
 COPY ./target/fs-ad-new-api.jar fs-ad-new-api.jar
 # 向外暴露的接口,最好与项目yml文件中的端口一致

+ 3 - 4
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -31,18 +31,17 @@ public class LandingPageController {
      * 落地页访问
      */
     @PostMapping("/h5/home")
-    public Result<LandingIndexRes> track(
+    public Result<LandingIndexRes> h5Home(
             @RequestBody LandingIndexReq req) {
-        log.info("落地页访问追踪:req={}", req);
         // 查询落地页模板
-        return Result.success(facadeService.getLandingIndexBySiteId(req.getAllParams()));
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(),req.getAllParams()));
     }
 
     /**
      * 小程序访问
      */
     @PostMapping("/mini/home")
-    public Result<LandingIndexRes> track(@RequestBody WeChatLandingIndexReq req) {
+    public Result<LandingIndexRes> miniHome(@RequestBody WeChatLandingIndexReq req) {
         return Result.success(facadeService.getWxLandingIndexBySiteId(req));
     }
 

+ 14 - 10
fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java

@@ -1,15 +1,13 @@
 package com.fs.app.controller;
 
+import com.fs.newAdv.dto.req.TraceIdDto;
 import com.fs.newAdv.integration.client.advertiser.BaiduApiClient;
 import com.fs.newAdv.enums.SystemEventTypeEnum;
 import com.fs.newAdv.event.ConversionEventPublisher;
 import com.fs.newAdv.service.IPromotionAccountService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * 广告商监测链接
@@ -28,15 +26,21 @@ public class TestController {
     @Autowired
     private IPromotionAccountService promotionAccountService;
 
-    @GetMapping("/test1/{traceId}")
-    public void test1(@PathVariable("traceId") String traceId) {
+    @PostMapping("/test1")
+    public void test1(@RequestBody TraceIdDto traceId) {
         log.info("模拟 当日加群 事件完成");
-        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.GROUP_TODAY);
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.GROUP_TODAY);
     }
 
-    @GetMapping("/test2/{traceId}")
-    public void test2(@PathVariable("traceId") String traceId) {
+    @PostMapping("/test2")
+    public void test2(@RequestBody TraceIdDto traceId) {
         log.info("模拟 当日加微 事件完成");
-        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.WEI_CHAT_TODAY);
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.WEI_CHAT_TODAY);
+    }
+
+    @PostMapping("/test3")
+    public void test3(@RequestBody TraceIdDto traceId) {
+        log.info("模拟 微信授权 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.AUTH_TODAY_CREATE);
     }
 }

+ 26 - 2
fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java

@@ -1,6 +1,10 @@
 package com.fs.app.controller;
 
 import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.app.facade.IConversionService;
+import com.fs.common.result.Result;
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -22,6 +26,8 @@ public class TrackingController {
     private CallbackProcessingFacadeService facadeService;
 
 
+    @Autowired
+    private IConversionService conversionService;
     /**
      * 监测链接端口
      *
@@ -29,14 +35,14 @@ public class TrackingController {
      * @param response  HTTP响应
      */
     @GetMapping("/click/{advertiserCode}")
-    public void trackBaidu(
+    public void track(
             @RequestParam Map<String, String> allParams,
             @PathVariable("advertiserCode") Long advertiserCode,
             HttpServletResponse response) {
         log.info("接收监测请求 | params={}", allParams);
         try {
             // 2. 保存点击追踪记录
-            facadeService.saveClickTrace(advertiserCode,allParams);
+            facadeService.saveClickTrace(advertiserCode, allParams);
             // 3. 返回 200 OK
             response.setStatus(HttpServletResponse.SC_OK);
         } catch (Exception e) {
@@ -45,4 +51,22 @@ public class TrackingController {
         }
     }
 
+    /**
+     * 绑定企微用户与线索
+     */
+    @PostMapping("/bind/qwTrack")
+    public Result<String> qwExternalIdBindTrack(@RequestBody QwExternalIdBindTrackReq req) {
+        facadeService.qwExternalIdBindTrack(req);
+        return Result.success();
+    }
+
+    /**
+     * 绑定企微用户与线索
+     */
+    @PostMapping("/bind/qwTrack/test/{id1}/{id2}")
+    public Result<String> qwExternalIdBindTrack1(@PathVariable String id1, @PathVariable String id2) {
+        boolean success = conversionService.reportTrackingToAdvertiser(SystemEventTypeEnum.getByCode(id1), id2);
+        return Result.success();
+    }
+
 }

+ 6 - 4
fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java

@@ -50,7 +50,8 @@ public class WeChatController {
             try {
                 String access_token = advMiniConfig.getAccessToken();
                 // 判断token是否过期
-                if (advMiniConfig.getExpiresIn().isBefore(LocalDateTime.now().plusMinutes(10))) {
+                if (LocalDateTime.now().plusMinutes(10).isAfter(advMiniConfig.getExpiresIn())) {
+                    // 提前10分钟刷新Token
                     HttpResponse execute2 = HttpRequest.get("https://api.weixin.qq.com/cgi-bin/token")
                             .form("grant_type", "client_credential")
                             .form("appid", advMiniConfig.getAppId())
@@ -59,15 +60,16 @@ public class WeChatController {
                             .execute();
                     JSONObject obj = JSONObject.parseObject(execute2.body());
                     access_token = obj.getString("access_token");
+                    log.info("getSchemeUrl:{}", obj);
                     advMiniConfig.setAccessToken(access_token);
-                    advMiniConfig.setExpiresIn(LocalDateTime.now().plusSeconds(obj.getLong("expires_in")));
+                    advMiniConfig.setExpiresIn(LocalDateTime.now().plusSeconds(obj.getInteger("expires_in")));
                     advMiniConfigService.updateById(advMiniConfig);
                 }
                 Map<String, Object> map = new HashMap<>();
                 Map<String, Object> map2 = new HashMap<>();
-                map2.put("path", "/pages/shopping/productDetails");
+                map2.put("path", "pages/home/productList");
                 map2.put("query", "traceId=" + traceId);
-                map2.put("env_version", "trial");
+                    map2.put("env_version", "trial");
                 map.put("jump_wxa", map2);
                 map.put("is_expire", false);
                 HttpResponse execute = HttpRequest.post("https://api.weixin.qq.com/wxa/generatescheme?access_token=" + access_token)

+ 7 - 3
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -1,5 +1,6 @@
 package com.fs.app.facade;
 
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 
@@ -10,20 +11,23 @@ public interface CallbackProcessingFacadeService {
 
     /**
      * 链化追踪点击
+     *
      * @param allParams
      */
-    void saveClickTrace(Long advertiserCode,Map<String,String> allParams);
+    void saveClickTrace(Long advertiserCode, Map<String, String> allParams);
+
     /**
      * 根据站点ID获取落地页信息
      *
-     * @param siteId
      * @return
      */
-    LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams);
+    LandingIndexRes getLandingIndexBySiteId(String viewUrl,Map<String, String> allParams);
 
     //----------------------code回调---------------------------------
     void gdtGetAuthCode(Integer code, Long state);
 
 
     LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req);
+
+    void qwExternalIdBindTrack(QwExternalIdBindTrackReq req);
 }

+ 54 - 41
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -1,22 +1,24 @@
 package com.fs.app.facade;
 
+import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
-import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.common.exception.base.BusinessException;
 import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.SnowflakeUtil;
 import com.fs.newAdv.domain.LandingPageTemplate;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.newAdv.service.ILandingPageTemplateService;
 import com.fs.newAdv.service.ILeadService;
 import com.fs.newAdv.service.ISiteService;
@@ -72,50 +74,53 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     public void saveClickTrace(Long advertiserCode, Map<String, String> allParams) {
         IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserCode));
         Lead lead = advertiserAdapter.adaptCallbackData(allParams);
-        Lead byTraceId = leadService.getByTraceId(lead.getTraceId());
-        if (ObjectUtil.isNotEmpty(byTraceId)) {
-            throw new BusinessException("监测信息已存在: " + lead.getTraceId());
+        Lead byClickId = leadService.getByClickId(lead.getTraceId());
+        if (ObjectUtil.isNotEmpty(byClickId)) {
+            log.info("线索已存在:{}", lead.getTraceId());
+            return;
         }
         lead.setStatus(0);
         lead.setClickTrigger(1);
         lead.setTraceRawParams(JSONUtil.toJsonStr(allParams));
+        lead.setTraceId(SnowflakeUtil.randomUUID());
         boolean saved = leadService.save(lead);
         if (!saved) {
             log.error("线索保存失败:{}", lead);
-            throw new RuntimeException("线索保存失败");
         }
     }
 
     private String getTraceIdByAdvertiser(AdvertiserTypeEnum byCode, Map<String, String> allParams) {
-        String traceId;
+        String clickId;
         switch (byCode) {
             case OCEANENGINE:
             case TENCENT:
+                clickId = allParams.get("clickid");
+                break;
             case OPPO:
-                traceId = allParams.get("click_id");
+                clickId = allParams.get("tid");
                 break;
             case BAIDU:
-                traceId = allParams.get("bd_vid");
+                clickId = allParams.get("bd_vid");
                 break;
             case VIVO:
-                traceId = allParams.get("requestId");
+                clickId = allParams.get("requestId");
                 break;
             case IQIYI:
-                traceId = allParams.get("traceId");
+                clickId = allParams.get("traceId");
                 break;
             default:
-                traceId = "ylrz_test";
+                clickId = "ylrz_test";
         }
-        if (StrUtil.isEmpty(traceId)) {
-            traceId = "ylrz_test";
+        if (StrUtil.isEmpty(clickId)) {
+            clickId = "ylrz_test"+ IdUtil.randomUUID();
         }
 
-        return traceId;
+        return clickId;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams) {
+    public LandingIndexRes getLandingIndexBySiteId(String viewUrl,Map<String, String> allParams) {
         // 站点信息
         String paramsSiteId = allParams.get("siteId");
         if (ObjectUtil.isEmpty(paramsSiteId)) {
@@ -131,25 +136,25 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         // 广告商信息
         Long advertiserId = byId.getAdvertiserId();
         // 访问链路id
-        String traceId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
+        String clickId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
         // 线索信息
-        Lead byTraceId = leadService.getByTraceId(traceId);
-        boolean isNewLead = ObjectUtil.isEmpty(byTraceId);
+        Lead lead = leadService.getByClickId(clickId);
+        boolean isNewLead = ObjectUtil.isEmpty(lead);
         if (isNewLead) {
             IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserId));
-            byTraceId = advertiserAdapter.adaptCallbackData(allParams);
-            byTraceId.setAdvertiserId(advertiserId);
-            byTraceId.setSiteId(siteId);
-            byTraceId.setTraceId(traceId);
+            lead = advertiserAdapter.adaptCallbackData(allParams);
+            lead.setAdvertiserId(advertiserId);
+            lead.setSiteId(siteId);
+            lead.setClickId(clickId);
             // 设置站点和落地页的关联
-            setSiteByIdeaId(siteId, byTraceId.getIdeaId());
+            setSiteByIdeaId(siteId, lead.getIdeaId());
         } else {
             // 检查站点和广告商信息是否异常
-            if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
-                log.info("落地页站点信息异常:{}---{}", byTraceId.getSiteId(), siteId);
+            if (!Objects.equals(lead.getSiteId(), siteId)) {
+                log.info("落地页站点信息异常:{}---{}", lead.getSiteId(), siteId);
             }
-            if (!Objects.equals(byTraceId.getAdvertiserId(), advertiserId)) {
-                log.info("落地页广告商信息异常:{}---{}", byTraceId.getAdvertiserId(), advertiserId);
+            if (!Objects.equals(lead.getAdvertiserId(), advertiserId)) {
+                log.info("落地页广告商信息异常:{}---{}", lead.getAdvertiserId(), advertiserId);
             }
         }
         // 模板缓存
@@ -170,26 +175,28 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
         JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
         // 替换二维码链接
-        updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
+        updateQrCodeInTemplate(jsonObject, byId, lead);
         String templateData = JSONUtil.toJsonStr(jsonObject);
 
         // 保存或更新 线索信息
         LocalDateTime now = LocalDateTime.now();
-        byTraceId.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
-        byTraceId.setLandingPageTrigger(1);
-        byTraceId.setLandingPageTs(now);
-        byTraceId.setUpdateTime(now);
+        lead.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
+        lead.setLandingPageTrigger(1);
+        lead.setLandingPageTs(now);
+        lead.setUpdateTime(now);
+        lead.setViewUrl(viewUrl);
         if (isNewLead) {
-            leadService.save(byTraceId);
+            lead.setTraceId(SnowflakeUtil.randomUUID());
+            leadService.save(lead);
         } else {
-            leadService.updateById(byTraceId);
+            leadService.updateById(lead);
         }
 
         // 封装返回结果
         LandingIndexRes res = new LandingIndexRes();
-        redisUtil.set(TEMPLATE_DATA + traceId, templateData, 24, TimeUnit.HOURS);
+        redisUtil.set(TEMPLATE_DATA + lead.getTraceId(), templateData, 24, TimeUnit.HOURS);
         res.setTemplateData(templateData);
-        res.setTraceId(byTraceId.getTraceId());
+        res.setTraceId(lead.getTraceId());
         return res;
     }
 
@@ -202,7 +209,7 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     /**
      * 更新模板中的二维码信息
      */
-    private void updateQrCodeInTemplate(JSONObject templateData, String traceId, Site site, Lead lead) {
+    private void updateQrCodeInTemplate(JSONObject templateData, Site site, Lead lead) {
         JSONArray configList = templateData.getJSONArray("configList");
         if (configList == null || configList.isEmpty()) {
             return;
@@ -227,7 +234,7 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
                                                Integer allocationRule,
                                                Long allocationRuleId,
                                                Lead byTraceId) {
-        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId ,byTraceId);
+        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId, byTraceId);
         // 二维码
         String qrCode = "";
         if (allocationRule == 1) {
@@ -285,10 +292,16 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     @Override
     public LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req) {
         // 更新授权页访问信息记录
-        leadService.updateAuthIndex(req.getTraceId(),req.getType());
+        leadService.updateAuthIndex(req.getTraceId(), req.getType());
         String templateData = String.valueOf(redisUtil.get(TEMPLATE_DATA + req.getTraceId()));
         LandingIndexRes landingIndexRes = new LandingIndexRes();
         landingIndexRes.setTemplateData(templateData);
         return landingIndexRes;
     }
+
+    @Override
+    public void qwExternalIdBindTrack(QwExternalIdBindTrackReq req) {
+        // 广告线索处理
+        leadService.updateAddMemberLead(req.getQwExternalId(), req.getUnionid());
+    }
 }

+ 3 - 1
fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java

@@ -96,7 +96,7 @@ public class ConversionServiceImpl implements IConversionService {
             return false;
         }
 
-        Map<String, Object> params = JSONUtil.toBean(lead.getTraceRawParams(), Map.class);
+        Map<String, Object> params = JSONUtil.toBean(lead.getLandingPageRawParams(), Map.class);
         // 构建回传参数
         Map<String, Object> conversionData = new HashMap<>();
         // ------------------------------通用参数----------
@@ -109,6 +109,8 @@ public class ConversionServiceImpl implements IConversionService {
         conversionData.put("traceId", traceId);
         conversionData.put("eventType", advertiserEventType.getAdvertiserEventType());
         conversionData.put("timestamp", System.currentTimeMillis() / 1000);
+        conversionData.put("viewUrl", lead.getViewUrl());
+
         // 添加平台适应参数
         IAdvertiserAdapter adapter = advertiserHandlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(callbackAccount.getAdvertiserId()));
         adapter.uploadConversionData(conversionData, params);

+ 2 - 14
fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java

@@ -2,17 +2,13 @@ package com.fs.app.mq.consumer;
 
 import com.fs.app.facade.IConversionService;
 import com.fs.common.annotation.DistributeLock;
-import com.fs.common.utils.RedisUtil;
-import com.fs.common.utils.TraceIdUtil;
 import com.fs.newAdv.constant.ConversionTrackingMessage;
 import com.fs.newAdv.constant.MqTopicConstant;
 import com.fs.newAdv.enums.SystemEventTypeEnum;
-import com.fs.newAdv.mapper.ConversionLogMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.spring.annotation.ConsumeMode;
 import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
 import org.apache.rocketmq.spring.core.RocketMQListener;
-import org.jboss.logging.MDC;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -25,23 +21,17 @@ import org.springframework.stereotype.Component;
 @Component
 @RocketMQMessageListener(
         topic = MqTopicConstant.CONVERSION_TRACKING_TOPIC,
-        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP,
+        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_GROUP,
         // 并发消费模式(多线程并发消费,线程数由RocketMQ自动管理)
         consumeMode = ConsumeMode.CONCURRENTLY,
         // 最大重试次数(RocketMQ默认16次)
-        maxReconsumeTimes = 16
+        maxReconsumeTimes = 3
 )
 public class ConversionTrackingMessageConsumer implements RocketMQListener<ConversionTrackingMessage> {
 
     @Autowired
     private IConversionService conversionService;
 
-    @Autowired
-    private RedisUtil redisUtil;
-
-    @Autowired
-    private ConversionLogMapper conversionLogMapper;
-
     /**
      * 消费转化消息
      *
@@ -50,8 +40,6 @@ public class ConversionTrackingMessageConsumer implements RocketMQListener<Conve
     @Override
     @DistributeLock(scene = "mq", keyExpression = "#message.traceId", waitTime = 0, errorMsg = "重复消费")
     public void onMessage(ConversionTrackingMessage message) {
-        TraceIdUtil.put(message.getTrackId());
-
         String traceId = message.getTraceId();
         SystemEventTypeEnum eventType = message.getEventType();
 

+ 54 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/RocketMQTraceIdAspect.java

@@ -0,0 +1,54 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.utils.TraceIdUtil;
+import com.fs.newAdv.constant.ConversionTrackingMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * RocketMQ消费者链路ID切面
+ * 在消费消息前设置链路ID到MDC,保证整个消费链路的日志可追踪
+ *
+ * @author zhangqin
+ */
+@Aspect
+@Component
+@Slf4j
+@Order(1) // 优先于 DistributeLockAspect 执行
+public class RocketMQTraceIdAspect {
+
+    @Pointcut("execution(* com.fs.app.mq.consumer.*.onMessage(..))")
+    public void mqConsumerPointcut() {
+    }
+
+    @Around("mqConsumerPointcut()")
+    public Object around(ProceedingJoinPoint pjp) throws Throwable {
+        String trackId = null;
+        try {
+            // 从消息参数中提取 trackId
+            Object[] args = pjp.getArgs();
+            if (args != null && args.length > 0) {
+                Object message = args[0];
+                if (message instanceof ConversionTrackingMessage) {
+                    trackId = ((ConversionTrackingMessage) message).getTrackId();
+                }
+            }
+            // 设置链路ID到MDC
+            if (trackId != null && !trackId.isEmpty()) {
+                TraceIdUtil.put(trackId);
+            } else {
+                // 如果没有 trackId,则生成一个新的
+                TraceIdUtil.init();
+            }
+            return pjp.proceed();
+        } finally {
+            // 清理MDC,避免线程复用时链路ID污染
+            TraceIdUtil.clear();
+        }
+    }
+}

+ 1 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -1146,6 +1146,7 @@ public class Task {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);

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

@@ -100,7 +100,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
             return AjaxResult.error("请筛选数据导出");
         }
 
-        List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVO(fsStoreAfterSales);
+        List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVOExport(fsStoreAfterSales);
         if("北京卓美".equals(signProjectName)){
             List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                     .map(vo -> {
@@ -123,7 +123,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
                             zmvo.setRealName(vo.getUserName());
                             zmvo.setUserPhone(vo.getUserPhone());
                             zmvo.setUserAddress(vo.getUserAddress());
-                            zmvo.setCreateTime(vo.getCreateTime());
+                            zmvo.setCreateTime(vo.getOrderCreateTime());
                             zmvo.setPayTime(vo.getOrderPayTime());
                             zmvo.setDeliverySn(vo.getOrderDeliverySn());
                             zmvo.setDeliveryName(vo.getOrderDeliveryName());

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

@@ -106,8 +106,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if (list != null) {
             LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
             for (FsStoreOrderVO vo : list) {
-                if(vo.getPhone()!=null){
+                if(StringUtils.isNotEmpty(vo.getPhone())){
                     vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if (StringUtils.isNotEmpty(vo.getUserPhone())){
                     vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (CloudHostUtils.hasCloudHostName("康年堂")){

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

@@ -135,6 +135,7 @@ public class FsStorePaymentScrmController extends BaseController
             V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
             request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
             request.setOrgHfSeqId(payment.getTradeNo());
+            request.setAppId(payment.getAppId());
             HuiFuQueryOrderResult o = null;
             try {
                 o = huiFuService.queryOrder(request);

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

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -76,7 +77,7 @@ public class FsStoreStatisticsScrmController extends BaseController
             List<JSONObject> jsonObjectList = storeOrderService.selectFsStoreOrderCounts(timeEntity.toMap());
             List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
             List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
-            List<Integer> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("payPrice")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
             //表格数据
             List<FsStoreOrderCountsVO> tableData = storeOrderService.selectFsStoreOrderCountsByDept(timeEntity.toMap(),param.getDeptId());
             return R.ok().put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice).put("tableData",tableData);

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

@@ -266,6 +266,7 @@ public class LiveTask {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);

+ 38 - 0
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -34,7 +34,11 @@ import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderPayment;
 import com.fs.pay.pay.dto.OrderQueryDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.store.config.StoreConfig;
@@ -50,6 +54,7 @@ import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.Date;
@@ -160,6 +165,39 @@ public class MallStoreTask
 
     //@Autowired
     //private IFsUserOnlineStateService fsUserOnlineStateService;
+    @Autowired
+    private HuiFuService huiFuService;
+
+    // 订单银行回调数据丢失补偿
+    public void recoveryBankOrder() {
+        // 查询出来最近15分钟的订单 待支付 未退款
+        List<FsStoreOrderScrm> list = fsStoreOrderMapper.selectBankOrder();
+        if(list == null || list.isEmpty()) return;
+        for (FsStoreOrderScrm order : list) {
+            List<FsStorePaymentScrm> orderPayments = fsStorePaymentMapper.selectFsStorePaymentByOrderId(order.getId());
+            if(orderPayments == null || orderPayments.isEmpty()) continue;
+            for (FsStorePaymentScrm payment : orderPayments) {
+                V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
+                HuiFuQueryOrderResult o = null;
+                try {
+                    o = huiFuService.queryOrder(request);
+                } catch (Exception e) {
+                    log.error("查询失败:"+e.getMessage());
+                    continue;
+                }
+                log.info("汇付返回"+o);
+                if ("00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                    String[] orderSpilt=o.getOrg_req_seq_id().split("-");
+                    if ("store".equals(orderSpilt[0])) {
+                        orderService.payConfirm(1, null, orderSpilt[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    }
+                }
+            }
+        }
+    }
 
     public void PushErp() throws ParseException {
         List<Long> ids;

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

@@ -114,7 +114,7 @@ public class LiveAfterSalesController extends BaseController
     {
         PageHelper.clearPage();
         PageHelper.startPage(1, 10000, "");
-        List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
+        List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoListExport(liveAfterSales);
         if("北京卓美".equals(signProjectName)){
             List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                     .map(vo -> {

+ 2 - 0
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -88,6 +88,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
         
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -126,6 +127,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {

+ 2 - 0
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -37,6 +37,8 @@ public class LiveKeysConstant {
     public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
     public static final String LIVE_TAG_MARK_CACHE = "live:tag:mark:%s"; //直播间打标签缓存,存储直播间ID、开始时间和视频时长
+    //记录用户观看直播间信息 直播间id、用户id、外部联系人id、qwUserId
+    public static final String LIVE_USER_WATCH_LOG_CACHE = "live:user:watch:log:%s:%s:%s:%s";
 
 
 }

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

@@ -199,7 +199,7 @@ public class CompanyUserController extends BaseController {
                     }
                     //是否绑定
                     if(QwStatusEnum.BOUND.getCode() == companyUserQwListVO.getQwStatus()){
-                        if(!companyUserQwListVO.getQwUserId().isEmpty()){
+                        if(!StringUtil.strIsNullOrEmpty(companyUserQwListVO.getQwUserId())){
                             Long[] ids = Arrays.stream(companyUserQwListVO.getQwUserId().split(","))
                                     .map(Long::parseLong)
                                     .toArray(Long[]::new);

+ 89 - 5
fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java

@@ -3,6 +3,9 @@ package com.fs.company.controller.company;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.service.ICompanyService;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.statis.StatisticsRedisConstant;
@@ -12,7 +15,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 import static com.fs.statis.StatisticsRedisConstant.*;
 
@@ -31,6 +37,11 @@ public class IndexStatisticsController {
     @Autowired
     private IStatisticsService statisticsService;
 
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    CloudHostProper cloudHostProper;
     /**
      * 分析概览
      */
@@ -94,12 +105,85 @@ public class IndexStatisticsController {
             userType = 0;
         }
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        Long companyId = loginUser.getCompany().getCompanyId();
-        param.setCompanyId(companyId);
 
-        String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
-        List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(key);
-        return R.ok().put("data", deaMemberTopTenDTOS);
+        if("四川致医".equals(cloudHostProper.getCompanyName())){
+            Long companyId1 = loginUser.getCompany().getCompanyId();
+            Company company = loginUser.getCompany();
+            param.setCompanyId(companyId1);
+            List<WatchEndPlayTrendDTO> watchEndPlayTrendDTOS;
+
+            // 参考watchCourseTopTen方法的处理逻辑
+            if ((param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+                String key = String.format("%s:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType);
+                watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+            }else if(param.getCompanyId() != null){
+                String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
+                watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+            }else{
+                Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+                List<WatchEndPlayTrendDTO> tempDTOS = new ArrayList<>();
+                for(Long companyId : companyIds){
+                    String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,companyId);
+                    List<WatchEndPlayTrendDTO> companyData = redisCache.getCacheObject(key);
+                    if (companyData != null) {
+                        tempDTOS.addAll(companyData);
+                    }
+                }
+                // 根据startDate 和 x 分组,合并watchUserCount和completedUserCount 限制最多返回10条记录
+                watchEndPlayTrendDTOS = tempDTOS.stream()
+                        .collect(Collectors.groupingBy(
+                                dto -> dto.getStartDate() + ":" + dto.getX(),  // 根据startDate和x分组
+                                Collectors.reducing(new WatchEndPlayTrendDTO(), (dto1, dto2) -> {
+                                    // 合并watchUserCount和completedUserCount
+                                    WatchEndPlayTrendDTO result = new WatchEndPlayTrendDTO();
+                                    // 复制分组标识字段
+                                    if (dto2 != null && dto2.getStartDate() != null) {
+                                        result.setStartDate(dto2.getStartDate());
+                                    } else if (dto1 != null) {
+                                        result.setStartDate(dto1.getStartDate());
+                                    }
+
+                                    if (dto2 != null && dto2.getX() != null) {
+                                        result.setX(dto2.getX());
+                                    } else if (dto1 != null) {
+                                        result.setX(dto1.getX());
+                                    }
+
+                                    // 合并数值字段
+                                    result.setWatchUserCount(
+                                            (dto1 == null || dto1.getWatchUserCount() == null ? 0 : dto1.getWatchUserCount()) +
+                                                    (dto2 == null || dto2.getWatchUserCount() == null ? 0 : dto2.getWatchUserCount())
+                                    );
+
+                                    result.setCompletedUserCount(
+                                            (dto1 == null || dto1.getCompletedUserCount() == null ? 0 : dto1.getCompletedUserCount()) +
+                                                    (dto2 == null || dto2.getCompletedUserCount() == null ? 0 : dto2.getCompletedUserCount())
+                                    );
+
+                                    return result;
+                                })
+                        ))
+                        .values()
+                        .stream()
+                        .filter(Objects::nonNull)  // 过滤掉null值
+                        .limit(10)
+                        .sorted(Comparator.comparing(WatchEndPlayTrendDTO::getX))
+                        .collect(Collectors.toList());
+            }
+
+            if(watchEndPlayTrendDTOS == null){
+                watchEndPlayTrendDTOS = new ArrayList<>();
+            }
+
+            return R.ok().put("data", watchEndPlayTrendDTOS);
+        }else{
+            Long companyId = loginUser.getCompany().getCompanyId();
+            param.setCompanyId(companyId);
+
+            String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
+            List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(key);
+            return R.ok().put("data", deaMemberTopTenDTOS);
+        }
     }
 
     /**

+ 13 - 1
fs-company/src/main/java/com/fs/company/controller/course/FsUserCoursePeriodController.java

@@ -31,6 +31,8 @@ import com.fs.framework.service.TokenService;
 import com.fs.his.vo.OptionsVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import com.hc.openapi.tool.fastjson.JSON;
+import com.hc.openapi.tool.fastjson.JSONObject;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -115,6 +117,15 @@ public class FsUserCoursePeriodController extends BaseController {
             } else {
                 vo.setIsNeedRegisterMember("0");
             }
+
+            // 看课休息判断
+            if(StringUtils.isNotBlank(vo.getIsOpenRestFlag())){
+                JSONObject  jsonObject= JSON.parseObject(vo.getIsOpenRestFlag());
+                vo.setIsOpenRestReminder(Integer.parseInt(jsonObject.get(currentCompanyId.toString()).toString()));
+            }else {
+                vo.setIsOpenRestReminder(null);
+            }
+
         }
         PageInfo<FsUserCoursePeriodVO> pageInfo = new PageInfo<>(list);
         Map<String, Object> result = new HashMap<>();
@@ -200,7 +211,8 @@ public class FsUserCoursePeriodController extends BaseController {
     @PutMapping("/updatePeriodIsOpenRestReminder")
     public AjaxResult updatePeriodIsOpenRestReminder(@RequestBody FsUserCoursePeriod fsUserCoursePeriod)
     {
-        return toAjax(fsUserCoursePeriodService.updatePeriod(fsUserCoursePeriod));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(fsUserCoursePeriodService.updatePeriod(fsUserCoursePeriod,loginUser.getCompany().getCompanyId()));
 
     }
 

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

@@ -15,6 +15,7 @@ import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
+import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveUserDetailExportVO;
 import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -61,7 +62,7 @@ public class LiveDataController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
     @GetMapping("/getLiveUserDetailListBySql")
-    public R getLiveUserDetailListBySql(@RequestParam Long liveId, 
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId,
                                         @RequestParam(defaultValue = "1") Integer pageNum,
                                         @RequestParam(defaultValue = "100") Integer pageSize,
                                         HttpServletRequest request) {
@@ -163,11 +164,11 @@ public class LiveDataController extends BaseController
     @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
     @Log(title = "直播数据", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(LiveData liveData)
+    public AjaxResult export(LiveDataParam param)
     {
-        List<LiveData> list = liveDataService.selectLiveDataList(liveData);
-        ExcelUtil<LiveData> util = new ExcelUtil<LiveData>(LiveData.class);
-        return util.exportExcel(list, "直播数据数据");
+        List<LiveDataListVo> liveDataListVos = liveDataService.exportLiveData(param);
+        ExcelUtil<LiveDataListVo> util = new ExcelUtil<LiveDataListVo>(LiveDataListVo.class);
+        return util.exportExcel(liveDataListVos, "直播数据数据");
     }
 
     /**

+ 28 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java

@@ -0,0 +1,28 @@
+package com.fs.company.controller.live;
+
+import com.fs.live.service.ILiveWatchUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/18 下午3:26)
+ */
+
+@RestController
+@RequestMapping("/live/LiveMixLiuTestOpen")
+public class LiveMixLiuTestOpenController {
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @GetMapping("/goToMarkUser/{liveId}")
+    public void goToMarkUser(@PathVariable Long liveId){
+        liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+
+
+    }
+}

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

@@ -66,7 +66,8 @@ public class OrderController extends BaseController
         if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
             return getDataTable(new ArrayList<>());
         }
-
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
         startPage();
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         for (MergedOrderVO vo : list) {
@@ -96,6 +97,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -138,6 +140,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -952,4 +952,10 @@ public class QwUserController extends BaseController
         List<QwUserVO> list = qwUserService.selectQwUserListByCompanyIdAndCorpIdAndNickName(companyId, corpId, nickName);
         return getDataTable(list);
     }
+
+    @GetMapping("/updateFastGptRoleStatusById/{id}")
+    public R updateFastGptRoleStatusById(@PathVariable Long id)
+    {
+        return qwUserService.updateQwUserFastGptRoleStatusById(id);
+    }
 }

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -132,6 +132,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/druid/**").anonymous()
                 .antMatchers("/qw/data/**").anonymous()
                 .antMatchers("/qw/user/selectCloudByCompany").anonymous()
+                .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

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

@@ -43,6 +43,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -182,7 +183,7 @@ public class FsStoreOrderScrmController extends BaseController
                 if (vo.getUserAddress()!=null){
                     vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
                 }
-
+                vo.setCost(BigDecimal.ZERO);
             }
         }
 
@@ -391,6 +392,7 @@ public class FsStoreOrderScrmController extends BaseController
                     catch (Exception e){
                     }
                 }
+                vo.setCost(BigDecimal.ZERO);
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<FsStoreOrderItemExportVO>(FsStoreOrderItemExportVO.class);

+ 2 - 1
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java

@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -80,7 +81,7 @@ public class FsStoreStatisticsScrmController extends BaseController
             List<JSONObject> jsonObjectList = storeOrderService.selectFsStoreOrderCounts(timeEntity.toMap());
             List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
             List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
-            List<Integer> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("payPrice")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
             return R.ok().put("list",list).put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice);
         }
         else {

+ 25 - 4
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -9,14 +9,14 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
-import com.fs.common.utils.StringUtils;
 import com.fs.company.cache.ICompanyUserCacheService;
+import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.course.dto.BatchSendCourseDTO;
 import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.PhoneUtil;
@@ -37,8 +37,6 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Date;
 
-import static com.fs.his.utils.PhoneUtil.encryptPhone;
-
 @Api(tags = "会员管理接口")
 @RestController
 @Slf4j
@@ -64,6 +62,9 @@ public class FsUserAdminController extends BaseController {
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private IFsUserCompanyUserService fsUserCompanyUserService;
+
     @PreAuthorize("@ss.hasPermi('user:fsUser:list')")
     @PostMapping("/list")
     @ApiOperation("会员列表(与移动端使用的相同查询)")
@@ -146,6 +147,15 @@ public class FsUserAdminController extends BaseController {
         return AjaxResult.success(fsUserService.selectFsUserPageListVOByUserId(userId));
     }
 
+    /**
+     * 获取项目用户详细信息
+     */
+    @GetMapping(value = "/member/{id}")
+    public AjaxResult getMemberInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserService.selectFsMemberUserPageListVOById(id));
+    }
+
     /**
      * 修改用户
      */
@@ -157,6 +167,17 @@ public class FsUserAdminController extends BaseController {
         return toAjax(fsUserService.updateFsUser(fsUser));
     }
 
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('user:fsUser:edit')")
+    @Log(title = "用户", businessType = BusinessType.UPDATE)
+    @PutMapping("/member")
+    public AjaxResult editMemberUser(@RequestBody FsUserCompanyUser fsUser)
+    {
+        return toAjax(fsUserCompanyUserService.updateFsUserCompanyUser(fsUser));
+    }
+
 
     @ApiOperation("后台会员批量发送课程消息")
     @PostMapping("/batchSendCourse")

+ 5 - 0
fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java

@@ -8,6 +8,9 @@ import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
 import java.util.Arrays;
@@ -17,11 +20,13 @@ import java.util.Set;
 @Aspect
 @Component
 @Slf4j
+@Order(Ordered.LOWEST_PRECEDENCE - 1)  // 调整切面优先级
 public class LiveWatchUserAspect {
 
 
 
     @Autowired
+    @Lazy
     private ILiveWatchUserService liveWatchUserService;
 
     @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +

+ 83 - 11
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -33,12 +33,14 @@ import javax.annotation.PostConstruct;
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 import static com.fs.common.constant.LiveKeysConstant.LIVE_COUPON_NUM;
+import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 
 @Component
 @AllArgsConstructor
@@ -812,12 +814,12 @@ public class Task {
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
         try {
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
                 return;
             }
-
             for (Live live : activeLives) {
                 try {
                     Long liveId = live.getLiveId();
@@ -860,25 +862,32 @@ public class Task {
                             }
 
                             // 获取用户的在线观看时长
+                            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+                            Long existingEntryTime = redisCache.getCacheObject(entryTimeKey);
                             Long onlineSeconds = user.getOnlineSeconds();
+                            if((onlineSeconds == null || onlineSeconds <= 0) && null != existingEntryTime){
+                                onlineSeconds = (System.currentTimeMillis() - existingEntryTime)/1000;
+                            }
                             if (onlineSeconds == null || onlineSeconds <= 0) {
                                 continue;
                             }
-                            
                             // 获取用户的 companyId 和 companyUserId
                             LiveUserFirstEntry liveUserFirstEntry =
                                     liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
                             if (liveUserFirstEntry == null) {
                                 continue;
                             }
-                            
+
                             Long qwUserId = liveUserFirstEntry.getQwUserId();
                             Long externalContactId = liveUserFirstEntry.getExternalContactId();
 
                             if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
                                 continue;
                             }
-
+                            //更新最新用户活跃时间
+                            String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                            LocalDateTime now = LocalDateTime.now();
+                            redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                             // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
                             updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
                                     onlineSeconds, totalVideoDuration, updateLog);
@@ -942,13 +951,13 @@ public class Task {
                 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秒
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断) lmx-这个逻辑不合理,不能这样判定看课中断
+//                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+//                    newLogType = 4;
+//                    needUpdate = true;
+//                } else
+                    // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
                     newLogType = 2;
                     log.setFinishTime(now);
                     needUpdate = true;
@@ -972,6 +981,69 @@ public class Task {
         }
     }
 
+    /**
+     * 每分钟扫描一次用户在线状态用于更新用户观看记录值
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
+    public void updateLiveWatchUserStatus() {
+        try {
+            Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
+            LocalDateTime now = LocalDateTime.now();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            List<LiveWatchLog> updateLog = new ArrayList<>();
+            if (keys != null && !keys.isEmpty()) {
+                for (String key : keys) {
+                    String[] split = key.split(":");
+                    String cacheTime = redisCache.getCacheObject(key);
+                    //判断缓存的值是否已经距离现在超过一分钟
+                    if (StringUtils.isNotBlank(cacheTime)) {
+                        try {
+                            LocalDateTime cachedDateTime = LocalDateTime.parse(cacheTime, formatter);
+                            // 比较时间,判断是否超过1分钟(60秒)
+                            long secondsBetween = java.time.Duration.between(cachedDateTime, now).getSeconds();
+                            if (secondsBetween >= 60) {
+                                // 距离上次记录已超过1分钟,更新状态为看课中断
+                                // 查询 LiveWatchLog
+                                LiveWatchLog queryLog = new LiveWatchLog();
+                                queryLog.setLiveId(Long.valueOf(split[4]));
+                                queryLog.setQwUserId(String.valueOf(split[7]));
+                                queryLog.setExternalContactId(Long.valueOf(split[6]));
+                                queryLog.setLogType(1);
+                                List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                                if (logs != null && !logs.isEmpty()) {
+                                    for (LiveWatchLog log : logs) {
+                                        if (log.getLogType() != null && log.getLogType() == 2) {
+                                            continue;
+                                        }
+                                        log.setLogType(4);
+                                        updateLog.add(log);
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.error("解析缓存时间失败: cacheTime={}, error={}", cacheTime, e.getMessage());
+                        }
+                    }
+                }
+                // 批量插入回放用户数据
+                if (!updateLog.isEmpty()) {
+                    int batchSize = 500;
+                    for (int i = 0; i < updateLog.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, updateLog.size());
+                        List<LiveWatchLog> batch = updateLog.subList(i, end);
+                        liveWatchLogService.batchUpdateLiveWatchLog(batch);
+                    }
+                    for (LiveWatchLog liveWatchLog : updateLog) {
+                        redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+        }
+    }
+
     /**
      * 批量同步Redis中的观看时长到数据库
      * 每2分钟执行一次,减少数据库压力

+ 154 - 52
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -36,7 +36,10 @@ import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
 import java.io.EOFException;
 import java.io.IOException;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.locks.Lock;
@@ -83,9 +86,9 @@ public class WebSocketServer {
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.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
+    public static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -135,7 +138,7 @@ 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);
@@ -145,7 +148,7 @@ public class WebSocketServer {
                 redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
             }
             // 如果是重连,不覆盖进入时间,保持原来的进入时间以便正确计算总时长
-            
+
             // 直播间浏览量 +1
             redisCache.incr(PAGE_VIEWS_KEY + liveId, 1);
 
@@ -222,7 +225,7 @@ public class WebSocketServer {
                 }
             } else {
                 // 这个用户A邀请用户b,b的业绩算a的销售的
-                if (companyUserId == -2L) {
+                if (companyId == -2L) {
                     LiveUserFirstEntry clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
                     companyId = clientB.getCompanyId();
                     companyUserId = clientB.getCompanyUserId();
@@ -246,6 +249,9 @@ public class WebSocketServer {
             }
             redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
 
+            // 推送完课积分倒计时配置信息给前端
+            sendCompletionPointsConfigToUser(session, liveId, userId, live);
+
 
         } else {
             adminRoom.add(session);
@@ -353,7 +359,7 @@ public class WebSocketServer {
                     long watchUserId = (long) userProperties.get("userId");
 
 
-                    
+
                     if (msg.getData() != null && !msg.getData().isEmpty()) {
                         try {
                             Long currentDuration = Long.parseLong(msg.getData());
@@ -371,18 +377,21 @@ public class WebSocketServer {
                                 isLiveStarted = true;
                             } else if (currentLive.getStartTime() != null) {
                                 // 判断当前时间是否已超过开播时间
-                                LocalDateTime now = java.time.LocalDateTime.now();
+                                LocalDateTime now = LocalDateTime.now();
                                 isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
                             }
-                            
-                            if (!isLiveStarted) {
-                                break;
-                            }
 
-                            
                             // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
                             String hashKey = "live:watch:duration:hash:" + liveId;
                             String userIdField = String.valueOf(watchUserId);
+
+                            if (!isLiveStarted) {
+                                redisCache.hashDelete(hashKey, userIdField);
+                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
+                                break;
+                            }
+
+                            // 直播已开始,记录观看时长
                             // 获取现有时长
                             Object existingDuration = redisCache.hashGet(hashKey, userIdField);
                             // 只有当新的时长更大时才更新
@@ -396,11 +405,11 @@ public class WebSocketServer {
 
                             }
                         } catch (Exception e) {
-                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}", 
+                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
                                     liveId, watchUserId, msg.getData(), e);
                         }
                     }
-                    
+
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -741,7 +750,7 @@ public class WebSocketServer {
      */
     public void broadcastWebMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        
+
         if (room.isEmpty()) {
             return;
         }
@@ -867,7 +876,7 @@ public class WebSocketServer {
         for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> roomEntry : rooms.entrySet()) {
             Long liveId = roomEntry.getKey();
             ConcurrentHashMap<Long, Session> room = roomEntry.getValue();
-            
+
             // 如果房间为空,跳过
             if (room.isEmpty()) {
                 continue;
@@ -879,12 +888,12 @@ public class WebSocketServer {
             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);
@@ -954,11 +963,11 @@ public class WebSocketServer {
      */
     public void broadcastLikeMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        
+
         if (room.isEmpty()) {
             return;
         }
-        
+
         // 使用快照遍历,避免并发修改
         for (Map.Entry<Long, Session> entry : room.entrySet()) {
             Session session = entry.getValue();
@@ -1119,31 +1128,31 @@ public class WebSocketServer {
             // 从 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();
@@ -1152,7 +1161,7 @@ public class WebSocketServer {
                 }
                 liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
                 liveWatchUser.setUpdateTime(now);
-                
+
                 // 更新数据库
                 liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
                 // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
@@ -1164,15 +1173,15 @@ public class WebSocketServer {
 //                            liveWatchUser.getOnlineSeconds());
 //                }
             }
-            
+
             // 删除 Redis 中的进入时间记录
             redisCache.deleteObject(entryTimeKey);
         } catch (Exception e) {
-            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}", 
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 在连接时更新 LiveWatchLog 的 logType
      * 如果 logType 类型不是 2,修改 logType 类型为 1(看课中)
@@ -1183,7 +1192,7 @@ public class WebSocketServer {
             queryLog.setLiveId(liveId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(externalContactId);
-            
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs != null && !logs.isEmpty()) {
                 for (LiveWatchLog log : logs) {
@@ -1191,15 +1200,18 @@ public class WebSocketServer {
                     if (log.getLogType() == null || log.getLogType() != 2) {
                         log.setLogType(1);
                         liveWatchLogService.updateLiveWatchLog(log);
+                        String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                        LocalDateTime now = LocalDateTime.now();
+                        redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                     }
                 }
             }
         } catch (Exception e) {
-            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}", 
+            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 实时更新用户看课状态(在心跳时调用)
      * 在直播期间实时更新用户的看课状态,而不是等到关闭 WebSocket 或清理无效会话时才更新
@@ -1212,36 +1224,36 @@ public class WebSocketServer {
             // 获取当前直播/回放状态
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
             Integer currentLiveFlag = flagMap.get("liveFlag");
-            
+
             // 只在直播状态(liveFlag = 1)时更新
             if (currentLiveFlag == null || currentLiveFlag != 1) {
                 return;
             }
-            
+
             // 获取用户的 companyId 和 companyUserId(使用带缓存的查询方法)
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
             if (liveUserFirstEntry == null) {
                 return;
             }
-            
+
             Long companyId = liveUserFirstEntry.getCompanyId();
             Long companyUserId = liveUserFirstEntry.getCompanyUserId();
-            
+
             // 如果 companyId 和 companyUserId 有效,则更新看课状态
             if (companyId != null && companyId > 0 && companyUserId != null && companyUserId > 0) {
                 // 检查是否达到关键观看时长节点,在这些节点实时更新
                 // 关键节点:3分钟(180秒)、20分钟(1200秒)、30分钟(1800秒)
                 boolean isKeyDuration = (watchDuration == 180 || watchDuration == 1200 || watchDuration == 1800) ||
                                        (watchDuration > 180 && watchDuration % 60 == 0); // 每分钟更新一次
-                
+
                 // 使用 Redis 缓存控制更新频率,避免频繁更新数据库
                 // 策略:在关键节点立即更新,其他时候每60秒更新一次
                 String updateLockKey = "live:watch:log:update:lock:" + liveId + ":" + userId;
                 String lastUpdateKey = "live:watch:log:last:duration:" + liveId + ":" + userId;
-                
+
                 // 获取上次更新的时长
                 Long lastUpdateDuration = redisCache.getCacheObject(lastUpdateKey);
-                
+
                 // 如果达到关键节点,或者距离上次更新已超过60秒,则更新
                 boolean shouldUpdate = false;
                 if (isKeyDuration) {
@@ -1251,11 +1263,11 @@ public class WebSocketServer {
                     // 每60秒更新一次
                     shouldUpdate = true;
                 }
-                
+
                 if (shouldUpdate) {
                     // 使用分布式锁,避免并发更新(锁超时时间10秒)
                     Boolean canUpdate = redisCache.setIfAbsent(updateLockKey, "1", 10, TimeUnit.SECONDS);
-                    
+
                     if (Boolean.TRUE.equals(canUpdate)) {
                         // 异步更新,避免阻塞心跳处理
                         CompletableFuture.runAsync(() -> {
@@ -1264,7 +1276,7 @@ public class WebSocketServer {
                                 // 更新上次更新的时长
                                 redisCache.setCacheObject(lastUpdateKey, watchDuration, 2, TimeUnit.HOURS);
                             } catch (Exception e) {
-                                log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+                                log.error("实时更新看课状态异常:liveId={}, userId={}, error={}",
                                         liveId, userId, e.getMessage(), e);
                             } finally {
                                 // 释放锁
@@ -1275,11 +1287,11 @@ public class WebSocketServer {
                 }
             }
         } catch (Exception e) {
-            log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+            log.error("实时更新看课状态异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 根据在线时长更新 LiveWatchLog 的 logType
      * @param liveId 直播间ID
@@ -1288,7 +1300,7 @@ public class WebSocketServer {
      * @param companyUserId 销售ID
      * @param onlineSeconds 在线时长(秒)
      */
-    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId, 
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId,
                                                    Long companyUserId, Long onlineSeconds) {
         try {
             // 获取直播视频总时长(videoType = 1 的视频,使用带缓存的查询方法)
@@ -1300,13 +1312,13 @@ public class WebSocketServer {
                         .mapToLong(LiveVideo::getDuration)
                         .sum();
             }
-            
+
             // 查询 LiveWatchLog
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
             queryLog.setCompanyId(companyId);
             queryLog.setCompanyUserId(companyUserId);
-            
+
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs == null || logs.isEmpty()) {
                 return;
@@ -1315,7 +1327,7 @@ public class WebSocketServer {
             for (LiveWatchLog log : logs) {
                 boolean needUpdate = false;
                 Integer newLogType = log.getLogType();
-                
+
                 // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
                 if (onlineSeconds <= 180) { // 3分钟 = 180秒
                     newLogType = 4;
@@ -1333,7 +1345,7 @@ public class WebSocketServer {
                     log.setFinishTime(now);
                     needUpdate = true;
                 }
-                
+
                 // 如果 logType 已经是 2(完课),不再更新
                 if (needUpdate && (log.getLogType() == null || log.getLogType() != 2)) {
                     log.setLogType(newLogType);
@@ -1341,7 +1353,7 @@ public class WebSocketServer {
                 }
             }
         } catch (Exception e) {
-            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}", 
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
@@ -1389,5 +1401,95 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 向用户推送完课积分倒计时配置信息
+     * 在用户连接WebSocket时调用,让前端能够显示倒计时
+     * @param session WebSocket会话
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param live 直播信息
+     */
+    private void sendCompletionPointsConfigToUser(Session session, Long liveId, Long userId, Live live) {
+        try {
+
+            boolean isLiveStarted = false;
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                isLiveStarted = true;
+            } else if (live.getStartTime() != null) {
+                LocalDateTime now = LocalDateTime.now();
+                isLiveStarted = now.isAfter(live.getStartTime()) || now.isEqual(live.getStartTime());
+            }
+
+            if (!isLiveStarted) {
+                // 直播未开始,不推送完课配置
+                log.debug("[完课配置推送] 直播未开始,跳过推送, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            String configJson = live.getConfigJson();
+            if (configJson == null || configJson.isEmpty()) {
+                return;
+            }
+
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+            boolean enabled = jsonConfig.getBooleanValue("enabled");
+            if (!enabled) {
+                return;
+            }
+
+            Integer completionRate = jsonConfig.getInteger("completionRate");
+            if (completionRate == null || completionRate <= 0 || completionRate > 100) {
+                return;
+            }
+
+            // 3. 计算完课所需观看时长
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                return;
+            }
+
+            // 完课所需时长(秒) = 视频总时长 × 完课比例 / 100
+            long requiredDuration = (long) Math.ceil(videoDuration * completionRate / 100.0);
+
+            // 4. 获取用户当前观看时长
+            String hashKey = "live:watch:duration:hash:" + liveId;
+            String userIdField = String.valueOf(userId);
+            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+            long currentDuration = existingDuration != null ? Long.parseLong(existingDuration.toString()) : 0L;
+
+            // 5. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+            LiveCompletionPointsRecord todayRecord = completionPointsRecordService.selectByUserAndDate(liveId, userId, currentDate);
+
+            boolean hasCompletedToday = (todayRecord != null);
+
+            // 6. 构建配置信息
+            JSONObject configData = new JSONObject();
+            configData.put("videoDuration", videoDuration);  // 视频总时长(秒)
+            configData.put("completionRate", completionRate);  // 完课比例(%)
+            configData.put("requiredDuration", requiredDuration);  // 完课所需时长(秒)
+            configData.put("currentDuration", currentDuration);  // 当前观看时长(秒)
+            configData.put("remainingDuration", Math.max(0, requiredDuration - currentDuration));  // 剩余时长(秒)
+            configData.put("hasCompletedToday", hasCompletedToday);  // 今天是否已完课
+
+            // 7. 推送配置消息
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setCmd("completionPointsConfig");
+            sendMsgVo.setMsg("完课积分配置");
+            sendMsgVo.setData(configData.toJSONString());
+
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+
+            log.debug("[完课配置推送] 推送成功, liveId={}, userId={}, 所需时长={}秒, 当前时长={}秒, 剩余={}秒",
+                    liveId, userId, requiredDuration, currentDuration, Math.max(0, requiredDuration - currentDuration));
+
+        } catch (Exception e) {
+            log.error("[完课配置推送] 推送失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
 }
 

+ 1 - 1
fs-qw-api/Dockerfile

@@ -1,4 +1,4 @@
-FROM openjdk:8-jre
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
 # java版本,最好使用openjdk,而不是类似于Java:1.8
 COPY ./target/fs-qw-api.jar fs-qw-api.jar
 # 向外暴露的接口,最好与项目yml文件中的端口一致

+ 2 - 19
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -236,24 +236,7 @@ public class QwDataCallbackService {
                                             redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
 
                                             // 广告线索处理
-                                            leadService.updateAddMemberLead(externalUserID,userID,corpId,State)
-                                                    .thenAccept(result -> {
-                                                        if (StrUtil.isNotEmpty(result)) {
-                                                            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-                                                            if (qwExternalContact == null){
-                                                                try {
-                                                                    Thread.sleep(2000);
-                                                                } catch (InterruptedException e) {
-                                                                    throw new RuntimeException(e);
-                                                                }
-                                                                qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-                                                            }
-                                                            QwExternalContact temp = new QwExternalContact();
-                                                            temp.setId(qwExternalContact.getId());
-                                                            temp.setTraceId(result);
-                                                            qwExternalContactMapper.updateById(temp);
-                                                        }
-                                                    });
+                                            leadService.updateAddMemberLead(externalUserID,userID,corpId,State);
                                         } catch (Exception e) {
                                             // 7. 业务逻辑失败时,删除缓存
                                             redisCache.deleteObject(cacheKey);
@@ -431,7 +414,7 @@ public class QwDataCallbackService {
                                     if (qwGroupChatUserOld==null) {
                                         qwGroupChatUserService.insertQwGroupChatUser(qwGroupChatUser);
                                         // 群员入群 广告判断记录
-                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getUserId(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
+                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getName(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
                                     }else {
                                         qwGroupChatUserOld.setIsOut(1L);
                                         qwGroupChatUserOld.setCorpId(corpId);

+ 89 - 7
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -694,6 +694,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         int type = content.getType();
         Long courseId = content.getCourseId();
         Long videoId = content.getVideoId();
+        Long liveId = content.getLiveId();
         Integer isOfficial = content.getIsOfficial() != null ? Integer.valueOf(content.getIsOfficial()) : 0;
 
 
@@ -752,7 +753,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 ruleTimeVO.setType(2);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
+                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies,liveId);
             }
 //            if (content.getIndex() == 0) {
 //                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
@@ -782,7 +783,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(),contactId.getIsDaysNotStudy());
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                             type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
-                            null,config, miniMap, grade, sendMsgType,companies);
+                            null,config, miniMap, grade, sendMsgType,companies,liveId);
                 } catch (Exception e) {
                     log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
                 }
@@ -898,7 +899,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                       String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
                                       QwGroupChat groupChat,CourseConfig config,
                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                      Integer grade, Integer sendMsgType ,List<Company> companies ) {
+                                      Integer grade, Integer sendMsgType ,List<Company> companies ,Long liveId) {
         switch (type) {
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo);
@@ -920,6 +921,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             case 7:
                 handleVoiceMessage(sopLogs, content, companyUserId);
                 break;
+            //直播间发送类型
+            case 20:
+                handleLiveMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo,liveId);
             default:
                 log.error("未知的消息类型 {},跳过处理。", type);
                 break;
@@ -1003,6 +1007,83 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         enqueueQwSopLogs(sopLogs);
     }
 
+    /**
+     * 处理直播消息
+     */
+    public void handleLiveMessage(QwSopLogs sopLogs,QwSopTempSetting.Content content, String companyUserId, String companyId,
+                                  boolean isGroupChat,String qwUserId,QwGroupChat groupChat,String externalId,SopUserLogsVo logVo,Long liveId){
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+        clonedContent.setLiveId(liveId);
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        if (settings == null || settings.isEmpty()) {
+            log.error("Cloned content settings are empty, skipping.");
+            return;
+        }
+
+        //直播发送类型
+        sopLogs.setSendType(20);
+
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //直播小程序单独
+                case "12":
+                    clonedContent.setLiveId(setting.getLiveId());
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 1, qwUserId,logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+        }
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+
+        enqueueQwSopLogs(sopLogs);
+    }
+
     private void handleAIMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
         sopLogs.setContentJson(JSON.toJSONString(content));
         sopLogs.setSort(3);
@@ -1988,17 +2069,18 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     )
     public void batchInsertLiveWatchLog(List<LiveWatchLog> liveWatchLogToInsert) {
         try {
-            List<LiveWatchLog> lastInsertList = new ArrayList<>();
+            //更改为set 避免同一批生成的消息里面有重复数据 插入会报错
+            Set<LiveWatchLog> lastInsertSet = new HashSet<>();
             //判断是否存在数据 liveId + his_qw_external_contact_id + qwUserId 唯一
             for (LiveWatchLog liveWatchLog : liveWatchLogToInsert) {
                 //判断是否存在数据 存在的数据直接更新发送时间
                 if(liveWatchLogMapper.updateLiveWatchLogCondition(liveWatchLog) > 0){
                     continue;
                 }
-                lastInsertList.add(liveWatchLog);
+                lastInsertSet.add(liveWatchLog);
             }
-            if(!lastInsertList.isEmpty()){
-                liveWatchLogMapper.insertLiveWatchLogBatch(lastInsertList);
+            if(!lastInsertSet.isEmpty()){
+                liveWatchLogMapper.insertLiveWatchLogBatch(new ArrayList<>(lastInsertSet));
             }
 //            log.info("批量插入 LiveWatchLog 完成,共插入 {} 条记录。", liveWatchLogToInsert.size());
         } catch (Exception e) {

+ 1 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -351,4 +351,5 @@ public interface CompanyUserMapper
      */
     List<com.fs.hisStore.domain.FsUserScrm> selectBoundFsUsersByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
+    CompanyUser selectCompanyUserByQwUserId(@Param("qwUserId") Long id);
 }

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

@@ -50,6 +50,7 @@ import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import com.fs.company.service.ICompanyService;
@@ -120,6 +121,7 @@ public class CompanyServiceImpl implements ICompanyService
     private TransactionTemplate transactionTemplate;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
     @Autowired
     private LiveOrderMapper liveOrderMapper;

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

@@ -122,4 +122,6 @@ public class FsUserCoursePeriod
 
     // 控制休息提示是否打开要暂停  0-关闭 1-打开 null-默认打开
     private Integer IsOpenRestReminder;
+    //  控制休息提示是否打开要暂停  0-关闭 1-打开 Json串 key值为companyId
+    private String isOpenRestFlag;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/course/service/IFsUserCoursePeriodService.java

@@ -106,5 +106,5 @@ public interface IFsUserCoursePeriodService
 
     List<SysDictData> selectFsUserCoursePeriodListLabel(FsUserCoursePeriod fsUserCoursePeriod);
 
-    int updatePeriod(FsUserCoursePeriod fsUserCoursePeriod);
+    int updatePeriod(FsUserCoursePeriod fsUserCoursePeriod, Long companyId);
 }

+ 30 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java

@@ -16,7 +16,10 @@ import com.fs.course.param.PeriodStatisticCountParam;
 import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseStaticsCountVO;
 import com.fs.course.vo.FsUserCoursePeriodVO;
+import com.hc.openapi.tool.fastjson.JSON;
+import com.hc.openapi.tool.fastjson.JSONObject;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -143,6 +146,15 @@ public class FsUserCoursePeriodServiceImpl implements IFsUserCoursePeriodService
             coursePeriodDays.setStartDateTime(startDateTime);
             coursePeriodDays.setEndDateTime(endDateTime);
             coursePeriodDays.setLastJoinTime(lastJsonTime);
+
+            if(LocalDate.now().isBefore(coursePeriodDays.getStartDateTime().toLocalDate())){
+                coursePeriodDays.setStatus(0);
+            } else if(LocalDate.now().isAfter(coursePeriodDays.getStartDateTime().toLocalDate())){
+                coursePeriodDays.setStatus(2);
+            } else{
+                coursePeriodDays.setStatus(1);
+            }
+
             fsUserCoursePeriodDaysMapper.updateFsUserCoursePeriodDays(coursePeriodDays); // 更新数据库中的课程日期
         }
 
@@ -352,7 +364,24 @@ public class FsUserCoursePeriodServiceImpl implements IFsUserCoursePeriodService
      * @Date 2025/12/19 10:52
      */
     @Override
-    public int updatePeriod(FsUserCoursePeriod fsUserCoursePeriod) {
+    public int updatePeriod(FsUserCoursePeriod fsUserCoursePeriod, Long companyId) {
+        Integer flag=fsUserCoursePeriod.getIsOpenRestReminder(); // 0-关闭 1-打开
+        FsUserCoursePeriod period=fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(fsUserCoursePeriod.getPeriodId());
+        if (period==null){
+            return 0;
+        }
+
+        JSONObject jsonObject;
+        if (StringUtils.isNotBlank(period.getIsOpenRestFlag())){
+            jsonObject= JSON.parseObject(period.getIsOpenRestFlag());
+            jsonObject.put(companyId.toString(),flag);
+        }else {
+            jsonObject=new JSONObject();
+            jsonObject.put(companyId.toString(),flag);
+        }
+
+        fsUserCoursePeriod.setIsOpenRestFlag(jsonObject.toJSONString());
+
         return fsUserCoursePeriodMapper.updateFsUserCoursePeriod(fsUserCoursePeriod);
     }
 }

+ 41 - 25
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -48,6 +48,7 @@ import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.*;
+import com.fs.enums.ExceptionCodeEnum;
 import com.fs.his.config.AppConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserIntegralLogs;
@@ -713,11 +714,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
         //用户不存在唤起重新授权
         if (fsUser == null) {
-            return R.error(401, "未授权");
+            return R.error(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
         }
 
         if (fsUser.getStatus() == 0) {
-            return R.error("会员被停用,无权限,请联系客服!");
+            return R.error(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
         }
 //        if (param.getIsOpenCourse()!=null&&param.getIsOpenCourse()==1){
 //            FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByUserId(param.getUserId(), param.getVideoId());
@@ -763,7 +764,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         if (oneCompanyCourse && fsUser.getQwExtId() != null) {
             QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(fsUser.getQwExtId());
             if (qwExternalContact.getCompanyUserId() != null && !qwExternalContact.getCompanyUserId().equals(param.getCompanyUserId())) {
-                return R.error(500, "该用户(" + fsUser.getUserId() + ")已成为其他销售会员");
+                return R.error(ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getCode(), ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getFormattedDescription(fsUser.getUserId()));
             }
         }
 
@@ -775,7 +776,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactList(query);
             if (qwExternalContacts != null && !qwExternalContacts.isEmpty() && !Objects.equals(qwExternalContacts.get(0).getCompanyUserId(), param.getCompanyUserId())) {
                 String msgInfo = "该用户(" + fsUser.getUserId() + ")已在公司" + param.getCompanyId() + "成为其他销售会员";
-                return R.error(500, msgInfo);
+                return R.error(ExceptionCodeEnum.USER_COMPANY_OTHER_SALES_MEMBER.getCode(), ExceptionCodeEnum.USER_COMPANY_OTHER_SALES_MEMBER.getFormattedDescription(fsUser.getUserId(),param.getCompanyId()));
             }
         }
 
@@ -794,7 +795,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         } else {
             // 非法参数
             logger.warn("非法参数 isRoom: {}", isRoom);
-            return R.error("参数错误!");
+            return R.error(ExceptionCodeEnum.PARAM_ERROR.getCode(), ExceptionCodeEnum.PARAM_ERROR.getDescription());
         }
 
     }
@@ -804,13 +805,13 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 //                "\t\t\t\t\t<div style=\"color: #999;font-size: 14px;font-weight: bold;\">添加伴学助手免费领取会员权限</div>";
         QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(courseLink.getChatId());
         if (qwGroupChat == null) {
-            return R.error("群参数异常");
+            return R.error(ExceptionCodeEnum.GROUP_PARAM_ERROR.getCode(), ExceptionCodeEnum.GROUP_PARAM_ERROR.getDescription());
         }
         SopUserLogsInfo sopUserLogsInfo = new SopUserLogsInfo();
         sopUserLogsInfo.setChatId(courseLink.getChatId());
         List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);
         if (qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()) {
-            return R.error("群参数异常");
+            return R.error(ExceptionCodeEnum.GROUP_PARAM_ERROR.getCode(), ExceptionCodeEnum.GROUP_PARAM_ERROR.getDescription());
         }
         //群聊寻找用户新逻辑
         QwExternalContact qwExternalContact = null;
@@ -2204,7 +2205,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                      return R.error("奖励发送失败,请联系客服");
                  }
              }catch (Exception e){
-                 return R.error("发放奖励失败,请联系客服");
+                 return R.error(e.getMessage());
              }
 
             }
@@ -2454,8 +2455,17 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUserCourseVideoDetailsVO fsUserCourseVideoDetailsVO = new FsUserCourseVideoDetailsVO();
         BeanUtils.copyProperties(fsUserCourseVideo, fsUserCourseVideoDetailsVO);
 
+        //从配置中读取默认线路
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (Integer.valueOf(1).equals(config.getDefaultLine())) {
+            fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineTwo());
+        } else {
+            fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());
+        }
         //这里 改成取线路一值,返回给前端。VideoUrl 是原视频(用来算流量的),不要去改,lineOne是转码后的视频
-        fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());
+//        fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());
+
 
         // 获取课程相关的题库
         String questionBankId = fsUserCourseVideo.getQuestionBankId();
@@ -2529,7 +2539,10 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         //查询用户
         FsUser fsUser = fsUserMapper.selectFsUserById(param.getUserId());
         if (fsUser == null) {
-            return ResponseResult.fail(401, "当前用户信息不存在");
+            return ResponseResult.fail(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
+        }
+        if (fsUser.getStatus() == 0) {
+            return ResponseResult.fail(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
         }
         //公开课
         if (param.getIsOpenCourse() != null && param.getIsOpenCourse() == 1) {
@@ -2557,14 +2570,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         //判断该销售是否存在
         CompanyUser companyUser = companyUserMapper.selectCompanyUserById(param.getCompanyUserId());
         if (companyUser == null) {
-            return ResponseResult.fail(405, "当前销售不存在");
+            return ResponseResult.fail(ExceptionCodeEnum.SALES_NOT_FOUND.getCode(), ExceptionCodeEnum.SALES_NOT_FOUND.getDescription());
         }
 
         // 获取课程所属项目id
         FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
         Long courseProject = fsUserCourse.getProject();
         if (Objects.isNull(courseProject)) {
-            return ResponseResult.fail(504, "课程配置错误,项目归属为空,课程ID: " + param.getCourseId());
+            return ResponseResult.fail(ExceptionCodeEnum.COURSE_CONFIG_ERROR.getCode(),  ExceptionCodeEnum.COURSE_CONFIG_ERROR.getFormattedDescription(param.getCourseId()));
         }
         FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
         FsUserCompanyUser userCompanyUser = userCompanyUserService.selectByUserIdAndProjectId(fsUser.getUserId(), courseProject);
@@ -2577,7 +2590,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 Company company = companyService.selectCompanyById(param.getCompanyId());
 
                 if ((companyUser.getIsAllowedAllRegister() != null && companyUser.getIsAllowedAllRegister() != 1)) {
-                    return ResponseResult.fail(504, "当前销售禁止绑定会员,请联系销售!");
+                    return ResponseResult.fail(ExceptionCodeEnum.SALES_FORBIDDEN_BIND.getCode(), ExceptionCodeEnum.SALES_FORBIDDEN_BIND.getDescription());
                 }
                 // 使用 Stream API 检查是否包含 companyId
                 // 修正类型转换问题
@@ -2587,7 +2600,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                         && Arrays.stream(fsUserCoursePeriod.getIsNeedRegisterMember().split(","))
                         .map(String::trim)
                         .anyMatch(id -> id.equals(String.valueOf(company.getCompanyId()))))) {
-                    return ResponseResult.fail(504, "请联系销售发送邀请链接成为会员!");
+                    return ResponseResult.fail(ExceptionCodeEnum.CONTACT_SALES_FOR_INVITATION.getCode(), ExceptionCodeEnum.CONTACT_SALES_FOR_INVITATION.getDescription());
                 }
                 int defaultStatus = (company != null ? company.getFsUserIsDefaultBlack() : 0) == 1 ? 0 : 1;
                 userCompanyUser = userCompanyUserService.bindRelationship(param.getUserId(), courseProject, companyUser.getCompanyId(), companyUser.getUserId(), defaultStatus);
@@ -2598,7 +2611,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         if (!param.getCompanyUserId().equals(userCompanyUser.getCompanyUserId())) {
             log.error("进入::isAddCompanyUser 该用户fsUser={},companyUser={},param={}", fsUser, companyUser, param);
 
-            return ResponseResult.fail(500, "该用户(" + fsUser.getUserId() + ")已成为其他销售会员");
+            return ResponseResult.fail(ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getCode(), ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getFormattedDescription(fsUser.getUserId()));
         }
 
         // 如果开启了黑名单审核,需要提示
@@ -2609,9 +2622,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
         if (userCompanyUser.getStatus() == 2) {
             if ("福本源".equals(signProjectName)) {
-                return ResponseResult.fail(504, "服务暂时不可用,请联系管理员");
+                return ResponseResult.fail(ExceptionCodeEnum.SERVICE_UNAVAILABLE.getCode(), ExceptionCodeEnum.SERVICE_UNAVAILABLE.getDescription());
             } else {
-                return ResponseResult.fail(504, "已被拉黑,请联系管理员");
+                return ResponseResult.fail(ExceptionCodeEnum.USER_BLACKLISTED.getCode(), ExceptionCodeEnum.USER_BLACKLISTED.getDescription());
             }
         }
 
@@ -2620,14 +2633,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(), param.getPeriodId());
 
         if (!isUserCoursePeriodValid(param)) {
-            return ResponseResult.fail(504, "请观看最新的课程项目");
+            return ResponseResult.fail(ExceptionCodeEnum.WATCH_LATEST_COURSE.getCode(), ExceptionCodeEnum.WATCH_LATEST_COURSE.getDescription());
         }
         // 项目看课数限制
         if (!EXCLUDE_PROJECTS.contains(signProjectName) && !CloudHostUtils.hasCloudHostName("弘德堂")) {
             log.error("进入了看课限制:传入参数:={},watchCourseVideo={}",param, watchCourseVideo);
             Integer logCount = fsUserCourseMapper.selectTodayCourseWatchLogCountByUserIdAndProjectId(param.getUserId(), courseProject);
             if (Objects.isNull(watchCourseVideo) && logCount > 0) {
-                return ResponseResult.fail(504, "超过项目看课数量限制");
+                return ResponseResult.fail(ExceptionCodeEnum.EXCEED_COURSE_LIMIT.getCode(), ExceptionCodeEnum.EXCEED_COURSE_LIMIT.getDescription());
             }
         }else {
             log.error("没有进入看课限制:传入参数:={},watchCourseVideo={}存在问题 ",param, watchCourseVideo);
@@ -2637,7 +2650,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             if (!watchCourseVideo.getCompanyUserId().equals(param.getCompanyUserId())) {
                 //提示
                 log.error("数据库存在销售信息:{},分享得销售信息:{}", watchCourseVideo.getCompanyUserId(), param.getCompanyUserId());
-                return ResponseResult.fail(504, "已看过其他销售分享的此课程,不能重复观看");
+                return ResponseResult.fail(ExceptionCodeEnum.ALREADY_WATCHED_OTHER_SALES_COURSE.getCode(), ExceptionCodeEnum.ALREADY_WATCHED_OTHER_SALES_COURSE.getDescription());
             }
 
             FsCourseWatchLog updateLog = new FsCourseWatchLog();
@@ -2943,17 +2956,20 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
             if(watchLog.getPeriodId()!=null){
                 FsUserCoursePeriod period= fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(watchLog.getPeriodId());
-                if(period!=null && period.getIsOpenRestReminder()!=null){
-                    if(period.getIsOpenRestReminder()==0){
+                if(period!=null && watchLog.getCompanyId()!=null && StringUtils.isNotBlank(period.getIsOpenRestFlag()) ){
+                    //  Json 字符串  key值公司id value值  0-关闭 1-打开 没有获取到不管
+                    JSONObject jsonObject= JSON.parseObject(period.getIsOpenRestFlag());
+                    Integer flag=(Integer) jsonObject.get(watchLog.getCompanyId().toString());
+                    if(flag==0){
                         result=false;
                     }else {
                         result=true;
                     }
+
                 }
             }
         }
 
-
         return result;
     }
 
@@ -3678,11 +3694,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
         //用户不存在唤起重新授权
         if (fsUser == null) {
-            return R.error(504, "未授权");
+            return R.error(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
         }
 
         if (fsUser.getStatus() == 0) {
-            return R.error("会员被停用,无权限,请联系客服!");
+            return R.error(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
         }
         return createWatchIsOpen(param);
     }

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

@@ -93,4 +93,6 @@ public class FsUserCoursePeriodVO implements Serializable {
 
     // 控制休息提示是否打开要暂停  0-关闭 1-打开 null-默认打开
     private Integer IsOpenRestReminder;
+
+    private String IsOpenRestFlag;
 }

+ 54 - 0
fs-service/src/main/java/com/fs/enums/ExceptionCodeEnum.java

@@ -0,0 +1,54 @@
+package com.fs.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ExceptionCodeEnum {
+
+    // ============ 用户相关错误 (400-419) ============
+    USER_NOT_FOUND(401, "当前用户信息不存在,请重新授权"),
+
+    // ============ 会员相关错误 (420-439) ============
+    USER_BLACKLISTED(421, "已被拉黑,请联系管理员"),
+
+    // ============ 销售相关错误 (440-459) ============
+    SALES_NOT_FOUND(441, "当前销售不存在"),
+    SALES_FORBIDDEN_BIND(442, "当前销售禁止绑定会员,请联系销售!"),
+    CONTACT_SALES_FOR_INVITATION(443, "请联系销售发送邀请链接成为会员!"),
+
+    // ============ 会员关系错误 (460-479) ============
+    MEMBER_DISABLED(461, "会员被停用,无权限,请联系客服!"),
+    USER_ALREADY_OTHER_SALES_MEMBER(462, "该用户(%s)已成为其他销售会员"),
+    USER_COMPANY_OTHER_SALES_MEMBER(463, "该用户(%s)已在公司%s成为其他销售会员"),
+
+    // ============ 课程相关错误 (480-499) ============
+    COURSE_CONFIG_ERROR(481, "课程配置错误,项目归属为空,课程ID: %s"),
+    WATCH_LATEST_COURSE(482, "请观看最新的课程项目"),
+    EXCEED_COURSE_LIMIT(483, "超过项目看课数量限制"),
+    ALREADY_WATCHED_OTHER_SALES_COURSE(484, "已看过其他销售分享的此课程,不能重复观看"),
+
+    // ============ 参数相关错误 (500-519) ============
+    PARAM_ERROR(501, "参数错误!"),
+    GROUP_PARAM_ERROR(502, "群参数异常"),
+    LIVE_PARAM_ERROR(503, "直播参数错误"),
+
+    // ============ 系统相关错误 (520-539) ============
+    SERVICE_UNAVAILABLE(521, "服务暂时不可用,请联系管理员");
+
+    private final Integer code;
+    private final String description;
+
+    ExceptionCodeEnum(Integer code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    /**
+     * 获取格式化后的描述信息
+     * @param args 格式化参数
+     * @return 格式化后的描述
+     */
+    public String getFormattedDescription(Object... args) {
+        return String.format(description, args);
+    }
+}

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

@@ -555,8 +555,6 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
             List<ErpOrderQuery> erpOrders = query.getOrders().stream()
                     .map(this::convertToErpOrderQueryScrm)
                     .collect(Collectors.toList());
-            if ("Cancelled".equals(query.getOrders().get(0).getStatus()))
-                fsStoreOrderScrmService.cancelOrderByCode(query.getOrders().get(0).getOuterPayId());
             response.setOrders(erpOrders);
         } else {
             response.setOrders(Collections.emptyList());
@@ -603,9 +601,9 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
             List<ErpOrderQuery> erpOrders = query.getOrders().stream()
                     .map(this::convertToErpOrderQueryLive)
                     .collect(Collectors.toList());
-            if ("Cancelled".equals(query.getOrders().get(0).getStatus())) {
-                liveOrderMapper.cancelOrderByCode(query.getOrders().get(0).getOuterPayId());
-            }
+//            if ("Split".equals(query.getOrders().get(0).getStatus())) {
+//                this.splitLiveOrder(query.getOrders().get(0));
+//            }
             response.setOrders(erpOrders);
         } else {
             response.setOrders(Collections.emptyList());
@@ -614,6 +612,164 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         return response;
     }
 
+    private void splitLiveOrder(OrderQueryResponseDTO.Order order) {
+        // ① 首先查询原来的订单
+        LiveOrder liveOrder = liveOrderMapper.selectLiveOrderByOrderCode(order.getSoId());
+        if (liveOrder == null) {
+            log.error("直播拆分订单:未查询到数据,{}", order.getSoId());
+            return;
+        }
+
+        try {
+            // ② 重新组装请求,使用 OrderQueryRequestDTO 查询拆分订单
+            OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
+            requestDTO.setOrderItemFlds(Arrays.asList("status"));
+            requestDTO.setSoIds(Collections.singletonList(order.getSoId()));
+
+            // 调用ERP服务查询拆分订单
+            OrderQueryResponseDTO query = jstErpHttpService.query(requestDTO);
+
+            if (query == null || query.getOrders() == null || query.getOrders().isEmpty()) {
+                log.error("直播拆分订单:查询拆分订单失败, orderCode={}", order.getSoId());
+                return;
+            }
+
+            // ③ 把原来的订单状态修改为 6(被拆分)
+            liveOrder.setStatus(6);
+            liveOrderMapper.updateLiveOrder(liveOrder);
+            log.info("直播拆分订单:原订单状态已更新为被拆分, orderCode={}, orderId={}", order.getSoId(), liveOrder.getOrderId());
+
+            // ④ 查询出来的订单里面,除了原来的订单,将查询出来的订单新增到数据库里面
+            for (OrderQueryResponseDTO.Order splitOrder : query.getOrders()) {
+                // 跳过原来的订单(状态为 Split 的订单)
+                if ("Split".equals(splitOrder.getStatus())) {
+                    continue;
+                }
+
+                // 检查是否已经存在该拆分订单
+                LiveOrder existingOrder = liveOrderMapper.selectLiveOrderByOrderCode(splitOrder.getSoId());
+                if (existingOrder != null) {
+                    log.info("直播拆分订单:拆分订单已存在,跳过, orderCode={}", splitOrder.getSoId());
+                    continue;
+                }
+
+                // 创建新的拆分订单
+                LiveOrder newOrder = new LiveOrder();
+                // 复制原订单的基本信息
+                newOrder.setLiveId(liveOrder.getLiveId());
+                newOrder.setStoreId(liveOrder.getStoreId());
+                newOrder.setOrderCode(splitOrder.getSoId()); // 保存订单ID(soId)
+                newOrder.setUserId(liveOrder.getUserId()); // buyerId -> userId
+                newOrder.setUserName(liveOrder.getUserName());
+                newOrder.setRealName(liveOrder.getRealName());
+                newOrder.setUserPhone(liveOrder.getUserPhone());
+                newOrder.setUserAddress(liveOrder.getUserAddress());
+                newOrder.setCompanyId(liveOrder.getCompanyId());
+                newOrder.setCompanyUserId(liveOrder.getCompanyUserId());
+
+                // 设置支付金额
+                if (splitOrder.getPayAmount() != null) {
+                    newOrder.setPayMoney(splitOrder.getPayAmount());
+                    newOrder.setPayPrice(splitOrder.getPayAmount());
+                }
+
+                // 根据订单状态枚举设置状态
+                ErpQueryOrderStatusEnum statusEnum = ErpQueryOrderStatusEnum.getByCode(splitOrder.getStatus());
+                if (statusEnum != null) {
+                    // 根据状态类型设置订单状态
+                    // WaitPay -> 1 (待支付)
+                    // Sent -> 3 (待收货)
+                    // Cancelled -> 0 (已取消)
+                    // 其他状态根据业务需求设置
+                    if ("WaitPay".equals(splitOrder.getStatus())) {
+                        newOrder.setStatus(1); // 待支付
+                    } else if ("Sent".equals(splitOrder.getStatus())) {
+                        newOrder.setStatus(3); // 待收货
+                    } else if ("Cancelled".equals(splitOrder.getStatus())) {
+//                        newOrder.setStatus(0); // 已取消
+                    } else {
+                        newOrder.setStatus(2); // 默认待发货
+                    }
+                } else {
+                    newOrder.setStatus(2); // 默认待发货
+                }
+
+                // 设置物流信息
+                if (StringUtils.isNotEmpty(splitOrder.getLogisticsCompany())) {
+                    newOrder.setDeliveryName(splitOrder.getLogisticsCompany());
+                }
+                if (StringUtils.isNotEmpty(splitOrder.getLId())) {
+                    newOrder.setDeliverySn(splitOrder.getLId());
+                }
+                if (StringUtils.isNotEmpty(splitOrder.getLcId())) {
+                    newOrder.setDeliveryCode(splitOrder.getLcId());
+                }
+
+                // 设置发货时间
+                if (StringUtils.isNotEmpty(splitOrder.getSendDate())) {
+                    try {
+                        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                        Date sendDate = formatter.parse(splitOrder.getSendDate());
+                        newOrder.setDeliverySendTime(sendDate);
+                        newOrder.setDeliveryTime(splitOrder.getSendDate());
+                    } catch (Exception e) {
+                        log.error("解析发货时间失败: {}", splitOrder.getSendDate(), e);
+                    }
+                }
+
+                // 设置支付时间
+                if (StringUtils.isNotEmpty(splitOrder.getPayDate())) {
+                    try {
+                        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                        Date payDate = formatter.parse(splitOrder.getPayDate());
+                        newOrder.setPayTime(payDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
+                    } catch (Exception e) {
+                        log.error("解析支付时间失败: {}", splitOrder.getPayDate(), e);
+                    }
+                }
+
+                // 设置扩展订单ID
+                newOrder.setExtendOrderId(String.valueOf(splitOrder.getOId()));
+
+                // 计算订单总数量
+                if (splitOrder.getItems() != null && !splitOrder.getItems().isEmpty()) {
+                    int totalQty = splitOrder.getItems().stream()
+                            .mapToInt(OrderQueryResponseDTO.OrderItem::getQty)
+                            .sum();
+                    newOrder.setTotalNum(String.valueOf(totalQty));
+                }
+
+                // 设置订单总价
+                if (splitOrder.getAmount() != null) {
+                    newOrder.setTotalPrice(splitOrder.getAmount());
+                }
+
+                // 设置创建时间和更新时间
+                newOrder.setCreateTime(new Date());
+                newOrder.setUpdateTime(new Date());
+
+                // 插入订单
+                liveOrderMapper.insertLiveOrder(newOrder);
+
+                // 保存订单项(SKU)
+                if (splitOrder.getItems() != null && !splitOrder.getItems().isEmpty()) {
+                    for (OrderQueryResponseDTO.OrderItem item : splitOrder.getItems()) {
+                        LiveOrderItem orderItem = new LiveOrderItem();
+                        orderItem.setOrderId(newOrder.getOrderId());
+                        orderItem.setOrderCode(splitOrder.getSoId());
+                        orderItem.setNum(item.getQty() != null ? item.getQty().longValue() : 0L);
+                        // 可以根据需要设置其他字段,如 productId, goodsId 等
+                        // 这里需要根据业务逻辑从 item 中获取或从原订单项中复制
+                        liveOrderItemMapper.insertLiveOrderItem(orderItem);
+                    }
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("直播拆分订单处理异常, orderCode={}", order.getSoId(), e);
+        }
+    }
+
     /**
      * 将OrderQueryResponseDTO.Order转换为ErpOrderQuery
      *

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

@@ -9,8 +9,11 @@ import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.CompanyConfig;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyConfigMapper;
+import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.config.ai.AiHostProper;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
@@ -169,6 +172,12 @@ public class AiHookServiceImpl implements AiHookService {
     @Autowired
     private ICrmMsgService crmMsgService;
 
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
 
@@ -392,6 +401,12 @@ public class AiHookServiceImpl implements AiHookService {
             log.error("未绑定角色");
             return userIsReply(sender, uid, user);
         }
+
+        if(user.getAiStatus() == 1){
+            log.error("ai已下线:{}",user);
+            return R.ok();
+        }
+
         Long serverId = user.getServerId();
         log.info("服务器id"+serverId);
         if (serverId == null) {
@@ -1469,6 +1484,17 @@ public class AiHookServiceImpl implements AiHookService {
             //添加看客记录
             addCourseWatchLog(qwExternalContactsId);
             String msgC = (String)redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+
+            if (("今正科技".equals(cloudHostProper.getCompanyName()))) {
+                //处理名称替换
+                if (role.getReminderWords() != null && !role.getReminderWords().isEmpty() && role.getReminderWords().contains("#销售名称#")) {
+                    CompanyUser companyUser = companyUserMapper.selectCompanyUserByQwUserId(user.getId());
+                    if (companyUser != null) {
+                        role.setReminderWords(role.getReminderWords().replace("#销售名称#", companyUser.getNickName()));
+                    }
+                }
+            }
+
             //添加关键词
             addPromptWord(messageList,msgC,qwExternalContactsId,role.getReminderWords(), role.getContactInfo(),fastGptChatSession.getSessionId());
             R r = chatService.initiatingTakeChat(param, aiHostProper.getAiApi(), appKey);

+ 2 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java

@@ -32,6 +32,8 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_22(22,"首次完成积分商城下单"),
     TYPE_23(23,"管理员添加"),
     TYPE_24(24, "付费课程订阅"),
+    TYPE_25(25, "直播完课积分"),
+    TYPE_26(26, "直播红包积分"),
     ;
 
 

+ 3 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -188,6 +188,9 @@ public interface IFsUserService
 
     FsUserPageListVO selectFsUserPageListVOByUserId(Long userId);
 
+
+    FsUserPageListVO selectFsMemberUserPageListVOById(Long id);
+
     /**
      * 查询项目会员数据
      *

+ 5 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -389,6 +389,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
             request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(fsStorePayment.getCreateTime()));
             request.setOrgHfSeqId(fsStorePayment.getTradeNo());
+            request.setAppId(fsStorePayment.getAppId());
             HuiFuQueryOrderResult queryOrderResult = null;
             try {
                 queryOrderResult = huiFuService.queryOrder(request);
@@ -658,7 +659,8 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             return result;
         }catch (Exception e){
             logger.error("领取红包失败原因:{}", ExceptionUtils.getMessage(e),e);
-            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+            if (e instanceof WxPayException) {
                 WxPayException wxPayException = (WxPayException) e;
                 String customErrorMsg = wxPayException.getCustomErrorMsg();
                 if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
@@ -922,7 +924,8 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             return R.ok("发送红包成功").put("orderCode", transferBatchesResult.getOutBatchNo()).put("batchId", transferBatchesResult.getBatchId()).put("mchId", config.getMchId());
         } catch (Exception e) {
             logger.error("商家转账支付失败:参数: {} :原因: {}",JSON.toJSONString(param), e.getMessage(),e);
-            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+            if (e instanceof WxPayException) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
                 WxPayException wxPayException = (WxPayException) e;
                 String customErrorMsg = wxPayException.getCustomErrorMsg();
                 if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {

+ 39 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -1221,6 +1221,45 @@ public class FsUserServiceImpl implements IFsUserService {
         return item;
     }
 
+    @Override
+    public FsUserPageListVO selectFsMemberUserPageListVOById(Long id) {
+        FsUserCompanyUser userCompanyUser = userCompanyUserService.selectFsUserCompanyUserById(id);
+        if(userCompanyUser == null || userCompanyUser.getUserId() == null){
+            return null;
+        }
+
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(userCompanyUser.getUserId());
+        FsUserPageListVO item = new FsUserPageListVO();
+        BeanUtils.copyProperties(fsUser, item);
+        item.setNickname(fsUser.getNickName());
+        item.setStatus(userCompanyUser.getStatus()); // 取项目会员的状态
+        Map<Long, CompanyTag> tagMap = companyTagCacheService.queryAllTagMap();
+        if (item.getPhone() != null) {
+            item.setPhone(ParseUtils.parsePhone(item.getPhone()));
+        }
+        String userTagByUserId = null;
+        if (item.getUserId() != null && item.getCompanyUserId() != null) {
+            userTagByUserId = companyTagCacheService
+                    .findUserTagByUserId(item.getUserId(), item.getCompanyUserId());
+        }
+        if (StringUtils.isNotEmpty(userTagByUserId)) {
+            String[] split = userTagByUserId.split(",");
+            Set<String> tagNames = new HashSet<>();
+            for (String tag : split) {
+                if (StringUtils.isNotBlank(tag)) {
+                    Long tagL = Long.parseLong(tag);
+                    CompanyTag companyTag = tagMap.get(tagL);
+                    if (companyTag != null) {
+                        tagNames.add(companyTag.getTag());
+                    }
+                }
+            }
+            item.setTagIds(userTagByUserId);
+            item.setTag(String.join(",", tagNames));
+        }
+        return item;
+    }
+
     @Override
     @Transactional
     public void addMoney(FsStoreOrderScrm order) {

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

@@ -150,4 +150,10 @@ public class FsStoreAfterSalesScrm extends BaseEntity
     @TableField(exist = false)
     private String hfOrderCode;
 
+    /**
+     * 用于查询银行交易流水
+     */
+    @TableField(exist = false)
+    private String bankTransactionId;
+
 }

+ 71 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -102,6 +102,9 @@ public interface FsStoreAfterSalesScrmMapper
             "<if test =\"maps.hfOrderCode != null and  maps.hfOrderCode!='' \"> " +
               "and fsps.pay_code = #{maps.hfOrderCode} " +
             "</if>" +
+            "<if test =\"maps.bankTransactionId != null and  maps.bankTransactionId!='' \"> " +
+              "and fsps.bank_transaction_id = #{maps.bankTransactionId} " +
+            "</if>" +
             "<if test = 'maps.status != null    '> " +
             "and s.status = #{maps.status} " +
             "</if>" +
@@ -242,4 +245,72 @@ public interface FsStoreAfterSalesScrmMapper
 
     @Select(" SELECT id FROM fs_store_order_scrm WHERE order_code =#{id}")
     Long selectFsOrderIdByCode(Long id);
+
+    @Select({"<script> " +
+            "select s.*,o.delivery_status,o.delivery_id,u.phone as user_phone,c.company_name ,cu.nick_name as company_user_nick_name ," +
+            "cu.phonenumber as company_usere_phonenumber,o.pay_money,o.id as orderId,o.create_time as orderCreateTime,o.user_phone," +
+            "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage," +
+            "fsps.bank_serial_no,fsps.bank_transaction_id,o.delivery_id as orderDeliveryId,o.delivery_name as orderDeliveryName,o.delivery_sn as orderDeliverySn," +
+            "o.status as orderStatus,fsps.pay_code as payCode " +
+            " from fs_store_after_sales_scrm s " +
+            " INNER join fs_store_order_scrm o on o.order_code=s.order_code " +
+            " left join fs_user u on s.user_id=u.user_id " +
+            " left join company c on c.company_id=s.company_id " +
+            " left join company_user cu on cu.user_id=s.company_user_id " +
+            " left join fs_store_payment_scrm fsps on fsps.business_order_id = o.id and fsps.status in (-1,1) " +
+            " where 1=1 and s.is_del = 0  and o.status = -2 and fsps.bank_transaction_id is not null " +
+            "<if test =\"maps.hfOrderCode != null and  maps.hfOrderCode!='' \"> " +
+            "and fsps.pay_code = #{maps.hfOrderCode} " +
+            "</if>" +
+            "<if test = 'maps.status != null    '> " +
+            "and s.status = #{maps.status} " +
+            "</if>" +
+            "<if test = 'maps.salesStatus != null    '> " +
+            "and s.sales_status = #{maps.salesStatus} " +
+            "</if>" +
+            "<if test = 'maps.orderStatus != null    '> " +
+            "and s.order_status = #{maps.orderStatus} " +
+            "</if>" +
+            "<if test = 'maps.orderCode != null and  maps.orderCode !=  \"\" '> " +
+            "and o.order_code like concat('%', #{maps.orderCode}, '%') " +
+            "</if>" +
+            "<if test = 'maps.deliveryStatus != null    '> " +
+            "and o.delivery_status = #{maps.deliveryStatus} " +
+            "</if>" +
+            "<if test = 'maps.serviceType != null    '> " +
+            "and s.service_type = #{maps.serviceType} " +
+            "</if>" +
+            "<if test = 'maps.companyId != null    '> " +
+            "and s.company_id = #{maps.companyId} " +
+            "</if>" +
+            "<if test = 'maps.companyUserId != null    '> " +
+            "and s.company_user_id = #{maps.companyUserId} " +
+            "</if>" +
+            "<if test = 'maps.deliverySn != null and  maps.deliverySn !=  \"\" '> " +
+            " and ( o.delivery_id like concat('%', #{maps.deliverySn}, '%') or s.delivery_sn like concat('%', #{maps.deliverySn}, '%')) " +
+            "</if>" +
+            "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
+            "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
+            "</if>" +
+            "<if test = 'maps.params != null and maps.params != \"\"   '> " +
+            "<if test = 'maps.params.beginTime != null and maps.params.beginTime != \"\"   '> " +
+            " AND date_format(s.create_time,'%y%m%d') &gt;= date_format(#{maps.params.beginTime},'%y%m%d') " +
+            "</if>" +
+            "<if test = 'maps.params.endTime != null and maps.params.endTime != \"\"   '> " +
+            " AND date_format(s.create_time,'%y%m%d') &lt;= date_format(#{maps.params.endTime},'%y%m%d') " +
+            "</if>" +
+            "</if>" +
+            "<if test = 'maps.consigneePhone != null and  maps.consigneePhone !=\"\"     '> " +
+            "and o.user_phone like CONCAT('%',#{maps.consigneePhone},'%') " +
+            "</if>" +
+            "<if test = 'maps.productName != null and  maps.productName != \"\" '> " +
+            "and EXISTS (SELECT 1 FROM fs_store_order_item_scrm oi WHERE oi.order_id = o.id AND JSON_UNQUOTE(JSON_EXTRACT(oi.json_info, '$.productName')) LIKE CONCAT('%', #{maps.productName}, '%')) " +
+            "</if>" +
+            "<if test = 'maps.deptId != null    '> " +
+            "  AND (o.dept_id = #{maps.deptId} OR o.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors) )) " +
+            "</if>" +
+            " ${maps.params.dataScope} "+
+            "order by s.create_time desc "+
+            "</script>"})
+    List<FsStoreAfterSalesVO> selectFsStoreAfterSalesListVOExport(@Param("maps") FsStoreAfterSalesScrm fsStoreAfterSales);
 }

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

@@ -76,7 +76,7 @@ public interface FsStoreCouponUserScrmMapper
             "and (find_in_set( #{maps.packageCateId},c.package_cate_ids) or c.package_cate_ids=0) " +
             "</if>" +
             "<if test = 'maps.useMinPrice != null     '> " +
-            "and cu.use_min_price &lt; #{maps.useMinPrice} " +
+            "and cu.use_min_price &lt;= #{maps.useMinPrice} " +
             "</if>" +
             "<if test = 'maps.couponType != null     '> " +
             "and c.type = #{maps.couponType} " +

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -478,6 +478,9 @@ public interface FsStoreOrderScrmMapper
             "<if test = 'maps.userId != null     '> " +
             "and o.user_id=#{maps.userId} " +
             "</if>" +
+            "<if test = 'maps.appId != null and  maps.appId !=\"\"    '> " +
+            "and o.app_id =#{maps.appId} " +
+            "</if>" +
             " order by o.id desc "+
             "</script>"})
     List<FsMyStoreOrderListQueryVO> selectFsMyStoreOrderListVO(@Param("maps")FsMyStoreOrderQueryParam param);
@@ -1414,4 +1417,8 @@ public interface FsStoreOrderScrmMapper
 
     @Update("update fs_store_order_scrm set `status`=-3 where order_code=#{orderCode}")
     int cancelOrderByCode(@Param("orderCode") String orderCode);
+
+
+    @Select("SELECT * FROM fs_store_order_scrm WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 MINUTE) and status = 0")
+    List<FsStoreOrderScrm> selectBankOrder();
 }

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

@@ -274,7 +274,7 @@ public interface FsStoreProductScrmMapper
             "and  p.is_good=1 and p.is_display=1 order by p.sort desc limit #{count}")
     List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count,@Param("config") MedicalMallConfig  config);
     List<FsStoreProductListQueryVO> selectFsStoreProductTuiListQuery(@Param("config") MedicalMallConfig  config, @Param("param") BaseQueryParam param);
-    List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery(@Param("config") MedicalMallConfig  config);
+    List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery(@Param("config") MedicalMallConfig  config, @Param("param") BaseQueryParam param);
     @Select({"<script> " +
             "select count(1) from fs_store_product_scrm  " +
             "where 1=1 " +

+ 9 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java

@@ -124,6 +124,15 @@ public interface FsUserScrmMapper
     @Update("update fs_user set pay_count=pay_count+1" +
             " where user_id=#{userId}")
     int incPayCount(Long userId);
+    
+    /**
+     * 增加用户余额
+     * @param userId 用户ID
+     * @param integral 增加的积分
+     * @return 结果
+     */
+    @Update("update fs_user set integral = IFNULL(integral, 0) + #{integral} where user_id = #{userId}")
+    int incrIntegral(@Param("userId") Long userId, @Param("integral") BigDecimal integral);
     @Select("select * from fs_user where phone=#{phone}")
     FsUserScrm selectFsUserByPhone(String phone);
 

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsMyStoreOrderQueryParam.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.param;
 
 import com.fs.common.param.BaseQueryParam;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -13,4 +14,6 @@ public class FsMyStoreOrderQueryParam extends BaseQueryParam implements Serializ
     private Long companyId;
     private Long companyUserId;
     private Integer deliveryStatus;
+    @ApiModelProperty(value = "当前的appid")
+    private String appId;
 }

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

@@ -19,7 +19,7 @@ public class FsStoreOrderCreateParam implements Serializable
     @NotNull(message = "orderKey不能为空")
     private String orderKey;
     @ApiModelProperty(value = "地址ID")
-    @NotNull(message = "地址不能为空")
+//    @NotNull(message = "地址不能为空")
     private Long addressId;
 
     @ApiModelProperty(value = "来源")

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductPackageQueryParam.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.param;
 
 import com.fs.common.param.BaseQueryParam;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -12,4 +13,6 @@ public class FsStoreProductPackageQueryParam extends BaseQueryParam implements S
     private String title;
     private String cateId;
     private Integer status;
+    @ApiModelProperty(value = "当前的appid")
+    private String appId;
 }

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

@@ -117,4 +117,6 @@ public interface IFsStoreAfterSalesScrmService
     int noAuditing(FsStoreAfterSalesScrm fsStoreAfterSales);
 
     int storeRefundMoney(FsStoreAfterSalesScrm fsStoreAfterSales);
+
+    List<FsStoreAfterSalesVO> selectFsStoreAfterSalesListVOExport(FsStoreAfterSalesScrm fsStoreAfterSales);
 }

+ 3 - 3
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java

@@ -97,15 +97,15 @@ public interface IFsStoreProductScrmService
     void incProductStock(Long num, Long productId, Long productAttrValueId);
 
 
-    List<FsStoreProductListQueryVO> selectFsStoreProductNewQuery(int count);
+    List<FsStoreProductListQueryVO> selectFsStoreProductNewQuery(int count, String appId);
 
-    List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(int count);
+    List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(int count, String appId);
 
     List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count);
 
     List<FsStoreProductListQueryVO> selectFsStoreProductTuiListQuery(BaseQueryParam param);
 
-    List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery();
+    List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery(BaseQueryParam param);
 
     Long selectFsStoreProductCount(int type);
     Long selectFsStoreProductCount(int type,Long companyId);

+ 41 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -554,6 +554,47 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         return R.ok();
     }
 
+    @Override
+    @DataScope(deptAlias = "cu", userAlias = "cu")
+    public List<FsStoreAfterSalesVO> selectFsStoreAfterSalesListVOExport(FsStoreAfterSalesScrm fsStoreAfterSales) {
+        List<FsStoreAfterSalesVO> fsStoreAfterSalesVOS = fsStoreAfterSalesMapper.selectFsStoreAfterSalesListVOExport(fsStoreAfterSales);
+        List<Long> orderIds = new ArrayList<>();
+        Map<Long, List<FsStoreOrderItemVO>> orderItemMap = new HashMap<>();
+        if(null != fsStoreAfterSalesVOS && !fsStoreAfterSalesVOS.isEmpty()){
+            orderIds = fsStoreAfterSalesVOS.stream().map(e -> e.getOrderId()).collect(Collectors.toList());
+            if(null != orderIds && !orderIds.isEmpty()){
+                List<FsStoreOrderItemVO> fsStoreOrderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderIds(orderIds);
+                orderItemMap = fsStoreOrderItemVOS.stream()
+                        .collect(Collectors.groupingBy(FsStoreOrderItemVO::getOrderId));
+            }
+        }
+        boolean mapEmpty = orderItemMap.isEmpty();
+        if (null != fsStoreAfterSalesVOS && !fsStoreAfterSalesVOS.isEmpty()) {
+            for (FsStoreAfterSalesVO item : fsStoreAfterSalesVOS) {
+                if(!mapEmpty && orderItemMap.containsKey(item.getOrderId())){
+                    List<FsStoreOrderItemVO> orderItems = orderItemMap.get(item.getOrderId());
+                    for (FsStoreOrderItemVO orderItem : orderItems) {
+                        try {
+                            JSONObject jsO = JSONObject.parseObject(orderItem.getJsonInfo());
+                            item.setProductName(StringUtils.isNotBlank(item.getProductName()) ? item.getProductName() + "," + jsO.getString("productName") : jsO.getString("productName"));
+                            item.setProductBarCode(StringUtils.isNotBlank(item.getProductBarCode()) ? item.getProductBarCode() + "," + jsO.getString("barCode") : jsO.getString("barCode"));
+                            item.setSku(StringUtils.isNotBlank(item.getSku()) ? item.getSku() + "," + jsO.getString("sku") : jsO.getString("sku"));
+                            item.setNum(StringUtils.isNotBlank(item.getNum()) ? item.getNum() + "," + jsO.getString("num") : jsO.getString("num"));
+                            item.setPrice(StringUtils.isNotBlank(item.getPrice()) ? item.getPrice() + "," + jsO.getString("price") : jsO.getString("price"));
+                            item.setBarCode(StringUtils.isNotBlank(item.getBarCode()) ? item.getBarCode() + "," + jsO.getString("barCode") : jsO.getString("barCode"));
+                            item.setCost(StringUtils.isNotBlank(item.getCost()) ? item.getCost() + "," + orderItem.getCost() : orderItem.getCost());
+                            item.setCateName(StringUtils.isNotBlank(item.getCateName()) ? item.getCateName() + "," + orderItem.getCateName() : orderItem.getCateName());
+
+                        } catch (Exception ex) {
+                            logger.error("售后订单商品信息转换异常",ex);
+                        }
+                    }
+                }
+            }
+        }
+        return fsStoreAfterSalesVOS;
+    }
+
     @Override
     @DataScope(deptAlias = "cu", userAlias = "cu")
     public List<FsStoreAfterSalesVO> selectFsStoreAfterSalesListVO(FsStoreAfterSalesScrm fsStoreAfterSales) {

+ 22 - 8
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -629,8 +629,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     public List<FsStoreOrderVO> selectFsStoreOrderListVO(FsStoreOrderParam param) {
         List<FsStoreOrderVO> list = fsStoreOrderMapper.selectFsStoreOrderListVO(param);
         for (FsStoreOrderVO vo : list) {
-            String nickName = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
-            vo.setNickname(nickName);
+            if (StringUtils.isNotEmpty(vo.getUserPhone())){
+                String nickName = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+                vo.setNickname(nickName);
+            }
+
             if (StringUtils.isNotEmpty(vo.getItemJson())) {
                 JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
                 List<FsStoreOrderItemVO> items = JSONUtil.toList(jsonArray, FsStoreOrderItemVO.class);
@@ -648,8 +651,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     public List<FsStoreOrderVO> selectFsStoreOrderAllListVO(FsStoreOrderParam param) {
         List<FsStoreOrderVO> list = fsStoreOrderMapper.selectFsStoreOrderAllListVO(param);
         for (FsStoreOrderVO vo : list) {
-            String nickName = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
-            vo.setNickname(nickName);
+            if (StringUtils.isNotEmpty(vo.getUserPhone())){
+                String nickName = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+                vo.setNickname(nickName);
+            }
+
             if (StringUtils.isNotEmpty(vo.getItemJson())) {
                 JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
                 List<FsStoreOrderItemVO> items = JSONUtil.toList(jsonArray, FsStoreOrderItemVO.class);
@@ -829,6 +835,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Override
     @Transactional
     public R createOrder(long userId, FsStoreOrderCreateParam param) {
+        if (!CloudHostUtils.hasCloudHostName("鹤颜堂")){
+            log.error("进入到数据");
+            if (ObjectUtil.isEmpty(param.getAddressId())){
+                return R.error("地址不能为空!");
+            }
+        }
         FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
         BeanUtils.copyProperties(param, computedParam);
         //计算金额
@@ -902,10 +914,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
             storeOrder.setUserId(userId);
             storeOrder.setOrderCode(orderSn);
-            storeOrder.setRealName(address.getRealName());
-            storeOrder.setUserPhone(address.getPhone());
-            storeOrder.setUserAddress(address.getProvince() + " " + address.getCity() +
-                    " " + address.getDistrict() + " " + address.getDetail().trim());
+            if (ObjectUtil.isNotEmpty(address)){
+                storeOrder.setRealName(address.getRealName());
+                storeOrder.setUserPhone(address.getPhone());
+                storeOrder.setUserAddress(address.getProvince() + " " + address.getCity() +
+                        " " + address.getDistrict() + " " + address.getDetail().trim());
+            }
             storeOrder.setCartId(cartIds);
             storeOrder.setTotalNum(Long.parseLong(String.valueOf(carts.size())));
             storeOrder.setTotalPrice(dto.getTotalPrice());

+ 8 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -59,6 +59,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
@@ -121,6 +122,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
 
     @Autowired
@@ -1058,18 +1060,20 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     }
 
     @Override
-    public List<FsStoreProductListQueryVO> selectFsStoreProductNewQuery(int count) {
+    public List<FsStoreProductListQueryVO> selectFsStoreProductNewQuery(int count, String appId) {
         HashMap<String, Object> map = new HashMap<>();
         map.put("count", count);
         map.put("config", medicalMallConfig);
+        map.put("appId", appId);
         return fsStoreProductMapper.selectFsStoreProductNewQuery(map);
     }
 
     @Override
-    public List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(int count) {
+    public List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(int count, String appId) {
         HashMap<String, Object> map = new HashMap<>();
         map.put("count", count);
         map.put("config", medicalMallConfig);
+        map.put("appId", appId);
         return fsStoreProductMapper.selectFsStoreProductHotQuery(map);
     }
 
@@ -1084,8 +1088,8 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     }
 
     @Override
-    public List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery() {
-        return fsStoreProductMapper.selectFsStoreProductGoodListQuery(medicalMallConfig);
+    public List<FsStoreProductListQueryVO> selectFsStoreProductGoodListQuery(BaseQueryParam param) {
+        return fsStoreProductMapper.selectFsStoreProductGoodListQuery(medicalMallConfig, param);
     }
 
     @Override

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

@@ -49,7 +49,7 @@ public class FsStoreOrderItemExportRefundZMVO implements Serializable  {
     @Excel(name = "产品价格",sort =70)
     private String price;
 
-    @Excel(name = "成本价",sort =80)
+    @Excel(name = "成本价",sort =80)
     private String cost;
     @Excel(name = "结算价",sort =90)
     private BigDecimal FPrice;

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java

@@ -41,7 +41,7 @@ public class FsStoreOrderItemExportVO implements Serializable
     @Excel(name = "产品价格")
     private BigDecimal price;
 
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
     @Excel(name = "结算价")
     private BigDecimal FPrice;

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java

@@ -46,7 +46,7 @@ public class FsStoreOrderItemExportZMVO implements Serializable  {
     @Excel(name = "产品价格",sort =70)
     private BigDecimal price;
 
-    @Excel(name = "成本价",sort =80)
+    @Excel(name = "成本价",sort =80)
     private BigDecimal cost;
     @Excel(name = "结算价",sort =90)
     private BigDecimal FPrice;

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderPromotionExportVO.java

@@ -132,7 +132,7 @@ public class FsStoreOrderPromotionExportVO implements Serializable
     private String packageTitle;
 
     /** 成本价 */
-    @Excel(name = "订单成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
     private String phone;

+ 2 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderStatisticsVO.java

@@ -4,6 +4,7 @@ import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 @Data
 public class FsStoreOrderStatisticsVO implements Serializable
@@ -13,7 +14,7 @@ public class FsStoreOrderStatisticsVO implements Serializable
     @Excel(name = "订单数")
     Integer orderCount;
     @Excel(name = "订单金额")
-    Integer payPrice;
+    BigDecimal payPrice;
 
 
 

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderVO.java

@@ -194,7 +194,7 @@ public class FsStoreOrderVO implements Serializable
     private Integer isDel;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
     /** 结算价 */

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductExportVO.java

@@ -118,7 +118,7 @@ public class FsStoreProductExportVO implements Serializable {
     private BigDecimal giveIntegral;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
     /** 是否优品推荐 */

+ 12 - 0
fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayQueryRequest.java

@@ -47,6 +47,10 @@ public class V2TradePaymentScanpayQueryRequest extends BaseRequest {
     @JSONField(name = "party_order_id")
     private String partyOrderId;
 
+
+
+    String appId; //多小程序支付
+
     @Override
     public FunctionCodeEnum getFunctionCode() {
         return FunctionCodeEnum.V2_TRADE_PAYMENT_SCANPAY_QUERY;
@@ -121,4 +125,12 @@ public class V2TradePaymentScanpayQueryRequest extends BaseRequest {
         this.partyOrderId = partyOrderId;
     }
 
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
 }

+ 13 - 1
fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java

@@ -131,7 +131,19 @@ public class HuiFuServiceImpl implements HuiFuService {
 
     @Override
     public HuiFuQueryOrderResult queryOrder(V2TradePaymentScanpayQueryRequest request) throws Exception{
-        doInit(getMerConfig());
+        if (request.getAppId() != null) {
+            FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
+            FsHfpayConfig fsHfpayConfig = fsHfpayConfigMapper.selectByAppId(request.getAppId());
+            if (fsHfpayConfig != null) {
+                //多汇付支付获取配置
+                doInit(getMerConfig(fsHfpayConfig));
+            } else {
+                //多小程序
+                doInit(getMerConfig());
+            }
+        } else {
+            doInit(getMerConfig());
+        }
         Map<String, Object> response = doExecute(request);
         String jsonString = JSONObject.toJSONString(response);
         HuiFuQueryOrderResult huiFuQueryOrderResult = JSON.parseObject(jsonString, HuiFuQueryOrderResult.class);

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

@@ -145,7 +145,7 @@ public class LiveOrder extends BaseEntity {
     private String isDel;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal costPrice;
 
     /** 核销码 */

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

@@ -1,6 +1,8 @@
 package com.fs.live.domain;
 
 import java.util.Date;
+import java.util.Objects;
+
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
@@ -86,4 +88,19 @@ public class LiveWatchLog extends BaseEntity{
      */
     private Integer replayBuy;
 
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || getClass() != obj.getClass()) return false;
+        LiveWatchLog that = (LiveWatchLog) obj;
+        return Objects.equals(liveId, that.liveId) &&
+                Objects.equals(externalContactId, that.externalContactId) &&
+                Objects.equals(qwUserId, that.qwUserId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(liveId, externalContactId, qwUserId);
+    }
+
 }

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

@@ -133,4 +133,6 @@ public interface LiveAfterSalesMapper {
 
     @Select(" select  * from  live_after_sales where order_id = #{orderId} and sales_status = 0 ")
     LiveAfterSales getLiveAfterSalesByOrderId(@Param("orderId") Long orderId);
+
+    List<LiveAfterSalesVo> selectLiveAfterSalesVoListExport(LiveAfterSalesVo liveAfterSales);
 }

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

@@ -33,6 +33,12 @@ public interface LiveCompletionPointsRecordMapper {
      */
     LiveCompletionPointsRecord selectLatestByUser(@Param("userId") Long userId);
 
+    /**
+     * 查询用户在某直播间最近一次完课记录(不限制日期)
+     */
+    LiveCompletionPointsRecord selectLatestByUserAndLiveId(@Param("liveId") Long liveId, 
+                                                            @Param("userId") Long userId);
+
     /**
      * 查询用户未领取的完课记录列表
      */

+ 1 - 1
fs-service/src/main/java/com/fs/live/param/LiveOrderSearchParam.java

@@ -131,7 +131,7 @@ public class LiveOrderSearchParam extends BaseEntity {
     private String isDel;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal costPrice;
 
     /** 核销码 */

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

@@ -118,5 +118,7 @@ public class MergedOrderQueryParam extends BaseQueryParam implements Serializabl
 
     /** ERP电话 */
     private String erpPhoneNumber;
+    /** 汇付商户订单号 */
+    private String hfshh;
 }
 

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

@@ -93,4 +93,6 @@ public interface ILiveAfterSalesService {
     Integer selectLiveAfterSalesCount(long l, int i);
 
     R handleImmediatelyRefund(Long orderId);
+
+    List<LiveAfterSalesVo> selectLiveAfterSalesVoListExport(LiveAfterSalesVo liveAfterSales);
 }

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

@@ -2,6 +2,7 @@ package com.fs.live.service;
 
 import com.fs.live.domain.LiveCompletionPointsRecord;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -40,4 +41,13 @@ public interface ILiveCompletionPointsRecordService {
      * @return 完课记录列表
      */
     List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId);
+
+    /**
+     * 根据用户和日期查询完课记录
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @param date 日期
+     * @return 完课记录
+     */
+    LiveCompletionPointsRecord selectByUserAndDate(Long liveId, Long userId, Date date);
 }

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

@@ -167,4 +167,6 @@ public interface ILiveDataService {
      * @return 导出VO列表
      */
     List<LiveUserDetailExportVO> exportLiveUserDetail(Long liveId, Long companyId, Long companyUserId);
+
+    List<LiveDataListVo> exportLiveData(LiveDataParam param);
 }

+ 197 - 113
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -85,6 +85,9 @@ import com.fs.live.service.ILiveAfterSalesService;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 售后记录Service业务层处理
@@ -162,6 +165,9 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 //    @Autowired
 //    private FsStoreDeliversMapper fsStoreDeliversMapper;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
 
     /**
      * 查询售后记录
@@ -192,6 +198,59 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 
 //    @Autowired
 //    private FsStoreDeliversService fsStoreDeliversService;
+
+
+    @Override
+    public List<LiveAfterSalesVo> selectLiveAfterSalesVoListExport(LiveAfterSalesVo liveAfterSales) {
+        List<LiveAfterSalesVo> liveAfterSalesVos = baseMapper.selectLiveAfterSalesVoListExport(liveAfterSales);
+        List<Long> orderIds = new ArrayList<>();
+        Map<Long, List<LiveOrderItemListUVO>> orderItemMap = new HashMap<>();
+        if(null != liveAfterSalesVos && !liveAfterSalesVos.isEmpty()){
+            orderIds = liveAfterSalesVos.stream().map(e -> e.getOrderId()).collect(Collectors.toList());
+            if(null != orderIds && !orderIds.isEmpty()){
+                List<LiveOrderItemListUVO> liveOrderItemListUVOS = liveOrderItemMapper.selectLiveOrderItemListUVOByOrderIds(orderIds);
+                orderItemMap = liveOrderItemListUVOS.stream()
+                        .collect(Collectors.groupingBy(LiveOrderItemListUVO::getOrderId));
+            }
+        }
+        boolean mapEmpty = orderItemMap.isEmpty();
+        for (LiveAfterSalesVo item : liveAfterSalesVos) {
+            if(ObjectUtil.isNotNull(item.getUserId())) {
+                FsUser fsUser = fsUserCacheService.selectFsUserById(item.getUserId());
+                if(ObjectUtil.isNotNull(fsUser)) {
+                    item.setUserName(String.format("%s_%s",fsUser.getUserId(),fsUser.getNickname()));
+                }
+            }
+
+            if(ObjectUtil.isNull(item.getCompanyUserNickName())) {
+                item.setCompanyUserNickName("-");
+            }
+
+            if(ObjectUtil.isNull(item.getCompanyName())){
+                item.setCompanyName("-");
+            }
+
+            if(!mapEmpty && orderItemMap.containsKey(item.getOrderId())){
+                List<LiveOrderItemListUVO> liveOrderItemListUVOS = orderItemMap.get(item.getOrderId());
+                for (LiveOrderItemListUVO liveOrderItemListUVO : liveOrderItemListUVOS) {
+                    try {
+                        JSONObject jsO = JSONObject.parseObject(liveOrderItemListUVO.getJsonInfo());
+                        item.setProductName(StringUtils.isNotBlank(item.getProductName()) ? item.getProductName() + "," + jsO.getString("productName") : jsO.getString("productName"));
+                        item.setProductBarCode(StringUtils.isNotBlank(item.getProductBarCode()) ? item.getProductBarCode() + "," + jsO.getString("barCode") : jsO.getString("barCode"));
+                        item.setSku(StringUtils.isNotBlank(item.getSku()) ? item.getSku() + "," + jsO.getString("sku") : jsO.getString("sku"));
+                        item.setNum(StringUtils.isNotBlank(item.getNum()) ? item.getNum() + "," + jsO.getString("num") : jsO.getString("num"));
+                        item.setPrice(StringUtils.isNotBlank(item.getPrice()) ? item.getPrice() + "," + jsO.getString("price") : jsO.getString("price"));
+                        item.setCost(StringUtils.isNotBlank(item.getCost()) ? item.getCost() + "," + liveOrderItemListUVO.getCost() : liveOrderItemListUVO.getCost());
+                        item.setCateName(StringUtils.isNotBlank(item.getCateName()) ? item.getCateName() + "," + liveOrderItemListUVO.getCateName() : liveOrderItemListUVO.getCateName());
+                    } catch (Exception ex) {
+                        log.error("售后订单商品信息转换异常",ex);
+                    }
+                }
+            }
+
+        }
+        return liveAfterSalesVos;
+    }
     /**
      * 查询售后记录列表
      *
@@ -335,139 +394,163 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
     @Transactional
     public R applyForAfterSales(String userId, LiveAfterSalesParam param) {
         log.info("申请退款请求信息:"+JSONUtil.toJsonStr(param));
-        LiveOrder order=liveOrderService.selectOrderIdByOrderCode(param.getOrderCode());
-        if(!order.getUserId().equals(userId)){
-            throw new CustomException("非法操作");
-        }
-        if(order.getStatus()==0){
-            return R.error("未支付订单不能申请售后");
-        }
-        if("1".equals(configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"))
-                && StringUtils.isEmpty(order.getExtendOrderId())
-                && !CloudHostUtils.hasCloudHostName("康年堂")){
-            log.info("erpOpen:{}",configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
-            return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
-        }
-        if(order.getStatus()== OrderInfoEnum.STATUS_NE3.getValue()){
-            return R.error("已取消订单不能申请售后");
-        }
-        if(order.getStatus()== OrderInfoEnum.STATUS_NE1.getValue()){
-            return R.error("已提交申请,等待处理");
-        }
+        
+        // 使用Redis分布式锁,确保同一订单只能有一个服务器处理申请售后 因为卓美有一个订单生成了三个售后订单,数据一摸一样,除了id
+        String lockKey = "live:afterSales:apply:" + param.getOrderCode();
+        RLock lock = redissonClient.getLock(lockKey);
+        
+        try {
+            // 尝试获取锁,等待时间3秒,锁过期时间30秒
+            boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("申请售后订单锁获取失败,订单号:{}", param.getOrderCode());
+                return R.error("系统繁忙,请稍后重试");
+            }
+            
+            log.info("申请售后订单锁获取成功,订单号:{}", param.getOrderCode());
+            
+            LiveOrder order=liveOrderService.selectOrderIdByOrderCode(param.getOrderCode());
+            if(!order.getUserId().equals(userId)){
+                throw new CustomException("非法操作");
+            }
+            if(order.getStatus()==0){
+                return R.error("未支付订单不能申请售后");
+            }
+            if("1".equals(configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"))
+                    && StringUtils.isEmpty(order.getExtendOrderId())
+                    && !CloudHostUtils.hasCloudHostName("康年堂")){
+                log.info("erpOpen:{}",configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
+                return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
+            }
+            if(order.getStatus()== OrderInfoEnum.STATUS_NE3.getValue()){
+                return R.error("已取消订单不能申请售后");
+            }
+            if(order.getStatus()== OrderInfoEnum.STATUS_NE1.getValue()){
+                return R.error("已提交申请,等待处理");
+            }
 //        if(storeAfterSalesParam.getRefundAmount().compareTo(order.getPayPrice())==1){
 //            return R.error("退款金额不能大于支付金额");
 //        }
-        //已完成订单七天后不能申请退款
-        if(order.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
-            String json=configService.selectConfigByKey("store.config");
-            com.fs.hisStore.config.StoreConfig config=JSONUtil.toBean(json, com.fs.hisStore.config.StoreConfig.class);
-            //已完成订单
-            if (order.getFinishTime() != null) {
-                if (config.getStoreAfterSalesDay() != null && config.getStoreAfterSalesDay() > 0) {
-                    //判断完成时间是否超过指定时间
-                    Calendar calendarAfterSales = new GregorianCalendar();
-                    calendarAfterSales.setTime(order.getFinishTime());
-                    calendarAfterSales.add(calendarAfterSales.DATE, config.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
-                    if (calendarAfterSales.getTime().getTime() < new Date().getTime()) {
-                        return R.error("此订单已超过售后时间,不能提交售后");
-                    }
+            //已完成订单七天后不能申请退款
+            if(order.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
+                String json=configService.selectConfigByKey("store.config");
+                com.fs.hisStore.config.StoreConfig config=JSONUtil.toBean(json, com.fs.hisStore.config.StoreConfig.class);
+                //已完成订单
+                if (order.getFinishTime() != null) {
+                    if (config.getStoreAfterSalesDay() != null && config.getStoreAfterSalesDay() > 0) {
+                        //判断完成时间是否超过指定时间
+                        Calendar calendarAfterSales = new GregorianCalendar();
+                        calendarAfterSales.setTime(order.getFinishTime());
+                        calendarAfterSales.add(calendarAfterSales.DATE, config.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
+                        if (calendarAfterSales.getTime().getTime() < new Date().getTime()) {
+                            return R.error("此订单已超过售后时间,不能提交售后");
+                        }
 
+                    }
                 }
             }
-        }
-        //商品除去优惠后的总价格
-        //BigDecimal totalPrice = BigDecimal.ZERO;
-        //拿到所有的商品
-        List<LiveOrderItem> orderItems = liveOrderItemService.selectCheckedByOrderId(order.getOrderId());
-        for (LiveOrderItem item : orderItems) {
-            StoreOrderProductDTO cartInfo = JSONObject.parseObject(item.getJsonInfo(), StoreOrderProductDTO.class);
-            LiveAfterSalesProductParam prosuctParam = param.getProductList().stream().filter(p -> p.getProductId().equals(item.getProductId())).findFirst().orElse(new LiveAfterSalesProductParam());
-            if (prosuctParam.getProductId() != null) {
+            //商品除去优惠后的总价格
+            //BigDecimal totalPrice = BigDecimal.ZERO;
+            //拿到所有的商品
+            List<LiveOrderItem> orderItems = liveOrderItemService.selectCheckedByOrderId(order.getOrderId());
+            for (LiveOrderItem item : orderItems) {
+                StoreOrderProductDTO cartInfo = JSONObject.parseObject(item.getJsonInfo(), StoreOrderProductDTO.class);
+                LiveAfterSalesProductParam prosuctParam = param.getProductList().stream().filter(p -> p.getProductId().equals(item.getProductId())).findFirst().orElse(new LiveAfterSalesProductParam());
+                if (prosuctParam.getProductId() != null) {
 //                //商品优惠前总金额
 //                BigDecimal totalAmountOfGoods = NumberUtil.mul(cartInfo.getPrice(), item.getNum());
 //                //商品优惠总金额
 //                BigDecimal commodityDiscountAmount = NumberUtil.mul(NumberUtil.div(totalAmountOfGoods, NumberUtil.sub(order.getTotalPrice(), order.getPayPostage())), order.getCouponPrice());
 //                //商品优惠后总金额
 //                totalPrice = NumberUtil.add(totalPrice, NumberUtil.sub(totalAmountOfGoods, commodityDiscountAmount));
-                item.setIsAfterSales(1);
-                LiveOrderItem orderItem=new LiveOrderItem();
+                    item.setIsAfterSales(1);
+                    LiveOrderItem orderItem=new LiveOrderItem();
 //                BeanUtil.copyProperties(item, orderItem);
-                try {
-                    BeanUtils.copyProperties(orderItem,item);
-                } catch (IllegalAccessException e) {
-                    throw new RuntimeException(e);
-                } catch (InvocationTargetException e) {
-                    throw new RuntimeException(e);
-                }
-                liveOrderItemService.updateLiveOrderItem(orderItem);
+                    try {
+                        BeanUtils.copyProperties(orderItem,item);
+                    } catch (IllegalAccessException e) {
+                        throw new RuntimeException(e);
+                    } catch (InvocationTargetException e) {
+                        throw new RuntimeException(e);
+                    }
+                    liveOrderItemService.updateLiveOrderItem(orderItem);
 
+                }
             }
-        }
-        //更新订单状态
-        Integer orderStatus=order.getStatus();
-        order.setStatus(OrderInfoEnum.STATUS_NE1.getValue());
-        order.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_1.getValue()));
-        order.setRefundExplain(param.getReasons());
-        order.setCancelReason(param.getExplains());
-        order.setIsAfterSales(1);
-        order.setRefundTime(new Date());
-        liveOrderService.updateLiveOrder(order);
-        //生成售后订单
-        LiveAfterSales storeAfterSales = new LiveAfterSales();
-        storeAfterSales.setOrderId(order.getOrderId());
-        storeAfterSales.setRefundAmount(param.getRefundAmount());
-        storeAfterSales.setRefundType(param.getServiceType());
-        storeAfterSales.setReasons(param.getReasons());
-        storeAfterSales.setExplains(param.getExplains());
-        storeAfterSales.setExplainImg(param.getExplainImg());
-        storeAfterSales.setStatus(AfterSalesStatusEnum.STATUS_0.getValue());
-        storeAfterSales.setSalesStatus(0);
-        storeAfterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
-        storeAfterSales.setIsDel(0);
-        storeAfterSales.setOrderStatus(orderStatus);
-        storeAfterSales.setUserId(Long.valueOf(userId));
-        storeAfterSales.setCompanyId(order.getCompanyId());
-        storeAfterSales.setCompanyUserId(order.getCompanyUserId());
-        liveAfterSalesService.insertLiveAfterSales(storeAfterSales);
-        //售后商品详情
-        for (LiveAfterSalesProductParam productParam : param.getProductList()) {
-            LiveOrderItem item = orderItems.stream().filter(p -> p.getProductId().equals(productParam.getProductId())).findFirst().orElse(new LiveOrderItem());
-            LiveAfterSalesItem storeAfterSalesItem = new LiveAfterSalesItem();
-            storeAfterSalesItem.setAfterSalesId(storeAfterSales.getId());
-            storeAfterSalesItem.setProductId(item.getProductId());
-            storeAfterSalesItem.setJsonInfo(item.getJsonInfo());
-            storeAfterSalesItem.setIsDel(0);
-            liveAfterSalesItemMapper.insertLiveAfterSalesItem(storeAfterSalesItem);
-        }
-        //操作记录
-        LiveAfterSalesLogs storeAfterSalesStatus = new LiveAfterSalesLogs();
-        storeAfterSalesStatus.setStoreAfterSalesId(storeAfterSales.getId());
-        storeAfterSalesStatus.setChangeType(0);
-        storeAfterSalesStatus.setChangeMessage(AfterSalesStatusEnum.STATUS_0.getDesc());
-        storeAfterSalesStatus.setChangeTime(Timestamp.valueOf(LocalDateTime.now()));
-        FsUserScrm user=userService.selectFsUserById(Long.valueOf(userId));
-        storeAfterSalesStatus.setOperator(user.getNickname());
-        liveAfterSalesLogsMapper.insertLiveAfterSalesLogs(storeAfterSalesStatus);
+            //更新订单状态
+            Integer orderStatus=order.getStatus();
+            order.setStatus(OrderInfoEnum.STATUS_NE1.getValue());
+            order.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_1.getValue()));
+            order.setRefundExplain(param.getReasons());
+            order.setCancelReason(param.getExplains());
+            order.setIsAfterSales(1);
+            order.setRefundTime(new Date());
+            liveOrderService.updateLiveOrder(order);
+            //生成售后订单
+            LiveAfterSales storeAfterSales = new LiveAfterSales();
+            storeAfterSales.setOrderId(order.getOrderId());
+            storeAfterSales.setRefundAmount(param.getRefundAmount());
+            storeAfterSales.setRefundType(param.getServiceType());
+            storeAfterSales.setReasons(param.getReasons());
+            storeAfterSales.setExplains(param.getExplains());
+            storeAfterSales.setExplainImg(param.getExplainImg());
+            storeAfterSales.setStatus(AfterSalesStatusEnum.STATUS_0.getValue());
+            storeAfterSales.setSalesStatus(0);
+            storeAfterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
+            storeAfterSales.setIsDel(0);
+            storeAfterSales.setOrderStatus(orderStatus);
+            storeAfterSales.setUserId(Long.valueOf(userId));
+            storeAfterSales.setCompanyId(order.getCompanyId());
+            storeAfterSales.setCompanyUserId(order.getCompanyUserId());
+            liveAfterSalesService.insertLiveAfterSales(storeAfterSales);
+            //售后商品详情
+            for (LiveAfterSalesProductParam productParam : param.getProductList()) {
+                LiveOrderItem item = orderItems.stream().filter(p -> p.getProductId().equals(productParam.getProductId())).findFirst().orElse(new LiveOrderItem());
+                LiveAfterSalesItem storeAfterSalesItem = new LiveAfterSalesItem();
+                storeAfterSalesItem.setAfterSalesId(storeAfterSales.getId());
+                storeAfterSalesItem.setProductId(item.getProductId());
+                storeAfterSalesItem.setJsonInfo(item.getJsonInfo());
+                storeAfterSalesItem.setIsDel(0);
+                liveAfterSalesItemMapper.insertLiveAfterSalesItem(storeAfterSalesItem);
+            }
+            //操作记录
+            LiveAfterSalesLogs storeAfterSalesStatus = new LiveAfterSalesLogs();
+            storeAfterSalesStatus.setStoreAfterSalesId(storeAfterSales.getId());
+            storeAfterSalesStatus.setChangeType(0);
+            storeAfterSalesStatus.setChangeMessage(AfterSalesStatusEnum.STATUS_0.getDesc());
+            storeAfterSalesStatus.setChangeTime(Timestamp.valueOf(LocalDateTime.now()));
+            FsUserScrm user=userService.selectFsUserById(Long.valueOf(userId));
+            storeAfterSalesStatus.setOperator(user.getNickname());
+            liveAfterSalesLogsMapper.insertLiveAfterSalesLogs(storeAfterSalesStatus);
 
 //        //更新OMS
-        IErpOrderService erpOrderService = getErpService();
-        ErpRefundUpdateRequest request=new ErpRefundUpdateRequest();
-        request.setTid(order.getOrderCode());
-        request.setOid(order.getOrderCode());
-        request.setRefund_state(1);
-        request.setStoreAfterSalesId(storeAfterSales.getId());
-        request.setOrderStatus(orderStatus);
-        if (StringUtils.isNotBlank(order.getExtendOrderId())){
-            BaseResponse response=erpOrderService.refundUpdateLive(request);
-            if(response.getSuccess()){
-                return R.ok();
+            IErpOrderService erpOrderService = getErpService();
+            ErpRefundUpdateRequest request=new ErpRefundUpdateRequest();
+            request.setTid(order.getOrderCode());
+            request.setOid(order.getOrderCode());
+            request.setRefund_state(1);
+            request.setStoreAfterSalesId(storeAfterSales.getId());
+            request.setOrderStatus(orderStatus);
+            if (StringUtils.isNotBlank(order.getExtendOrderId())){
+                BaseResponse response=erpOrderService.refundUpdateLive(request);
+                if(response.getSuccess()){
+                    return R.ok();
+                }
+                else{
+                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+                    return R.error(response.getErrorDesc());
+                }
             }
-            else{
-                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                return R.error(response.getErrorDesc());
+            return R.ok();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } finally {
+            // 释放锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                log.info("申请售后订单锁释放成功,订单号:{}", param.getOrderCode());
             }
         }
-        return R.ok();
     }
 
     @Override
@@ -1124,4 +1207,5 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 
         return R.ok();
     }
+
 }

+ 10 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -222,9 +222,9 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         integralLog.setUserId(userId);
         integralLog.setIntegral(Long.valueOf(record.getPointsAwarded()));
         integralLog.setBalance(newIntegral);
-        integralLog.setLogType(5); // 5-直播完课积分
+        integralLog.setLogType(25); // 5-直播完课积分
         integralLog.setBusinessId("live_completion_" + recordId); // 业务ID:直播完课记录ID
-        integralLog.setBusinessType(5); // 5-直播完课
+        integralLog.setBusinessType(25); // 5-直播完课
         integralLog.setStatus(1);
         integralLog.setCreateTime(new Date());
         fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog);
@@ -261,6 +261,14 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         return recordMapper.selectRecordsByUser(liveId, userId);
     }
 
+    /**
+     * 根据用户和日期查询完课记录
+     */
+    @Override
+    public LiveCompletionPointsRecord selectByUserAndDate(Long liveId, Long userId, Date date) {
+        return recordMapper.selectByUserAndDate(liveId, userId, date);
+    }
+
     /**
      * 从直播配置中获取完课积分配置
      */

+ 30 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -192,6 +192,36 @@ public class LiveDataServiceImpl implements ILiveDataService {
         return R.ok().put("list", liveDataList).put("data", statistics).put("total", total);
     }
 
+    @Override
+    public List<LiveDataListVo> exportLiveData(LiveDataParam param){
+        List<Live> lives = liveMapper.listLiveData(param);
+        int total = liveMapper.listLiveDataCount(param);
+
+        if (lives == null || lives.isEmpty()) {
+            LiveDataStatisticsVo statistics = new LiveDataStatisticsVo();
+            return Collections.emptyList();
+        }
+
+        // 获取直播间ID列表
+        List<Long> liveIds = lives.stream()
+                .map(Live::getLiveId)
+                .collect(Collectors.toList());
+
+        // 查询统计数据(根据live_watch_user表查询用户的在线时长,计算平均时长
+        // 根据live_video的文件时长,判断用户的完课情况
+        // 根据live_order查询直播间的销量额和订单数)
+        LiveDataStatisticsVo statistics = baseMapper.selectLiveDataStatistics(liveIds);
+        if (statistics == null) {
+            statistics = new LiveDataStatisticsVo();
+        }
+
+        // 查询列表数据(每个直播间的详细统计数据)
+        List<LiveDataListVo> liveDataList = baseMapper.selectLiveDataListByLiveIds(liveIds);
+        if (liveDataList == null) {
+            liveDataList = Collections.emptyList();
+        }
+        return liveDataList;
+    }
     /**
      * 查询直播数据
      *

+ 17 - 12
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -732,14 +732,16 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             }
 
             // 佣金处理
-            if (order.getCompanyUserId() == -1L) {
-                companyService.addCompanyTuiLiveMoney(order);
-            } else {
-                // 目前是一级佣金
-                FsStoreProduct product = JSONUtil.toBean(order.getItemJson(), FsStoreProduct.class);
-                List<FsStoreProductAttrValueScrm> productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(product.getProductId());
-                if (productAttrValues != null && !productAttrValues.isEmpty()) {
-                    userService.addTuiLiveMoney(order, productAttrValues);
+            if (order.getCompanyUserId() != null || order.getCompanyId() != null) {
+                if (order.getCompanyUserId() == -1L) {
+                    companyService.addCompanyTuiLiveMoney(order);
+                } else {
+                    // 目前是一级佣金
+                    FsStoreProduct product = JSONUtil.toBean(order.getItemJson(), FsStoreProduct.class);
+                    List<FsStoreProductAttrValueScrm> productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(product.getProductId());
+                    if (productAttrValues != null && !productAttrValues.isEmpty()) {
+                        userService.addTuiLiveMoney(order, productAttrValues);
+                    }
                 }
             }
 
@@ -3193,7 +3195,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         });
                         String s = (String) resultMap.get("package");
                         resultMap.put("packageValue", s);
-                        return R.ok().put("payType", param.getPayType()).put("result", resultMap);
+                        return R.ok().put("payType", param.getPayType()).put("result", resultMap).put("type", "hf");
                     } else {
                         return R.error(result.getResp_desc());
                     }
@@ -3532,6 +3534,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                     request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                     request.setOrgHfSeqId(payment.getTradeNo());
+                    request.setAppId(payment.getAppId());
                     HuiFuQueryOrderResult queryOrderResult = null;
                     try {
                         queryOrderResult = huiFuService.queryOrder(request);
@@ -3624,9 +3627,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
 
         LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
-        liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
-        liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
-        liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        if (ObjectUtil.isNotEmpty(liveUserFirstEntry)) {
+            liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
+            liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
+            liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        }
         String orderSn = OrderCodeUtils.getOrderSn();
 //        String orderSn = "123"; // todo yhq
         log.info("订单生成:"+orderSn);

+ 36 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java

@@ -8,8 +8,13 @@ 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.his.domain.FsUserIntegralLogs;
+import com.fs.his.enums.FsUserIntegralLogTypeEnum;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.mapper.FsUserIntegralLogsScrmMapper;
+import com.fs.hisStore.mapper.FsUserScrmMapper;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveMapper;
 import com.fs.live.mapper.LiveRedConfMapper;
@@ -57,6 +62,11 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
     private LiveRewardRecordMapper liveRewardRecordMapper;
     @Autowired
     private ILiveAutoTaskService liveAutoTaskService;
+    @Autowired
+    private FsUserScrmMapper fsUserScrmMapper;
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
 
     private static final String REDPACKET_REMAININGLOTS_KEY = "live:red:remainingLots:";
     private static final String REDPACKET_REMAININGNUM_KEY = "live:red:remainingNum:";
@@ -223,7 +233,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
     @Transactional
     public R claimRedPacket(RedPO red) {
         // String claimKey = REDPACKET_CLAIM_KEY + red.getRedId();
-        Object o = redisCache.hashGet(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()));
+        Object o = redisCache.hashGet(String.format(LiveKeysConstant. LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()));
         if (ObjectUtil.isNotEmpty(o)) {
             return R.error("您已经领取过红包了!");
         }
@@ -270,6 +280,31 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         record.setUserId(red.getUserId());
         record.setIntegral(integral);
         record.setCreateTime(new Date());
+        // 更新用户余额
+        BigDecimal balanceAmount = BigDecimal.valueOf(integral);
+        int updateResult = fsUserScrmMapper.incrIntegral(red.getUserId(), balanceAmount);
+        if (updateResult <= 0) {
+            log.error("更新用户余额失败,userId: {}, balance: {}", red.getUserId(), balanceAmount);
+            return R.error("更新用户余额失败");
+        }
+
+        // 查询用户当前余额
+        com.fs.hisStore.domain.FsUserScrm user = fsUserScrmMapper.selectFsUserById(red.getUserId());
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + integral;
+
+        // 添加余额变动记录
+        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+        integralLogs.setUserId(red.getUserId());
+        integralLogs.setIntegral(integral);
+        integralLogs.setBalance(newIntegral);
+        integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_26.getValue()); // 3表示分享获得积分,可根据实际情况调整
+        integralLogs.setBusinessId(String.valueOf(red.getRedId()));
+        integralLogs.setBusinessType(Math.toIntExact(conf.getRedId())); // 1表示直播红包
+        integralLogs.setStatus(0);
+        integralLogs.setCreateTime(new Date());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+
         // WebSocket 通知
         //String msg = String.format("用户 %d 抢到了红包 %d,获得 %d 芳华币", userId, redId, integral);
         //WebSocketServer.notifyUsers(msg);

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

@@ -1,6 +1,7 @@
 package com.fs.live.service.impl;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -56,6 +57,7 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.fs.common.utils.sign.Md5Utils;
@@ -128,6 +130,10 @@ public class LiveServiceImpl implements ILiveService
     @Autowired
     LiveTagConfigMapper liveTagConfigMapper;
 
+    @Autowired
+    @Lazy
+    private ILiveWatchUserService liveWatchUserService;
+
     private static String TOKEN_VALID_CODE = "40001";
 
     private static volatile Integer version = 0;
@@ -294,6 +300,8 @@ public class LiveServiceImpl implements ILiveService
 			long seconds = live.getStartTime().until(now, ChronoUnit.SECONDS);
 			liveVo.setNowDuration(seconds);
 		}
+        Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(live.getLiveId());
+        liveVo.setLiveFlag(liveFlagWithCache.get("liveFlag"));
         ThreadUtil.execute(()->{
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()));
             redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()), liveVo,LiveKeysConstant.LIVE_HOME_PAGE_DETAIL_EXPIRE, TimeUnit.SECONDS);
@@ -975,7 +983,38 @@ public class LiveServiceImpl implements ILiveService
             redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
             redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
         });
+        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey);
+        String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey2);
+
+        // 将开启的直播间信息写入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);
+        }
 
         return R.ok();
     }

+ 39 - 10
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -13,6 +13,7 @@ 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.enums.ExceptionCodeEnum;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserService;
@@ -41,6 +42,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
@@ -399,7 +401,21 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         try {
             // 从 Redis 获取用户进入时间
             String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
-            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            Object entryTimeObj = redisCache.getCacheObject(entryTimeKey);
+            Long entryTime = null;
+            if (entryTimeObj != null) {
+                if (entryTimeObj instanceof Long) {
+                    entryTime = (Long) entryTimeObj;
+                } else if (entryTimeObj instanceof String) {
+                    try {
+                        entryTime = Long.parseLong((String) entryTimeObj);
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析进入时间字符串为Long: {}", entryTimeObj);
+                    }
+                } else if (entryTimeObj instanceof Number) {
+                    entryTime = ((Number) entryTimeObj).longValue();
+                }
+            }
             // 获取当前直播/回放状态
             Map<String, Integer> flagMap = this.getLiveFlagWithCache(liveId);
             Integer currentLiveFlag = flagMap.get("liveFlag");
@@ -654,10 +670,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
         //用户不存在唤起重新授权
         if (fsUser == null) {
-            return R.error(401, "未授权");
+            return R.error(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
         }
         if (fsUser.getStatus() == 0) {
-            return R.error("会员被停用,无权限,请联系客服!");
+            return R.error(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
         }
         //未注册提示
         String noRegisterMsg = "由于您还未完成注册,请联系伴学助手完成注册即可观看!";
@@ -669,7 +685,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         } else if (null != param.getQwExternalId()) {
             return handleLivePerson(param,fsUser, noMemberMsg, noRegisterMsg);
         } else {
-            return R.error("直播参数错误");
+            return R.error(ExceptionCodeEnum.LIVE_PARAM_ERROR.getCode(), ExceptionCodeEnum.LIVE_PARAM_ERROR.getDescription());
         }
 
     }
@@ -859,10 +875,19 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     @Override
     @Async
     public void qwTagMarkByLiveWatchLog(Long liveId) {
+        //休眠2分钟 等待看课中断的所有看课记录状态被正确处理标记
+        try {
+            Thread.sleep(120000L);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
         log.info("处理直播间打标签: liveId={}", liveId);
         //查询直播间的标签配置
         List<LiveTagItemVO> liveTagConfig = liveTagConfigMapper.getLiveTagListByliveId(liveId);
         log.info("处理直播间打标签: liveTagConfig={}", liveTagConfig);
+        if(null == liveTagConfig || liveTagConfig.isEmpty()){
+            return;
+        }
         /**
          * 8	回放已下单
          * 7	直播已下单
@@ -881,7 +906,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 ));
         //查询直播间的看课记录
         List<LiveWatchLog> liveWatchLogs = liveWatchLogMapper.selectLiveWatchLogByLiveId(liveId);
-
+        log.info("处理直播间打标签: liveWatchLogs={}", liveWatchLogs);
+        if(null == liveWatchLogs || liveWatchLogs.isEmpty()){
+            return;
+        }
         //根据配置给每位用户打上标签
         List<HandleUserTagVO> handleUserTagVOS = new ArrayList<>();
         liveWatchLogs.forEach(liveLog -> {
@@ -936,6 +964,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             }
             handleUserTagVOS.add(addItem);
         });
+        log.info("处理直播间打标签最终打标签:{}",handleUserTagVOS);
         handleUserTags2Qw(handleUserTagVOS);
     }
 
@@ -1065,7 +1094,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         try {
 
             LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
-            
+
             if (liveWatchUser != null) {
                 if (liveWatchUser.getOnlineSeconds() == null || duration > liveWatchUser.getOnlineSeconds()) {
                     liveWatchUser.setOnlineSeconds(duration);
@@ -1094,25 +1123,25 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     public Long getTotalWatchDuration(Long liveId, Long userId) {
         try {
             long totalDuration = 0L;
-            
+
             // 1. 查询直播观看记录(liveFlag=1, replayFlag=0)
             LiveWatchUser liveRecord = baseMapper.selectByUniqueIndex(liveId, userId, 1, 0);
             if (liveRecord != null && liveRecord.getOnlineSeconds() != null) {
                 totalDuration += liveRecord.getOnlineSeconds();
             }
-            
+
             // 2. 查询回放观看记录(liveFlag=0, replayFlag=1)
             LiveWatchUser replayRecord = baseMapper.selectByUniqueIndex(liveId, userId, 0, 1);
             if (replayRecord != null && replayRecord.getOnlineSeconds() != null) {
                 totalDuration += replayRecord.getOnlineSeconds();
             }
-            
+
             log.debug("查询总观看时长: liveId={}, userId={}, liveDuration={}, replayDuration={}, total={}",
                     liveId, userId,
                     liveRecord != null ? liveRecord.getOnlineSeconds() : 0,
                     replayRecord != null ? replayRecord.getOnlineSeconds() : 0,
                     totalDuration);
-            
+
             return totalDuration;
         } catch (Exception e) {
             log.error("查询总观看时长失败: liveId={}, userId={}", liveId, userId, e);

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

@@ -140,6 +140,14 @@ public class LiveAfterSalesVo {
     @Excel(name = "下单时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
+    /** 创建时间 */
+    @Excel(name = "下单开始时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTimeBegin;
+    /** 创建时间 */
+    @Excel(name = "下单结束时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTimeEnd;
 
 
 

+ 19 - 0
fs-service/src/main/java/com/fs/live/vo/LiveDataListVo.java

@@ -1,6 +1,7 @@
 package com.fs.live.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -15,59 +16,77 @@ import java.util.Date;
 @Data
 public class LiveDataListVo {
     /** 直播ID */
+    @Excel(name = "直播ID")
     private Long liveId;
 
     /** 直播名称 */
+    @Excel(name = "直播名称")
     private String liveName;
 
     /** 直播类型 1直播,2录播,3直播回放 */
+    @Excel(name = "直播类型 1直播,2录播,3直播回放")
     private Integer liveType;
 
     /** 直播状态 1未开播 2直播中 3已结束 4直播回放中 */
+    @Excel(name = "直播状态 1未开播 2直播中 3已结束 4直播回放中")
     private Integer status;
 
     /** 开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date startTime;
 
     /** 结束时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date finishTime;
 
     /** 累计观看人数 */
+    @Excel(name = "累计观看人数")
     private Long totalViewers = 0L;
 
     /** 直播观看人数 */
+    @Excel(name = "直播观看人数")
     private Long liveViewers = 0L;
 
     /** 回放观看人数 */
+    @Excel(name = "回放观看人数")
     private Long playbackViewers = 0L;
 
     /** 直播平均时长(秒) */
+    @Excel(name = "直播平均时长")
     private Long liveAvgDuration = 0L;
 
     /** 回放平均时长(秒) */
+    @Excel(name = "回放平均时长")
     private Long playbackAvgDuration = 0L;
 
     /** 累计完课人数 */
+    @Excel(name = "累计完课人数")
     private Long totalCompletedCourses = 0L;
 
     /** 直播完课人数 */
+    @Excel(name = "直播完课人数")
     private Long liveCompletedCourses = 0L;
 
     /** 回放完课人数 */
+    @Excel(name = "回放完课人数")
     private Long playbackCompletedCourses = 0L;
 
     /** GMV(总销售额) */
+    @Excel(name = "总销售额")
     private BigDecimal gmv = BigDecimal.ZERO;
 
     /** 付费人数 */
+    @Excel(name = "付费人数")
     private Long paidUsers = 0L;
 
     /** 付费单数 */
+    @Excel(name = "付费单数")
     private Long paidOrders = 0L;
 
     /** 销量统计 */
+    @Excel(name = "销量统计")
     private Long salesCount = 0L;
 }
 

+ 1 - 1
fs-service/src/main/java/com/fs/live/vo/LiveOrderListVo.java

@@ -136,7 +136,7 @@ public class LiveOrderListVo extends BaseEntity {
     private String isDel;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal costPrice;
 
     /** 核销码 */

+ 1 - 1
fs-service/src/main/java/com/fs/live/vo/LiveOrderVoZm.java

@@ -118,7 +118,7 @@ public class LiveOrderVoZm{
     @Excel(name = "销售价格")
     private BigDecimal price;
 
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
     @Excel(name = "结算价格")

+ 2 - 1
fs-service/src/main/java/com/fs/live/vo/LiveVo.java

@@ -58,7 +58,8 @@ public class LiveVo {
     private Integer previewVideoType;
     private Long previewVideoId;
     private Integer globalVisible;
-    
+    private Integer liveFlag;
+
     /** 是否开启直播完课积分功能 */
     private Boolean completionPointsEnabled;
     

+ 1 - 1
fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java

@@ -56,7 +56,7 @@ public class MergedOrderExportVO implements Serializable
     private BigDecimal price;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
     /** 商品金额 */

+ 1 - 0
fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java

@@ -24,4 +24,5 @@ public class MqTopicConstant {
      * 转化追踪consumer
      */
     public static final String CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP = "conversion-topic-tracking-consumer-group";
+    public static final String CONVERSION_TRACKING_GROUP = "conversion-tracking-group";
 }

+ 16 - 7
fs-service/src/main/java/com/fs/newAdv/domain/Lead.java

@@ -31,10 +31,12 @@ public class Lead implements Serializable {
     private Long siteId;
 
     /**
-     * 链路id唯一(clickId)
+     * 链路id唯一
      */
     private String traceId;
 
+    private String clickId;
+
     /**
      * 来源平台
      */
@@ -76,16 +78,15 @@ public class Lead implements Serializable {
     /**
      * 外部联系人id 添加人id
      */
-    private String externalUserId;
-    /**
-     * 属于用户id 被添加的id
-     */
-    private String userId;
-    private String corpId;
+    private Long externalId;
     /**
      * 群Id
      */
     private String chatId;
+    /**
+     * 企微主题Id
+     */
+    private String corpId;
     /**
      * 是否添加企微 1是 0否
      */
@@ -104,6 +105,10 @@ public class Lead implements Serializable {
      */
     private String openid;
     private String unionid;
+    /**
+     * 微信名称 unionid不存在时关联
+     */
+    private String weiChatName;
     /**
      * 小程序授权1是 0否
      */
@@ -179,5 +184,9 @@ public class Lead implements Serializable {
      * 落地页访问时间
      */
     private LocalDateTime landingPageTs;
+    /**
+     * 落地页原始Url
+     */
+    private String viewUrl;
 }
 

+ 12 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/QwExternalIdBindTrackReq.java

@@ -0,0 +1,12 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QwExternalIdBindTrackReq implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long qwExternalId;
+    private String unionid;
+}

+ 10 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/TraceIdDto.java

@@ -0,0 +1,10 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class TraceIdDto implements Serializable {
+    private String traceId;
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff