Bladeren bron

Merge remote-tracking branch 'origin/master'

ct 4 dagen geleden
bovenliggende
commit
abf0f8048e
100 gewijzigde bestanden met toevoegingen van 2340 en 555 verwijderingen
  1. 4 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java
  2. 12 1
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  3. 5 5
      fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java
  4. 15 14
      fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java
  5. 2 2
      fs-ad-new-api/src/main/resources/application.yml
  6. 1 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java
  7. 7 9
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  8. 10 14
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  9. 9 1
      fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java
  10. 78 23
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  11. 298 0
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  12. 5 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java
  13. 7 0
      fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java
  14. 1 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsIntegralGoodsController.java
  15. 2 2
      fs-company/src/main/resources/application.yml
  16. 130 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  17. 14 4
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  18. 8 6
      fs-live-app/src/main/java/com/fs/live/controller/LiveController.java
  19. 34 66
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  20. 308 53
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  21. 6 0
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  22. 2 0
      fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  23. 46 37
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  24. 7 2
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  25. 7 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  26. 7 5
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  27. 24 0
      fs-service/src/main/java/com/fs/erp/exception/JstRateLimitException.java
  28. 57 8
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  29. 4 3
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  30. 6 0
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  31. 1 1
      fs-service/src/main/java/com/fs/his/param/FsIntegralCartParam.java
  32. 4 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  33. 44 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  34. 46 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  35. 10 0
      fs-service/src/main/java/com/fs/his/utils/RedisCacheUtil.java
  36. 3 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralCartVO.java
  37. 49 0
      fs-service/src/main/java/com/fs/hisStore/enums/LiveEnum.java
  38. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  39. 4 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  40. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java
  41. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  42. 28 3
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  43. 8 0
      fs-service/src/main/java/com/fs/live/domain/LiveUserFirstEntry.java
  44. 6 0
      fs-service/src/main/java/com/fs/live/domain/LiveVideo.java
  45. 9 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  46. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  47. 14 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  48. 3 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  49. 2 1
      fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  50. 6 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  51. 20 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  52. 5 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  53. 6 6
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  54. 33 25
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  55. 29 4
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  56. 114 13
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  57. 9 0
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  58. 8 6
      fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java
  59. 7 0
      fs-service/src/main/java/com/fs/live/vo/MergedOrderVO.java
  60. 1 1
      fs-service/src/main/java/com/fs/newAdv/domain/Lead.java
  61. 1 0
      fs-service/src/main/java/com/fs/newAdv/domain/PromotionAccount.java
  62. 4 0
      fs-service/src/main/java/com/fs/newAdv/domain/Site.java
  63. 1 1
      fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java
  64. 47 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/AbstractApiClient.java
  65. 2 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/IAccessTokenClient.java
  66. 3 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/IApiClient.java
  67. 50 54
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/BaiduApiClient.java
  68. 1 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/IQIYIApiClient.java
  69. 1 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OPPOApiClient.java
  70. 110 89
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java
  71. 35 41
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/TencentApiClient.java
  72. 1 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/VIVOApiClient.java
  73. 22 18
      fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java
  74. 4 4
      fs-service/src/main/java/com/fs/newAdv/service/impl/SiteStatisticsServiceImpl.java
  75. 2 0
      fs-service/src/main/java/com/fs/newAdv/vo/AccessTokenVo.java
  76. 2 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java
  77. 2 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopTempSetting.java
  78. 3 1
      fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java
  79. 7 0
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  80. 2 2
      fs-service/src/main/resources/application-config-dev-jnlzjk.yml
  81. 1 0
      fs-service/src/main/resources/application-config-druid-cfryt-test.yml
  82. 2 1
      fs-service/src/main/resources/application-config-druid-cfryt.yml
  83. 1 0
      fs-service/src/main/resources/application-config-druid-fby.yml
  84. 1 0
      fs-service/src/main/resources/application-config-druid-gzzdy.yml
  85. 2 1
      fs-service/src/main/resources/application-config-druid-hat.yml
  86. 1 0
      fs-service/src/main/resources/application-config-druid-hst.yml
  87. 2 2
      fs-service/src/main/resources/application-config-druid-hsyy.yml
  88. 2 2
      fs-service/src/main/resources/application-config-druid-jnlzjk.yml
  89. 1 0
      fs-service/src/main/resources/application-config-druid-kyt.yml
  90. 2 1
      fs-service/src/main/resources/application-config-druid-qdtst.yml
  91. 8 0
      fs-service/src/main/resources/application-config-druid-sft.yml
  92. 104 0
      fs-service/src/main/resources/application-config-druid-shdn.yml
  93. 2 2
      fs-service/src/main/resources/application-config-druid-sxjz.yml
  94. 2 1
      fs-service/src/main/resources/application-config-druid-syysy.yml
  95. 2 1
      fs-service/src/main/resources/application-config-druid-yxj.yml
  96. 141 0
      fs-service/src/main/resources/application-config-shdn.yml
  97. 6 0
      fs-service/src/main/resources/application-dev-jnlzjk.yml
  98. 165 0
      fs-service/src/main/resources/application-druid-shdn.yml
  99. 1 0
      fs-service/src/main/resources/mapper/his/FsIntegralCartMapper.xml
  100. 1 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

+ 4 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java

@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDateTime;
 
 /**
  * 广告商回调接口
@@ -114,10 +115,13 @@ public class CallbackController {
                 .appId(byId.getAppId())
                 .authCode(authCode)
                 .appSecret(byId.getAppSecret())
+                .appSecret(byId.getAppSecret())
                 .build());
         if (ObjectUtil.isNotEmpty(accessToken)) {
             byId.setAccessToken(accessToken.getAccessToken());
             byId.setRefreshToken(accessToken.getRefreshToken());
+            byId.setExpireTime(accessToken.getExpireTime());
+            byId.setUpdateTime(LocalDateTime.now());
             promotionAccountService.updateById(byId);
         } else {
             log.error("获取accessToken失败:{}", byId.getId());

+ 12 - 1
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -5,6 +5,8 @@ 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;
@@ -134,10 +136,13 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         Lead byTraceId = leadService.getByTraceId(traceId);
         boolean isNewLead = ObjectUtil.isEmpty(byTraceId);
         if (isNewLead) {
-            byTraceId = new Lead();
+            IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserId));
+            byTraceId = advertiserAdapter.adaptCallbackData(allParams);
             byTraceId.setAdvertiserId(advertiserId);
             byTraceId.setSiteId(siteId);
             byTraceId.setTraceId(traceId);
+            // 设置站点和落地页的关联
+            setSiteByIdeaId(siteId, byTraceId.getIdeaId());
         } else {
             // 检查站点和广告商信息是否异常
             if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
@@ -182,6 +187,12 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         return res;
     }
 
+    private void setSiteByIdeaId(Long siteId, String ideaId) {
+        siteService.update(new LambdaUpdateWrapper<Site>()
+                .eq(Site::getId, siteId)
+                .set(Site::getIdeaId, ideaId));
+    }
+
     /**
      * 更新模板中的二维码信息
      */

+ 5 - 5
fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java

@@ -2,12 +2,12 @@ package com.fs.app.task;
 
 
 import cn.hutool.json.JSONUtil;
-import com.fs.newAdv.enums.AdvertiserTypeEnum;
-import com.fs.newAdv.integration.client.IApiClient;
-import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.common.annotation.DistributeLock;
 import com.fs.common.constant.SystemConstant;
 import com.fs.newAdv.domain.ConversionLog;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.newAdv.mapper.ConversionLogMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -37,8 +37,8 @@ public class ConversionRetryTask {
      * 转化回传重试任务
      * cron: 每10分钟执行
      */
-    // @Scheduled(cron = "0 */1 * * * ?")
-    @DistributeLock(scene = "task", key = "conversion_retry", waitTime = 0, errorMsg = "任务已执行")
+    @Scheduled(cron = "0 */5 * * * ?")
+    @DistributeLock(scene = "task", key = "conversion_retry", waitTime = 0, errorMsg = "conversion_retry任务已执行")
     public void execute() {
         // 查询待重试的转化记录
         List<ConversionLog> pendingList = conversionLogMapper.selectPendingConversions();

+ 15 - 14
fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java

@@ -2,6 +2,7 @@ package com.fs.app.task;
 
 import cn.hutool.core.date.DateUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fs.common.annotation.DistributeLock;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.service.ILeadService;
 import com.fs.newAdv.service.ISiteStatisticsService;
@@ -48,6 +49,7 @@ public class DataSyncTask {
      * cron: 每天00:10
      */
     @Scheduled(cron = "0 10 0 * * ?")
+    @DistributeLock(scene = "task", key = "sync_yesterday_data", waitTime = 0, errorMsg = "sync_yesterday_data任务已执行")
     public void syncYesterdayData() {
         String batchNo = DateUtil.format(LocalDateTime.now().minusDays(1), "yyyy-MM-dd");
         statisticsService.syncData(batchNo, 1);
@@ -57,8 +59,9 @@ public class DataSyncTask {
      * 数据同步任务->当日数据
      * cron: 每1小时统计站点数据
      */
-    @Scheduled(cron = "0 0 0/1 * * ?")
-    public void syncTodayData() {
+    @Scheduled(cron = "0 0/1 * * * ?")
+    @DistributeLock(scene = "task", key = "sync_today_data", waitTime = 0, errorMsg = "sync_today_data任务已执行")
+    public void syncTodayData() throws InterruptedException {
         String batchNo = DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd");
         statisticsService.syncData(batchNo, 1);
     }
@@ -68,9 +71,8 @@ public class DataSyncTask {
      * 今日加群数据
      * cron: 每1小时统计站点数据
      */
-    // @Scheduled(cron = "0 0 0/1 * * ?")
-    @Scheduled(cron = "0 0/1 * * * ?")
-
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_group_to_day_data", waitTime = 0, errorMsg = "wei_chat_group_to_day_data任务已执行")
     public void weiChatGroupToDayData() {
         // 统计今日加群数量
         Optional.ofNullable(leadService.getToDayGroupNum())
@@ -97,9 +99,8 @@ public class DataSyncTask {
      * 累计加群数据
      * cron: 每天00:20
      */
-    // @Scheduled(cron = "0 20 0 * * ?")
-    @Scheduled(cron = "0 0/1 * * * ?")
-
+    @Scheduled(cron = "0 20 0 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_group_count_data", waitTime = 0, errorMsg = "wei_chat_group_count_data任务已执行")
     public void weiChatGroupCountData() {
         // 统计累积加群数量
         Optional.ofNullable(leadService.getYesterdayGroupNum())
@@ -125,8 +126,8 @@ public class DataSyncTask {
      * 微信当天数据
      * cron: 一小时执行一次
      */
-    // @Scheduled(cron = "0 0 0/1 * * ?")
-    @Scheduled(cron = "0 0/1 * * * ?")
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_to_day_data", waitTime = 0, errorMsg = "wei_chat_to_day_data任务已执行")
     public void weiChatToDayData() {
         // 统计累积加微数量
         Optional.ofNullable(leadService.getToDayWeiChatNum())
@@ -174,8 +175,8 @@ public class DataSyncTask {
      * 微信累计数据
      * cron: 每天00:30
      */
-    // @Scheduled(cron = "0 30 0 * * ?")
-    @Scheduled(cron = "0 0/1 * * * ?")
+    @Scheduled(cron = "0 30 0 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_count_data", waitTime = 0, errorMsg = "wei_chat_count_data任务已执行")
     public void weiChatCountData() {
         // 统计累积加微数量
         Optional.ofNullable(leadService.getYesterdayWeiChatNum())
@@ -194,7 +195,7 @@ public class DataSyncTask {
                                             qwAssignRuleUserService.update(
                                                     new LambdaUpdateWrapper<QwAssignRuleUser>()
                                                             .eq(QwAssignRuleUser::getId, k)
-                                                            .set(QwAssignRuleUser::getAssignNumCount, v+byId.getAssignNumCount())
+                                                            .set(QwAssignRuleUser::getAssignNumCount, v + byId.getAssignNumCount())
                                             );
                                         }
                                     });
@@ -213,7 +214,7 @@ public class DataSyncTask {
                                             qwAssignRuleUserService.update(
                                                     new LambdaUpdateWrapper<QwAssignRuleUser>()
                                                             .eq(QwAssignRuleUser::getId, k)
-                                                            .set(QwAssignRuleUser::getAddNumCount, v+byId.getAddNumCount())
+                                                            .set(QwAssignRuleUser::getAddNumCount, v + byId.getAddNumCount())
                                             );
                                         }
                                     });

+ 2 - 2
fs-ad-new-api/src/main/resources/application.yml

@@ -4,6 +4,6 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: dev
-    active: druid-ylrz
+    active: dev
+#    active: druid-ylrz
 

+ 1 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java

@@ -117,6 +117,7 @@ public class FsIntegralGoodsController extends BaseController
     {
 
         redisCacheUtil.delRedisKey("getIntegralGoodsList");
+        redisCacheUtil.delSpringCacheKey("getIntegralGoodsById", fsIntegralGoods.getGoodsId());
         redisCacheUtil.delRedisKey("getIntegralGoodsById");
         return toAjax(fsIntegralGoodsService.updateFsIntegralGoods(fsIntegralGoods));
     }

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

@@ -246,23 +246,21 @@ public class LiveTask {
     public void deliveryOp() {
         List<LiveOrder> list = liveOrderService.selectUpdateExpress();
         if(list == null || list.isEmpty()) return;
-        if (list.size() > 50) {
-            list = list.subList(0, 50);
-        }
-        Date nowDate = DateUtils.getNowDate();
-        for (LiveOrder order : list) {
-            order.setUpdateTime(nowDate);
-        }
-        liveOrderService.batchUpdateTime(list);
+
         for (LiveOrder order : list) {
+            order.setUpdateTime(new Date());
+            liveOrderService.updateLiveOrder(order);
             ErpOrderQueryRequert request = new ErpOrderQueryRequert();
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
+            if(!response.getSuccess() && "429".equals(response.getCode())){
+                break;
+            }
             if (erpOrderService != dfOrderService) {
                 if (response.getOrders() != null && !response.getOrders().isEmpty()) {
                     for (ErpOrderQuery orderQuery : response.getOrders()) {
-                        if (orderQuery.getDeliverys() != null && orderQuery.getDeliverys().size() > 0) {
+                        if (orderQuery.getDeliverys() != null && !orderQuery.getDeliverys().isEmpty()) {
                             for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
                                 if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
                                     //更新商订单状态 删除REDIS

+ 10 - 14
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -221,30 +221,26 @@ public class MallStoreTask
     public void deliveryOp()
     {
         List<FsStoreOrderScrm> list = fsStoreOrderMapper.selectUpdateExpress();
-        if (list != null && list.size() > 50) {
-            list = list.subList(0, 50);
-        }
         Date nowDate = DateUtils.getNowDate();
-        for (FsStoreOrderScrm order : list) {
-            order.setUpdateTime(nowDate);
-        }
-        if (list!= null && !list.isEmpty()){
-            fsStoreOrderMapper.batchUpdateTime(list);
-        }
         for (FsStoreOrderScrm order : list){
+            order.setUpdateTime(new Date());
+            orderService.updateFsStoreOrderDb(order);
             ErpOrderQueryRequert request = new ErpOrderQueryRequert();
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getScrmOrder(request);
+            if(!response.getSuccess() && "429".equals(response.getCode())){
+                break;
+            }
             if (erpOrderService != dfOrderService) {
                 if(response.getOrders()!=null && !response.getOrders().isEmpty()){
                     for(ErpOrderQuery orderQuery : response.getOrders()){
-                        if(orderQuery.getDeliverys()!=null&&orderQuery.getDeliverys().size()>0){
-                            for(ErpDeliverys delivery:orderQuery.getDeliverys()){
-                                if(delivery.getDelivery()&&StringUtils.isNotEmpty(delivery.getMail_no())){
+                        if (orderQuery.getDeliverys() != null && !orderQuery.getDeliverys().isEmpty()) {
+                            for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+                                if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
                                     //更新商订单状态 删除REDIS
-                                    orderService.deliveryOrder(order.getOrderCode(),delivery.getMail_no(),delivery.getExpress_code(),delivery.getExpress_name());
-                                    redisCache.deleteObject(DELIVERY+":"+order.getExtendOrderId());
+                                    orderService.deliveryOrder(order.getOrderCode(), delivery.getMail_no(), delivery.getExpress_code(), delivery.getExpress_name());
+                                    redisCache.deleteObject(DELIVERY + ":" + order.getExtendOrderId());
                                 }
                             }
 

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

@@ -6,6 +6,9 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.hisStore.enums.CompanyEnum;
+import com.fs.hisStore.enums.LiveEnum;
 import com.fs.live.domain.LiveVideo;
 import com.fs.live.service.ILiveVideoService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +29,8 @@ public class LiveVideoController extends BaseController
 {
     @Autowired
     private ILiveVideoService liveVideoService;
-
+    @Autowired
+    private CloudHostProper cloudHostProper;
     /**
      * 查询直播视频列表
      */
@@ -89,6 +93,10 @@ public class LiveVideoController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody LiveVideo liveVideo)
     {
+        if (LiveEnum.contains(cloudHostProper.getCompanyName())) {
+            liveVideo.setVideoUrl(liveVideo.getLineOne());
+            liveVideo.setFinishStatus(1);
+        }
         return toAjax(liveVideoService.insertLiveVideo(liveVideo));
     }
 

+ 78 - 23
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -1,13 +1,21 @@
 package com.fs.live.controller;
 
+import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.service.IMergedOrderService;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
 import com.fs.live.param.MergedOrderQueryParam;
 import com.fs.live.vo.MergedOrderVO;
 import com.fs.live.vo.MergedOrderExportVO;
@@ -18,6 +26,7 @@ import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -25,6 +34,7 @@ import org.springframework.web.bind.annotation.RestController;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -43,6 +53,10 @@ public class OrderController extends BaseController
     // 设置最大导出数量限制为20000条
     private static final int maxExportCount = 20000;
 
+
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询合并订单列表
      */
@@ -65,6 +79,7 @@ public class OrderController extends BaseController
     /**
      * 导出合并订单列表
      */
+    @PreAuthorize("@ss.hasPermi('live:order:export')")
     @ApiOperation("导出合并订单列表")
     @Log(title = "合并订单", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
@@ -78,9 +93,21 @@ public class OrderController extends BaseController
         if (list != null && list.size() > maxExportCount) {
             return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
         }
-        
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                    System.out.println(e.getMessage());
+                }
+            }
+        }
+
         // 转换为导出VO
-        List<MergedOrderExportVO> exportList = convertToExportVO(list, false);
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, false,loginUser);
         
         // 如果数据量在限制范围内,正常导出
         ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
@@ -88,12 +115,13 @@ public class OrderController extends BaseController
     }
 
     /**
-     * 导出合并订单明细
+     * 导出合并订单(明文)
      */
-    @ApiOperation("导出合并订单明细")
-    @Log(title = "合并订单明细", businessType = BusinessType.EXPORT)
-    @GetMapping("/exportItems")
-    public AjaxResult exportItems(MergedOrderQueryParam param)
+    @ApiOperation("导出合并订单(明文)")
+    @Log(title = "合并订单(明文)", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('live:order:exportAll')")
+    @GetMapping("/exportDetails")
+    public AjaxResult exportDetails(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
@@ -103,18 +131,36 @@ public class OrderController extends BaseController
         if (list != null && list.size() > maxExportCount) {
             return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
         }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
-        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
-        return util.exportExcel(list, "合并订单明细");
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                }
+            }
+            //
+
+        }
+
+        // 转换为导出VO(明文模式,不脱敏)
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, true,loginUser);
+
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单(明文)");
     }
 
     /**
-     * 导出合并订单(明文)
+     * 导出合并订单明细
      */
-    @ApiOperation("导出合并订单(明文)")
-    @Log(title = "合并订单(明文)", businessType = BusinessType.EXPORT)
-    @GetMapping("/exportDetails")
-    public AjaxResult exportDetails(MergedOrderQueryParam param)
+    // 预留接口
+    @PreAuthorize("@ss.hasPermi('live:order:exportOther')")
+    @ApiOperation("导出合并订单明细")
+    @Log(title = "合并订单明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItems")
+    public AjaxResult exportItems(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
@@ -125,16 +171,17 @@ public class OrderController extends BaseController
             return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
         }
 
-        // 转换为导出VO(明文模式,不脱敏)
-        List<MergedOrderExportVO> exportList = convertToExportVO(list, true);
-
-        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
-        return util.exportExcel(exportList, "合并订单(明文)");
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细");
     }
 
+
+
     /**
      * 导出合并订单明细(明文)
      */
+    // 预留接口
+    @PreAuthorize("@ss.hasPermi('live:order:exportOther')")
     @ApiOperation("导出合并订单明细(明文)")
     @Log(title = "合并订单明细(明文)", businessType = BusinessType.EXPORT)
     @GetMapping("/exportItemsDetails")
@@ -178,7 +225,7 @@ public class OrderController extends BaseController
      * @param isPlainText 是否为明文模式(true:不脱敏,false:脱敏)
      * @return 导出VO列表
      */
-    private List<MergedOrderExportVO> convertToExportVO(List<MergedOrderVO> list, boolean isPlainText)
+    private List<MergedOrderExportVO> convertToExportVO(List<MergedOrderVO> list, boolean isPlainText,LoginUser loginUser)
     {
         if (list == null || list.isEmpty()) {
             return new ArrayList<>();
@@ -195,13 +242,13 @@ public class OrderController extends BaseController
             // 产品信息
             exportVO.setProductName(vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
-            exportVO.setProductSpec(vo.getProductSpec());
+            exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
             exportVO.setTotalNum(vo.getTotalNum());
             exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
             exportVO.setCost(vo.getCost());
-            exportVO.setFPrice(null); // 结算价,合并订单暂无此字段
+            exportVO.setFPrice(vo.getCost() != null ? vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())) : BigDecimal.ZERO); // 结算价,合并订单暂无此字段
+            exportVO.setPayPostage(vo.getPayDelivery());
             exportVO.setCateName(vo.getCateName());
-            
             // 收货信息
             exportVO.setRealName(vo.getRealName());
             if (isPlainText) {
@@ -245,6 +292,14 @@ public class OrderController extends BaseController
             exportVO.setPayMoney(vo.getPayMoney());
             exportVO.setPayPostage(vo.getPayDelivery()); // 额外运费,合并订单暂无此字段
             exportVO.setPayDelivery(vo.getPayDelivery());
+            if ((loginUser.getPermissions().contains("order:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
+                vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
+            } else {
+                vo.setPayPostage(BigDecimal.ZERO);
+                vo.setCost(BigDecimal.ZERO);
+                vo.setFPrice(BigDecimal.ZERO);
+                vo.setBankTransactionId("");
+            }
             
             return exportVO;
         }).collect(Collectors.toList());

+ 298 - 0
fs-company/src/main/java/com/fs/company/controller/live/OrderController.java

@@ -0,0 +1,298 @@
+package com.fs.company.controller.live;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.dto.StoreOrderProductDTO;
+import com.fs.hisStore.service.IMergedOrderService;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.vo.MergedOrderVO;
+import com.fs.live.vo.MergedOrderExportVO;
+import com.fs.common.utils.poi.ExcelUtil;
+import org.springframework.beans.BeanUtils;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.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.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 合并订单Controller
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Api("合并订单管理")
+@RestController
+@RequestMapping("/order")
+public class OrderController extends BaseController
+{
+    @Autowired
+    private IMergedOrderService mergedOrderService;
+    // 设置最大导出数量限制为20000条
+    private static final int maxExportCount = 20000;
+
+
+
+    /**
+     * 查询合并订单列表
+     */
+    @ApiOperation("查询合并订单列表")
+    @GetMapping("/list")
+    public TableDataInfo list(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return getDataTable(new ArrayList<>());
+        }
+
+        startPage();
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        for (MergedOrderVO vo : list) {
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setPhone(ParseUtils.parsePhone(vo.getPhone()));
+            vo.setSalesPhone(ParseUtils.parsePhone(vo.getSalesPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出合并订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:order:export')")
+    @ApiOperation("导出合并订单列表")
+    @Log(title = "合并订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return AjaxResult.error("请选择导出订单类型!");
+        }
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                    System.out.println(e.getMessage());
+                }
+            }
+        }
+
+        // 转换为导出VO
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, false);
+
+        // 如果数据量在限制范围内,正常导出
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单数据");
+    }
+
+    /**
+     * 导出合并订单(明文)
+     */
+    @PreAuthorize("@ss.hasPermi('live:order:exportAll')")
+    @ApiOperation("导出合并订单(明文)")
+    @Log(title = "合并订单(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportDetails")
+    public AjaxResult exportDetails(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return AjaxResult.error("请选择导出订单类型!");
+        }
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                }
+            }
+        }
+        // 转换为导出VO(明文模式,不脱敏)
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, true);
+
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单(明文)");
+    }
+
+    /**
+     * 导出合并订单明细
+     */
+    @ApiOperation("导出合并订单明细")
+    @Log(title = "合并订单明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItems")
+    public AjaxResult exportItems(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细");
+    }
+
+
+
+    /**
+     * 导出合并订单明细(明文)
+     */
+    @ApiOperation("导出合并订单明细(明文)")
+    @Log(title = "合并订单明细(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItemsDetails")
+    public AjaxResult exportItemsDetails(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细(明文)");
+    }
+
+    /**
+     * 导出合并订单发货单
+     */
+    @ApiOperation("导出合并订单发货单")
+    @Log(title = "合并订单发货单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportShipping")
+    public AjaxResult exportShipping(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单发货单");
+    }
+
+    /**
+     * 将 MergedOrderVO 转换为 MergedOrderExportVO
+     * @param list 原始数据列表
+     * @param isPlainText 是否为明文模式(true:不脱敏,false:脱敏)
+     * @return 导出VO列表
+     */
+    private List<MergedOrderExportVO> convertToExportVO(List<MergedOrderVO> list, boolean isPlainText)
+    {
+        if (list == null || list.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return list.stream().map(vo -> {
+            MergedOrderExportVO exportVO = new MergedOrderExportVO();
+
+            // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderCode(vo.getOrderCode());
+            exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
+            exportVO.setUserId(vo.getUserId());
+
+            // 产品信息
+            exportVO.setProductName(vo.getProductName());
+            exportVO.setBarCode(vo.getBarCode());
+            exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
+            exportVO.setTotalNum(vo.getTotalNum());
+            exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
+            exportVO.setCost(BigDecimal.ZERO);
+            exportVO.setFPrice(BigDecimal.ZERO); // 结算价,合并订单暂无此字段
+            exportVO.setPayPostage(vo.getPayDelivery());
+            exportVO.setCateName(vo.getCateName());
+
+            // 收货信息
+            exportVO.setRealName(vo.getRealName());
+            exportVO.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            exportVO.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            // 时间信息
+            exportVO.setCreateTime(vo.getCreateTime());
+            exportVO.setPayTime(vo.getPayTime());
+
+            // 物流信息
+            exportVO.setDeliverySn(vo.getDeliveryCode()); // 快递公司编号,合并订单暂无此字段
+            exportVO.setDeliveryName(vo.getDeliveryName()); // 快递公司,合并订单暂无此字段
+            exportVO.setDeliveryId(vo.getDeliveryId());
+
+            // 公司和销售信息
+            exportVO.setCompanyName(vo.getCompanyName());
+            exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
+
+            // 套餐信息
+            exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段
+            exportVO.setGroupBarCode(null); // 组合码,合并订单暂无此字段
+
+            // 凭证信息
+            exportVO.setIsUpload(null); // 是否上传凭证,合并订单暂无此字段
+            exportVO.setUploadTime(null); // 上传时间,合并订单暂无此字段
+
+            // 档期信息
+            exportVO.setScheduleName(null); // 归属档期,合并订单暂无此字段
+
+            // 银行交易流水号
+            exportVO.setBankTransactionId(vo.getBankTransactionId());
+
+            // 金额信息
+            exportVO.setTotalPrice(vo.getTotalPrice());
+            exportVO.setPayPrice(vo.getPayPrice());
+            exportVO.setPayMoney(vo.getPayMoney());
+            exportVO.setPayPostage(vo.getPayDelivery()); // 额外运费,合并订单暂无此字段
+            exportVO.setPayDelivery(vo.getPayDelivery());
+
+            return exportVO;
+        }).collect(Collectors.toList());
+    }
+}

+ 5 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java

@@ -10,6 +10,7 @@ import com.fs.newAdv.enums.AdvertiserTypeEnum;
 import com.fs.newAdv.service.IPromotionAccountService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -73,6 +74,7 @@ public class PromotionAccountController {
      * @param account 推广账号信息
      */
     @PostMapping
+    @Transactional(rollbackFor = Exception.class)
     public Result<Void> create(@RequestBody @Validated PromotionAccount account) {
 
         boolean success = promotionAccountService.save(account);
@@ -82,6 +84,9 @@ public class PromotionAccountController {
     }
 
     private void checkAuthUrl(PromotionAccount account) {
+        if (account.getApiSwitch() == 2){
+            return;
+        }
         if (account.getAdvertiserId().equals(AdvertiserTypeEnum.OCEANENGINE.getCode())){
             // 巨量
             account.setAuthUrl("https://open.oceanengine.com/audit/oauth.html?app_id="+account.getAppId()+"&state="+account.getId()+"&redirect_uri=https://track.mynatapp.cc/callback/oceanEngine/getAuthCode");

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java

@@ -82,6 +82,13 @@ public class FsUserController extends BaseController
     {
         fsUser.setIsDel(0);
         List<FsUser> list=fsUserService.selectFsUserList(fsUser);
+        if(list.isEmpty()){
+            //如果是加密的电话,需要加密后查询
+            if(StringUtils.isFullNumber(fsUser.getPhone()) && fsUser.getPhone().length() == 11){
+                fsUser.setPhone(encryptPhone(fsUser.getPhone()));
+            }
+            list = fsUserService.selectFsUserList(fsUser);
+        }
         return R.ok().put("data", list);
     }
 

+ 1 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsIntegralGoodsController.java

@@ -110,6 +110,7 @@ public class FsIntegralGoodsController extends BaseController
     {
 
         redisCacheUtil.delRedisKey("getIntegralGoodsList");
+        redisCacheUtil.delSpringCacheKey("getIntegralGoodsById", fsIntegralGoods.getGoodsId());
         redisCacheUtil.delRedisKey("getIntegralGoodsById");
         return toAjax(fsIntegralGoodsService.updateFsIntegralGoods(fsIntegralGoods));
     }

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

@@ -1,9 +1,9 @@
 server:
-  port: 7773
+  port: 8006
 # Spring配置
 spring:
   profiles:
-    active: druid-ylrz
+    active: dev
 #    active: druid-jnsyj-test
 #    active: druid-jnmy-test
 #    active: druid-jzzx-test

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

@@ -21,6 +21,8 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.ipad.IpadSendUtils;
 import com.fs.ipad.vo.*;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.mapper.LiveWatchLogMapper;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.domain.QwUserVideo;
@@ -63,6 +65,8 @@ public class IpadSendServer {
 
 
     private static final List<String> PROJECT_NAMES = Arrays.asList("济南联志健康", "北京存在文化","宽益堂");
+    private final LiveWatchLogMapper liveWatchLogMapper;
+
     private void sendMiniProgram(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap, Long companyId) {
         // 发送参数原本的appid
         String appid = content.getMiniprogramAppid();
@@ -432,6 +436,132 @@ public class IpadSendServer {
         return true;
     }
 
+    /**
+     * 直播间判定消息
+     * @param qwSopLogs
+     * @param setting
+     * @param qwUser
+     * @return
+     */
+    public boolean isSendLogsLive(QwSopLogs qwSopLogs, QwSopCourseFinishTempSetting setting, QwUser qwUser){
+        if(qwSopLogs.getSendStatus() != 3){
+            log.info("直播状态异常不发送:{}, LOGID: {}", qwUser.getQwUserName(), qwSopLogs.getId());
+            return false;
+        }
+        if(redisCache.getCacheObject("qw:user:id:" + qwUser.getId()) != null){
+            log.info("直播频率异常不发送:{}", qwUser.getQwUserName());
+            return false;
+        }
+
+        boolean noSop = qwSopLogs.getSendType() != 3 && qwSopLogs.getSendType() != 7;
+
+        if (qwSopLogs.getExpiryTime() == null && noSop) {
+            // 作废消息
+            log.warn("直播SOP_LOG_ID:{}, 直播SOP任务被删除", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "直播SOP任务被删除");
+            return false;
+        }
+
+        LocalDateTime sendTime = DateUtil.stringToLocalDateTime(qwSopLogs.getSendTime());
+        LocalDateTime expiryDateTime;
+
+        // 判断是否过期
+        if(qwSopLogs.getSendType() == 3 || qwSopLogs.getSendType() == 7){
+            expiryDateTime = sendTime.plusHours(12);
+        }else{
+            expiryDateTime = sendTime.plusHours(qwSopLogs.getExpiryTime());
+        }
+
+        if (LocalDateTime.now().isAfter(expiryDateTime)) {
+            // 作废消息
+            log.warn("直播SOP_LOG_ID:{}, 已过期,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "已过期,不发送");
+            return false;
+        }
+
+        if (setting.getCourseType() == null && noSop && setting.getType() == 2) {
+            log.warn("直播SOP_LOG_ID:{}, 模板未选消息类型,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "直播模板未选消息类型,不发送");
+            return false;
+        }
+
+        // 查询视频是否下架
+//        if(setting.getVideoId()!= null){
+//            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId( setting.getVideoId().longValue());
+//            if(video != null){
+//                if(video.getIsOnPut()!=null && video.getIsOnPut() == 1){
+//                    log.warn("SOP_LOG_ID:{}, 视频已下架,不发送", qwSopLogs.getId());
+//                    qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "视频已下架,不发送");
+//                    return false;
+//                }
+//            }
+//        }
+        Long queryLiveId = null;
+        queryLiveId = setting.getLiveId();
+        if(null == queryLiveId){
+            for (QwSopCourseFinishTempSetting.Setting a : setting.getSetting()) {
+                if (StringUtils.isNotBlank(a.getLiveId())) {
+                    queryLiveId = Long.valueOf(a.getLiveId());
+                    break;
+                }
+            }
+        }
+        LiveWatchLog liveWatchLog = liveWatchLogMapper.selectOneLogByLiveIdAndQwUserIdAndExternalId(
+                queryLiveId,
+                String.valueOf(qwUser.getId()),
+                qwSopLogs.getExternalId()
+        );
+        Integer courseType = setting.getCourseType();
+        String logId = qwSopLogs.getId();
+        if(null != liveWatchLog){
+                    if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
+                        // 作废消息
+                        log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
+                        qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
+                        return false;
+                    }
+        }
+        else{
+            log.warn("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "无观看记录,不发送");
+            return false;
+        }
+//        if (qwSopLogs.getSendType() != 6 && noSop) {
+//            // 客户的信息
+////            QwExternalContactHParam contactHParam = new QwExternalContactHParam();
+////            contactHParam.setUserId(qwUser.getQwUserId().trim());
+////            contactHParam.setExternalUserId(qwSopLogs.getExternalUserId().trim());
+////            contactHParam.setCorpId(qwUser.getCorpId().trim());
+//            Integer courseType = setting.getCourseType();
+//            if (setting.getType() == 2 && courseType != 0) {// 课程消息,进行复杂的条件判断
+////                log.debug("企微查询:{}", contactHParam);
+////                Long qwExternalContactId = qwExternalContactMapper.getQwExternalContactId(contactHParam);
+//                FsCourseWatchLog watchLog = watchLogService.getWatchCourseLogVideoBySop(
+//                        setting.getVideoId().longValue(),
+//                        String.valueOf(qwUser.getId()),
+//                        qwSopLogs.getExternalId()
+//                );
+//                log.debug("ID:{}-看课记录参数:videoID:{}, qwUserID:{}, extID:{}", qwSopLogs.getId(), setting.getVideoId().longValue(), qwUser.getId(), qwSopLogs.getExternalId());
+//                log.debug("ID:{}-看课记录:{}", qwSopLogs.getId(), watchLog);
+//                String logId = qwSopLogs.getId();
+//                if (watchLog != null) {
+//                    //新逻辑
+//                    if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, watchLog.getLogType())) {
+//                        // 作废消息
+//                        log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
+//                        qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
+//                        return false;
+//                    }
+//                } else {
+//                    log.warn("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
+//                    qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "无观看记录,不发送");
+//                    return false;
+//                }
+//            }
+//        }
+        return true;
+    }
+
     public void send(QwSopCourseFinishTempSetting.Setting content, QwUser qwUser, QwSopLogs qwSopLogs, Map<String, FsCoursePlaySourceConfig> miniMap, BaseVo parentVo) {
         BaseVo vo = new BaseVo();
         vo.setId(Long.parseLong(qwSopLogs.getId()));

+ 14 - 4
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -191,10 +191,20 @@ public class SendMsg {
         for (QwSopLogs qwSopLogs : qwSopLogList) {
             long start2 = System.currentTimeMillis();
             QwSopCourseFinishTempSetting setting = JSON.parseObject(qwSopLogs.getContentJson(), QwSopCourseFinishTempSetting.class);
-            // 判断消息状态是否满足发送条件
-            if (!sendServer.isSendLogs(qwSopLogs, setting, user)) {
-                log.info("销售:{}, 消息发送条件未满足:{}", user.getQwUserName(), qwSopLogs.getId());
-                continue;
+            //直播的sendType:20单独走判断 其他的走以前的逻辑
+            boolean isSendLive = Integer.valueOf(20).equals(qwSopLogs.getSendType());
+            if(isSendLive){
+                if (!sendServer.isSendLogsLive(qwSopLogs, setting, user)) {
+                    log.info("销售:{}, 直播消息发送条件未满足:{}", user.getQwUserName(), qwSopLogs.getId());
+                    continue;
+                }
+            }
+            else{
+                // 判断消息状态是否满足发送条件
+                if (!sendServer.isSendLogs(qwSopLogs, setting, user)) {
+                    log.info("销售:{}, 消息发送条件未满足:{}", user.getQwUserName(), qwSopLogs.getId());
+                    continue;
+                }
             }
             log.info("进入发送消息状态:{}", qwSopLogs.getId());
             String key = "qw:logs:pad:send:id:" + qwSopLogs.getId();

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

@@ -129,12 +129,14 @@ public class LiveController {
 		videoService.updateFinishStatus(string.replace(".mp4", ".m3u8"));
 
 		return R.ok();
-//		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,
-//				errcode=1, errmsg=The push client actively stopped the push, event_time=1755571239,
-//				event_type=0, height=1080, idc_id=38, node=113.250.23.118, push_duration=1051237,
-//				sequence=721865018844564968, set_id=2, stream_id=673,
-//				stream_param=txSecret=A3EF362C9484D3D091C2E9B08C2C08CB&txTime=68A53145,
-//				user_ip=113.248.98.28, width=1920}
+//		{EventName=WorkflowFinish, WorkflowExecution={RunId=i07d29fabdaed11f0ac79525400de87bb, BucketId=bjzmky-1323137866,
+//				Object=course/20251112/1762939096674.mp4, CosHeaders=[{Key=Content-Type, Value=video/mp4}],
+//				WorkflowId=w1d48658c808643bd98bc0f0761ab38a7, WorkflowName=720视频转码, CreateTime=2025-12-17 10:06:42+0800,
+//				State=Success, Tasks=[{Type=Transcode, CreateTime=2025-12-17 10:06:42+0800, EndTime=2025-12-17 10:06:57+0800,
+//				State=Success, JobId=j07dd4e30daed11f084a9fd5b71d7a75a, Name=Transcode_1765251580610, TemplateId=t025ee3d5a5ea14d32a5a3d52d4c28c7d3,
+//				TemplateName=H264-HLS-标清, ResultInfo={ObjectCount=1, ObjectInfo=[
+//						{ObjectName=course/20251112/1762939096674-720.m3u8, ObjectUrl=https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/course/20251112/1762939096674-720.m3u8}]}}]}}
+
 
 	}
 

+ 34 - 66
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -41,16 +41,20 @@ public class LiveCompletionPointsTask {
     /**
      * 定时检查观看时长并创建完课记录
      * 每分钟执行一次
-     * 优化:使用Hash结构 + 防重复推送
+     * 优化:防重复推送 + 只查询开启了完课积分的直播间
      */
     @Scheduled(cron = "0 */1 * * * ?")
     public void checkCompletionStatus() {
         try {
-            List<Live> activeLives = liveService.selectNoEndLiveList();
+            // 只查询开启了完课积分配置的直播间
+            List<Live> activeLives = liveService.selectLiveListWithCompletionPointsEnabled();
             
             if (activeLives == null || activeLives.isEmpty()) {
+                log.debug("当前没有开启完课积分的直播间");
                 return;
             }
+            
+            log.info("开始检查完课状态, 开启完课积分的直播间数量: {}", activeLives.size());
 
             for (Live live : activeLives) {
                 try {
@@ -58,19 +62,24 @@ public class LiveCompletionPointsTask {
                     
                     // 使用Hash结构获取该直播间所有用户的观看时长
                     String hashKey = "live:watch:duration:hash:" + liveId;
-                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
+                    Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
                     
                     if (userDurations == null || userDurations.isEmpty()) {
+                        log.warn("直播间没有观看时长数据, liveId={}, liveName={}, Redis Key: {}, userDurations={}", 
+                                liveId, live.getLiveName(), hashKey, userDurations);
                         continue;
                     }
                     
+                    log.info("直播间有观看数据, liveId={}, liveName={}, 用户数: {}", 
+                            liveId, live.getLiveName(), userDurations.size());
+                    
                     // 3. 逐个用户处理
                     for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
                         try {
                             Long userId = Long.parseLong(entry.getKey().toString());
+                            Long duration = Long.parseLong(entry.getValue().toString());  // 从 Redis 直接获取观看时长
                             
-                            // 4. 检查并创建完课记录(传null,自动累计直播+回放时长)
-                            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, null);
+                            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
 
                             // 5. 检查是否有新的完课记录待领取,推送弹窗消息(防重复)
                             sendCompletionNotificationOnce(liveId, userId);
@@ -90,70 +99,29 @@ public class LiveCompletionPointsTask {
         }
     }
 
-    /**
-     * 发送完课通知(通过WebSocket推送弹窗) - 防重复版本
-     */
-    private void sendCompletionNotificationOnce(Long liveId, Long userId) {
-        try {
-            // 1. 检查 Redis 是否已推送过(防止每分钟都推送)
-            String notifyKey = "live:completion:notified:" + liveId + ":" + userId;
-            Boolean hasNotified = redisCache.hasKey(notifyKey);
-            
-            if (Boolean.TRUE.equals(hasNotified)) {
-                return;  // 已经推送过,不再重复推送
-            }
-            
-            // 2. 查询未领取的完课记录
-            List<LiveCompletionPointsRecord> unreceivedRecords = 
-                completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
-            
-            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
-                // 3. 构造弹窗消息
-                SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(liveId);
-                sendMsgVo.setUserId(userId);
-                sendMsgVo.setCmd("completionPoints");
-                sendMsgVo.setMsg("完成任务!");
-                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
-
-                // 4. 通过WebSocket发送给特定用户
-                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
-                
-                // 5. 记录已推送,24小时后过期(第二天可以再次推送)
-                redisCache.setCacheObject(notifyKey, "1", 24, TimeUnit.HOURS);
-                
-                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}, points={}", 
-                        liveId, userId, unreceivedRecords.get(0).getPointsAwarded());
-            }
-        } catch (Exception e) {
-            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+   private void sendCompletionNotificationOnce(Long liveId, Long userId) {
+    try {
+        // 查询未领取的完课记录
+        List<LiveCompletionPointsRecord> unreceivedRecords = 
+            completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+        
+        if (unreceivedRecords == null || unreceivedRecords.isEmpty()) {
+            return;
         }
-    }
 
-    /**
-     * 发送完课通知(通过WebSocket推送弹窗) - 旧版本(保留)
-     */
-    private void sendCompletionNotification(Long liveId, Long userId) {
-        try {
-            // 查询未领取的完课记录
-            List<LiveCompletionPointsRecord> unreceivedRecords = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
-            
-            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
-                // 构造弹窗消息
-                SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(liveId);
-                sendMsgVo.setUserId(userId);
-                sendMsgVo.setCmd("completionPoints");
-                sendMsgVo.setMsg("完成任务!");
-                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
-
-                // 通过WebSocket发送给特定用户(调用已有的发送方法)
-                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
-                
-                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}", liveId, userId);
-            }
+        SendMsgVo sendMsgVo = new SendMsgVo();
+        sendMsgVo.setLiveId(liveId);
+        sendMsgVo.setUserId(userId);
+        sendMsgVo.setCmd("completionPoints");
+        sendMsgVo.setMsg("完成任务!");
+        sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+        
+        webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+        
+        log.info("发送完课积分弹窗通知, liveId={}, userId={}, points={}", 
+                liveId, userId, unreceivedRecords.get(0).getPointsAwarded());
         } catch (Exception e) {
-            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+        log.error("发送完课通知失败", e);
         }
     }
 }

+ 308 - 53
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -73,6 +73,10 @@ public class Task {
     private ILiveCouponIssueService liveCouponIssueService;
     @Autowired
     private ILiveVideoService liveVideoService;
+    @Autowired
+    private ILiveWatchLogService liveWatchLogService;
+    @Autowired
+    private ILiveUserFirstEntryService liveUserFirstEntryService;
 
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
@@ -161,8 +165,8 @@ public class Task {
                     collect.forEach(liveAutoTask -> {
                         liveAutoTask.setCreateTime(null);
                         liveAutoTask.setUpdateTime(null);
-                        redisCache.redisTemplate.opsForZSet().add(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-                        redisCache.redisTemplate.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
+                        redisCache.zSetAdd(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
+                        redisCache.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
                 
@@ -205,7 +209,7 @@ public class Task {
                 webSocketServer.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
                 List<LiveAutoTask> collect = liveAutoTasks.stream().filter(liveAutoTask -> liveAutoTask.getLiveId().equals(live.getLiveId())).collect(Collectors.toList());
                 if (!collect.isEmpty()) {
-                    redisCache.redisTemplate.delete(key + live.getLiveId());
+                    redisCache.deleteObject(key + live.getLiveId());
                     collect.forEach(liveAutoTask -> {
                         liveAutoTask.setCreateTime(null);
                         liveAutoTask.setUpdateTime(null);
@@ -627,6 +631,7 @@ public class Task {
     @DistributeLock(key = "scanLiveTagMark", scene = "task")
     public void scanLiveTagMark() {
         try {
+
             // 获取所有打标签缓存的key
             String pattern = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, "*");
             Set<String> keys = redisCache.redisTemplate.keys(pattern);
@@ -636,8 +641,9 @@ public class Task {
             }
             
             long currentTimeMillis = System.currentTimeMillis();
+            LocalDateTime now = LocalDateTime.now();
             List<Long> processedLiveIds = new ArrayList<>();
-            
+            Date nowDate = new Date();
             for (String key : keys) {
                 try {
                     // 从Redis获取直播间信息
@@ -658,21 +664,114 @@ public class Task {
                         continue;
                     }
                     
+                    // 查询直播间信息
+                    Live live = liveService.selectLiveDbByLiveId(liveId);
+                    if (live == null || live.getStartTime() == null) {
+                        continue;
+                    }
                     // 计算结束时间:开始时间 + 视频时长(秒转毫秒)
                     long endTimeMillis = startTimeMillis + (videoDuration * 1000);
-                    
+
                     // 如果当前时间已经超过了结束时间,执行打标签操作
                     if (currentTimeMillis >= endTimeMillis) {
-                        log.info("直播间视频播放完成,开始打标签: liveId={}, startTime={}, videoDuration={}, endTime={}, currentTime={}", 
-                                liveId, startTimeMillis, videoDuration, endTimeMillis, currentTimeMillis);
+                        // 查询当前直播间的在线用户(liveFlag = 1, replayFlag = 0)
+                        LiveWatchUser queryUser = new LiveWatchUser();
+                        queryUser.setLiveId(liveId);
+                        queryUser.setLiveFlag(1);
+                        queryUser.setReplayFlag(0);
+                        List<LiveWatchUser> liveUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+
+                        if (liveUsers != null && !liveUsers.isEmpty()) {
+
+                            List<LiveWatchUser> updateLiveUsers = new ArrayList<>(); // 需要更新的直播用户
+                            List<LiveWatchUser> replayUsers = new ArrayList<>(); // 回放用户数据
+
+                            for (LiveWatchUser liveUser : liveUsers) {
+                                Long userId = liveUser.getUserId();
+                                if (userId == null) {
+                                    continue;
+                                }
+
+                                // 1. 计算并更新直播用户的在线时长
+                                // 优先从 Redis 获取进入时间
+                                String entryTimeKey = String.format("live:user:entry:time:%s:%s", liveId, userId);
+                                Long entryTime = redisCache.getCacheObject(entryTimeKey);
+
+                                // 如果没有 Redis 记录,使用数据库中的 updateTime
+                                if (entryTime == null) {
+                                    if (liveUser.getUpdateTime() != null) {
+                                        entryTime = liveUser.getUpdateTime().getTime();
+                                    } else if (liveUser.getCreateTime() != null) {
+                                        entryTime = liveUser.getCreateTime().getTime();
+                                    }
+                                }
+
+                                // 计算当前观看时长(秒)
+                                long currentWatchDuration = 0L;
+                                if (entryTime != null) {
+                                    currentWatchDuration = (currentTimeMillis - entryTime) / 1000;
+                                    if (currentWatchDuration < 0) {
+                                        currentWatchDuration = 0L;
+                                    }
+                                }
+
+                                // 加上历史在线时长
+                                Long historyOnlineSeconds = liveUser.getOnlineSeconds();
+                                if (historyOnlineSeconds == null) {
+                                    historyOnlineSeconds = 0L;
+                                }
+                                long totalOnlineSeconds = historyOnlineSeconds + currentWatchDuration;
+
+                                // 更新直播用户的在线时长
+                                liveUser.setOnlineSeconds(totalOnlineSeconds);
+                                liveUser.setUpdateTime(nowDate);
+                                updateLiveUsers.add(liveUser);
+
+                                // 2. 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始
+                                LiveWatchUser replayUser = new LiveWatchUser();
+                                replayUser.setLiveId(liveUser.getLiveId());
+                                replayUser.setUserId(liveUser.getUserId());
+                                replayUser.setMsgStatus(liveUser.getMsgStatus());
+                                replayUser.setOnline(liveUser.getOnline());
+                                replayUser.setOnlineSeconds(0L); // 回放观看时长从0开始,重新计时
+                                replayUser.setGlobalVisible(liveUser.getGlobalVisible());
+                                replayUser.setSingleVisible(liveUser.getSingleVisible());
+                                replayUser.setLiveFlag(0); // 回放标记
+                                replayUser.setReplayFlag(1); // 回放标记
+                                replayUser.setLocation(liveUser.getLocation());
+                                replayUser.setCreateTime(nowDate);
+                                replayUser.setUpdateTime(nowDate);
+                                replayUsers.add(replayUser);
+                            }
+
+                            // 批量更新直播用户的在线时长
+                            if (!updateLiveUsers.isEmpty()) {
+                                int batchSize = 500;
+                                for (int i = 0; i < updateLiveUsers.size(); i += batchSize) {
+                                    int end = Math.min(i + batchSize, updateLiveUsers.size());
+                                    List<LiveWatchUser> batch = updateLiveUsers.subList(i, end);
+                                    liveWatchUserService.batchUpdateLiveWatchUser(batch);
+                                }
+                            }
+
+                            // 批量插入回放用户数据
+                            if (!replayUsers.isEmpty()) {
+                                int batchSize = 500;
+                                for (int i = 0; i < replayUsers.size(); i += batchSize) {
+                                    int end = Math.min(i + batchSize, replayUsers.size());
+                                    List<LiveWatchUser> batch = replayUsers.subList(i, end);
+                                    liveWatchUserService.batchInsertLiveWatchUser(batch);
+                                }
+                            }
+
+                            // 清理直播间状态缓存
+                            liveWatchUserService.clearLiveFlagCache(liveId);
+                        }
 
                         // 标记为已处理,稍后删除缓存
                         processedLiveIds.add(liveId);
-                        
                         // 调用打标签方法
                         liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
-                        
-
                     }
                 } catch (Exception e) {
                     log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
@@ -684,7 +783,6 @@ public class Task {
                 try {
                     String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, liveId);
                     redisCache.deleteObject(tagMarkKey);
-                    log.info("已删除已处理的直播间打标签缓存: liveId={}", liveId);
                 } catch (Exception e) {
                     log.error("删除直播间打标签缓存失败: liveId={}, error={}", liveId, e.getMessage(), e);
                 }
@@ -695,73 +793,230 @@ public class Task {
     }
 
     /**
-     * 批量同步Redis中的观看时长到数据库
-     * 每2分钟执行一次,减少数据库压力
+     * 实时扫描用户直播数据,根据用户的直播在线时长更新观看记录状态
+     * 每30秒执行一次
      */
-    @Scheduled(cron = "0 0/2 * * * ?")
-    @DistributeLock(key = "batchSyncWatchDuration", scene = "task")
-    public void batchSyncWatchDuration() {
+    @Scheduled(cron = "0/30 * * * * ?")
+    @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
+    public void scanLiveWatchUserStatus() {
         try {
-            log.info("开始批量同步观看时长到数据库");
-            
-            // 优化:从所有直播间的Hash中批量获取数据
+            // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
-            
             if (activeLives == null || activeLives.isEmpty()) {
-                log.debug("当前没有活跃的直播间");
                 return;
             }
-            
-            int totalCount = 0;
-            int successCount = 0;
-            int failCount = 0;
-            
-            // 逐个直播间处理
+
             for (Live live : activeLives) {
                 try {
                     Long liveId = live.getLiveId();
-                    
-                    // 使用Hash结构存储每个直播间的观看时长
-                    String hashKey = "live:watch:duration:hash:" + liveId;
-                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
-                    
-                    if (userDurations == null || userDurations.isEmpty()) {
+                    if (liveId == null) {
                         continue;
                     }
-                    
-                    // 获取直播/回放标记(一次查询,所有用户复用)
+                    // 获取直播间的直播/回放状态
                     Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
                     Integer liveFlag = flagMap.get("liveFlag");
-                    Integer replayFlag = flagMap.get("replayFlag");
-                    
-                    // 批量处理该直播间的所有用户
-                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                    // 只处理直播状态的用户(liveFlag = 1)
+                    if (liveFlag == null || liveFlag != 1) {
+                        continue;
+                    }
+                    // 查询该直播间的在线用户(liveFlag = 1, replayFlag = 0)
+                    LiveWatchUser queryUser = new LiveWatchUser();
+                    queryUser.setLiveId(liveId);
+                    queryUser.setLiveFlag(1);
+                    queryUser.setReplayFlag(0);
+                    queryUser.setOnline(0); // 在线用户
+                    List<LiveWatchUser> onlineUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+                    if (onlineUsers == null || onlineUsers.isEmpty()) {
+                        continue;
+                    }
+                    // 获取直播视频总时长
+                    List<LiveVideo> videos = liveVideoService.listByLiveIdWithCache(liveId, 1);
+                    long totalVideoDuration = 0L;
+                    if (videos != null && !videos.isEmpty()) {
+                        totalVideoDuration = videos.stream()
+                                .filter(v -> v.getDuration() != null)
+                                .mapToLong(LiveVideo::getDuration)
+                                .sum();
+                    }
+
+                    // 处理每个在线用户
+                    for (LiveWatchUser user : onlineUsers) {
                         try {
-                            Long userId = Long.parseLong(entry.getKey().toString());
-                            Long duration = Long.parseLong(entry.getValue().toString());
+                            Long userId = user.getUserId();
+                            if (userId == null) {
+                                continue;
+                            }
+
+                            // 获取用户的在线观看时长
+                            Long onlineSeconds = user.getOnlineSeconds();
+                            if (onlineSeconds == null || onlineSeconds <= 0) {
+                                continue;
+                            }
                             
-                            totalCount++;
+                            // 获取用户的 companyId 和 companyUserId
+                            LiveUserFirstEntry liveUserFirstEntry =
+                                    liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
+                            if (liveUserFirstEntry == null) {
+                                continue;
+                            }
                             
-                            // 异步更新数据库
-                            liveWatchUserService.updateWatchDuration(liveId, userId, liveFlag, replayFlag, duration);
-                            successCount++;
+                            Long qwUserId = liveUserFirstEntry.getQwUserId();
+                            Long externalContactId = liveUserFirstEntry.getExternalContactId();
+
+                            if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
+                                continue;
+                            }
+
+                            // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
+                            updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
+                                    onlineSeconds, totalVideoDuration);
                             
                         } catch (Exception e) {
-                            failCount++;
-                            log.error("同步用户观看时长失败: liveId={}, userId={}, error={}", 
-                                    liveId, entry.getKey(), e.getMessage());
+                            log.error("处理用户观看记录状态异常: liveId={}, userId={}, error={}",
+                                    liveId, user.getUserId(), e.getMessage(), e);
                         }
                     }
                     
                 } catch (Exception e) {
-                    log.error("处理直播间观看时长失败: liveId={}, error={}", live.getLiveId(), e.getMessage());
+                    log.error("处理直播间观看记录状态异常: liveId={}, error={}",
+                            live.getLiveId(), e.getMessage(), e);
                 }
             }
-            
-            log.info("批量同步观看时长完成: 总数={}, 成功={}, 失败={}", totalCount, successCount, failCount);
-            
         } catch (Exception e) {
-            log.error("批量同步观看时长任务异常", e);
+            log.error("实时扫描用户直播数据任务异常: error={}", e.getMessage(), e);
         }
     }
+
+    /**
+     * 根据在线时长更新 LiveWatchLog 的 logType(复用 WebSocketServer 中的逻辑)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param qwUserId 邀请人id
+     * @param  exId 外部人id
+     * @param onlineSeconds 在线时长(秒)
+     * @param totalVideoDuration 视频总时长(秒)
+     */
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long qwUserId,
+                                                   Long exId, Long onlineSeconds, long totalVideoDuration) {
+        try {
+            // 查询 LiveWatchLog
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setQwUserId(String.valueOf(qwUserId));
+            queryLog.setExternalContactId(exId);
+
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs == null || logs.isEmpty()) {
+                return;
+            }
+
+            Date now = new Date();
+            for (LiveWatchLog log : logs) {
+                if (log.getLogType() != null && log.getLogType() == 2) {
+                    continue;
+                }
+                boolean needUpdate = false;
+                Integer newLogType = log.getLogType();
+
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
+                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+                    newLogType = 4;
+                    needUpdate = true;
+                }
+                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+                // 如果直播视频 >= 20分钟且 < 40分钟,在线时长 >= 20分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 1200 && totalVideoDuration < 2400 && onlineSeconds >= 1200) { // 20分钟 = 1200秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+
+                // 如果 logType 已经是 2(完课),不再更新
+                if (needUpdate) {
+                    log.setLogType(newLogType);
+                    liveWatchLogService.updateLiveWatchLog(log);
+                }
+            }
+        } catch (Exception e) {
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}",
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 批量同步Redis中的观看时长到数据库
+     * 每2分钟执行一次,减少数据库压力
+     */
+//    @Scheduled(cron = "0 0/2 * * * ?")
+//    @DistributeLock(key = "batchSyncWatchDuration", scene = "task")
+//    public void batchSyncWatchDuration() {
+//        try {
+//            log.info("开始批量同步观看时长到数据库");
+//
+//            // 优化:从所有直播间的Hash中批量获取数据
+//            List<Live> activeLives = liveService.selectNoEndLiveList();
+//
+//            if (activeLives == null || activeLives.isEmpty()) {
+//                log.debug("当前没有活跃的直播间");
+//                return;
+//            }
+//
+//            int totalCount = 0;
+//            int successCount = 0;
+//            int failCount = 0;
+//
+//            // 逐个直播间处理
+//            for (Live live : activeLives) {
+//                try {
+//                    Long liveId = live.getLiveId();
+//
+//                    // 使用Hash结构存储每个直播间的观看时长
+//                    String hashKey = "live:watch:duration:hash:" + liveId;
+//                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
+//
+//                    if (userDurations == null || userDurations.isEmpty()) {
+//                        continue;
+//                    }
+//
+//                    // 获取直播/回放标记(一次查询,所有用户复用)
+//                    Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+//                    Integer liveFlag = flagMap.get("liveFlag");
+//                    Integer replayFlag = flagMap.get("replayFlag");
+//
+//                    // 批量处理该直播间的所有用户
+//                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+//                        try {
+//                            Long userId = Long.parseLong(entry.getKey().toString());
+//                            Long duration = Long.parseLong(entry.getValue().toString());
+//
+//                            totalCount++;
+//
+//                            // 异步更新数据库
+//                            liveWatchUserService.updateWatchDuration(liveId, userId, liveFlag, replayFlag, duration);
+//                            successCount++;
+//
+//                        } catch (Exception e) {
+//                            failCount++;
+//                            log.error("同步用户观看时长失败: liveId={}, userId={}, error={}",
+//                                    liveId, entry.getKey(), e.getMessage());
+//                        }
+//                    }
+//
+//                } catch (Exception e) {
+//                    log.error("处理直播间观看时长失败: liveId={}, error={}", live.getLiveId(), e.getMessage());
+//                }
+//            }
+//
+//            log.info("批量同步观看时长完成: 总数={}, 成功={}, 失败={}", totalCount, successCount, failCount);
+//
+//        } catch (Exception e) {
+//            log.error("批量同步观看时长任务异常", e);
+//        }
+//    }
 }

+ 6 - 0
fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -55,6 +55,12 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
         if (parameterMap.containsKey(AttrConstant.LOCATION)) {
             userProperties.put(AttrConstant.LOCATION, parameterMap.get(AttrConstant.LOCATION).get(0));
         }
+        if (parameterMap.containsKey(AttrConstant.QW_USER_ID)) {
+            userProperties.put(AttrConstant.QW_USER_ID, parameterMap.get(AttrConstant.QW_USER_ID).get(0));
+        }
+        if (parameterMap.containsKey(AttrConstant.EXTERNAL_CONTACT_ID)) {
+            userProperties.put(AttrConstant.EXTERNAL_CONTACT_ID, parameterMap.get(AttrConstant.EXTERNAL_CONTACT_ID).get(0));
+        }
 
         // 验证token
         if (parameterMap.containsKey(tokenKey)) {

+ 2 - 0
fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java

@@ -13,6 +13,8 @@ public class AttrConstant {
     public static final String COMPANY_ID = "companyId";
     public static final String COMPANY_USER_ID = "companyUserId";
     public static final String LOCATION = "location";
+    public static final String QW_USER_ID = "qwUserId";
+    public static final String EXTERNAL_CONTACT_ID = "externalContactId";
 
     // 定义 AttributeKey 保存必要参数
     public static final AttributeKey<Long> ATTR_LIVE_ID = AttributeKey.valueOf(LIVE_ID);

+ 46 - 37
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -4,6 +4,7 @@ package com.fs.live.websocket.service;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.his.domain.FsUser;
@@ -96,6 +97,8 @@ public class WebSocketServer {
         long liveId = (long) userProperties.get("liveId");
         long userId = (long) userProperties.get("userId");
         long userType = (long) userProperties.get("userType");
+        long qwUserId = -1;
+        long externalContactId = -1;
         String location = (String) userProperties.get("location");  // 获取location参数
 
         Live live = liveService.selectLiveByLiveId(liveId);
@@ -110,6 +113,12 @@ public class WebSocketServer {
         if (!Objects.isNull(userProperties.get("companyUserId"))) {
             companyUserId = (long) userProperties.get("companyUserId");
         }
+        if (!Objects.isNull(userProperties.get("qwUserId"))) {
+            qwUserId = (long) userProperties.get("qwUserId");
+        }
+        if (!Objects.isNull(userProperties.get("externalContactId"))) {
+            externalContactId = (long) userProperties.get("externalContactId");
+        }
 
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
@@ -178,21 +187,29 @@ public class WebSocketServer {
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
             // 如果用户连上了 socket,并且公司ID和销售ID大于0,更新 LiveWatchLog 的 logType
 
-            if ((companyId > 0 && companyUserId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
+            if ((qwUserId > 0 && externalContactId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
                 // 获取当前直播/回放状态
                 Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
                 Integer currentLiveFlag = flagMap.get("liveFlag");
 
                 // 如果当前是直播状态(liveFlag = 1),更新 logType
                 if (currentLiveFlag != null && currentLiveFlag == 1) {
-                    updateLiveWatchLogTypeOnConnect(liveId, userId, companyId, companyUserId);
+                    updateLiveWatchLogTypeOnConnect(liveId, userId, qwUserId, externalContactId);
                 }
             }
+
+
             if (liveUserFirstEntry != null) {
                 // 处理第一次自己进入,第二次扫码销售进入
                 if (liveUserFirstEntry.getCompanyUserId() == -1L && companyUserId != -1L) {
                     liveUserFirstEntry.setCompanyId(companyId);
                     liveUserFirstEntry.setCompanyUserId(companyUserId);
+                    if (qwUserId != -1) {
+                        liveUserFirstEntry.setQwUserId(qwUserId);
+                    }
+                    if (externalContactId!= -1) {
+                        liveUserFirstEntry.setExternalContactId(externalContactId);
+                    }
                     liveUserFirstEntryService.updateLiveUserFirstEntry(liveUserFirstEntry);
                 }
             } else {
@@ -211,8 +228,15 @@ public class WebSocketServer {
                 liveUserFirstEntry.setEntryDate(date);
                 liveUserFirstEntry.setFirstEntryTime(date);
                 liveUserFirstEntry.setUpdateTime( date);
+                if (qwUserId != -1) {
+                    liveUserFirstEntry.setQwUserId(qwUserId);
+                }
+                if (externalContactId!= -1) {
+                    liveUserFirstEntry.setExternalContactId(externalContactId);
+                }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
             }
+            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
 
 
         } else {
@@ -254,7 +278,6 @@ public class WebSocketServer {
                 throw new BaseException("用户信息错误");
             }
             // 计算并更新用户在线时长
-            updateUserOnlineDuration(liveId, userId, companyId, companyUserId);
             room.remove(userId);
             if (room.isEmpty()) {
                 rooms.remove(liveId);
@@ -320,30 +343,28 @@ public class WebSocketServer {
 
                     // 心跳时同步更新观看时长到Redis Hash
                     long watchUserId = (long) userProperties.get("userId");
+
+
                     
                     if (msg.getData() != null && !msg.getData().isEmpty()) {
                         try {
                             Long currentDuration = Long.parseLong(msg.getData());
-                            
                             // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
                             String hashKey = "live:watch:duration:hash:" + liveId;
                             String userIdField = String.valueOf(watchUserId);
-                            
                             // 获取现有时长
-                            Object existingDuration = redisCache.redisTemplate.opsForHash().get(hashKey, userIdField);
-                            
-                            // 只有当新的时长更大时才更新(避免时间倒退)
+                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+                            // 只有当新的时长更大时才更新
                             if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
                                 // 更新Hash中的用户时长
-                                redisCache.redisTemplate.opsForHash().put(hashKey, userIdField, currentDuration.toString());
+                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
                                 // 设置过期时间(2小时)
-                                redisCache.redisTemplate.expire(hashKey, 2, TimeUnit.HOURS);
-                                
-                                // 实时更新用户看课状态(仅在直播期间)
-                                updateWatchLogTypeInRealTime(liveId, watchUserId, currentDuration);
+                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
+
                             }
                         } catch (Exception e) {
-                            log.error("心跳更新观看时长失败, liveId={}, userId={}", liveId, watchUserId, e);
+                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}", 
+                                    liveId, watchUserId, msg.getData(), e);
                         }
                     }
                     
@@ -838,18 +859,7 @@ public class WebSocketServer {
                         if (session.isOpen()) {
                             session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
                         }
-                        
-                        // 计算并更新用户在线时长(心跳超时断开连接)
-                        Map<String, Object> userProperties = session.getUserProperties();
-                        long companyId = -1L;
-                        long companyUserId = -1L;
-                        if (!Objects.isNull(userProperties.get("companyId"))) {
-                            companyId = (long) userProperties.get("companyId");
-                        }
-                        if (!Objects.isNull(userProperties.get("companyUserId"))) {
-                            companyUserId = (long) userProperties.get("companyUserId");
-                        }
-                        updateUserOnlineDuration(liveId, userId, companyId, companyUserId);
+                        liveWatchUserService.close(null, liveId, userId);
                     } catch (Exception e) {
                         log.error("关闭超时会话失败: sessionId={}, liveId={}, userId={}",
                                 session.getId(), liveId, userId, e);
@@ -1112,15 +1122,14 @@ public class WebSocketServer {
                 
                 // 更新数据库
                 liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
-                
                 // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
-                if (currentLiveFlag != null && currentLiveFlag == 1 
-                        && liveWatchUser.getCompanyId() != null && liveWatchUser.getCompanyId() > 0
-                        && liveWatchUser.getCompanyUserId() != null && liveWatchUser.getCompanyUserId() > 0) {
-                    updateLiveWatchLogTypeByDuration(liveId, userId, 
-                            liveWatchUser.getCompanyId(), liveWatchUser.getCompanyUserId(), 
-                            liveWatchUser.getOnlineSeconds());
-                }
+//                if (currentLiveFlag != null && currentLiveFlag == 1
+//                        && liveWatchUser.getCompanyId() != null && liveWatchUser.getCompanyId() > 0
+//                        && liveWatchUser.getCompanyUserId() != null && liveWatchUser.getCompanyUserId() > 0) {
+//                    updateLiveWatchLogTypeByDuration(liveId, userId,
+//                            liveWatchUser.getCompanyId(), liveWatchUser.getCompanyUserId(),
+//                            liveWatchUser.getOnlineSeconds());
+//                }
             }
             
             // 删除 Redis 中的进入时间记录
@@ -1135,13 +1144,13 @@ public class WebSocketServer {
      * 在连接时更新 LiveWatchLog 的 logType
      * 如果 logType 类型不是 2,修改 logType 类型为 1(看课中)
      */
-    private void updateLiveWatchLogTypeOnConnect(Long liveId, Long userId, Long companyId, Long companyUserId) {
+    private void updateLiveWatchLogTypeOnConnect(Long liveId, Long userId, Long qwUserId, Long externalContactId) {
         try {
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
             queryLog.setUserId(userId);
-            queryLog.setCompanyId(companyId);
-            queryLog.setCompanyUserId(companyUserId);
+            queryLog.setQwUserId(String.valueOf(qwUserId));
+            queryLog.setExternalContactId(externalContactId);
             
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs != null && !logs.isEmpty()) {

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

@@ -950,6 +950,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             switch (setting.getContentType()) {
                 //直播小程序单独
                 case "12":
+                    //直播发送类型
+                    sopLogs.setSendType(20);
+                    clonedContent.setLiveId(setting.getLiveId());
                     String sortLiveLink;
                     sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
                     String json = configService.selectConfigByKey("his.config");
@@ -1198,6 +1201,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //直播小程序单独
                 case "12":
+                    sopLogs.setSendType(20);
+                    clonedContent.setLiveId(setting.getLiveId());
                     String sortLiveLink;
                     sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId()+"&corpId=" +logVo.getCorpId()+"&qwUserId=" + qwUserId;
                     String json = configService.selectConfigByKey("his.config");
@@ -1984,7 +1989,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     public void batchInsertLiveWatchLog(List<LiveWatchLog> liveWatchLogToInsert) {
         try {
             List<LiveWatchLog> lastInsertList = new ArrayList<>();
-            //判断是否存在数据 liveId + his_qw_external_contact_id 唯一
+            //判断是否存在数据 liveId + his_qw_external_contact_id + qwUserId 唯一
             for (LiveWatchLog liveWatchLog : liveWatchLogToInsert) {
                 //判断是否存在数据 存在的数据直接更新发送时间
                 if(liveWatchLogMapper.updateLiveWatchLogCondition(liveWatchLog) > 0){
@@ -1997,7 +2002,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             }
 //            log.info("批量插入 LiveWatchLog 完成,共插入 {} 条记录。", liveWatchLogToInsert.size());
         } catch (Exception e) {
-            log.error("批量插入 LiveWatchLog 失败: {}", e.getMessage(), e);
+            log.error("批量插入 LiveWatchLog 失败: {}", liveWatchLogToInsert, e);
             // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
         }
     }

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

@@ -246,6 +246,12 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
             "WHERE file_key = #{fileKey}")
     void updateFsUserCourseVideoByFileKey(FsUserCourseVideo courseVideo);
 
+    @Update("UPDATE fs_user_course_video " +
+            "SET line_two = #{lineTwo}, " +
+            "    update_time = NOW() " +  // 添加更新时间
+            "WHERE file_key = #{fileKey}")
+    void updateFsUserCourseVideoByFileKeyForHsy(FsUserCourseVideo courseVideo);
+
     @Select("select title from fs_user_course_video WHERE video_id=#{videoId}")
     String selectFsUserCourseVideoByVideoForTitle(@Param("videoId") Long videoId);
 
@@ -267,7 +273,7 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     @MapKey("videoId")
     Map<Long, FsUserCourseVideo> selectAllMap();
 
-    @Select("select * from fs_video_resource where line2 is not null and job_id is null")
+    @Select("select * from fs_video_resource where line2 is not null and (job_id is null or job_id='')")
     List<FsVideoResource> selectVideoByHuaWei();
 
     @Select("select * from fs_video_resource where job_id is not null and  (hsy_vid is null or hsy_vid='')")

+ 7 - 5
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -2621,10 +2621,13 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
         // 项目看课数限制
         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, "超过项目看课数量限制");
             }
+        }else {
+            log.error("没有进入看课限制:传入参数:={},watchCourseVideo={}存在问题 ",param, watchCourseVideo);
         }
         //添加判断:该用户是否已经存在此课程的看课记录,并且看课记录的销售id不是传入的销售id
         if (watchCourseVideo != null) {
@@ -4290,9 +4293,6 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 System.out.println(resp.getResponseMetadata().getError());
                 System.exit(-1);
             }else {
-                if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
-                    return;
-                }
                 FsVideoResource video = new FsVideoResource();
                 video.setId(videoResource.getId());
                 //视频上传失败,清空jobid
@@ -4330,7 +4330,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     updateMediaPublishStatus(videoResource.getHsyVid());
                 }
                 //更新视频资源
-                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+//                String storeUri = resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+//                String uri = storeUri.substring(storeUri.indexOf("/") + 1);
+                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getFileName();
 
                 FsVideoResource fsVideoResource = new FsVideoResource();
                 fsVideoResource.setId(videoResource.getId());
@@ -4341,7 +4343,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 FsUserCourseVideo courseVideo = new FsUserCourseVideo();
                 courseVideo.setFileKey(videoResource.getFileKey());
                 courseVideo.setLineTwo(url);
-                fsUserCourseVideoMapper.updateFsUserCourseVideoByFileKey(courseVideo);
+                fsUserCourseVideoMapper.updateFsUserCourseVideoByFileKeyForHsy(courseVideo);
 
             }
             System.out.println(resp);

+ 24 - 0
fs-service/src/main/java/com/fs/erp/exception/JstRateLimitException.java

@@ -0,0 +1,24 @@
+package com.fs.erp.exception;
+
+/**
+ * 聚水潭接口限流异常
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+public class JstRateLimitException extends RuntimeException {
+    
+    private static final long serialVersionUID = 1L;
+    
+    private final Integer errorCode;
+    
+    public JstRateLimitException(String message, Integer errorCode) {
+        super(message);
+        this.errorCode = errorCode;
+    }
+    
+    public Integer getErrorCode() {
+        return errorCode;
+    }
+}
+

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

@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.erp.constant.AfterSalesOrderStatusEnum;
 import com.fs.erp.constant.ErpQueryOrderStatusEnum;
@@ -11,6 +12,7 @@ import com.fs.erp.constant.OrderStatusEnum;
 import com.fs.erp.constant.TaskStatusEnum;
 import com.fs.erp.domain.*;
 import com.fs.erp.dto.*;
+import com.fs.erp.exception.JstRateLimitException;
 import com.fs.erp.http.JstErpHttpService;
 import com.fs.erp.mapper.FsJstAftersalePushMapper;
 import com.fs.erp.mapper.FsJstAftersalePushScrmMapper;
@@ -46,6 +48,7 @@ import org.springframework.util.CollectionUtils;
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -84,6 +87,14 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
     @Autowired
     private LiveOrderItemMapper liveOrderItemMapper;
 
+    @Autowired
+    private RedisCache redisCache;
+
+    // 限流配置:每分钟最多100次请求,超过95次返回429
+    private static final int MAX_REQUESTS_PER_MINUTE = 100;
+    private static final int RATE_LIMIT_THRESHOLD = 95;
+    private static final String RATE_LIMIT_KEY_PREFIX = "jst:query:rate_limit:";
+
     @Override
     public ErpOrderResponse addOrder(ErpOrder order) {
         FsStoreOrder fsStoreOrder = fsStoreOrderService.selectFsStoreOrderByOrderCode(order.getPlatform_code());
@@ -513,12 +524,29 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
         requestDTO.setOIds(Collections.singletonList(Long.valueOf(param.getCode())));
 
+        // 限流检查:每分钟最多100次请求,超过95次返回429
+        String rateLimitKey = RATE_LIMIT_KEY_PREFIX + System.currentTimeMillis() / 60000; // 每分钟一个key
 
-        // 2. 调用ERP服务查询订单
-        OrderQueryResponseDTO query = jstErpHttpService.query(requestDTO);
+        // 使用原子操作增加计数,并获取增加后的值
+        Long currentCount = redisCache.incr(rateLimitKey, 1L);
 
+        // 如果是第一次请求,设置过期时间为1分钟
+        if (currentCount == 1) {
+            redisCache.expire(rateLimitKey, 1, TimeUnit.MINUTES);
+        }
         // 3. 构建响应对象
         ErpOrderQueryResponse response = new ErpOrderQueryResponse();
+        // 如果当前分钟内请求次数超过95次,直接返回429错误
+        if (currentCount >= RATE_LIMIT_THRESHOLD) {
+            response.setCode("429");
+            response.setSuccess(false);
+            return response;
+        }
+
+
+        // 2. 调用ERP服务查询订单
+        OrderQueryResponseDTO query = jstErpHttpService.query(requestDTO);
+
 
         // 4. 设置基本响应信息
 
@@ -527,17 +555,37 @@ 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());
         }
-
+        response.setSuccess(true);
         return response;
     }
 
     @Override
     public ErpOrderQueryResponse getLiveOrder(ErpOrderQueryRequert param) {
+        // 限流检查:每分钟最多100次请求,超过95次返回429
+        String rateLimitKey = RATE_LIMIT_KEY_PREFIX + System.currentTimeMillis() / 60000; // 每分钟一个key
+
+        // 使用原子操作增加计数,并获取增加后的值
+        Long currentCount = redisCache.incr(rateLimitKey, 1L);
+
+        // 如果是第一次请求,设置过期时间为1分钟
+        if (currentCount == 1) {
+            redisCache.expire(rateLimitKey, 1, TimeUnit.MINUTES);
+        }
+        // 3. 构建响应对象
+        ErpOrderQueryResponse response = new ErpOrderQueryResponse();
+        // 如果当前分钟内请求次数超过95次,直接返回429错误
+        if (currentCount >= RATE_LIMIT_THRESHOLD) {
+            response.setCode("429");
+            response.setSuccess(false);
+            return response;
+        }
+
         // 1. 构建查询请求DTO
         OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
         requestDTO.setOIds(Collections.singletonList(Long.valueOf(param.getCode())));
@@ -546,8 +594,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         // 2. 调用ERP服务查询订单
         OrderQueryResponseDTO query = jstErpHttpService.query(requestDTO);
 
-        // 3. 构建响应对象
-        ErpOrderQueryResponse response = new ErpOrderQueryResponse();
+
 
         // 4. 设置基本响应信息
 
@@ -556,12 +603,14 @@ 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());
+            }
             response.setOrders(erpOrders);
         } else {
             response.setOrders(Collections.emptyList());
         }
-
+        response.setSuccess(true);
         return response;
     }
 

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

@@ -465,8 +465,8 @@ public class AiHookServiceImpl implements AiHookService {
             }
 
             //对用户处理的内容做处理,去除手机号替换
-            //String maskedContent = processContent(qwContent);
-            String contentEmj = replaceWxEmo(qwContent);
+            String maskedContent = processContent(qwContent);
+            String contentEmj = replaceWxEmo(maskedContent);
             if(!contentEmj.contains("表情包")){
                 if(!contentEmj.isEmpty()){
                     addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
@@ -972,7 +972,8 @@ public class AiHookServiceImpl implements AiHookService {
      * @return
      */
     private @Nullable String processContent(String qwContent) {
-        String maskedContent = SensitiveDataUtils.maskMobileNumbers(qwContent);
+        //String maskedContent = SensitiveDataUtils.maskMobileNumbers(qwContent);
+        String maskedContent = qwContent;
         if(maskedContent != null && !maskedContent.isEmpty()){
             FastGptChatReplaceText fastGptChatReplaceText = new FastGptChatReplaceText();
             fastGptChatReplaceText.setStatus(1);

+ 6 - 0
fs-service/src/main/java/com/fs/his/domain/FsUser.java

@@ -232,6 +232,12 @@ public class FsUser extends BaseEntity
     @TableField(exist = false)
     private String nicknameExact;
 
+//    /**
+//     * 搜索关键词-电话号码/会员id/会员昵称
+//     * **/
+//    @TableField(exist = false)
+//    private String keywords;
+
     public String getNickname() {
         return nickname;
     }

+ 1 - 1
fs-service/src/main/java/com/fs/his/param/FsIntegralCartParam.java

@@ -16,7 +16,7 @@ public class FsIntegralCartParam {
 
     @NotNull(message = "添加数量不能为空")
     @Min(value = 1, message = "数量不再合法范围内")
-    @Max(value = 999, message = "数量不再合法范围内")
+    @Max(value = 9999, message = "数量不再合法范围内")
     @ApiModelProperty("数量")
     private Integer cartNum;
 

+ 4 - 0
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -119,6 +119,10 @@ public interface IFsStorePaymentService
     String v3TransferNotify(String notifyData, HttpServletRequest request);
 
 
+    String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
+
+
+
     R sendRedPacketTest(WxSendRedPacketParam param);
 
     List<FsStorePaymentVO> selectFsStorePaymentListQueryVO(FsStorePaymentParam fsStorePayment);

+ 44 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -690,6 +690,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         integralParam.setBusinessId(order.getOrderId().toString());
         userIntegralLogsService.addIntegralTemplate(integralParam);
 
+        clearCartAfterPayment(order);
+
         order.setIsPay(1);
         order.setStatus(1);
         order.setPayTime(LocalDateTime.now());
@@ -697,6 +699,48 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         return R.ok();
     }
 
+    /**
+     * 支付成功后清空购物车
+     * @param order 订单信息
+     */
+    private void clearCartAfterPayment(FsIntegralOrder order) {
+        try {
+            if (StringUtils.isEmpty(order.getItemJson())) {
+                return;
+            }
+
+            List<Long> goodsIds = new ArrayList<>();
+            
+            // 解析订单商品信息,获取商品ID列表
+            if (order.getItemJson().startsWith("[") && order.getItemJson().endsWith("]")) {
+                // 多个商品
+                List<FsIntegralGoods> goodsItem = JSONUtil.toBean(order.getItemJson(), new TypeReference<List<FsIntegralGoods>>(){}, true);
+                for (FsIntegralGoods goods : goodsItem) {
+                    if (goods.getGoodsId() != null) {
+                        goodsIds.add(goods.getGoodsId());
+                    }
+                }
+            } else if (order.getItemJson().startsWith("{") && order.getItemJson().endsWith("}")) {
+                // 单个商品
+                FsIntegralGoods goods = JSONUtil.toBean(order.getItemJson(), FsIntegralGoods.class);
+                if (goods.getGoodsId() != null) {
+                    goodsIds.add(goods.getGoodsId());
+                }
+            }
+
+            // 删除购物车中对应商品
+            if (!goodsIds.isEmpty()) {
+                Wrapper<FsIntegralCart> wrapper = Wrappers.<FsIntegralCart>lambdaQuery()
+                        .eq(FsIntegralCart::getUserId, order.getUserId())
+                        .in(FsIntegralCart::getGoodsId, goodsIds);
+                cartService.remove(wrapper);
+                log.info("支付成功后清空购物车,userId: {}, goodsIds: {}", order.getUserId(), goodsIds);
+            }
+        } catch (Exception e) {
+            log.error("清空购物车失败,orderId: {}, error: {}", order.getOrderId(), e.getMessage(), e);
+        }
+    }
+
     @Override
     public AjaxResult export(FsIntegralOrder fsIntegralOrder) {
         List<FsIntegralOrder> list = fsIntegralOrderMapper.selectFsIntegralOrderList(fsIntegralOrder);

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

@@ -921,6 +921,13 @@ 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)) {
+                WxPayException wxPayException = (WxPayException) e;
+                String customErrorMsg = wxPayException.getCustomErrorMsg();
+                if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    return R.error("[红包领取] 账户余额不足,请联系管理员!");
+                }
+            }
             throw new RuntimeException(e);
         }
     }
@@ -1128,6 +1135,45 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
     }
 
+    @Override
+    public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
+        logger.info("分公司回调V3::companyId:{}",companyId);
+        logger.info("zyp \n【收到转账回调V3::分公司】:{}",notifyData);
+        try {
+//            String json = configService.selectConfigByKey("redPacket.config");
+            String json = companyConfigMapper.selectRedPacketConfigByKey(companyId);
+            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+
+            //创建微信订单
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config,payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            SignatureHeader signatureHeader = new SignatureHeader();
+            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+            logger.info("到零钱回调1:{}",result.getResult());
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+                logger.info("result:{}",r);
+                if (r.get("code").equals(200)){
+                    return WxPayNotifyResponse.success("处理成功");
+                }else {
+                    return WxPayNotifyResponse.fail("");
+                }
+            }else {
+                return WxPayNotifyResponse.fail("");
+            }
+        } catch (WxPayException e) {
+            e.printStackTrace();
+            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
     @Override
     @Transactional
     public R sendRedPacketTest(WxSendRedPacketParam param) {

+ 10 - 0
fs-service/src/main/java/com/fs/his/utils/RedisCacheUtil.java

@@ -20,4 +20,14 @@ public class RedisCacheUtil {
             redisCache.deleteObject(key);
         }
     }
+
+    /**
+     * 删除 Spring Cache 格式的缓存(带双冒号)
+     * @param cacheName 缓存名称(如 getIntegralGoodsById)
+     * @param cacheKey 缓存key(如 商品ID)
+     */
+    public void delSpringCacheKey(String cacheName, Object cacheKey) {
+        String key = cacheName + "::" + cacheKey;
+        redisCache.deleteObject(key);
+    }
 }

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

@@ -34,6 +34,9 @@ public class FsIntegralCartVO {
     @ApiModelProperty("最新所需金额")
     private BigDecimal newCash;
 
+    @ApiModelProperty("库存")
+    private Long stock;
+
     @ApiModelProperty("数量")
     private Integer cartNum;
 

+ 49 - 0
fs-service/src/main/java/com/fs/hisStore/enums/LiveEnum.java

@@ -0,0 +1,49 @@
+package com.fs.hisStore.enums;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 设置不转码的项目
+ */
+public enum LiveEnum {
+    KANGNIAN_TANG("康年堂"),
+    SIFU_TANG("四福堂"),
+    NMG_MYT("内蒙古一贴"),
+    CQ_TYT("重庆泰医堂"),
+    HDT("弘德堂"),
+    JNMY("金牛明医"),
+    HYT("鹤颜堂"),
+    Z_K("中康");
+
+    private final String companyName;
+
+    LiveEnum(String companyName) {
+        this.companyName = companyName;
+    }
+
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    /**
+     * 静态集合,避免每次调用都重新创建
+     */
+    private static final Set<String> COMPANY_NAMES = Collections.unmodifiableSet(
+            Arrays.stream(values())
+                    .map(LiveEnum::getCompanyName)
+                    .collect(Collectors.toSet())
+    );
+
+    /**
+     * 比较是否存在
+     *
+     * @param companyName
+     * @return
+     */
+    public static boolean contains(String companyName) {
+        return COMPANY_NAMES.contains(companyName);
+    }
+}

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

@@ -1411,4 +1411,7 @@ public interface FsStoreOrderScrmMapper
     List<FsStoreOrderScrm> getUnsettledOrder();
 
     void batchUpdateTime(@Param("list") List<FsStoreOrderScrm> list);
+
+    @Update("update fs_store_order_scrm set `status`=-3 where order_code=#{orderCode}")
+    int cancelOrderByCode(@Param("orderCode") String orderCode);
 }

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

@@ -345,4 +345,8 @@ public interface IFsStoreOrderScrmService
      * @return
      */
     R createPackageSalesOrder(CompanyUser companyUser, String packageId, Integer orderType, Integer orderMedium);
+
+    void cancelOrderByCode(String outerPayId);
+
+    void updateFsStoreOrderDb(FsStoreOrderScrm order);
 }

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

@@ -304,7 +304,7 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
         }
         //顺丰轨迹查询处理
         String lastFourNumber = "";
-        if (StringUtils.equals(liveOrder.getDeliveryCode(),ShipperCodeEnum.SF.getValue())) {
+        if (StringUtils.equals(liveOrder.getDeliveryCode(),ShipperCodeEnum.SF.getValue()) || StringUtils.equals(liveOrder.getDeliveryCode(),ShipperCodeEnum.ZTO.getValue())) {
             lastFourNumber = getLastFourNum(liveOrder.getUserPhone());
             // 添加日志 - 顺丰单号
             logger.info("顺丰单号处理,获取用户手机号后四位:{}", lastFourNumber);

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

@@ -479,7 +479,7 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         if (storeAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_1.getValue()) ) {
             if(StringUtils.isNotEmpty(order.getExtendOrderId())){
                 //更新订单code
-                String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
+                String orderSn = OrderCodeUtils.getOrderSn();
                 FsStoreOrderScrm orderMap=new FsStoreOrderScrm();
                 orderMap.setId(order.getId());
                 orderMap.setOrderCode(orderSn);

+ 28 - 3
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -1383,6 +1383,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             order.setStatus(OrderInfoEnum.STATUS_2.getValue());
             order.setDeliveryId(deliveryId);
             order.setDeliverySendTime(new Date());
+            order.setUpdateTime(new Date());
 
             fsStoreOrderMapper.updateFsStoreOrder(order);
             orderStatusService.create(order.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
@@ -1413,6 +1414,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             fsWxExpressTask.setUserId(order.getUserId());
             fsWxExpressTask.setStatus(0);
             fsWxExpressTask.setRetryCount(0);
+            fsWxExpressTask.setType(0);
             fsWxExpressTask.setCreateTime(LocalDateTime.now());
             fsWxExpressTask.setUpdateTime(LocalDateTime.now());
             fsWxExpressTask.setOrderCode(order.getOrderCode());
@@ -3111,10 +3113,23 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
             //获取运费模板区域列表按照城市排序
             List<FsShippingTemplatesRegionScrm> shippingTemplatesRegionList = shippingTemplatesRegionService.selectFsShippingTemplatesRegionListByTempIdsAndCityIds(StringUtils.join(tempIds, ","), StringUtils.join(citys, ","));
-
+            boolean isQg = false;
+
+            if (CollectionUtils.isEmpty(shippingTemplatesRegionList)&&CollectionUtils.isNotEmpty(shippingTemplatesList)&&shippingTemplatesList.size()<2) {
+                List<RegionInfoDTO> regionList = JSONObject.parseArray(
+                        shippingTemplatesList.get(0).getRegionInfo(),
+                        RegionInfoDTO.class
+                );
+                if (regionList != null && !regionList.isEmpty()) {
+                    RegionInfoDTO regionDTO = regionList.get(0);
+                    if ("默认全国".equals(regionDTO.getRegionName())) {
+                        isQg = true;
+                    }
+                }
+            }
             // 有运费模板,但当前城市没有匹配的区域
             if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
-                    && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())) {
+                    && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())&&!isQg) {
                 logger.error("运费模板存在,但城市不在运费模板区域内,cityId: {}", cityId);
                 return badCode;
             }
@@ -3143,7 +3158,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 // 如果商品有运费模板,但没有找到对应的区域配置,返回错误码
                 if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
                         && shippingTemplatesList.stream().anyMatch(t -> t.getId().equals(tempId))
-                        && shippingTemplatesRegion == null) {
+                        && shippingTemplatesRegion == null&&!isQg) {
                     logger.error("商品运费模板存在,但城市不在运费模板区域内,tempId: {}, cityId: {}", tempId, cityId);
                     return badCode;
                 }
@@ -5304,6 +5319,16 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return R.ok().put("orderKey", uuid);
     }
 
+    @Override
+    public void cancelOrderByCode(String outerPayId) {
+        fsStoreOrderMapper.cancelOrderByCode(outerPayId);
+    }
+
+    @Override
+    public void updateFsStoreOrderDb(FsStoreOrderScrm order) {
+        fsStoreOrderMapper.updateFsStoreOrder(order);
+    }
+
     private static final DateTimeFormatter CST_FORMATTER = DateTimeFormatter
             .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US)
             .withZone(ZoneId.of("Asia/Shanghai"));

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

@@ -33,6 +33,14 @@ public class LiveUserFirstEntry extends BaseEntity{
     @Excel(name = "公司id")
     private Long companyId;
 
+    /** 企微ID */
+    @Excel(name = "企微ID")
+    private Long qwUserId;
+
+    /** 外部联系人ID */
+    @Excel(name = "外部联系人ID")
+    private Long externalContactId;
+
     /** 公司用户id */
     @Excel(name = "公司用户id")
     private Long companyUserId;

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

@@ -43,4 +43,10 @@ public class LiveVideo extends BaseEntity {
     private Long sort;
     @Excel(name = "转码状态")
     private Integer finishStatus;
+
+
+    /**
+     * 未转码数据
+     */
+    private String lineOne;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -118,6 +118,15 @@ public interface LiveMapper
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1")
     List<Live> selectNoEndLiveList();
 
+    /**
+     * 查询开启了完课积分配置的直播间(用于完课积分定时任务)
+     * @return 直播列表
+     */
+    @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
+            "and config_json is not null " +
+            "and JSON_EXTRACT(config_json, '$.enabled') = true")
+    List<Live> selectLiveListWithCompletionPointsEnabled();
+
     void updateStatusAndTimeBatchById(@Param("liveList") List<Live> list);
 
     @Select("select * from live where (company_id = #{companyId} or company_id is null)  and is_audit = 1 and is_del = 0 and is_show = 1 and status != 3\n")

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

@@ -92,6 +92,9 @@ public interface LiveOrderMapper {
     @Update("update live_order set `status`=-3 where order_id=#{orderId}")
     int cancelOrder(Long orderId);
 
+    @Update("update live_order set `status`=-3 where order_code=#{orderCode}")
+    int cancelOrderByCode(@Param("orderCode") String orderCode);
+
     @Select({"<script> " +
             "select * from live_order " +
             "<where>" +

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

@@ -149,4 +149,18 @@ public interface LiveWatchUserMapper {
             " left join live_user_first_entry lufe on lwu.live_id = lufe.live_id and lwu.user_id = lufe.user_id" +
             " where lwu.live_id = #{liveId} and lwu.user_id = #{userId} and lwu.live_flag = #{liveFlag} and lwu.replay_flag = #{replayFlag} limit 1 ")
     LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(@Param("liveId") Long liveId,@Param("userId") Long userId,@Param("liveFlag") Integer liveFlag,@Param("replayFlag") Integer replayFlag);
+
+    /**
+     * 批量更新直播间观看用户
+     * @param liveWatchUsers 需要更新的观看用户列表
+     * @return 更新的记录数
+     */
+    int batchUpdateLiveWatchUser(@Param("list") List<LiveWatchUser> liveWatchUsers);
+
+    /**
+     * 批量插入直播间观看用户
+     * @param liveWatchUsers 需要插入的观看用户列表
+     * @return 插入的记录数
+     */
+    int batchInsertLiveWatchUser(@Param("list") List<LiveWatchUser> liveWatchUsers);
 }

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

@@ -50,6 +50,9 @@ public class MergedOrderQueryParam extends BaseQueryParam implements Serializabl
     /** 产品名称 */
     private String productName;
 
+    /** productId */
+    private Long productId;
+
     /** 物流状态 */
     private Integer deliveryStatus;
 

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

@@ -8,6 +8,7 @@ import com.fs.live.vo.LiveAfterSalesListUVO;
 import com.fs.live.vo.LiveAfterSalesQueryVO;
 import com.fs.live.vo.LiveAfterSalesVo;
 
+import java.text.ParseException;
 import java.util.List;
 
 /**
@@ -67,7 +68,7 @@ public interface ILiveAfterSalesService {
 
     R applyAfterSales(String userId, LiveAfterSalesApplyParam param);
 
-    R revoke(String userId, LiveAfterSalesRevokeParam param);
+    R revoke(String userId, LiveAfterSalesRevokeParam param)  throws ParseException;
 
     R addDelivery(LiveAfterSalesDeliveryParam param);
 

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

@@ -164,6 +164,12 @@ public interface ILiveService
 
     List<Live> selectNoEndLiveList();
 
+    /**
+     * 查询开启了完课积分配置的直播间
+     * @return 直播列表
+     */
+    List<Live> selectLiveListWithCompletionPointsEnabled();
+
     void updateBatchById(List<Live> list);
 
     void updateStatusAndTimeBatchById(List<Live> list);

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

@@ -157,4 +157,24 @@ public interface ILiveWatchUserService {
      * @return 总观看时长(秒)
      */
     Long getTotalWatchDuration(Long liveId, Long userId);
+
+    /**
+     * 批量更新直播间观看用户
+     * @param liveWatchUsers 需要更新的观看用户列表
+     * @return 更新的记录数
+     */
+    int batchUpdateLiveWatchUser(List<LiveWatchUser> liveWatchUsers);
+
+    /**
+     * 批量插入直播间观看用户
+     * @param liveWatchUsers 需要插入的观看用户列表
+     * @return 插入的记录数
+     */
+    int batchInsertLiveWatchUser(List<LiveWatchUser> liveWatchUsers);
+
+    /**
+     * 清理直播间状态缓存
+     * @param liveId 直播间ID
+     */
+    void clearLiveFlagCache(Long liveId);
 }

+ 5 - 10
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.live.service.impl;
 
 import java.lang.reflect.InvocationTargetException;
 import java.sql.Timestamp;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.util.*;
@@ -876,8 +877,8 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 
 
     @Override
-    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
-    public R revoke(String userId, LiveAfterSalesRevokeParam param) {
+    @Transactional
+    public R revoke(String userId, LiveAfterSalesRevokeParam param) throws ParseException {
         LiveAfterSales storeAfterSales = baseMapper.selectLiveAfterSalesById(param.getId());
         if (storeAfterSales == null) {
             throw new CustomException("未查询到售后订单信息");
@@ -919,6 +920,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                 orderMap.setOrderCode(orderSn);
                 orderMap.setStatus(order.getStatus());
                 liveOrderService.updateLiveOrder(orderMap);
+                liveOrderItemService.updateFsStoreOrderCode(order.getOrderId(), orderSn);
                 //生成新的订单
                 List<LiveOrderPayment> payments = liveOrderPaymentMapper.selectLiveOrderPaymentByPay(5, order.getOrderId());
                 for (LiveOrderPayment payment : payments) {
@@ -927,14 +929,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                     livePayment.setBusinessCode(orderSn);
                     liveOrderPaymentMapper.updateLiveOrderPayment(livePayment);
                 }
-
-                try {
-                    if (liveOrderPaymentMapper.selectByBuissnessId(order.getOrderId()) != null) {
-                        liveOrderService.createOmsOrder(order.getOrderId());
-                    }
-                } catch (Exception e) {
-                    log.error("推送ERP订单失败!",e);
-                }
+                liveOrderService.createOmsOrder(order.getOrderId());
             }
         }
         baseMapper.updateLiveAfterSales(storeAfterSales);

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

@@ -143,16 +143,16 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
                 long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
 
-                if (daysBetween == 1) {
+                if (daysBetween == 0) {
+                    continuousDays = latestRecord.getContinuousDays();
+                    log.debug("今天已有其他直播间完课记录,继承连续天数, liveId={}, userId={}, continuousDays={}", 
+                            liveId, userId, continuousDays);
+                } else if (daysBetween == 1) {
                     // 昨天完课了,连续天数+1
                     continuousDays = latestRecord.getContinuousDays() + 1;
-                } else if (daysBetween > 1) {
+                } else {
                     // 中断了,重新开始
                     continuousDays = 1;
-                } else {
-                    // daysBetween == 0 说明今天已经有记录了(理论上不会进入这里,因为前面已经检查过)
-                    log.warn("异常情况: 今天已有完课记录, liveId={}, userId={}", liveId, userId);
-                    return;
                 }
             }
 

+ 33 - 25
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -750,6 +750,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             order.setStatus(OrderInfoEnum.STATUS_1.getValue());
             order.setPayTime(LocalDateTime.now());
             order.setIsPay("1");
+
             baseMapper.updateLiveOrder(order);
             try {
                 this.updateLiveWatchLog(order);
@@ -1697,6 +1698,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         order.setDeliverySn(null);
         order.setDeliveryCode(null);
         order.setDeliveryName(null);
+        order.setUpdateTime(new Date());
         //写入日志
         log.info("ErpCreate:" + order.getOrderCode() + ":" + JSONUtil.toJsonStr(response));
         //支付成功后 将订单号写入待发货的REDIS中
@@ -1853,6 +1855,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             order.setStatus(OrderInfoEnum.STATUS_2.getValue());
             order.setDeliverySn(deliveryId);
             order.setDeliverySendTime(new Date());
+            order.setUpdateTime(new Date());
 
             liveOrderMapper.updateLiveOrder(order);
             liveOrderLogsService.create(order.getOrderId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
@@ -1865,31 +1868,36 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
             }
-            expressService.subscribeEspress(order.getOrderCode(), order.getDeliveryCode(), order.getDeliverySn(), lastFourNumber);
-
-            TemplateBean templateBean = TemplateBean.builder()
-                    .orderId(order.getOrderId().toString())
-                    .orderCode(order.getOrderCode())
-                    .deliveryId(order.getDeliverySn())
-                    .deliveryName(order.getDeliveryName())
-                    .userId(Long.valueOf(order.getUserId()))
-                    .templateType(TemplateListenEnum.TYPE_2.getValue())
-                    .build();
-            publisher.publishEvent(new TemplateEvent(this, templateBean));
-
-            LiveOrderPayment fsStorePayment  = liveOrderPaymentMapper.selectLiveOrderLatestPayByOrderId(order.getOrderId());
-            FsWxExpressTask fsWxExpressTask = new FsWxExpressTask();
-            fsWxExpressTask.setUserId(Long.valueOf(order.getUserId()));
-            fsWxExpressTask.setStatus(0);
-            fsWxExpressTask.setRetryCount(0);
-            fsWxExpressTask.setType(1);
-            fsWxExpressTask.setCreateTime(LocalDateTime.now());
-            fsWxExpressTask.setUpdateTime(LocalDateTime.now());
-            fsWxExpressTask.setOrderCode(order.getOrderCode());
-            fsWxExpressTask.setExpressCompany(express.getCode());
-            fsWxExpressTask.setExpressNo(deliveryId);
-            fsWxExpressTask.setAppid(fsStorePayment.getAppId());
-            fsWxExpressTaskMapper.insert(fsWxExpressTask);
+            try {
+                expressService.subscribeEspress(order.getOrderCode(), order.getDeliveryCode(), order.getDeliverySn(), lastFourNumber);
+                TemplateBean templateBean = TemplateBean.builder()
+                        .orderId(order.getOrderId().toString())
+                        .orderCode(order.getOrderCode())
+                        .deliveryId(order.getDeliverySn())
+                        .deliveryName(order.getDeliveryName())
+                        .userId(Long.valueOf(order.getUserId()))
+                        .templateType(TemplateListenEnum.TYPE_2.getValue())
+                        .build();
+                publisher.publishEvent(new TemplateEvent(this, templateBean));
+
+                LiveOrderPayment fsStorePayment  = liveOrderPaymentMapper.selectLiveOrderLatestPayByOrderId(order.getOrderId());
+                FsWxExpressTask fsWxExpressTask = new FsWxExpressTask();
+                fsWxExpressTask.setUserId(Long.valueOf(order.getUserId()));
+                fsWxExpressTask.setStatus(0);
+                fsWxExpressTask.setRetryCount(0);
+                fsWxExpressTask.setType(1);
+                fsWxExpressTask.setCreateTime(LocalDateTime.now());
+                fsWxExpressTask.setUpdateTime(LocalDateTime.now());
+                fsWxExpressTask.setOrderCode(order.getOrderCode());
+                fsWxExpressTask.setExpressCompany(express.getCode());
+                fsWxExpressTask.setExpressNo(deliveryId);
+                fsWxExpressTask.setAppid(fsStorePayment.getAppId());
+                fsWxExpressTaskMapper.insert(fsWxExpressTask);
+            } catch (Exception e) {
+                log.error("订阅物流失败:{},{},{},{}",order.getOrderCode(), order.getDeliveryCode(), order.getDeliverySn(), lastFourNumber);
+                order.setDeliveryType("订阅物流失败");
+            }
+            liveOrderMapper.updateLiveOrder(order);
         }
     }
 

+ 29 - 4
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -55,6 +55,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
 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.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.fs.common.utils.sign.Md5Utils;
@@ -123,7 +124,7 @@ public class LiveServiceImpl implements ILiveService
     private CompanyMapper companyMapper;
     @Autowired
     private LiveCouponMapper liveCouponMapper;
-    
+
     @Autowired
     LiveTagConfigMapper liveTagConfigMapper;
 
@@ -228,6 +229,11 @@ public class LiveServiceImpl implements ILiveService
         return baseMapper.selectNoEndLiveList();
     }
 
+    @Override
+    public List<Live> selectLiveListWithCompletionPointsEnabled() {
+        return baseMapper.selectLiveListWithCompletionPointsEnabled();
+    }
+
     @Override
     public void updateBatchById(List<Live> list) {
         baseMapper.updateLiveList(list);
@@ -265,6 +271,19 @@ public class LiveServiceImpl implements ILiveService
         // liveVo.setStoreId(storeId);
 		BeanUtils.copyProperties(live, liveVo);
 		liveVo.setNowDuration(200L);
+
+        Boolean completionPointsEnabled = false;
+        String configJson = live.getConfigJson();
+        if (StringUtils.isNotEmpty(configJson)) {
+            try {
+                JSONObject jsonConfig = JSON.parseObject(configJson);
+                completionPointsEnabled = jsonConfig.getBooleanValue("enabled");
+            } catch (Exception e) {
+                log.warn("解析直播完课积分配置失败, liveId={}", id, e);
+            }
+        }
+        liveVo.setCompletionPointsEnabled(completionPointsEnabled);
+
         LiveVideo liveVideo = liveVideoService.selectLiveVideoByLiveIdAndType(id, 3);
         if (liveVideo != null) {
             liveVo.setPreviewUrl(liveVideo.getVideoUrl());
@@ -618,7 +637,7 @@ public class LiveServiceImpl implements ILiveService
             liveTagConfigMapper.deleteByLiveId(live.getLiveId());
             insertLiveTagConfig(live.getLiveTagList(),live.getLiveId(),live.getCreateBy());
         }
-        
+
         return result;
     }
 
@@ -919,8 +938,14 @@ public class LiveServiceImpl implements ILiveService
         if (live.getCompanyId() != null && exist.getCompanyId() != null && !Objects.equals(exist.getCompanyId(), live.getCompanyId())) {
             return R.error("您没有权限操作此直播间");
         }
-        String rtmpPushUrl = generateRtmpPushUrl("rtmp://200149.push.tlivecloud.com", "live", exist.getLiveId().toString());
-        String hlvPlayUrl = generateHlvPlayUrl("https://live.test.ifeiyu100.com", "live", exist.getLiveId().toString());
+
+        SysConfig sysConfig = sysConfigService.selectConfigByConfigKey("living.config");
+        Map<String, String> livingConfigMap = JSON.parseObject(sysConfig.getConfigValue(), Map.class);
+        if (livingConfigMap == null || livingConfigMap.isEmpty()) {
+            return R.error("缺失直播配置");
+        }
+        String rtmpPushUrl = generateRtmpPushUrl(livingConfigMap.get("domain"), livingConfigMap.get("app"), exist.getLiveId().toString());
+        String hlvPlayUrl = generateHlvPlayUrl(livingConfigMap.get("http"), livingConfigMap.get("app"), exist.getLiveId().toString());
         Date now = new Date();
         exist.setRtmpUrl(rtmpPushUrl);
         exist.setFlvHlsUrl(hlvPlayUrl);

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

@@ -150,6 +150,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     public int insertLiveWatchUser(LiveWatchUser liveWatchUser)
     {
         liveWatchUser.setCreateTime(DateUtils.getNowDate());
+        liveWatchUser.setUpdateTime(DateUtils.getNowDate());
         return baseMapper.insertLiveWatchUser(liveWatchUser);
     }
 
@@ -166,6 +167,59 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return baseMapper.updateLiveWatchUser(liveWatchUser);
     }
 
+    /**
+     * 批量更新直播间观看用户
+     * @param liveWatchUsers 需要更新的观看用户列表
+     * @return 更新的记录数
+     */
+    @Override
+    public int batchUpdateLiveWatchUser(List<LiveWatchUser> liveWatchUsers) {
+        if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+            return 0;
+        }
+        Date now = DateUtils.getNowDate();
+        // 设置统一的更新时间
+        for (LiveWatchUser user : liveWatchUsers) {
+            if (user.getUpdateTime() == null) {
+                user.setUpdateTime(now);
+            }
+        }
+        return baseMapper.batchUpdateLiveWatchUser(liveWatchUsers);
+    }
+
+    /**
+     * 批量插入直播间观看用户
+     * @param liveWatchUsers 需要插入的观看用户列表
+     * @return 插入的记录数
+     */
+    @Override
+    public int batchInsertLiveWatchUser(List<LiveWatchUser> liveWatchUsers) {
+        if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+            return 0;
+        }
+        Date now = DateUtils.getNowDate();
+        // 设置统一的创建时间和更新时间
+        for (LiveWatchUser user : liveWatchUsers) {
+            if (user.getCreateTime() == null) {
+                user.setCreateTime(now);
+            }
+            if (user.getUpdateTime() == null) {
+                user.setUpdateTime(now);
+            }
+        }
+        return baseMapper.batchInsertLiveWatchUser(liveWatchUsers);
+    }
+
+    /**
+     * 清理直播间状态缓存
+     * @param liveId 直播间ID
+     */
+    @Override
+    public void clearLiveFlagCache(Long liveId) {
+        String cacheKey = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, liveId);
+        redisCache.deleteObject(cacheKey);
+    }
+
     /**
      * 批量删除直播间观看用户
      *
@@ -329,28 +383,75 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         redisCache.hashPut(hashKey, String.valueOf(userId), JSON.toJSONString(liveWatchUser));
         return liveWatchUser;
     }
+    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s";
     @Override
     public LiveWatchUser close(FsUserScrm fsUser,long liveId, long userId) {
-
         // 查询直播间信息
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new RuntimeException("直播间不存在");
         }
+        try {
+            // 从 Redis 获取用户进入时间
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            // 获取当前直播/回放状态
+            Map<String, Integer> flagMap = this.getLiveFlagWithCache(liveId);
+            Integer currentLiveFlag = flagMap.get("liveFlag");
+            Integer currentReplayFlag = flagMap.get("replayFlag");
+            // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
+            LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, currentLiveFlag, currentReplayFlag);
+            if (liveWatchUser == null) {
+                return null;
+            }
+            long currentTimeMillis = System.currentTimeMillis();
+            if (entryTime == null) {
+                // 如果没有进入时间记录,可使用用户更新时间
+                if (liveWatchUser.getUpdateTime() == null) {
+                    entryTime = currentTimeMillis;
+                } else {
+                    entryTime = liveWatchUser.getUpdateTime().getTime();
+                }
+            }
 
-        // 获取直播/回放状态(带缓存)
-        Map<String, Integer> flagMap = getLiveFlagWithCache(liveId);
-        Integer liveFlag = flagMap.get("liveFlag");
-        Integer replayFlag = flagMap.get("replayFlag");
+            Date now = new Date();
+            // 计算在线时长(秒)
+            long durationSeconds = (currentTimeMillis - entryTime) / 1000;
+            if (durationSeconds <= 0) {
+                return liveWatchUser;
+            }
+            if (liveWatchUser != null) {
+                // 累加在线时长
+                Long onlineSeconds = liveWatchUser.getOnlineSeconds();
+                if (onlineSeconds == null) {
+                    onlineSeconds = 0L;
+                }
+                liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
+                liveWatchUser.setUpdateTime(now);
+                liveWatchUser.setOnline(1);
+                baseMapper.updateLiveWatchUser(liveWatchUser);
+                String hashKey  = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
+                redisCache.hashDelete(hashKey, String.valueOf(userId));
+                // 删除 Redis 中的进入时间记录
+                redisCache.deleteObject(entryTimeKey);
+                return liveWatchUser;
+                // 更新数据库
+//                liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
+                // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
+//                if (currentLiveFlag != null && currentLiveFlag == 1
+//                        && liveWatchUser.getCompanyId() != null && liveWatchUser.getCompanyId() > 0
+//                        && liveWatchUser.getCompanyUserId() != null && liveWatchUser.getCompanyUserId() > 0) {
+//                    updateLiveWatchLogTypeByDuration(liveId, userId,
+//                            liveWatchUser.getCompanyId(), liveWatchUser.getCompanyUserId(),
+//                            liveWatchUser.getOnlineSeconds());
+//                }
+            }
 
-        // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
-        LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
-        liveWatchUser.setUpdateTime(DateUtils.getNowDate());
-        liveWatchUser.setOnline(1);
-        baseMapper.updateLiveWatchUser(liveWatchUser);
-        String hashKey  = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
-        redisCache.hashDelete(hashKey, String.valueOf(userId));
-        return liveWatchUser;
+        } catch (Exception e) {
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}",
+                    liveId, userId, e.getMessage(), e);
+        }
+        return null;
     }
 
     /**

+ 9 - 0
fs-service/src/main/java/com/fs/live/vo/LiveVo.java

@@ -33,6 +33,9 @@ public class LiveVo {
     private String liveImgUrl;
 
     private String liveConfig;
+    
+    /** 直播配置 */
+    private String configJson;
 
     private String videoUrl;
     private Long videoId;
@@ -55,4 +58,10 @@ public class LiveVo {
     private Integer previewVideoType;
     private Long previewVideoId;
     private Integer globalVisible;
+    
+    /** 是否开启直播完课积分功能 */
+    private Boolean completionPointsEnabled;
+    
+    /** 今天是否已领取完课奖励 */
+    private Boolean todayRewardReceived;
 }

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

@@ -59,6 +59,14 @@ public class MergedOrderExportVO implements Serializable
     @Excel(name = "结算价")
     private BigDecimal FPrice;
 
+    /** 额外运费 */
+    @Excel(name = "额外运费")
+    private BigDecimal payDelivery;
+
+    /** 额外运费 */
+//    @Excel(name = "额外运费")
+    private BigDecimal payPostage;
+
     /** 商品分类 */
     @Excel(name = "商品分类")
     private String cateName;
@@ -142,12 +150,6 @@ public class MergedOrderExportVO implements Serializable
     @Excel(name = "实付金额")
     private BigDecimal payMoney;
 
-    /** 额外运费 */
-    @Excel(name = "额外运费")
-    private BigDecimal payPostage;
 
-    /** 物流代收金额 */
-    @Excel(name = "物流代收金额")
-    private BigDecimal payDelivery;
 }
 

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

@@ -1,5 +1,6 @@
 package com.fs.live.vo;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
@@ -50,9 +51,13 @@ public class MergedOrderVO implements Serializable
     /** 运费 */
     private BigDecimal payDelivery;
 
+    /** 运费 */
+    private BigDecimal payPostage;
+
     /** 成本价 */
     private BigDecimal cost;
 
+
     /** 订单状态 */
     @Excel(name = "订单状态",dictType="sys_live_order_status")
     private Integer status;
@@ -196,6 +201,8 @@ public class MergedOrderVO implements Serializable
     /** 银行交易流水号 */
     private String bankTransactionId;
 
+    private BigDecimal FPrice;
+
 
 }
 

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

@@ -148,7 +148,7 @@ public class Lead implements Serializable {
      */
     private String unitId;
     /**
-     * 创意ID
+     * 广告投流的最小单元 创意ID
      */
     private String ideaId;
     /**

+ 1 - 0
fs-service/src/main/java/com/fs/newAdv/domain/PromotionAccount.java

@@ -103,6 +103,7 @@ public class PromotionAccount implements Serializable {
      * 应用授权链接
      */
     private String refreshToken;
+    private LocalDateTime expireTime;
 
     /**
      * 创建时间

+ 4 - 0
fs-service/src/main/java/com/fs/newAdv/domain/Site.java

@@ -120,6 +120,10 @@ public class Site implements Serializable {
      */
     private Long allocationRuleId;
 
+    /**
+     * 广告投流的最小单元 创意ID
+     */
+    private String ideaId;
     /**
      * 创建时间
      */

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java

@@ -35,7 +35,7 @@ public class ConversionEventListener {
      *
      * @param event 转化事件
      */
-    @Async("asyncExecutor")
+    @Async
     @EventListener
     public void handleConversionEvent(ConversionEvent event) {
         String traceId = event.getTraceId();

+ 47 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/AbstractApiClient.java

@@ -4,19 +4,29 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.HttpResponse;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.newAdv.service.IApiCallLogService;
+import com.fs.newAdv.service.IPromotionAccountService;
+import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.formula.functions.T;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import java.time.LocalDateTime;
 import java.util.Map;
+import java.util.function.Function;
 
 @Slf4j
 public abstract class AbstractApiClient implements IApiClient {
 
     @Autowired
     private IApiCallLogService apiCallLogService;
-
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
 
     /**
      * 构建回传接口上下文信息 (traceId, userId等)
@@ -60,4 +70,40 @@ public abstract class AbstractApiClient implements IApiClient {
     protected interface ApiCall {
         HttpResponse call() throws Exception;
     }
+
+    protected String getAccessToken(Long promotionAccountId) {
+        PromotionAccount byId = promotionAccountService.getById(promotionAccountId);
+        // 判断token是否过期 提前1小时刷新
+        if (byId.getExpireTime().isBefore(LocalDateTime.now().plusHours(1))) {
+            // 获取请求参数
+            IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(byId.getAdvertiserId()));
+            IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;
+            AccessTokenVo accessTokenVo = tokenClient.refreshAccessToken(byId);
+            byId.setAccessToken(accessTokenVo.getAccessToken());
+            byId.setRefreshToken(accessTokenVo.getRefreshToken());
+            byId.setExpireTime(accessTokenVo.getExpireTime());
+            byId.setUpdateTime(LocalDateTime.now());
+            promotionAccountService.updateById(byId);
+            return accessTokenVo.getAccessToken();
+        }
+        return byId.getAccessToken();
+    }
+
+    protected <R> R executeToken(Function<JSONObject, R> mapper, ApiCall action) {
+        HttpResponse execute = null;
+        try {
+            execute = action.call();
+        } catch (Exception e) {
+            log.error("广告token调用失败", e);
+        }
+        String body = execute.body();
+        log.info("广告 token 接口响应: {}", body);
+        JSONObject res = new JSONObject(body);
+        int code = res.getInt("code");
+        if (code == 0 || code == 200) {
+            // 不同广告商返回的字段可能不一样 兼容
+            return mapper.apply(res);
+        }
+        return null;
+    }
 }

+ 2 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/IAccessTokenClient.java

@@ -1,10 +1,11 @@
 package com.fs.newAdv.integration.client;
 
+import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
 import com.fs.newAdv.vo.AccessTokenVo;
 
 public interface IAccessTokenClient extends IApiClient {
-    AccessTokenVo refreshAccessToken(String appId, String appSecret, String refreshToken);
+    AccessTokenVo refreshAccessToken(PromotionAccount promotionAccount);
 
     AccessTokenVo getAccessTokenByAuthCode(AccessTokenByAuthCodeVo codeVo);
 }

+ 3 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/IApiClient.java

@@ -1,9 +1,11 @@
 package com.fs.newAdv.integration.client;
 
+import cn.hutool.json.JSONObject;
 import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.domain.Site;
 import com.fs.newAdv.domain.SiteStatistics;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.vo.AccessTokenVo;
 
 import java.util.Map;
 
@@ -20,6 +22,6 @@ public interface IApiClient {
 
     AdvertiserTypeEnum getAdvertiserType();
 
-    SiteStatistics getDataReport(PromotionAccount account,String startDate,String endDate);
+    SiteStatistics getDataReport(PromotionAccount account,String ideaId,String startDate,String endDate);
 }
 

+ 50 - 54
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/BaiduApiClient.java

@@ -6,13 +6,13 @@ import cn.hutool.http.HttpResponse;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import com.fs.newAdv.integration.client.AbstractApiClient;
-import com.fs.newAdv.integration.client.IAccessTokenClient;
 import com.fs.common.constant.SystemConstant;
 import com.fs.common.exception.ThirdPartyException;
 import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.domain.SiteStatistics;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.AbstractApiClient;
+import com.fs.newAdv.integration.client.IAccessTokenClient;
 import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
 import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
@@ -39,8 +39,8 @@ public class BaiduApiClient extends AbstractApiClient implements IAccessTokenCli
      */
     private static final String CONVERSION_API_URL = "https://ocpc.baidu.com/ocpcapi/api/uploadConvertData";
     private static final String REPORT_DATA_API_URL = "https://api.baidu.com/json/sms/service/OpenApiReportService/getReportData";
-    private static final String ACCESSTOKEN_URL = "https://u.baidu.com/oauth/accessToken";
-    private static final String REFRESHTOKEN_URL = "https://u.baidu.com/oauth/refreshToken";
+    private static final String ACCESS_TOKEN_URL = "https://u.baidu.com/oauth/accessToken";
+    private static final String REFRESH_TOKEN_URL = "https://u.baidu.com/oauth/refreshToken";
 
 
     /**
@@ -118,12 +118,12 @@ public class BaiduApiClient extends AbstractApiClient implements IAccessTokenCli
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account, String startDate, String endDate) {
+    public SiteStatistics getDataReport(PromotionAccount account, String ideaId, String startDate, String endDate) {
         // 构建请求参数
         Map<String, Object> map = new HashMap<>();
         Map<String, Object> header = new HashMap<>();
         header.put("accessToken", account.getAccessToken());
-        header.put("userName", "BDCC-yyt19");
+        header.put("userName", account.getAccountShortName());
         map.put("header", header);
         Map<String, Object> body = new HashMap<>();
         // 基础信息
@@ -142,8 +142,13 @@ public class BaiduApiClient extends AbstractApiClient implements IAccessTokenCli
         body.put("columns", Arrays.asList("impression", "click", "cost", "ctr", "cpc", "cpm", "phoneButtonClicks"));
         body.put("startRow", 0);
         body.put("rowCount", 1000);
+        Map<String,Object> filters = new HashMap<>();
+        filters.put("column","adGroupId");
+        filters.put("operator","IN");
+        filters.put("values",Arrays.asList(ideaId));
+        body.put("filters", filters);
         map.put("body", body);
-
+        log.info("百度数据请求参数:{}", JSONUtil.toJsonStr(map));
         HttpResponse execute = HttpRequest.post(REPORT_DATA_API_URL)
                 .header("Content-Type", "application/json")
                 .body(JSONUtil.toJsonStr(map))
@@ -166,58 +171,49 @@ public class BaiduApiClient extends AbstractApiClient implements IAccessTokenCli
 
 
     @Override
-    public AccessTokenVo refreshAccessToken(String appId, String appSecret, String refreshToken) {
-        // 调用接口换取授权令牌
-        Map<String, Object> requestMap = new HashMap<>();
-        requestMap.put("appId", appId);
-        requestMap.put("secretKey", appSecret);
-        requestMap.put("refreshToken", refreshToken);
-        // requestMap.put("userId", userId);
-        String paramsJson = JSONUtil.toJsonStr(requestMap);
-        HttpResponse execute = HttpRequest.post(REFRESHTOKEN_URL)
-                .header("Content-Type", "application/json")
-                .body(paramsJson)
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
-        JSONObject res = new JSONObject(execute.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken(data.getStr("accessToken"))
-                    .refreshToken(data.getStr("refreshToken"))
-                    .build();
-        }
-        return null;
+    public AccessTokenVo refreshAccessToken(PromotionAccount promotionAccount) {
+        return executeToken(this::getAccessTokenVo, () -> {
+            // 调用接口换取授权令牌
+            Map<String, Object> requestMap = new HashMap<>();
+            requestMap.put("appId", promotionAccount.getAppId());
+            requestMap.put("secretKey", promotionAccount.getAppSecret());
+            requestMap.put("refreshToken", promotionAccount.getRefreshToken());
+            requestMap.put("userId", promotionAccount.getAdAccountId());
+            // 发送HTTP请求
+            return HttpRequest.post(REFRESH_TOKEN_URL)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(requestMap))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
     }
 
     @Override
     public AccessTokenVo getAccessTokenByAuthCode(AccessTokenByAuthCodeVo codeVo) {
-        // 调用接口换取授权令牌
-        Map<String, Object> requestMap = new HashMap<>();
-        requestMap.put("appId", codeVo.getAppId());
-        requestMap.put("secretKey", codeVo.getAppSecret());
-        requestMap.put("authCode", codeVo.getAuthCode());
-        requestMap.put("grantType", "access_token");
-        requestMap.put("userId", codeVo.getUserId());
-        String paramsJson = JSONUtil.toJsonStr(requestMap);
-
-        HttpResponse execute = HttpRequest.post(ACCESSTOKEN_URL)
-                .header("Content-Type", "application/json")
-                .body(paramsJson)
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
+        return executeToken(this::getAccessTokenVo, () -> {
+            // 调用接口换取授权令牌
+            Map<String, Object> requestMap = new HashMap<>();
+            requestMap.put("appId", codeVo.getAppId());
+            requestMap.put("secretKey", codeVo.getAppSecret());
+            requestMap.put("authCode", codeVo.getAuthCode());
+            requestMap.put("grantType", "access_token");
+            requestMap.put("userId", codeVo.getUserId());
+            // 发送HTTP请求
+            return HttpRequest.post(ACCESS_TOKEN_URL)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(requestMap))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
+    }
 
-        JSONObject res = new JSONObject(execute.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken(data.getStr("accessToken"))
-                    .refreshToken(data.getStr("refreshToken"))
-                    .build();
-        }
-        return null;
+    public AccessTokenVo getAccessTokenVo(JSONObject res) {
+        JSONObject data = res.getJSONObject("data");
+        return AccessTokenVo.builder()
+                .accessToken((String) data.get("accessToken"))
+                .refreshToken((String) data.get("refreshToken"))
+                .expireTime(LocalDateTime.now().plusSeconds(data.getLong("expiresIn")))
+                .build();
     }
 }
 

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/IQIYIApiClient.java

@@ -70,7 +70,7 @@ public class IQIYIApiClient extends AbstractApiClient {
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account,String startDate,String endDate) {
+    public SiteStatistics getDataReport(PromotionAccount account,String ideaId,String startDate,String endDate) {
         return null;
     }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OPPOApiClient.java

@@ -92,7 +92,7 @@ public class OPPOApiClient extends AbstractApiClient {
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account,String startDate,String endDate) {
+    public SiteStatistics getDataReport(PromotionAccount account,String ideaId,String startDate,String endDate) {
         return null;
     }
 }

+ 110 - 89
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java

@@ -1,22 +1,28 @@
 package com.fs.newAdv.integration.client.advertiser;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import com.fs.newAdv.integration.client.AbstractApiClient;
-import com.fs.newAdv.integration.client.IAccessTokenClient;
+import com.baidu.dev2.thirdparty.jackson.core.JsonProcessingException;
+import com.baidu.dev2.thirdparty.jackson.databind.ObjectMapper;
 import com.fs.common.constant.SystemConstant;
-import com.fs.common.exception.ThirdPartyException;
 import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.domain.SiteStatistics;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.AbstractApiClient;
+import com.fs.newAdv.integration.client.IAccessTokenClient;
 import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
 import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.utils.URIBuilder;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -35,7 +41,8 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
     /**
      * 巨量引擎转化回传API地址
      */
-    private static final String CONVERSION_API_URL = "https://ad.oceanengine.com/track/activate";
+    private static final String CONVERSION_API_URL = "https://ad.oceanengine.com/track/activate/";
+    private static final String REPORT_API_URL = "https://api.oceanengine.com/open_api/v3.0/report/custom/get/";
     /**
      * 获取Access Token
      */
@@ -80,104 +87,118 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
         return params;
     }
 
-    /**
-     * 构建上下文信息
-     *
-     * @param conversionData 转化数据
-     * @return 上下文Map
-     */
-    private Map<String, Object> buildContext(Map<String, Object> conversionData) {
-        Map<String, Object> context = new HashMap<>();
-
-        // 必填:点击ID
-        context.put("ad", buildAdContext(conversionData));
-
-        // 用户信息
-        if (conversionData.containsKey("userId")) {
-            Map<String, Object> user = new HashMap<>();
-            user.put("user_id", conversionData.get("userId"));
-            context.put("user", user);
-        }
-
-        return context;
-    }
-
-    /**
-     * 构建广告上下文
-     *
-     * @param conversionData 转化数据
-     * @return 广告上下文
-     */
-    private Map<String, Object> buildAdContext(Map<String, Object> conversionData) {
-        Map<String, Object> ad = new HashMap<>();
-
-        // 点击ID(必填)
-        String clickId = (String) conversionData.get("traceId");
-        if (StrUtil.isBlank(clickId)) {
-            throw new ThirdPartyException("点击ID不能为空");
-        }
-        ad.put("callback", clickId);
-
-        return ad;
-    }
-
     @Override
     public AdvertiserTypeEnum getAdvertiserType() {
         return AdvertiserTypeEnum.OCEANENGINE;
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account,String startDate,String endDate) {
-        return null;
+    public SiteStatistics getDataReport(PromotionAccount account, String ideaId, String startDate, String endDate) {
+        // 构建请求参数
+        Map<String, Object> map = new HashMap<>();
+        map.put("advertiser_id", Long.valueOf(account.getAdAccountId()));
+        // 纬度--天
+        map.put("dimensions", Arrays.asList("stat_time_day"));
+        // 过滤条件
+        Map<String, Object> filters = new HashMap<>();
+        filters.put("field", "cdp_promotion_id");
+        filters.put("type", 2);
+        filters.put("operator", 1);
+        filters.put("values", Arrays.asList(ideaId));
+        map.put("filters", Arrays.asList(filters));
+        /**
+         * stat_cost 消耗(元)
+         * show_cnt 展示数
+         * cpm_platform 平均千次展现费用(元)
+         * click_cnt 点击数
+         * ctr 点击率
+         * cpc_platform 平均点击单价(元)
+         * convert_cnt 转化数
+         * conversion_cost 平均转化成本
+         * conversion_rate 转化率
+         */
+        map.put("metrics", Arrays.asList("stat_cost",
+                "show_cnt", "cpm_platform", "click_cnt", "ctr", "cpc_platform", "cpc_platform", "convert_cnt",
+                "conversion_cost", "conversion_rate"));
+        map.put("start_time", startDate);
+        map.put("end_time", endDate);
+        map.put("order_by", Collections.emptyList());
+        String url;
+        try {
+            URIBuilder ub = new URIBuilder(REPORT_API_URL);
+            ObjectMapper objectMapper = new ObjectMapper();
+            map.forEach((k, v) -> {
+                try {
+                    ub.addParameter(k, v instanceof String ? (String) v : objectMapper.writeValueAsString(v));
+                } catch (JsonProcessingException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            url = ub.build().toURL().toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        HttpResponse execute = HttpRequest.get(url)
+                .header("Access-Token", getAccessToken(account.getId()))
+                .timeout(SystemConstant.API_TIMEOUT)
+                .execute();
+        JSONObject jsonObject = JSONUtil.parseObj(execute.body());
+        JSONObject data = jsonObject.getJSONObject("data");
+        JSONArray rows = data.getJSONArray("rows");
+        JSONObject jsonObject1 = rows.getJSONObject(0);
+        JSONObject jsonObject2 = jsonObject1.getJSONObject("metrics");
+        SiteStatistics siteStatistics = new SiteStatistics();
+        siteStatistics.setImpressionCount(Integer.valueOf(jsonObject2.getStr("show_cnt")));
+        siteStatistics.setClickCount(Integer.valueOf(jsonObject2.getStr("click_cnt")));
+        siteStatistics.setActualCost(new BigDecimal(jsonObject2.getStr("stat_cost")));
+        siteStatistics.setAccountCost(new BigDecimal(jsonObject2.getStr("stat_cost")));
+        siteStatistics.setClickRate(new BigDecimal(jsonObject2.getStr("ctr")));
+        siteStatistics.setAvgClickPrice(new BigDecimal(jsonObject2.getStr("cpc_platform")));
+        return siteStatistics;
     }
 
+
     @Override
-    public AccessTokenVo refreshAccessToken(String appId, String appSecret, String refreshToken) {
-        Map<String,Object> map = new HashMap<>();
-        map.put("app_id", appId);
-        map.put("secret", appSecret);
-        map.put("refresh_token", refreshToken);
-        HttpResponse response = HttpRequest.post(ACCESS_TOKEN_URL)
-                .header("Content-Type", "application/json")
-                .form(JSONUtil.toJsonStr(map))
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
-        JSONObject res = new JSONObject(response.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken((String) data.get("access_token"))
-                    .refreshToken((String) data.get("refresh_token"))
-                    .build();
-        }
-        return null;
+    public AccessTokenVo refreshAccessToken(PromotionAccount promotionAccount) {
+        return executeToken(this::getAccessTokenVo, () -> {
+            Map<String, Object> map = new HashMap<>();
+            map.put("app_id", promotionAccount.getAppId());
+            map.put("secret", promotionAccount.getAppSecret());
+            map.put("refresh_token", promotionAccount.getRefreshToken());
+            // 发送HTTP请求
+            return HttpRequest.post(REFRESH_TOKEN_URL)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(map))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
     }
 
 
     @Override
     public AccessTokenVo getAccessTokenByAuthCode(AccessTokenByAuthCodeVo request) {
-        Map<String,Object> map = new HashMap<>();
-        map.put("app_id", request.getAppId());
-        map.put("secret", request.getAppSecret());
-        map.put("auth_code", request.getAuthCode());
-        HttpResponse response = HttpRequest.post(ACCESS_TOKEN_URL)
-                .header("Content-Type", "application/json")
-                .body(JSONUtil.toJsonStr(map))
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
-        String body = response.body();
-        log.info("巨量获取token数据{}", body);
-        JSONObject res = new JSONObject(response.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken((String) data.get("access_token"))
-                    .refreshToken((String) data.get("refresh_token"))
-                    .build();
-        }
-        return null;
+        return executeToken(this::getAccessTokenVo, () -> {
+            // 调用接口换取授权令牌
+            Map<String, Object> map = new HashMap<>();
+            map.put("app_id", request.getAppId());
+            map.put("secret", request.getAppSecret());
+            map.put("auth_code", request.getAuthCode());
+            // 发送HTTP请求
+            return HttpRequest.post(ACCESS_TOKEN_URL)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(map))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
+    }
+
+    public AccessTokenVo getAccessTokenVo(JSONObject res) {
+        JSONObject data = res.getJSONObject("data");
+        return AccessTokenVo.builder()
+                .accessToken((String) data.get("access_token"))
+                .refreshToken((String) data.get("refresh_token"))
+                .expireTime(LocalDateTime.now().plusSeconds(data.getLong("refresh_token_expires_in")))
+                .build();
     }
 }
 

+ 35 - 41
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/TencentApiClient.java

@@ -1,21 +1,21 @@
 package com.fs.newAdv.integration.client.advertiser;
 
 import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import com.fs.newAdv.integration.client.AbstractApiClient;
-import com.fs.newAdv.integration.client.IAccessTokenClient;
 import com.fs.common.constant.SystemConstant;
 import com.fs.common.utils.SnowflakeUtil;
 import com.fs.newAdv.domain.PromotionAccount;
 import com.fs.newAdv.domain.SiteStatistics;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.AbstractApiClient;
+import com.fs.newAdv.integration.client.IAccessTokenClient;
 import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
 import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -83,52 +83,46 @@ public class TencentApiClient extends AbstractApiClient implements IAccessTokenC
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account, String startDate, String endDate) {
+    public SiteStatistics getDataReport(PromotionAccount account, String ideaId, String startDate, String endDate) {
         return null;
     }
 
 
-    public AccessTokenVo refreshAccessToken(String appId, String appSecret, String refreshToken) {
-        HttpResponse response = HttpRequest.get(TOKEN_API_URL)
-                .form("client_id", appId)
-                .form("client_secret", appSecret)
-                .form("grant_type", "refresh_token")
-                .form("refresh_token", refreshToken)
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
-
-        JSONObject res = new JSONObject(response.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken(data.getStr("access_token"))
-                    .refreshToken(data.getStr("refresh_token"))
-                    .build();
-        }
-        return null;
+    public AccessTokenVo refreshAccessToken(PromotionAccount promotionAccount) {
+        return executeToken(this::getAccessTokenVo, () -> {
+            // 发送HTTP请求
+            return HttpRequest.get(TOKEN_API_URL)
+                    .form("client_id", promotionAccount.getAppId())
+                    .form("client_secret", promotionAccount.getAppSecret())
+                    .form("grant_type", "refresh_token")
+                    .form("refresh_token", promotionAccount.getRefreshToken())
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
     }
 
     @Override
     public AccessTokenVo getAccessTokenByAuthCode(AccessTokenByAuthCodeVo codeVo) {
-        HttpResponse response = HttpRequest.get(TOKEN_API_URL)
-                .form("client_id", codeVo.getAppId())
-                .form("client_secret", codeVo.getAppSecret())
-                .form("grant_type", "authorization_code")
-                .form("authorization_code", codeVo.getAuthCode())
-                .form("redirect_uri", "authorization_code")
-                .timeout(SystemConstant.API_TIMEOUT)
-                .execute();
-        JSONObject res = new JSONObject(response.body());
-        int code = (int) res.get("code");
-        if (code == 0) {
-            JSONObject data = res.getJSONObject("data");
-            return AccessTokenVo.builder()
-                    .accessToken(data.getStr("access_token"))
-                    .refreshToken(data.getStr("refresh_token"))
-                    .build();
-        }
-        return null;
+        return executeToken(this::getAccessTokenVo, () -> {
+            // 发送HTTP请求
+            return HttpRequest.get(TOKEN_API_URL)
+                    .form("client_id", codeVo.getAppId())
+                    .form("client_secret", codeVo.getAppSecret())
+                    .form("grant_type", "authorization_code")
+                    .form("authorization_code", codeVo.getAuthCode())
+                    .form("redirect_uri", "authorization_code")
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+        });
+    }
+
+    public AccessTokenVo getAccessTokenVo(JSONObject res) {
+        JSONObject data = res.getJSONObject("data");
+        return AccessTokenVo.builder()
+                .accessToken((String) data.get("access_token"))
+                .refreshToken((String) data.get("refresh_token"))
+                .expireTime(LocalDateTime.now().plusSeconds(data.getLong("access_token_expires_in")))
+                .build();
     }
 }
 

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/VIVOApiClient.java

@@ -86,7 +86,7 @@ public class VIVOApiClient extends AbstractApiClient {
     }
 
     @Override
-    public SiteStatistics getDataReport(PromotionAccount account,String startDate,String endDate) {
+    public SiteStatistics getDataReport(PromotionAccount account,String ideaId,String startDate,String endDate) {
         return null;
     }
 }

+ 22 - 18
fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java

@@ -53,7 +53,7 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
     }
 
     @Override
-    @Async("asyncExecutor")
+    @Async
     public void updateGroupAddMemberLead(String userId, String chatId, String corpId, String unionid) {
         // 末次归因逻辑
         Lead lead = this.getOne(new LambdaQueryWrapper<Lead>()
@@ -79,7 +79,7 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
     }
 
     @Override
-    @Async("asyncExecutor")
+    @Async
     public CompletableFuture<String> updateAddMemberLead(String externalUserID, String userID, String corpId, String state) {
         QwExternalContactResult externalContactResult = qwApiService.getExternalcontact(externalUserID, corpId);
         String unionid = externalContactResult.getExternal_contact().getUnionid();
@@ -117,26 +117,30 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
     }
 
     @Override
-    @Async("asyncExecutor")
+    @Async
     public void weChatAuthorizationLead(String traceId, String unionId, String mpOpenId, String phone) {
-        Lead byTraceId = this.getByTraceId(traceId);
-        if (byTraceId == null) {
-            return;
-        }
-        this.update(new LambdaUpdateWrapper<Lead>()
-                .eq(Lead::getTraceId, traceId)
-                .set(Lead::getUnionid, unionId)
-                .set(Lead::getPhone, phone)
-                .set(Lead::getOpenid, mpOpenId)
-                .set(Lead::getMiniAuth, 1));
-        if (ObjectUtil.isNotEmpty(byTraceId.getLandingPageTs()) && byTraceId.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
-            // 微信授权且当日创建事件
-            conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.AUTH_TODAY_CREATE);
-        }
+       try{
+           Lead byTraceId = this.getByTraceId(traceId);
+           if (byTraceId == null) {
+               return;
+           }
+           this.update(new LambdaUpdateWrapper<Lead>()
+                   .eq(Lead::getTraceId, traceId)
+                   .set(Lead::getUnionid, unionId)
+                   .set(Lead::getPhone, phone)
+                   .set(Lead::getOpenid, mpOpenId)
+                   .set(Lead::getMiniAuth, 1));
+           if (ObjectUtil.isNotEmpty(byTraceId.getLandingPageTs()) && byTraceId.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
+               // 微信授权且当日创建事件
+               conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.AUTH_TODAY_CREATE);
+           }
+       }catch (Exception e){
+           e.printStackTrace();
+       }
     }
 
     @Override
-    @Async("asyncExecutor")
+    @Async
     public void updateAuthIndex(String traceId, Integer type) {
         LambdaUpdateWrapper<Lead> eq = new LambdaUpdateWrapper<Lead>().eq(Lead::getTraceId, traceId);
         if (type == 1) {

+ 4 - 4
fs-service/src/main/java/com/fs/newAdv/service/impl/SiteStatisticsServiceImpl.java

@@ -82,7 +82,7 @@ public class SiteStatisticsServiceImpl extends ServiceImpl<SiteStatisticsMapper,
                                 log.warn("账户 {} 未关联站点,跳过同步", account.getAccountName());
                                 return false;
                             }
-                            syncAccountData(account, site.getId(), batchNo);
+                            syncAccountData(account, site.getId(),site.getIdeaId(), batchNo);
                             return true;
                         } catch (Exception e) {
                             log.error("同步账户 {} 数据失败", account.getAccountName(), e);
@@ -98,9 +98,9 @@ public class SiteStatisticsServiceImpl extends ServiceImpl<SiteStatisticsMapper,
     }
 
     /**
-     * 同步账户数据
+     * 同步账户广告数据
      */
-    private void syncAccountData(PromotionAccount account, Long siteId, String batchNo) {
+    private void syncAccountData(PromotionAccount account, Long siteId,String ideaId, String batchNo) {
         log.info("开始同步账户:{}", account.getAccountName());
 
         // 查询现有统计记录
@@ -115,7 +115,7 @@ public class SiteStatisticsServiceImpl extends ServiceImpl<SiteStatisticsMapper,
         if (Integer.valueOf(1).equals(account.getApiSwitch())) {
             IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(account.getAdvertiserId()));
             try {
-                siteStatistics = apiClient.getDataReport(account, batchNo, batchNo);
+                siteStatistics = apiClient.getDataReport(account,ideaId, batchNo, batchNo);
             } catch (Exception e) {
                 siteStatistics = new SiteStatistics();
                 log.error("获取账户数据失败:{} {}", account.getAccountName(), siteId, e);

+ 2 - 0
fs-service/src/main/java/com/fs/newAdv/vo/AccessTokenVo.java

@@ -4,10 +4,12 @@ import lombok.Builder;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.time.LocalDateTime;
 
 @Data
 @Builder
 public class AccessTokenVo implements Serializable {
     private String accessToken;
     private String refreshToken;
+    private LocalDateTime expireTime;
 }

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

@@ -23,6 +23,8 @@ public class QwSopCourseFinishTempSetting implements Serializable,Cloneable{
     //课节
     private Integer videoId;
 
+    private Long liveId;
+
     private List<Setting> setting;
 
     @Override

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

@@ -47,6 +47,8 @@ public class QwSopTempSetting implements Serializable{
         
         private Integer isAtAll;
 
+        private Long liveId;
+
         @Override
         public Content clone() {
             try {

+ 3 - 1
fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java

@@ -61,7 +61,9 @@ public class QwSopLogs implements Serializable {
     private String msgId;
 
     /**
-    * 发送类型 1企微发送 2 Ai接口发送  3:完课发送  4:AI对话 5一键群发 6一键群发app
+    * 发送类型  1企微发送 2 Ai接口发送  3:完课发送  4:AI对话 5一键群发 6客户群群发
+     * 7欢迎语补发 8AI 9清除草稿 10发送草稿 11 课程模板类型 12 一键群发APP
+     * 13 注册链接 14 福袋  20直播间发送
     */
     private Integer sendType;
 

+ 7 - 0
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -690,6 +690,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 break;
                             //直播小程序单独
                             case "12":
+                                sopLogs.setSendType(20);
+                                setting.setLiveId(Long.valueOf(st.getLiveId()));
                                 String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + vo.getId().toString();
                                 st.setContentType("4");
                                 String js = configService.selectConfigByKey("his.config");
@@ -881,6 +883,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 break;
                             //直播小程序单独
                             case "12":
+                                sopLogs.setSendType(20);
+                                setting.setLiveId(setting.getLiveId());
                                 String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" +param.getCorpId()+"&qwUserId=" + qwUser.getId() + "&chatId=" + groupChat.getChatId();
                                 st.setContentType("4");
                                 String js = configService.selectConfigByKey("his.config");
@@ -1154,6 +1158,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             break;
                         //直播小程序单独
                         case "12":
+                            sopLogs.setSendType(20);
+                            setting.setLiveId(Long.valueOf(st.getLiveId()));
                             String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUserId +"&externalId=" + item.getExternalId().toString();
                             st.setContentType("4");
                             String js = configService.selectConfigByKey("his.config");
@@ -1746,6 +1752,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 //直播小程序单独
                 case "12":
                     String sortLiveLink;
+                    sopLogs.setSendType(20);
                     sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + item.getExternalId().toString();
 
 

+ 2 - 2
fs-service/src/main/resources/application-config-dev-jnlzjk.yml

@@ -86,8 +86,8 @@ tmp_secret_config:
 cloud_host:
   company_name: 济南联志健康
   projectCode: LZJK
-  spaceName:
-  volcengineUrl: https://jnlzvolcengine.ylrztop.com
+  spaceName: lzjk-2114522511
+  volcengineUrl: https://jnlzjkvolcengine.ylrztop.com
 headerImg:
   imgUrl:
 

+ 1 - 0
fs-service/src/main/resources/application-config-druid-cfryt-test.yml

@@ -77,6 +77,7 @@ cloud_host:
   company_name: 赤峰润
   projectCode: ryt
   spaceName:
+  volcengineUrl:
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://ysy-1329817240.cos.ap-guangzhou.myqcloud.com/ysy/20250820/2c47e4f105b641b4a49df50a77338e32.png

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

@@ -82,7 +82,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 赤峰润
   projectCode: ryt
-  spaceName:
+  spaceName: ryt-2114522511
+  volcengineUrl: https://rytvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://ysy-1329817240.cos.ap-guangzhou.myqcloud.com/ysy/20250820/2c47e4f105b641b4a49df50a77338e32.png

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

@@ -107,6 +107,7 @@ cloud_host:
   company_name: 福本源
   projectCode: FBY
   spaceName:
+  volcengineUrl:
 headerImg:
   imgUrl: https://fbylive.obs.cn-southwest-2.myhuaweicloud.com/fs/20250730/1753840024082.png
 ipad:

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

@@ -84,6 +84,7 @@ cloud_host:
   company_name: 广州郑多燕
   projectCode: GZZDY
   spaceName:
+  volcengineUrl:
 #看课授权时显示的头像
 headerImg:
   imgUrl:

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

@@ -87,7 +87,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 恒安图
   projectCode: HAT
-  spaceName:
+  spaceName: hat-2114522511
+  volcengineUrl: https://hatvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://hat-1323137866.cos.ap-chongqing.myqcloud.com/fs/20250928/hatlogo.png

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

@@ -84,6 +84,7 @@ cloud_host:
   company_name: 鸿森堂
   projectCode: HST
   spaceName:
+  volcengineUrl:
 #看课授权时显示的头像
 headerImg:
   imgUrl:

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

@@ -82,8 +82,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 河山医院
   projectCode: heshanyy
-  spaceName:
-  volcengineUrl:
+  spaceName: heshanyy-2114522511
+  volcengineUrl: https://heshanyyvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://hsyy-1348049832.cos.ap-chongqing.myqcloud.com/hsyy.jpg

+ 2 - 2
fs-service/src/main/resources/application-config-druid-jnlzjk.yml

@@ -87,8 +87,8 @@ tmp_secret_config:
 cloud_host:
   company_name: 济南联志健康
   projectCode: LZJK
-  spaceName:
-  volcengineUrl: https://jnlzvolcengine.ylrztop.com
+  spaceName: lzjk-2114522511
+  volcengineUrl: https://jnlzjkvolcengine.ylrztop.com
 headerImg:
   imgUrl:
 

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

@@ -84,6 +84,7 @@ cloud_host:
   company_name: 宽益堂
   projectCode: KYT
   spaceName:
+  volcengineUrl:
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://kuanyitang-1317640934.cos.ap-shanghai.myqcloud.com/kuanyitang/20250813/6b3b62e01672407c98f0561b73e35f6a.jpg

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

@@ -90,7 +90,8 @@ tmp_secret_config:
 cloud_host:
   company_name: 同顺堂
   projectCode: QDTST
-  spaceName:
+  spaceName: qdtst-2114522511
+  volcengineUrl: https://qdtstvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://qdtst-1360717104.cos.ap-nanjing.myqcloud.com/qdtst-1360717104/20250624/937019e4090f46788ef29c4e7df479c3.jpg

+ 8 - 0
fs-service/src/main/resources/application-config-druid-sft.yml

@@ -62,6 +62,12 @@ watch:
 fs :
   commonApi: http://172.30.0.11:8010
   h5CommonApi: http://119.29.195.254:8010
+  jwt:
+    # 加密秘钥
+    secret: 9a3f7b1c2e8d45a0f632b819e4c56d12
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
 nuonuo:
   key: 10924508
   secret: A2EB20764D304D16
@@ -77,6 +83,8 @@ cloud_host:
   company_name: 四福堂
   projectCode: SFT
   spaceName:
+  volcengineUrl:
+
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://sft-1361917636.cos.ap-chongqing.myqcloud.com/sft/20250606/b08b1a6212f44f2998423c8c5d7712ee.png

+ 104 - 0
fs-service/src/main/resources/application-config-druid-shdn.yml

@@ -0,0 +1,104 @@
+baidu:
+  token: 12313231232
+  back-domain: https://www.xxxx.com
+#配置
+logging:
+  level:
+    org.springframework.web: INFO
+    com.github.binarywang.demo.wx.cp: DEBUG
+    me.chanjar.weixin: DEBUG
+wx:
+  miniapp:
+    configs:
+      - appid: wx94951f52d3ac5e25   #北京存在文化
+        secret: bfe27b20c6e3c4232a1d4ef36228e84b #北京存在文化
+        token: Ncbnd7lJvkripxxna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+  cp:
+    corpId: wwa46ffb9ff6ac35b8 #企业ID北京存在文化
+    appConfigs:
+      - agentId: 1000070       #北京存在文化
+        secret: pu2EFz6gY2Fo2K-aRUxLPaAkKIaMJJRp8ES9JdpHkp4 #北京存在文化
+        token: PPKOdAlCoMO
+        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+  pay:
+    appId:  #微信公众号或者小程序等的appid
+    mchId:  #微信支付商户号
+    mchKey:  #微信支付商户密钥
+    subAppId:  #服务商模式下的子商户公众账号ID
+    subMchId:  #服务商模式下的子商户号
+    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
+  mp:
+    useRedis: false
+    redisConfig:
+      host: 127.0.0.1
+      port: 6379
+      timeout: 2000
+    configs:
+#      - appId: wxce847c8ebe5e62aa # 第一个公众号的appid  //公众号名称:济南联智健康
+#        secret: 37f7c5e3b7ff07794343957f7ced8de4 # 公众号的appsecret--济南联智健康
+      - appId: wxd6905bed94e45ef0 # 第一个公众号的appid  //公众号名称:济南联智健康
+        secret: c1a042a55bac24033535da50ce8ab6a2 # 公众号的appsecret--济南联智健康
+        token: PPKOdAlCoMO # 接口配置里的Token值
+        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+aifabu:  #爱链接
+  appKey: 7b471be905ab17e00f3b858c6710dd117601d008
+watch:
+  watchUrl: watch.ylrzcloud.com/prod-api
+  #  account: tcloud
+  #  password: mdf-m2h_6yw2$hq
+  account1: ccif #866655060138751
+  password1: cp-t5or_6xw7$mt
+  account2: tcloud #rt500台
+  password2: mdf-m2h_6yw2$hq
+  account3: whr
+  password3: v9xsKuqn_$d2y
+
+fs :
+  commonApi: http://10.206.0.12:7771
+  h5CommonApi: http://10.206.0.12:7771
+  jwt:
+    # 加密秘钥
+    secret: 3e6d9c0b4a7f1e2d5c4e0d3c6b9a2f5e
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
+nuonuo:
+  key: 10924508
+  secret: A2EB20764D304D16
+
+# 存储捅配置
+tencent_cloud_config:
+  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+  bucket: jnlzjk-1323137866
+  app_id: 1323137866
+  region: ap-chongqing
+  proxy: jnlzjk
+tmp_secret_config:
+  secret_id: AKIDCj7NSNAovtqeJpBau8GZ4CGB71thXIxX
+  secret_key: lTB5zwqqz7CNhzDOWivFWedgfTBgxgBT
+  bucket: fs-1319721001
+  app_id: 1319721001
+  region: ap-chongqing
+  proxy: fs
+cloud_host:
+  company_name: 济南联志健康
+  projectCode: LZJK
+  spaceName:
+  volcengineUrl: https://jnlzvolcengine.ylrztop.com
+headerImg:
+  imgUrl:
+
+ipad:
+  ipadUrl: http://ipad.ljhehualu.com
+  aiApi: http://49.232.181.28:3000/api
+  voiceApi: http://129.28.187.88:8667
+  commonApi: http://129.28.187.88:7771
+wx_miniapp_temp:
+  pay_order_temp_id:
+  inquiry_temp_id:
+
+

+ 2 - 2
fs-service/src/main/resources/application-config-druid-sxjz.yml

@@ -87,8 +87,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 今正科技
   projectCode: SXJZ
-  spaceName:
-  volcengineUrl:
+  spaceName: sxjz-2114522511
+  volcengineUrl: https://sxjzvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://jz-cos-1356808054.cos.ap-chengdu.myqcloud.com/fs/20250515/0877754b59814ea8a428fa3697b20e68.png

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

@@ -76,7 +76,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 益善缘
   projectCode: SYYSY
-  spaceName:
+  spaceName: syysy-2114522511
+  volcengineUrl: https://syysyvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://ysy-1329817240.cos.ap-guangzhou.myqcloud.com/ysy/20250820/2c47e4f105b641b4a49df50a77338e32.png

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

@@ -82,7 +82,8 @@ tencent_cloud_config:
 cloud_host:
   company_name: 易行健
   projectCode: whyxj
-  spaceName:
+  spaceName: whyxj-2114522511
+  volcengineUrl: https://whyxjvolcengine.ylrztop.com
 #看课授权时显示的头像
 headerImg:
   imgUrl: https://yxj-1323137866.cos.ap-chongqing.myqcloud.com/app/yxj.jpg

+ 141 - 0
fs-service/src/main/resources/application-config-shdn.yml

@@ -0,0 +1,141 @@
+#配置
+fsConfig:
+  #快递鸟
+  kdnId: 1886082
+  kdnKeyId: 5a66df03-3d88-469a-ab42-23cb082b57ac
+  kdnUrl: http://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx
+  kdnSubscribeUrl: https://api.kdniao.com/api/dist
+  kdnAddressUrl: https://api.kdniao.com/api/dist
+  #ERP配置
+  erpOpen: 1
+  erpAppKey: 108123
+  erpSessionKey: 9caae15474cb443ea22235e7bb86016b
+  erpSecret: 96f774dbd60847b59a16f92fd963a0c8
+  erpUrl: http://v2.api.guanyierp.com/rest/erp_open
+  erpShopCode: test
+  #ERP-hc
+  erpWdAppKey: beiliyou2-gw
+  erpWdAppsecret: 37c7cebf6e5af56c783d865b63553993
+  erpWdSid: beiliyou2
+  erpWdShopCode: ziyou123456
+  erpWdBaseUrl: https://api.wangdian.cn/openapi2/
+  erpWarehouseCode: "02"
+  #第三方支付配置
+  payOpen: 1
+  payPartnerId: 22051909542647100020
+  payKey: f256bd35aa36115d729537e1a1e01b92
+  payGateWayUrl: https://openapi.t2bank.cn/gateway.html
+  payNotifyUrl: https://api.yjf.runtzh.com/app/pay/payNotify
+  refundNotifyUrl: https://api.yjf.runtzh.com/app/pay/refundNotify
+  # 腾讯云IM
+  sdkAppId: 1400693126
+  sdkAppKey: 9afa6e63db943293680e37b3ba032e52cdb238112750806e82e58e9240604b70
+  # 处方接口Test
+  #  prescribeUrl: https://yixian-new-test.yixianmedical.com/platform-shenfang/nethosp/webservice/jsonapi
+  #  actId:  uporder
+  #  appId: 1646204278
+  #  manuId:  3981112bfcc64bf68f7744ffec7e3ca7
+  #  callbackUrl:  https://api.hospital.ifeiyu100.com/app/prescribe/presribeNotify
+  # 处方接口g
+  prescribeUrl: https://app3.nxk520.com/platform-shenfang/nethosp/webservice/jsonapi
+  actId: uporder
+  appId: 1661496555
+  manuId: 0212af1e742b41b09089afeec98f8276
+  callbackUrl: https://api.yjf.runtzh.com/app/prescribe/presribeNotify
+  commonApi: http://192.168.0.224:7011
+logging:
+  level:
+    org.springframework.web: INFO
+    com.github.binarywang.demo.wx.cp: DEBUG
+    me.chanjar.weixin: DEBUG
+wx:
+  cp:
+    corpId: wwb2a1055fb6c9a7c2
+    appConfigs:
+      - agentId: 1000002
+        secret: bhj3402rPCT0YGcosffyTO3eUMs1G2MFHMspXVBNf-c
+        token: PPKOdAlCoMO
+        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+  miniapp:
+    configs:
+      - appid: wx11a2ce7c2bbc4521   #倍力优会员商城
+        secret: d680dc8ff20258b158c9355f8b7769ae
+        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+      - appid: wx301ab2fad04c658a   #倍力优看课小程序
+        secret: 35018f10929b84c8c4a225de253bbcc6
+        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+
+  pay:
+    appId: wx11a2ce7c2bbc4521 #微信公众号或者小程序等的appid
+    mchId: 1703311381 #微信支付商户号
+    mchKey: FotTIbIzn4AisMW7de712LJQIazSqqAl #微信支付商户密钥
+    v3Key: y5Eo99q93qzdQRAs6E2BDKIF7f3EnS3G
+    subAppId:  #服务商模式下的子商户公众账号ID
+    subMchId:  #服务商模式下的子商户号
+    keyPath: c:\\Tools\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl:  https://userapp.bly.ylrztop.com/app/wxpay/wxPayNotify
+  mp:
+    useRedis: false
+    redisConfig:
+      host: 127.0.0.1
+      port: 6379
+      timeout: 2000
+    configs:
+      - appId: wx568ea6b70350c585 # 第一个公众号的appid  倍力优
+        secret: b14343e22871b1c207df5d3321e826b4 # 公众号的appsecret
+        token: PPKOdAlCoMO # 接口配置里的Token值
+        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+jpush:
+  appKey: cc9a0120a3e4270c9cba340d
+  masterSecret: cfc2575d3cd7470d584c990c
+  liveTime: 1000
+  apnsProduction: true
+aifabu:  #爱链接
+  appKey: 7b471be905ab17e00f3b858c6710dd117601d008
+
+tencent_cloud_config:
+  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+  bucket: beliyo-1323137866
+  app_id: 1323137866
+  region: ap-chongqing
+  proxy: beliyo
+cloud_host:
+  company_name: 倍力优
+  projectCode: BLY
+  spaceName:
+headerImg:
+  imgUrl: https://beiliyo-2025.obs.cn-north-4.myhuaweicloud.com/fs/20250115/1736944490230.png
+
+baidu:
+  token: 1231321232
+  back-domain: admin.muyi88.com
+watch:
+  watchUrl: watch.ylrzcloud.com/prod-api
+  #  account: tcloud
+  #  password: mdf-m2h_6yw2$hq
+  account1: ccif #866655060138751
+  password1: cp-t5or_6xw7$mt
+  account2: tcloud #rt500台
+  password2: mdf-m2h_6yw2$hq
+  account3: whr
+  password3: v9xsKuqn_$d2y
+
+fs:
+  commonApi: http://172.21.76.167:8010
+nuonuo:
+  key: 10924508
+  secret: A2EB20764D304D16
+ipad:
+  ipadUrl:
+  aiApi:
+  voiceApi:
+  commonApi:
+wx_miniapp_temp:
+  pay_order_temp_id: VXEvKaGNPFuJmhWK9O_QPrTZxe9umDCukq-maI8Vdek
+  inquiry_temp_id: 9POPYeqhI48LOPvq-Rfoklze7H-9SlunJKh10Qt4_2I
+

+ 6 - 0
fs-service/src/main/resources/application-dev-jnlzjk.yml

@@ -42,6 +42,9 @@ spring:
                   url: jdbc:mysql://nj-cdb-22nvjajz.sql.tencentcdb.com:29237/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
                   username: root
                   password: Ylrz_1q2w3e4r5t6y
+#                  url: jdbc:mysql://139.186.77.83:3306/fs_his_jnlzjk_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+#                  username: Rtroot
+#                  password: Rtroot
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
@@ -97,6 +100,9 @@ spring:
                     url: jdbc:mysql://nj-cdb-22nvjajz.sql.tencentcdb.com:29237/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
                     password: Ylrz_1q2w3e4r5t6y
+#                    url: jdbc:mysql://139.186.77.83:3306/fs_his_jnlzjk_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+#                    username: Rtroot
+#                    password: Rtroot
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 165 - 0
fs-service/src/main/resources/application-druid-shdn.yml

@@ -0,0 +1,165 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-druid-shdn,common
+    # redis 配置
+    redis:
+        host: 10.206.0.10
+        port: 6579
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 30s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 100
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+#        clickhouse:
+#            type: com.alibaba.druid.pool.DruidDataSource
+#            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
+#            url: jdbc:clickhouse://1.14.104.71:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
+#            username: rt_2024
+#            password: Yzx_19860213
+#            initialSize: 10
+#            maxActive: 100
+#            minIdle: 10
+#            maxWait: 6000
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                  url: jdbc:mysql://172.17.0.12:65535/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+                  username: root
+                  password: QWEqwe123!@#
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    enabled: false
+                    url:
+                    username:
+                    password:
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+        sop:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://172.17.0.12:65535/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+                    username: root
+                    password: QWEqwe123!@#
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+rocketmq:
+    name-server: rmq-33namm2jeq.rocketmq.nj.qcloud.tencenttdmq.com:8080
+    producer:
+        group: my-producer-group
+        access-key: ak33namm2jeq90148878a325 # 替换为实际的 accessKey
+        secret-key: sk0256ccecd9f96742 # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak33namm2jeq90148878a325 # 替换为实际的 accessKey
+        secret-key: sk0256ccecd9f96742 # 替换为实际的 secretKey
+openIM:
+    secret: openIM123
+    userID: imAdmin
+    url: https://web.im.fbylive.com/api
+#是否使用新im
+im:
+    type: NONE
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: false
+qw:
+    enableAutoTag: 1
+tag:
+    thread:
+        num: 10
+    rate:
+        limit: 50
+

+ 1 - 0
fs-service/src/main/resources/mapper/his/FsIntegralCartMapper.xml

@@ -61,6 +61,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             ic.cash,
             ig.integral newIntegral,
             ig.cash newCash,
+            ig.stock,
             ic.cart_num,
             ic.create_time,
             ic.update_time

+ 1 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -1736,6 +1736,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 <!--        <if test="maps.productName != null and  maps.productName !=  ''   ">-->
 <!--            group by o.id-->
 <!--        </if>-->
+        group by o.id
             order by
         <if test="maps.sortField == 'companyUserName'">
             cu.nick_name

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