浏览代码

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 3 天之前
父节点
当前提交
4fbfacf38a
共有 53 个文件被更改,包括 1833 次插入484 次删除
  1. 1 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java
  2. 5 5
      fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java
  3. 14 13
      fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java
  4. 7 9
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  5. 10 14
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  6. 78 23
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  7. 298 0
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  8. 5 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java
  9. 8 6
      fs-live-app/src/main/java/com/fs/live/controller/LiveController.java
  10. 308 53
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  11. 6 0
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  12. 2 0
      fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  13. 38 41
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  14. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  15. 24 0
      fs-service/src/main/java/com/fs/erp/exception/JstRateLimitException.java
  16. 57 8
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  17. 44 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  18. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  19. 4 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  20. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java
  21. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  22. 12 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  23. 8 0
      fs-service/src/main/java/com/fs/live/domain/LiveUserFirstEntry.java
  24. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  25. 14 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  26. 3 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  27. 2 1
      fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  28. 20 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  29. 5 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  30. 33 25
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  31. 114 13
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  32. 3 0
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  33. 8 6
      fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java
  34. 7 0
      fs-service/src/main/java/com/fs/live/vo/MergedOrderVO.java
  35. 1 0
      fs-service/src/main/java/com/fs/newAdv/domain/PromotionAccount.java
  36. 47 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/AbstractApiClient.java
  37. 2 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/IAccessTokenClient.java
  38. 2 0
      fs-service/src/main/java/com/fs/newAdv/integration/client/IApiClient.java
  39. 50 54
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/BaiduApiClient.java
  40. 47 105
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java
  41. 35 41
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/TencentApiClient.java
  42. 2 0
      fs-service/src/main/java/com/fs/newAdv/vo/AccessTokenVo.java
  43. 2 2
      fs-service/src/main/resources/application-config-druid-qdtst.yml
  44. 55 33
      fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml
  45. 2 2
      fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml
  46. 1 2
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  47. 3 0
      fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml
  48. 11 1
      fs-service/src/main/resources/mapper/live/LiveUserFirstEntryMapper.xml
  49. 83 0
      fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
  50. 275 10
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveController.java
  51. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  52. 1 1
      fs-user-app/src/main/java/com/fs/app/facade/LiveFacadeService.java
  53. 62 1
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

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

@@ -120,6 +120,7 @@ public class CallbackController {
         if (ObjectUtil.isNotEmpty(accessToken)) {
             byId.setAccessToken(accessToken.getAccessToken());
             byId.setRefreshToken(accessToken.getRefreshToken());
+            byId.setExpireTime(accessToken.getExpireTime());
             byId.setUpdateTime(LocalDateTime.now());
             promotionAccountService.updateById(byId);
         } else {

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

+ 14 - 13
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);
@@ -58,7 +60,8 @@ public class DataSyncTask {
      * cron: 每1小时统计站点数据
      */
     @Scheduled(cron = "0 0/1 * * * ?")
-    public void syncTodayData() {
+    @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())
                                             );
                                         }
                                     });

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

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

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

+ 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.hashEntries(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);

+ 38 - 41
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -97,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);
@@ -111,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);
@@ -179,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 {
@@ -212,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 {
@@ -255,7 +278,6 @@ public class WebSocketServer {
                 throw new BaseException("用户信息错误");
             }
             // 计算并更新用户在线时长
-            updateUserOnlineDuration(liveId, userId, companyId, companyUserId);
             room.remove(userId);
             if (room.isEmpty()) {
                 rooms.remove(liveId);
@@ -322,36 +344,23 @@ public class WebSocketServer {
                     // 心跳时同步更新观看时长到Redis Hash
                     long watchUserId = (long) userProperties.get("userId");
 
-                    log.info("[心跳-观看时长] 接收心跳, liveId={}, userId={}, data={}",
-                            liveId, watchUserId, msg.getData());
+
                     
                     if (msg.getData() != null && !msg.getData().isEmpty()) {
                         try {
                             Long currentDuration = Long.parseLong(msg.getData());
-                            log.info("[心跳-观看时长] 解析成功, duration={}", currentDuration);
-
                             // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
                             String hashKey = "live:watch:duration:hash:" + liveId;
                             String userIdField = String.valueOf(watchUserId);
-                            
-                            log.info("[心跳-观看时长] 开始处理, liveId={}, userId={}, hashKey={}, userIdField={}, currentDuration={}", 
-                                    liveId, watchUserId, hashKey, userIdField, currentDuration);
-                            
                             // 获取现有时长
                             Object existingDuration = redisCache.hashGet(hashKey, userIdField);
-                            
                             // 只有当新的时长更大时才更新
                             if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
                                 // 更新Hash中的用户时长
                                 redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
                                 // 设置过期时间(2小时)
                                 redisCache.expire(hashKey, 2, TimeUnit.HOURS);
-                                
-                                log.info("[心跳-观看时长] 更新成功, liveId={}, userId={}, duration={}, hashKey={}", 
-                                        liveId, watchUserId, currentDuration, hashKey);
-                                
-                                // 实时更新用户看课状态(仅在直播期间)
-                                updateWatchLogTypeInRealTime(liveId, watchUserId, currentDuration);
+
                             }
                         } catch (Exception e) {
                             log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}", 
@@ -850,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);
@@ -1124,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 中的进入时间记录
@@ -1147,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()) {

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

@@ -273,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='')")

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

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

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

+ 12 - 0
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());
@@ -5317,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;

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

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

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

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

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

@@ -61,4 +61,7 @@ public class LiveVo {
     
     /** 是否开启直播完课积分功能 */
     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 - 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;
 
     /**
      * 创建时间

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

+ 2 - 0
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;
 

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

+ 47 - 105
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java

@@ -1,6 +1,5 @@
 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;
@@ -9,7 +8,6 @@ import cn.hutool.json.JSONUtil;
 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;
@@ -22,9 +20,11 @@ import org.apache.http.client.utils.URIBuilder;
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.*;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * 巨量引擎API客户端
@@ -87,65 +87,24 @@ 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 ideaId, String startDate, String endDate) {
+    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<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 消耗(元)
@@ -180,20 +139,10 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
             throw new RuntimeException(e);
         }
         HttpResponse execute = HttpRequest.get(url)
-                .header("Access-Token", account.getAccessToken())
+                .header("Access-Token", getAccessToken(account.getId()))
                 .timeout(SystemConstant.API_TIMEOUT)
                 .execute();
         JSONObject jsonObject = JSONUtil.parseObj(execute.body());
-        if (jsonObject.getInt("code") != 0) {
-            // 刷新token重新请求
-            log.info("巨量刷新token重新请求");
-            AccessTokenVo accessTokenVo = refreshAccessToken(account.getAppId(), account.getAppSecret(), account.getRefreshToken());
-            execute = HttpRequest.get(url)
-                    .header("Access-Token", accessTokenVo.getAccessToken())
-                    .timeout(SystemConstant.API_TIMEOUT)
-                    .execute();
-            jsonObject = JSONUtil.parseObj(execute.body());
-        }
         JSONObject data = jsonObject.getJSONObject("data");
         JSONArray rows = data.getJSONArray("rows");
         JSONObject jsonObject1 = rows.getJSONObject(0);
@@ -208,55 +157,48 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
         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 ideaId, 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();
     }
 }
 

+ 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 - 2
fs-service/src/main/resources/application-config-druid-qdtst.yml

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

+ 55 - 33
fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml

@@ -18,7 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       o.STATUS,
       o.is_package,
       o.package_json,
-      o.item_json,
+        item_latest.json_info as item_json,
       o.delivery_id,
       o.delivery_sn as deliveryCode,
       o.delivery_name as deliveryName,
@@ -27,22 +27,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       o.create_time,
       o.pay_time,
       o.delivery_send_time,
-      NULL AS total_num,
-      NULL AS discount_money,
+      o.total_num AS total_num,
+      o.deduction_price AS discount_money,
       1 AS order_type,
 
         cu.phonenumber as salesPhone,
         cu.create_time as salesCreateTime,
-        u.user_id as userId,
+        o.user_id as userId,
         u.order_count as userOrderCount,
         u.total_amount as userTotalAmount,
         u.level as userLevel,
         fspc.product_id as productId,
         fspc.product_name as productName,
-        fspc.cost as cost,
+        fspc.prescribe_spec as productSpec,
+        COALESCE(fspc.cost, 0) as cost,
         o.pay_postage as payDelivery,
         o.coupon_price as discountMoney,
-        fspc.prescribe_spec as productSpec,
         fss.store_id as storeId,
         fss.store_name as storeName,
         fspcs.cate_name as cateName,
@@ -51,7 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       c.company_name,
       cu.user_name AS sales_name,
       cu.nick_name AS company_user_nick_name,
-      u.nickname,
+        ifnull(u.nickname,u.nick_name) as nickname,
       u.phone,
       o.real_name,
       o.user_phone,
@@ -64,22 +64,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       sp_latest.bank_transaction_id
       FROM
       fs_store_order_scrm o
-      left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.item_id  ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id and item_latest.rn = 1
+      left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.order_id ORDER BY fsois.item_id ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id and item_latest.rn = 1
       LEFT JOIN fs_user u ON o.user_id = u.user_id
 
       LEFT JOIN fs_store_product_scrm  fspc ON fspc.product_id = item_latest.product_id
       LEFT JOIN fs_store_scrm  fss ON fspc.store_id = fss.store_id
       left join fs_store_product_category_scrm fspcs on fspc.cate_id = fspcs.cate_id
 
-      LEFT JOIN company c ON c.company_id = o.company_id
+
       LEFT JOIN company_user cu ON cu.user_id = o.company_user_id
+        LEFT JOIN company c ON c.company_id = cu.company_id
       LEFT JOIN ( SELECT sp.*, ROW_NUMBER() OVER ( PARTITION BY sp.business_code ORDER BY sp.create_time DESC ) AS rn FROM fs_store_payment_scrm sp ) sp_latest ON sp_latest.business_code = o.order_code
       AND sp_latest.rn = 1
       LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id
-          WHERE o.is_del = 0 AND o.is_sys_del = 0 AND o.company_user_id IS NOT NULL AND o.company_user_id != 0
+          WHERE  o.company_id IS NOT NULL
           <if test="maps.status != null and maps.status != ''">
             AND o.status = #{maps.status}
           </if>
+        <if test="maps.productId != null and maps.productId != ''">
+            AND fspc.product_id = #{maps.productId}
+        </if>
           <if test="maps.orderCode != null and maps.orderCode != ''">
             AND o.order_code LIKE CONCAT('%', #{maps.orderCode}, '%')
           </if>
@@ -95,6 +99,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           <if test="maps.userPhone != null and maps.userPhone != ''">
             AND o.user_phone LIKE CONCAT('%', #{maps.userPhone}, '%')
           </if>
+        <if test="maps.userAddress != null and maps.userAddress != ''">
+            AND o.user_address LIKE CONCAT('%', #{maps.userAddress}, '%')
+        </if>
           <if test="maps.realName != null and maps.realName != ''">
             AND o.real_name LIKE CONCAT('%', #{maps.realName}, '%')
           </if>
@@ -123,10 +130,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND cu.nick_name LIKE CONCAT('%', #{maps.companyUserNickName}, '%')
           </if>
           <if test="maps.createTimeRange != null and maps.createTimeRange != ''">
-            AND DATE(o.create_time) BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
+            AND o.create_time BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
           </if>
           <if test="maps.payTimeRange != null and maps.payTimeRange != ''">
-            AND DATE(o.pay_time) BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
+            AND o.pay_time BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
           </if>
           <if test="maps.deliverySendTimeRange != null and maps.deliverySendTimeRange != ''">
             AND DATE(o.delivery_send_time) BETWEEN SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', -1)
@@ -152,7 +159,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       o.STATUS,
       o.is_package,
       o.package_json,
-      o.item_json,
+
+        item_latest.json_info as item_json,
       o.delivery_id,
     o.delivery_sn as deliveryCode,
     o.delivery_name as deliveryName,
@@ -160,20 +168,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       o.create_time,
       o.pay_time,
       o.delivery_send_time,
-      NULL AS total_num,
-      NULL AS discount_money,
+      o.total_num AS total_num,
+      o.deduction_price AS discount_money,
       2 AS order_type,
 
     cu.phonenumber as salesPhone,
     cu.create_time as salesCreateTime,
-    u.user_id as userId,
+    o.user_id as userId,
     u.order_count as userOrderCount,
     u.total_amount as userTotalAmount,
     u.level as userLevel,
     fspc.product_id as productId,
     fspc.product_name as productName,
     fspc.prescribe_spec as productSpec,
-        fspc.cost as cost,
+        COALESCE(fspc.cost, 0) as cost,
         o.pay_postage as payDelivery,
         o.coupon_price as discountMoney,
     fss.store_id as storeId,
@@ -184,7 +192,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       c.company_name,
       cu.user_name AS sales_name,
       cu.nick_name AS company_user_nick_name,
-      u.nickname,
+        ifnull(u.nickname,u.nick_name) as nickname,
       u.phone,
       o.real_name,
       o.user_phone,
@@ -197,22 +205,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       sp_latest.bank_transaction_id
       FROM
       fs_store_order_scrm o
-        left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.item_id  ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id
+        left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.order_id ORDER BY fsois.item_id ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id and item_latest.rn = 1
       LEFT JOIN fs_user u ON o.user_id = u.user_id
 
         LEFT JOIN fs_store_product_scrm  fspc ON fspc.product_id = item_latest.product_id
         LEFT JOIN fs_store_scrm  fss ON fspc.store_id = fss.store_id
         left join fs_store_product_category_scrm fspcs on fspc.cate_id = fspcs.cate_id
 
-      LEFT JOIN company c ON c.company_id = o.company_id
+
       LEFT JOIN company_user cu ON cu.user_id = o.company_user_id
+        LEFT JOIN company c ON c.company_id = cu.company_id
       LEFT JOIN ( SELECT sp.*, ROW_NUMBER() OVER ( PARTITION BY sp.business_code ORDER BY sp.create_time DESC ) AS rn FROM fs_store_payment_scrm sp ) sp_latest ON sp_latest.business_code = o.order_code
       AND sp_latest.rn = 1
       LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id
-          WHERE o.is_del = 0 AND o.is_sys_del = 0 AND (o.company_user_id IS NULL OR o.company_user_id = 0)
+          WHERE  o.company_id is null
           <if test="maps.status != null and maps.status != ''">
             AND o.status = #{maps.status}
           </if>
+        <if test="maps.productId != null and maps.productId != ''">
+            AND fspc.product_id = #{maps.productId}
+        </if>
           <if test="maps.orderCode != null and maps.orderCode != ''">
             AND o.order_code LIKE CONCAT('%', #{maps.orderCode}, '%')
           </if>
@@ -228,6 +240,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           <if test="maps.userPhone != null and maps.userPhone != ''">
             AND o.user_phone LIKE CONCAT('%', #{maps.userPhone}, '%')
           </if>
+        <if test="maps.userAddress != null and maps.userAddress != ''">
+            AND o.user_address LIKE CONCAT('%', #{maps.userAddress}, '%')
+        </if>
           <if test="maps.realName != null and maps.realName != ''">
             AND o.real_name LIKE CONCAT('%', #{maps.realName}, '%')
           </if>
@@ -256,10 +271,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND cu.nick_name LIKE CONCAT('%', #{maps.companyUserNickName}, '%')
           </if>
           <if test="maps.createTimeRange != null and maps.createTimeRange != ''">
-            AND DATE(o.create_time) BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
+              AND o.create_time BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
           </if>
-          <if test="maps.payTimeRange != null and maps.payTimeRange != ''">
-            AND DATE(o.pay_time) BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
+        <if test="maps.payTimeRange != null and maps.payTimeRange != ''">
+            AND o.pay_time BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
           </if>
           <if test="maps.deliverySendTimeRange != null and maps.deliverySendTimeRange != ''">
             AND DATE(o.delivery_send_time) BETWEEN SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', -1)
@@ -285,7 +300,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       o.STATUS,
       NULL AS is_package,
       NULL AS package_json,
-      o.item_json,
+        loi.json_info as item_json,
       o.delivery_sn AS delivery_id,
 
         o.delivery_code as deliveryCode,
@@ -300,14 +315,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
         cu.phonenumber as salesPhone,
         cu.create_time as salesCreateTime,
-        u.user_id as userId,
+        o.user_id as userId,
         u.order_count as userOrderCount,
         u.total_amount as userTotalAmount,
         u.level as userLevel,
         fspc.product_id as productId,
         fspc.product_name as productName,
         fspc.prescribe_spec as productSpec,
-        fspc.cost as cost,
+        COALESCE(fspc.cost, 0) as cost,
         o.pay_postage as payDelivery,
         o.discount_money as discountMoney,
         fss.store_id as storeId,
@@ -318,7 +333,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       c.company_name,
       cu.user_name AS sales_name,
       cu.nick_name AS company_user_nick_name,
-      u.nickname,
+      ifnull(u.nickname,u.nick_name) as nickname,
       u.phone,
       o.user_name AS real_name,
       o.user_phone,
@@ -339,17 +354,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         left join fs_store_product_category_scrm fspcs on fspc.cate_id = fspcs.cate_id
 
 
-      LEFT JOIN company c ON c.company_id = o.company_id
+
       LEFT JOIN company_user cu ON cu.user_id = o.company_user_id
+        LEFT JOIN company c ON c.company_id = cu.company_id
       LEFT JOIN ( SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY t.order_id ORDER BY t.create_time DESC ) AS rn FROM live_after_sales t ) a ON o.order_id = a.order_id
       AND a.rn = 1
       LEFT JOIN ( SELECT sp.*, ROW_NUMBER() OVER ( PARTITION BY sp.business_code ORDER BY sp.create_time DESC ) AS rn FROM live_order_payment sp ) sp_latest ON sp_latest.business_code = o.order_code
       AND sp_latest.rn = 1
       LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id
-          WHERE o.is_del = 0
+          WHERE o.is_del = 0 and fspc.product_id IS NOT NULL
           <if test="maps.status != null and maps.status != ''">
             AND o.status = #{maps.status}
           </if>
+        <if test="maps.productId != null and maps.productId != ''">
+            AND fspc.product_id = #{maps.productId}
+        </if>
           <if test="maps.orderCode != null and maps.orderCode != ''">
             AND o.order_code LIKE CONCAT('%', #{maps.orderCode}, '%')
           </if>
@@ -365,6 +384,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           <if test="maps.userPhone != null and maps.userPhone != ''">
             AND o.user_phone LIKE CONCAT('%', #{maps.userPhone}, '%')
           </if>
+        <if test="maps.userAddress != null and maps.userAddress != ''">
+            AND o.user_address LIKE CONCAT('%', #{maps.userAddress}, '%')
+          </if>
           <if test="maps.realName != null and maps.realName != ''">
             AND o.user_name LIKE CONCAT('%', #{maps.realName}, '%')
           </if>
@@ -393,10 +415,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND cu.nick_name LIKE CONCAT('%', #{maps.companyUserNickName}, '%')
           </if>
           <if test="maps.createTimeRange != null and maps.createTimeRange != ''">
-            AND DATE(o.create_time) BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
+              AND o.create_time BETWEEN SUBSTRING_INDEX(#{maps.createTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.createTimeRange}, '--', -1)
           </if>
-          <if test="maps.payTimeRange != null and maps.payTimeRange != ''">
-            AND DATE(o.pay_time) BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
+        <if test="maps.payTimeRange != null and maps.payTimeRange != ''">
+            AND o.pay_time BETWEEN SUBSTRING_INDEX(#{maps.payTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.payTimeRange}, '--', -1)
           </if>
           <if test="maps.deliverySendTimeRange != null and maps.deliverySendTimeRange != ''">
             AND DATE(o.delivery_send_time) BETWEEN SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', -1)

+ 2 - 2
fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml

@@ -63,10 +63,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
     </select>
     <select id="selectLiveAfterSalesVoList" parameterType="com.fs.live.vo.LiveAfterSalesVo" resultType="com.fs.live.vo.LiveAfterSalesVo">
-        select las.id, las.live_id, las.store_id, las.order_id, las.refund_amount,
+        select las.id, las.live_id, las.store_id, las.refund_amount,
         las.refund_type, las.reasons, las.explains, las.explain_img, las.delivery_code, las.delivery_sn, las.delivery_name, las.status, las.sales_status,
         las.order_status, las.create_time, las.is_del, las.user_id, las.consignee, las.phone_number, las.address, las.company_id, las.company_user_id, las.dept_id,
-        cu.nick_name as company_user_nick_name, c.company_name,lo.order_id,lo.order_code,lo.user_phone,las.user_id,lo.item_json,lo.pay_time as orderPayTime,
+        cu.nick_name as company_user_nick_name, c.company_name,lo.order_id,lo.order_code,lo.user_phone,lo.item_json,lo.pay_time as orderPayTime,
         lo.user_address,lo.user_name,lo.pay_price,lo.total_postage,lop.bank_serial_no,lo.delivery_sn as orderDeliveryId,lo.delivery_name as orderDeliveryName,
         lo.delivery_code as orderDeliverySn,lo.status as orderStatus,lop.bank_transaction_id,lo.pay_money,lop.pay_code as payCode
         from live_after_sales las

+ 1 - 2
fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml

@@ -101,8 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <!-- 查询用户的完课积分领取记录列表 -->
     <select id="selectRecordsByUser" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record
-        WHERE live_id = #{liveId}
-          AND user_id = #{userId}
+        WHERE user_id = #{userId}
         ORDER BY current_completion_date DESC
     </select>
 

+ 3 - 0
fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml

@@ -1066,6 +1066,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">
                 AND o.company_user_id = #{companyUserId}
             </if>
+            <if test="companyUserName != null">
+                AND cu.nick_name like concat('%',#{companyUserName},'%')
+            </if>
             <if test="productId != null">
                 AND o.product_id = #{productId}
             </if>

+ 11 - 1
fs-service/src/main/resources/mapper/live/LiveUserFirstEntryMapper.xml

@@ -14,10 +14,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="updateTime"    column="update_time"    />
         <result property="companyId"    column="company_id"    />
         <result property="companyUserId"    column="company_user_id"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="externalContactId"    column="external_contact_id"    />
     </resultMap>
 
     <sql id="selectLiveUserFirstEntryVo">
-        select id, user_id, live_id, entry_date, first_entry_time,company_id,company_user_id, create_time, update_time from live_user_first_entry
+        select id, user_id, live_id, entry_date, first_entry_time,company_id,company_user_id, create_time, update_time,qw_user_id,external_contact_id from live_user_first_entry
     </sql>
 
     <select id="selectLiveUserFirstEntryList" parameterType="LiveUserFirstEntry" resultMap="LiveUserFirstEntryResult">
@@ -29,6 +31,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="firstEntryTime != null "> and first_entry_time = #{firstEntryTime}</if>
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
             <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="qwUserId != null "> and qw_user_id = #{qwUserId}</if>
+            <if test="externalContactId != null "> and external_contact_id = #{externalContactId}</if>
         </where>
     </select>
 
@@ -48,6 +52,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time,</if>
             <if test="companyUserId != null">company_user_id,</if>
             <if test="companyId != null">company_id,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="externalContactId != null">external_contact_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null and userId != ''">#{userId},</if>
@@ -58,6 +64,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">#{updateTime},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="companyId != null">#{companyId},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="externalContactId != null">#{externalContactId},</if>
          </trim>
     </insert>
 
@@ -72,6 +80,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="externalContactId != null">external_contact_id = #{externalContactId},</if>
         </trim>
         where id = #{id}
     </update>

+ 83 - 0
fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml

@@ -260,4 +260,87 @@
             location = VALUES(location),
             update_time = VALUES(update_time)
     </insert>
+
+    <!-- 批量更新直播间观看用户 -->
+    <update id="batchUpdateLiveWatchUser" parameterType="java.util.List">
+        UPDATE live_watch_user
+        <set>
+            <if test="list != null and list.size() > 0 and list[0].onlineSeconds != null">
+                online_seconds = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.onlineSeconds}
+                </foreach>
+                ELSE online_seconds
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].updateTime != null">
+                update_time = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.updateTime}
+                </foreach>
+                ELSE update_time
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].msgStatus != null">
+                msg_status = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.msgStatus}
+                </foreach>
+                ELSE msg_status
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].online != null">
+                online = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.online}
+                </foreach>
+                ELSE online
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].liveFlag != null">
+                live_flag = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.liveFlag}
+                </foreach>
+                ELSE live_flag
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].replayFlag != null">
+                replay_flag = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.replayFlag}
+                </foreach>
+                ELSE replay_flag
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].location != null">
+                location = CASE id
+                <foreach collection="list" item="item">
+                    WHEN #{item.id} THEN #{item.location}
+                </foreach>
+                ELSE location
+                END
+            </if>
+        </set>
+        WHERE id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.id}
+        </foreach>
+    </update>
+
+    <!-- 批量插入直播间观看用户 -->
+    <insert id="batchInsertLiveWatchUser" parameterType="java.util.List">
+        INSERT INTO live_watch_user (
+            live_id, user_id, msg_status, online, online_seconds,
+            global_visible, single_visible, live_flag, replay_flag, location,
+            create_time, update_time
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+                #{item.liveId}, #{item.userId}, #{item.msgStatus}, #{item.online}, #{item.onlineSeconds},
+                #{item.globalVisible}, #{item.singleVisible}, #{item.liveFlag}, #{item.replayFlag}, #{item.location},
+                #{item.createTime}, #{item.updateTime}
+            )
+        </foreach>
+    </insert>
 </mapper>

+ 275 - 10
fs-user-app/src/main/java/com/fs/app/controller/live/LiveController.java

@@ -19,10 +19,16 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.erp.domain.ErpDeliverys;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
+import com.fs.erp.service.IErpOrderService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
-import com.fs.live.domain.Live;
-import com.fs.live.domain.LiveMsg;
+import com.fs.live.domain.*;
+import com.fs.live.mapper.LiveVideoMapper;
+import com.fs.live.mapper.LiveWatchUserMapper;
 import com.fs.live.param.LiveNotifyParam;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveVo;
@@ -36,12 +42,16 @@ import lombok.AllArgsConstructor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.fs.hisStore.constants.StoreConstants.DELIVERY;
 
 
 @Api("直播信息接口")
@@ -67,13 +77,272 @@ public class LiveController extends AppBaseController {
 	private ICompanyUserService companyUserService;
 	@Autowired
 	private IFsUserService userService;
+	@Autowired
+	@Qualifier("JSTErpOrderServiceImpl")
+	private IErpOrderService jSTOrderService;
+	@Autowired
+	private ILiveOrderService liveOrderService;
+	@Autowired
+	private LiveWatchUserMapper liveWatchUserService;
+    @Autowired
+    private LiveVideoMapper liveVideoMapper;
+    @Autowired
+    private com.fs.live.mapper.LiveUserFirstEntryMapper liveUserFirstEntryMapper;
+    @Autowired
+    private com.fs.live.mapper.LiveOrderMapper liveOrderMapper;
+
+
+	@GetMapping("/test/{liveId}")
+	public void test(@PathVariable("liveId") Long liveId) {
+		Live live = liveService.selectLiveDbByLiveId(liveId);
+		List<LiveVideo> liveVideos = liveVideoMapper.selectByIdAndType(liveId, 1);
+		Long duration = liveVideos.get(0).getDuration();
+		List<LiveWatchUser> liveWatchUsers = liveWatchUserService.selectLiveWatchUserListByLiveId(live.getLiveId());
+		List<LiveWatchUser> online = liveWatchUsers.stream().filter(liveWatchUser -> 1 == liveWatchUser.getLiveFlag()).collect(Collectors.toList());
+		List<LiveWatchUser> offline = liveWatchUsers.stream().filter(liveWatchUser -> 1 == liveWatchUser.getReplayFlag()).collect(Collectors.toList());
+		this.adjustWatchDuration(liveId,liveWatchUsers,online,offline,liveWatchUsers.size(),duration);
+	}
+
+	/**
+	 * 调整观看时长以满足统计要求
+	 * 按照每个分公司学员人数,随机50%的人,添加看课时长
+	 * 40%的人看课是30min-40min随机,其中一定包含购买商品用户
+	 * 10%的人看课是20min-29min随机,其中一定包含购买商品用户
+	 * 剩余50%看课是0-19min随机
+	 * 回放规则同上
+	 */
+	private void adjustWatchDuration(Long liveId, List<LiveWatchUser> allUsers,
+									 List<LiveWatchUser> liveUsers, List<LiveWatchUser> replayUsers,
+									 long totalViewers, Long videoDuration) {
+
+		// 查询购买商品的用户(is_pay = '1')
+		com.fs.live.domain.LiveOrder orderQuery = new com.fs.live.domain.LiveOrder();
+		orderQuery.setLiveId(liveId);
+		List<com.fs.live.domain.LiveOrder> orders = liveOrderMapper.selectLiveOrderList(orderQuery);
+		Set<Long> paidUserIds = orders.stream()
+				.filter(o -> "1".equals(o.getIsPay()) && o.getUserId() != null && !o.getUserId().isEmpty())
+				.map(o -> {
+					try {
+						return Long.parseLong(o.getUserId());
+					} catch (NumberFormatException e) {
+						return null;
+					}
+				})
+				.filter(java.util.Objects::nonNull)
+				.collect(Collectors.toSet());
+
+		// 查询每个分公司的学员(通过 live_user_first_entry 表)
+		com.fs.live.domain.LiveUserFirstEntry firstEntryQuery = new com.fs.live.domain.LiveUserFirstEntry();
+		firstEntryQuery.setLiveId(liveId);
+		List<com.fs.live.domain.LiveUserFirstEntry> firstEntries = liveUserFirstEntryMapper.selectLiveUserFirstEntryList(firstEntryQuery);
+		
+		// 按分公司分组用户
+		Map<Long, List<Long>> companyUserMap = new HashMap<>();
+		Map<Long, Long> userCompanyMap = new HashMap<>(); // 用户ID -> 分公司ID
+		Set<Long> usersWithCompany = new HashSet<>(); // 有分公司的用户
+		
+		for (com.fs.live.domain.LiveUserFirstEntry entry : firstEntries) {
+			if (entry.getCompanyId() != null && entry.getUserId() != null) {
+				companyUserMap.computeIfAbsent(entry.getCompanyId(), k -> new ArrayList<>()).add(entry.getUserId());
+				userCompanyMap.put(entry.getUserId(), entry.getCompanyId());
+				usersWithCompany.add(entry.getUserId());
+			}
+		}
+		
+		// 处理没有分公司的用户(归到一个虚拟分公司,companyId = null 或 0)
+		Long noCompanyId = 0L;
+		for (LiveWatchUser user : allUsers) {
+			Long userId = user.getUserId();
+			if (userId != null && !usersWithCompany.contains(userId)) {
+				companyUserMap.computeIfAbsent(noCompanyId, k -> new ArrayList<>()).add(userId);
+				userCompanyMap.put(userId, noCompanyId);
+			}
+		}
+
+		// 按用户ID分组,合并同一用户的观看时长(把观看时间平分到各个用户身上)
+		Map<Long, List<LiveWatchUser>> allUserMap = new HashMap<>();
+		Map<Long, LiveWatchUser> liveUserMap = new HashMap<>();
+		Map<Long, LiveWatchUser> replayUserMap = new HashMap<>();
+
+		for (LiveWatchUser user : allUsers) {
+			Long userId = user.getUserId();
+			if (userId == null) continue;
+
+			allUserMap.computeIfAbsent(userId, k -> new ArrayList<>()).add(user);
+
+			if (user.getLiveFlag() != null && user.getLiveFlag() == 1 && (user.getReplayFlag() == null || user.getReplayFlag() == 0)) {
+				LiveWatchUser existing = liveUserMap.get(userId);
+				if (existing == null) {
+					liveUserMap.put(userId, user);
+				} else {
+					// 合并观看时长:把观看时间平分到各个用户身上
+					long totalSeconds = (existing.getOnlineSeconds() != null ? existing.getOnlineSeconds() : 0L) +
+							(user.getOnlineSeconds() != null ? user.getOnlineSeconds() : 0L);
+					existing.setOnlineSeconds(totalSeconds);
+				}
+			} else if (user.getReplayFlag() != null && user.getReplayFlag() == 1 && (user.getLiveFlag() == null || user.getLiveFlag() == 0)) {
+				LiveWatchUser existing = replayUserMap.get(userId);
+				if (existing == null) {
+					replayUserMap.put(userId, user);
+				} else {
+					// 合并观看时长:把观看时间平分到各个用户身上
+					long totalSeconds = (existing.getOnlineSeconds() != null ? existing.getOnlineSeconds() : 0L) +
+							(user.getOnlineSeconds() != null ? user.getOnlineSeconds() : 0L);
+					existing.setOnlineSeconds(totalSeconds);
+				}
+			}
+		}
+
+		// 按分公司处理直播用户观看时长
+		adjustWatchDurationByCompany(liveUserMap, companyUserMap, userCompanyMap, paidUserIds, true);
+		
+		// 按分公司处理回放用户观看时长(规则同上)
+		adjustWatchDurationByCompany(replayUserMap, companyUserMap, userCompanyMap, paidUserIds, false);
+
+		// 更新数据库
+		Date now = new Date();
+		int batchSize = 500;
+		for (int i = 0; i < allUsers.size(); i += batchSize) {
+			int end = Math.min(i + batchSize, allUsers.size());
+			List<LiveWatchUser> batch = allUsers.subList(i, end);
+			for (LiveWatchUser user : batch) {
+				user.setUpdateTime(now);
+			}
+			liveWatchUserService.batchUpdateLiveWatchUser(batch);
+		}
+
+		log.info("直播间 {} 观看时长调整完成:总观看人数={}, 视频时长={}秒",
+				liveId, totalViewers, videoDuration);
+	}
+
+	/**
+	 * 按分公司调整观看时长
+	 * @param userMap 用户观看记录Map(userId -> LiveWatchUser)
+	 * @param companyUserMap 分公司用户Map(companyId -> List<userId>)
+	 * @param userCompanyMap 用户分公司Map(userId -> companyId)
+	 * @param paidUserIds 购买商品的用户ID集合
+	 * @param isLive 是否为直播(true=直播,false=回放)
+	 */
+	private void adjustWatchDurationByCompany(Map<Long, LiveWatchUser> userMap,
+											   Map<Long, List<Long>> companyUserMap,
+											   Map<Long, Long> userCompanyMap,
+											   Set<Long> paidUserIds,
+											   boolean isLive) {
+		
+		// 遍历每个分公司
+		for (Map.Entry<Long, List<Long>> companyEntry : companyUserMap.entrySet()) {
+			Long companyId = companyEntry.getKey();
+			List<Long> companyUserIds = companyEntry.getValue();
+			
+			// 筛选出该分公司有观看记录的用户
+			List<Long> companyWatchUserIds = companyUserIds.stream()
+					.filter(userId -> userMap.containsKey(userId))
+					.collect(Collectors.toList());
+			
+			if (companyWatchUserIds.isEmpty()) {
+				continue;
+			}
+			
+			// 随机选择50%的人添加看课时长
+			Collections.shuffle(companyWatchUserIds);
+			int selectedCount = (int) Math.round(companyWatchUserIds.size() * 0.5);
+			List<Long> selectedUserIds = companyWatchUserIds.subList(0, Math.min(selectedCount, companyWatchUserIds.size()));
+			
+			// 分离购买商品的用户和未购买商品的用户
+			List<Long> paidUsers = selectedUserIds.stream()
+					.filter(paidUserIds::contains)
+					.collect(Collectors.toList());
+			List<Long> unpaidUsers = selectedUserIds.stream()
+					.filter(userId -> !paidUserIds.contains(userId))
+					.collect(Collectors.toList());
+			
+			// 计算各区间人数
+			// 40%的人(即总人数的20%)看课30-40分钟,必须包含购买商品用户
+			// 10%的人(即总人数的5%)看课20-29分钟,必须包含购买商品用户
+			// 剩余50%的人(即总人数的25%)看课0-19分钟
+			int count30to40 = (int) Math.round(selectedUserIds.size() * 0.4);
+			int count20to29 = (int) Math.round(selectedUserIds.size() * 0.1);
+			int count0to19 = selectedUserIds.size() - count30to40 - count20to29;
+			
+			// 确保购买商品的用户被包含在30-40分钟或20-29分钟组中
+			List<Long> users30to40 = new ArrayList<>();
+			List<Long> users20to29 = new ArrayList<>();
+			List<Long> users0to19 = new ArrayList<>();
+			
+			// 优先将购买商品的用户分配到30-40分钟组
+			int paidIndex = 0;
+			for (int i = 0; i < count30to40 && paidIndex < paidUsers.size(); i++) {
+				users30to40.add(paidUsers.get(paidIndex++));
+			}
+			
+			// 如果30-40分钟组还有空位,从购买商品用户中继续分配
+			for (int i = users30to40.size(); i < count30to40 && paidIndex < paidUsers.size(); i++) {
+				users30to40.add(paidUsers.get(paidIndex++));
+			}
+			
+			// 如果还有购买商品的用户,分配到20-29分钟组
+			for (int i = 0; i < count20to29 && paidIndex < paidUsers.size(); i++) {
+				users20to29.add(paidUsers.get(paidIndex++));
+			}
+			
+			// 剩余的购买商品用户分配到0-19分钟组
+			while (paidIndex < paidUsers.size()) {
+				users0to19.add(paidUsers.get(paidIndex++));
+			}
+			
+			// 填充30-40分钟组(从未购买用户中随机选择)
+			Collections.shuffle(unpaidUsers);
+			int unpaidIndex = 0;
+			for (int i = users30to40.size(); i < count30to40 && unpaidIndex < unpaidUsers.size(); i++) {
+				users30to40.add(unpaidUsers.get(unpaidIndex++));
+			}
+			
+			// 填充20-29分钟组(从未购买用户中随机选择)
+			for (int i = users20to29.size(); i < count20to29 && unpaidIndex < unpaidUsers.size(); i++) {
+				users20to29.add(unpaidUsers.get(unpaidIndex++));
+			}
+			
+			// 剩余用户分配到0-19分钟组
+			while (unpaidIndex < unpaidUsers.size()) {
+				users0to19.add(unpaidUsers.get(unpaidIndex++));
+			}
+			
+			// 设置30-40分钟的用户观看时长(1800-2400秒)
+			for (Long userId : users30to40) {
+				LiveWatchUser user = userMap.get(userId);
+				if (user != null) {
+					user.setOnlineSeconds(1800L + (long)(Math.random() * 600)); // 30-40分钟
+				}
+			}
+			
+			// 设置20-29分钟的用户观看时长(1200-1740秒)
+			for (Long userId : users20to29) {
+				LiveWatchUser user = userMap.get(userId);
+				if (user != null) {
+					user.setOnlineSeconds(1200L + (long)(Math.random() * 540)); // 20-29分钟
+				}
+			}
+			
+			// 设置0-19分钟的用户观看时长(0-1140秒)
+			for (Long userId : users0to19) {
+				LiveWatchUser user = userMap.get(userId);
+				if (user != null) {
+					user.setOnlineSeconds((long)(Math.random() * 1140)); // 0-19分钟
+				}
+			}
+			
+			log.debug("分公司 {} {}用户观看时长调整:总人数={}, 选中人数={}, 30-40分钟={}, 20-29分钟={}, 0-19分钟={}, 购买用户数={}",
+					companyId, isLive ? "直播" : "回放", companyWatchUserIds.size(), selectedUserIds.size(),
+					users30to40.size(), users20to29.size(), users0to19.size(), paidUsers.size());
+		}
+	}
 
 	/**
 	 * 查询未结束直播间(销售专用)
 	 */
 	@GetMapping("/listToLiveNoEnd")
-	public TableDataInfo listToLiveNoEnd(Live live)
-	{
+	public TableDataInfo listToLiveNoEnd(Live live) {
+
 		startPage();
 		List<Live> list = liveService.listToLiveNoEnd(live);
 		return getDataTable(list);
@@ -117,7 +386,8 @@ public class LiveController extends AppBaseController {
 //			liveVo.setNowPri(BigDecimal.valueOf(liveVo.getDuration()).divide(BigDecimal.valueOf(liveVo.getNowDuration()), 20, RoundingMode.UP));
 //		}
 		return R.ok().put("data", liveVo).put("storeId", storeId);*/
-		return liveFacadeService.liveDetail(id);
+		Long userId = Long.parseLong(getUserId());
+		return liveFacadeService.liveDetail(id, userId);
 	}
 
 	@Login
@@ -286,11 +556,6 @@ public class LiveController extends AppBaseController {
 		return liveService.subNotifyLive(param);
 	}
 
-	@GetMapping("/test")
-	@Transactional
-	public void test() {
-
-	}
 
 	@Autowired
 	private WxMaProperties properties;

+ 5 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -35,10 +35,13 @@ import com.fs.hisStore.dto.FsStoreOrderComputeDTO;
 import com.fs.hisStore.enums.OrderInfoEnum;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.param.*;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.huifuPay.domain.HuiFuCreateOrder;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.domain.HuifuCreateOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderPayment;
@@ -129,6 +132,8 @@ public class LiveOrderController extends AppBaseController
     private HuiFuService huiFuService;
 
 
+
+
     /**
      * 查询订单列表
      */

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/facade/LiveFacadeService.java

@@ -13,7 +13,7 @@ public interface LiveFacadeService {
 
     TableDataInfo watchUserList(LiveWatchUser param);
 
-    R liveDetail(Long id);
+    R liveDetail(Long id, Long userId);
 
     R currentActivities(Long liveId, String userId);
 

+ 62 - 1
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -52,6 +52,9 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
+    
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
 
     @Override
     public R liveList(PageRequest pageRequest) {
@@ -115,7 +118,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
 
     @Override
-    public R liveDetail(Long id) {
+    public R liveDetail(Long id, Long userId) {
         Object o = redisCache.hashGet(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, String.valueOf(id));
         LiveVo liveVo;
         if (ObjectUtil.isNotEmpty(o)) {
@@ -129,8 +132,66 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         if(liveVo.getIsShow() == 2) {
             return R.error("直播未开放");
         }
+        
+        // 查询用户今天是否已领取完课奖励
+        if (userId != null) {
+            try {
+                List<LiveCompletionPointsRecord> unreceivedRecords = 
+                    completionPointsRecordService.getUserUnreceivedRecords(id, userId);
+                
+                // 判断是否有未领取的奖励,如果有则说明今天还未领取
+                // 如果没有未领取的,再查询是否有已领取的记录
+                if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
+                    liveVo.setTodayRewardReceived(false);
+                } else {
+                    // 查询所有记录(包括已领取和未领取)
+                    List<LiveCompletionPointsRecord> allRecords = 
+                        completionPointsRecordService.getUserRecords(id, userId);
+                    
+                    if (allRecords != null && !allRecords.isEmpty()) {
+                        // 检查最近一条记录是否是今天的且已领取
+                        LiveCompletionPointsRecord latestRecord = allRecords.get(0);
+                        Date today = new Date();
+                        Date recordDate = latestRecord.getCurrentCompletionDate();
+                        
+                        // 判断是否为同一天
+                        boolean isSameDay = recordDate != null && 
+                            isSameDay(recordDate, today);
+                        
+                        if (isSameDay && latestRecord.getReceiveStatus() == 1) {
+                            liveVo.setTodayRewardReceived(true);
+                        } else {
+                            liveVo.setTodayRewardReceived(false);
+                        }
+                    } else {
+                        liveVo.setTodayRewardReceived(false);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("查询用户完课奖励领取状态失败, liveId={}, userId={}", id, userId, e);
+                liveVo.setTodayRewardReceived(false);
+            }
+        } else {
+            liveVo.setTodayRewardReceived(false);
+        }
+        
         return R.ok().put("data", liveVo);
     }
+    
+    /**
+     * 判断两个日期是否为同一天
+     */
+    private boolean isSameDay(Date date1, Date date2) {
+        if (date1 == null || date2 == null) {
+            return false;
+        }
+        Calendar cal1 = Calendar.getInstance();
+        Calendar cal2 = Calendar.getInstance();
+        cal1.setTime(date1);
+        cal2.setTime(date2);
+        return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+               cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
+    }
 
     @Override
     public R currentActivities(Long liveId, String userId) {