Kaynağa Gözat

直播调试 和导出openid修复

wangxy 3 gün önce
ebeveyn
işleme
0d6b0b0d92
58 değiştirilmiş dosya ile 1129 ekleme ve 302 silme
  1. 50 5
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  2. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  3. 9 0
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  4. 15 1
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  5. 9 5
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  6. 2 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  7. 10 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  8. 12 3
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  9. 7 3
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  10. 5 0
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  11. 1 8
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  12. 107 16
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  13. 2 2
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  14. 164 61
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  15. 19 1
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseComplaintRecordPageListVO.java
  16. 2 2
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  17. 1 1
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  18. 2 2
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  19. 1 0
      fs-service/src/main/java/com/fs/his/vo/UserOpenIdVO.java
  20. 12 0
      fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayQueryRequest.java
  21. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java
  22. 2 2
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  23. 17 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java
  24. 2 0
      fs-service/src/main/java/com/fs/live/mapper/LiveAfterSalesMapper.java
  25. 6 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  26. 21 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  27. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java
  28. 1 1
      fs-service/src/main/java/com/fs/live/param/LiveOrderSearchParam.java
  29. 2 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  30. 3 1
      fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  31. 10 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  32. 7 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  33. 14 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchLogService.java
  34. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  35. 64 23
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  36. 18 15
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  37. 1 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java
  38. 11 7
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  39. 100 78
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  40. 63 17
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  41. 10 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveVideoServiceImpl.java
  42. 66 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java
  43. 16 6
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  44. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveOrderListVo.java
  45. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveOrderVoZm.java
  46. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveUserDetailExportVO.java
  47. 21 12
      fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java
  48. 2 0
      fs-service/src/main/java/com/fs/live/vo/MergedOrderVO.java
  49. 5 0
      fs-service/src/main/java/com/fs/qw/vo/QwExternalContactComplaintVO.java
  50. 7 1
      fs-service/src/main/resources/mapper/course/FsUserCourseComplaintRecordMapper.xml
  51. 53 0
      fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml
  52. 11 2
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  53. 7 7
      fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
  54. 2 0
      fs-service/src/main/resources/mapper/live/LiveUserRedRecordMapper.xml
  55. 91 0
      fs-service/src/main/resources/mapper/live/LiveWatchLogMapper.xml
  56. 2 1
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  57. 48 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedController.java
  58. 2 1
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

+ 50 - 5
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -1,5 +1,6 @@
 package com.fs.his.controller;
 
+import java.io.IOException;
 import java.util.*;
 
 import com.alibaba.fastjson.JSON;
@@ -33,6 +34,10 @@ import com.google.common.collect.Lists;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.beans.BeanUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -55,6 +60,8 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.servlet.http.HttpServletResponse;
+
 import static com.fs.his.utils.PhoneUtil.*;
 
 /**
@@ -157,11 +164,49 @@ public class FsUserController extends BaseController
     }
 
     @PreAuthorize("@ss.hasPermi('his:user:exportOpenId')")
-    @GetMapping("/exportOpenId")
-    public  AjaxResult exportOpenIdList(){
-        List<UserOpenIdVO> list = fsUserService.selectOpenIdList();
-        ExcelUtil<UserOpenIdVO> util = new ExcelUtil<UserOpenIdVO>(UserOpenIdVO.class);
-        return util.exportExcel(list, "用户openId数据");
+    @PostMapping("/exportOpenId")
+    public  void exportOpenIdList(HttpServletResponse response) throws IOException {
+        int batchSize = 10000; // 每批处理1000条
+        long lastId = 0; // 从0开始
+        boolean hasMore = true;
+
+        // 设置响应头
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-Disposition", "attachment; filename=用户openId数据.xlsx");
+
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("用户openId数据");
+
+            // 创建表头
+            Row headerRow = sheet.createRow(0);
+            headerRow.createCell(0).setCellValue("OpenID");
+
+            int rowNum = 1;
+
+            while (hasMore) {
+                List<UserOpenIdVO> batchList = fsUserService.selectOpenIdList(lastId, batchSize);
+
+                if (batchList.isEmpty()) {
+                    hasMore = false;
+                    break;
+                }
+
+                //创建行和单元格
+                for (UserOpenIdVO item : batchList) {
+                    Row row = sheet.createRow(rowNum++);
+                    row.createCell(0).setCellValue(item.getOpenId());
+
+                    // 更新lastId用于下一次查询
+                    lastId = item.getUserId();
+                }
+
+                //当前批次数据
+                if (batchList.size() < batchSize) {
+                    hasMore = false;
+                }
+            }
+            workbook.write(response.getOutputStream());
+        }
     }
 
 

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

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

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

@@ -221,4 +221,13 @@ public class LiveController extends BaseController {
         return getDataTable(list);
     }
 
+    /**
+     * 清除直播间缓存
+     */
+    @Log(title = "直播", businessType = BusinessType.UPDATE)
+    @PostMapping("/clearCache/{liveId}")
+    public R clearCache(@PathVariable("liveId") Long liveId) {
+        return liveService.clearLiveCache(liveId);
+    }
+
 }

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

@@ -8,6 +8,8 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.web.service.TokenService;
 import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
@@ -29,6 +31,8 @@ public class LiveDataController extends BaseController {
 
     @Autowired
     private ILiveDataService liveDataService;
+    @Autowired
+    private TokenService tokenService;
 
     /**
      * 直播数据页面卡片数据
@@ -128,11 +132,21 @@ public class LiveDataController extends BaseController {
     /**
      * 查询直播间用户详情列表(SQL方式)
      * @param liveId 直播间ID
+     * @param pageNum 页码
+     * @param pageSize 每页大小
      * @return 用户详情列表
      */
     @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
     @GetMapping("/getLiveUserDetailListBySql")
-    public R getLiveUserDetailListBySql(@RequestParam Long liveId) {
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId,
+                                        @RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "100") Integer pageSize) {
+        // 限制最大每页查询条数为1000
+        if (pageSize > 1000) {
+            pageSize = 1000;
+        }
+
+        PageHelper.startPage(pageNum, pageSize);
         return liveDataService.getLiveUserDetailListBySql(liveId,null,null);
     }
 

+ 9 - 5
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -51,7 +51,7 @@ public class OrderController extends BaseController
     @Autowired
     private IMergedOrderService mergedOrderService;
     // 设置最大导出数量限制为20000条
-    private static final int maxExportCount = 20000;
+    private static final int maxExportCount = 50000;
 
 
     @Autowired
@@ -88,6 +88,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
         
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -126,6 +127,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -235,20 +237,21 @@ public class OrderController extends BaseController
             MergedOrderExportVO exportVO = new MergedOrderExportVO();
             
             // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderTypeName(vo.getOrderTypeName());
             exportVO.setOrderCode(vo.getOrderCode());
             exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
             exportVO.setUserId(vo.getUserId());
             
             // 产品信息
-            exportVO.setProductName(vo.getProductName());
+            exportVO.setProductName(StringUtils.isEmpty(vo.getProductName()) ? "产品被删除" : vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
             exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
             exportVO.setTotalNum(vo.getTotalNum());
             exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
-            exportVO.setCost(vo.getCost());
+            exportVO.setCost(vo.getCost() != null ? vo.getCost() : BigDecimal.ZERO);
             exportVO.setFPrice(vo.getCost() != null ? vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())) : BigDecimal.ZERO); // 结算价,合并订单暂无此字段
             exportVO.setPayPostage(vo.getPayDelivery());
-            exportVO.setCateName(vo.getCateName());
+            exportVO.setCateName(StringUtils.isEmpty(vo.getCateName()) ? "产品被删除" : vo.getCateName());
             // 收货信息
             exportVO.setRealName(vo.getRealName());
             if (isPlainText) {
@@ -262,7 +265,8 @@ public class OrderController extends BaseController
             // 时间信息
             exportVO.setCreateTime(vo.getCreateTime());
             exportVO.setPayTime(vo.getPayTime());
-            
+            exportVO.setHfshh(vo.getHfshh());
+
             // 物流信息
             exportVO.setDeliverySn(vo.getDeliveryCode()); // 快递公司编号,合并订单暂无此字段
             exportVO.setDeliveryName(vo.getDeliveryName()); // 快递公司,合并订单暂无此字段

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

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

+ 10 - 2
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -15,6 +15,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsPayConfig;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.service.HuiFuService;
@@ -28,7 +29,10 @@ import com.fs.live.service.ILiveCompanyCodeService;
 import com.fs.live.service.ILiveOrderService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.oss.OSSFactory;
+import com.fs.wx.miniapp.config.WxMaProperties;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import io.swagger.annotations.ApiOperation;
@@ -343,6 +347,9 @@ public class LiveController extends BaseController
         }
     }
 
+    @Autowired
+    private WxMaProperties properties;
+
     @ApiOperation("生成微信小程序码")
     @GetMapping("/getWxaCodeUnLimit")
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
@@ -350,9 +357,10 @@ public class LiveController extends BaseController
         String url="https://api.weixin.qq.com/cgi-bin/stable_token";
         HashMap<String, String> map = new HashMap<>();
         map.put("grant_type","client_credential");
+
         // 百域承品
-        map.put("appid","wx44beed5640bcb1ba");
-        map.put("secret","1bfcfa420f741801575a74d94752d014");
+        map.put("appid",properties.getConfigs().get(0).getAppid());
+        map.put("secret",properties.getConfigs().get(0).getSecret());
         String accessToken = HttpUtils.endApi(url, null, map);
         // 创建Gson对象
         Gson gson = new Gson();

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

@@ -55,17 +55,26 @@ public class LiveDataController extends BaseController
     /**
      * 查询直播间用户详情列表(SQL方式)
      * @param liveId 直播间ID
+     * @param pageNum 页码
+     * @param pageSize 每页大小
      * @return 用户详情列表
      */
     @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
     @GetMapping("/getLiveUserDetailListBySql")
-    public R getLiveUserDetailListBySql(@RequestParam Long liveId, HttpServletRequest request) {
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId, 
+                                        @RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "100") Integer pageSize,
+                                        HttpServletRequest request) {
+        // 限制最大每页查询条数为1000
+        if (pageSize > 1000) {
+            pageSize = 1000;
+        }
+        PageHelper.startPage(pageNum, pageSize);
         CompanyUser user = tokenService.getLoginUser(request).getUser();
         if ("00".equals(user.getUserType())) {
             return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),null);
         }
-        return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),user.getUserId());
-    }
+        return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),user.getUserId());    }
 
     /**
      * 查询直播间详情数据(查询数据服务器处理方式)

+ 7 - 3
fs-company/src/main/java/com/fs/company/controller/live/OrderController.java

@@ -52,7 +52,7 @@ public class OrderController extends BaseController
     @Autowired
     private IMergedOrderService mergedOrderService;
     // 设置最大导出数量限制为20000条
-    private static final int maxExportCount = 20000;
+    private static final int maxExportCount = 50000;
 
 
 
@@ -96,6 +96,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -138,6 +139,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -239,12 +241,13 @@ public class OrderController extends BaseController
             MergedOrderExportVO exportVO = new MergedOrderExportVO();
 
             // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderTypeName(vo.getOrderTypeName());
             exportVO.setOrderCode(vo.getOrderCode());
             exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
             exportVO.setUserId(vo.getUserId());
 
             // 产品信息
-            exportVO.setProductName(vo.getProductName());
+            exportVO.setProductName(StringUtils.isEmpty(vo.getProductName()) ? "产品被删除" : vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
             exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
             exportVO.setTotalNum(vo.getTotalNum());
@@ -252,7 +255,7 @@ public class OrderController extends BaseController
             exportVO.setCost(BigDecimal.ZERO);
             exportVO.setFPrice(BigDecimal.ZERO); // 结算价,合并订单暂无此字段
             exportVO.setPayPostage(vo.getPayDelivery());
-            exportVO.setCateName(vo.getCateName());
+            exportVO.setCateName(StringUtils.isEmpty(vo.getCateName()) ? "产品被删除" : vo.getCateName());
 
             // 收货信息
             exportVO.setRealName(vo.getRealName());
@@ -270,6 +273,7 @@ public class OrderController extends BaseController
             // 公司和销售信息
             exportVO.setCompanyName(vo.getCompanyName());
             exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
+            exportVO.setHfshh(vo.getHfshh());
 
             // 套餐信息
             exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段

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

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

+ 1 - 8
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -53,8 +53,6 @@ public class LiveCompletionPointsTask {
                 log.debug("当前没有开启完课积分的直播间");
                 return;
             }
-            
-            log.info("开始检查完课状态, 开启完课积分的直播间数量: {}", activeLives.size());
 
             for (Live live : activeLives) {
                 try {
@@ -65,14 +63,9 @@ public class LiveCompletionPointsTask {
                     Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
                     
                     if (userDurations == null || userDurations.isEmpty()) {
-                        log.warn("直播间没有观看时长数据, liveId={}, liveName={}, Redis Key: {}, userDurations={}", 
-                                liveId, live.getLiveName(), hashKey, userDurations);
+
                         continue;
                     }
-                    
-                    log.info("直播间有观看数据, liveId={}, liveName={}, 用户数: {}", 
-                            liveId, live.getLiveName(), userDurations.size());
-                    
                     // 3. 逐个用户处理
                     for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
                         try {

+ 107 - 16
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -33,6 +33,7 @@ import javax.annotation.PostConstruct;
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -169,7 +170,10 @@ public class Task {
                         redisCache.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
-                
+                // 清理小程序缓存 和 直播标签缓存
+                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+                redisCache.deleteObject(cacheKey);
+                liveWatchUserService.clearLiveFlagCache(live.getLiveId());
                 // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
                 try {
                     // 获取视频时长
@@ -216,8 +220,10 @@ public class Task {
                         redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
                     });
                 }
+                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+                redisCache.deleteObject(cacheKey);
                 webSocketServer.removeLikeCountCache(live.getLiveId());
-                
+
                 // 删除打标签缓存
                 try {
                     String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
@@ -410,9 +416,13 @@ public class Task {
         for (Live openRewardLive : openRewardLives) {
             String configJson = openRewardLive.getConfigJson();
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-            if (config.getEnabled()) {
+            if (config.getEnabled() && 1 == config.getParticipateCondition()) {
+                List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
+                if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+                    continue;
+                }
                 // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
-                List<LiveWatchUser> onlineUser = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now)
+                List<LiveWatchUser> onlineUser = liveWatchUsers
                         .stream().filter(user -> (now.getTime() - user.getUpdateTime().getTime() + ( user.getOnlineSeconds() == null ? 0L : user.getOnlineSeconds())) > config.getWatchDuration() * 60 * 1000)
                         .collect(Collectors.toList());
                 if(onlineUser.isEmpty()) continue;
@@ -624,7 +634,7 @@ public class Task {
     }
 
     /**
-     * 定时扫描开启的直播间,检查是否到了打标签的时间
+     * 定时扫描开启的直播间,检查是否到了打标签的时间,然后把正在看直播的用户拆分为 直播用户和回放用户
      * 每10秒执行一次
      */
     @Scheduled(cron = "0/10 * * * * ?")
@@ -679,7 +689,8 @@ public class Task {
                         queryUser.setLiveId(liveId);
                         queryUser.setLiveFlag(1);
                         queryUser.setReplayFlag(0);
-                        List<LiveWatchUser> liveUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+                        queryUser.setOnline(0);
+                        List<LiveWatchUser> liveUsers = liveWatchUserService.selectAllWatchUser(queryUser);
 
                         if (liveUsers != null && !liveUsers.isEmpty()) {
 
@@ -725,6 +736,7 @@ public class Task {
                                 // 更新直播用户的在线时长
                                 liveUser.setOnlineSeconds(totalOnlineSeconds);
                                 liveUser.setUpdateTime(nowDate);
+                                liveUser.setOnline(1);
                                 updateLiveUsers.add(liveUser);
 
                                 // 2. 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始
@@ -732,7 +744,7 @@ public class Task {
                                 replayUser.setLiveId(liveUser.getLiveId());
                                 replayUser.setUserId(liveUser.getUserId());
                                 replayUser.setMsgStatus(liveUser.getMsgStatus());
-                                replayUser.setOnline(liveUser.getOnline());
+                                replayUser.setOnline(0);
                                 replayUser.setOnlineSeconds(0L); // 回放观看时长从0开始,重新计时
                                 replayUser.setGlobalVisible(liveUser.getGlobalVisible());
                                 replayUser.setSingleVisible(liveUser.getSingleVisible());
@@ -742,6 +754,7 @@ public class Task {
                                 replayUser.setCreateTime(nowDate);
                                 replayUser.setUpdateTime(nowDate);
                                 replayUsers.add(replayUser);
+                                redisCache.setCacheObject(entryTimeKey,now);
                             }
 
                             // 批量更新直播用户的在线时长
@@ -800,6 +813,7 @@ public class Task {
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
         try {
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
@@ -824,8 +838,7 @@ public class Task {
                     queryUser.setLiveId(liveId);
                     queryUser.setLiveFlag(1);
                     queryUser.setReplayFlag(0);
-                    queryUser.setOnline(0); // 在线用户
-                    List<LiveWatchUser> onlineUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+                    List<LiveWatchUser> onlineUsers = liveWatchUserService.selectAllWatchUser(queryUser);
                     if (onlineUsers == null || onlineUsers.isEmpty()) {
                         continue;
                     }
@@ -840,6 +853,7 @@ public class Task {
                     }
 
                     // 处理每个在线用户
+                    List<LiveWatchLog> updateLog = new ArrayList<>();
                     for (LiveWatchUser user : onlineUsers) {
                         try {
                             Long userId = user.getUserId();
@@ -852,7 +866,7 @@ public class Task {
                             if (onlineSeconds == null || onlineSeconds <= 0) {
                                 continue;
                             }
-                            
+
                             // 获取用户的 companyId 和 companyUserId
                             LiveUserFirstEntry liveUserFirstEntry =
                                     liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
@@ -866,16 +880,31 @@ public class Task {
                             if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
                                 continue;
                             }
-
+                            //更新最新用户活跃时间
+                            String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                            LocalDateTime now = LocalDateTime.now();
+                            redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                             // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
                             updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
-                                    onlineSeconds, totalVideoDuration);
+                                    onlineSeconds, totalVideoDuration, updateLog);
                             
                         } catch (Exception e) {
                             log.error("处理用户观看记录状态异常: liveId={}, userId={}, error={}",
                                     liveId, user.getUserId(), e.getMessage(), e);
                         }
                     }
+                    // 批量插入回放用户数据
+                    if (!updateLog.isEmpty()) {
+                        int batchSize = 500;
+                        for (int i = 0; i < updateLog.size(); i += batchSize) {
+                            int end = Math.min(i + batchSize, updateLog.size());
+                            List<LiveWatchLog> batch = updateLog.subList(i, end);
+                            liveWatchLogService.batchUpdateLiveWatchLog(batch);
+                        }
+                        for (LiveWatchLog liveWatchLog : updateLog) {
+                            redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                        }
+                    }
                     
                 } catch (Exception e) {
                     log.error("处理直播间观看记录状态异常: liveId={}, error={}",
@@ -897,16 +926,15 @@ public class Task {
      * @param totalVideoDuration 视频总时长(秒)
      */
     private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long qwUserId,
-                                                   Long exId, Long onlineSeconds, long totalVideoDuration) {
+                                                   Long exId, Long onlineSeconds, long totalVideoDuration, List<LiveWatchLog> updateLog) {
         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);
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogByLogIdWithCache(queryLog);
             if (logs == null || logs.isEmpty()) {
                 return;
             }
@@ -940,7 +968,7 @@ public class Task {
                 // 如果 logType 已经是 2(完课),不再更新
                 if (needUpdate) {
                     log.setLogType(newLogType);
-                    liveWatchLogService.updateLiveWatchLog(log);
+                    updateLog.add(log);
                 }
             }
         } catch (Exception e) {
@@ -949,6 +977,69 @@ public class Task {
         }
     }
 
+    /**
+     * 每分钟扫描一次用户在线状态用于更新用户观看记录值
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
+    public void updateLiveWatchUserStatus() {
+        try {
+            Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
+            LocalDateTime now = LocalDateTime.now();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            List<LiveWatchLog> updateLog = new ArrayList<>();
+            if (keys != null && !keys.isEmpty()) {
+                for (String key : keys) {
+                    String[] split = key.split(":");
+                    String cacheTime = redisCache.getCacheObject(key);
+                    //判断缓存的值是否已经距离现在超过一分钟
+                    if (StringUtils.isNotBlank(cacheTime)) {
+                        try {
+                            LocalDateTime cachedDateTime = LocalDateTime.parse(cacheTime, formatter);
+                            // 比较时间,判断是否超过1分钟(60秒)
+                            long secondsBetween = java.time.Duration.between(cachedDateTime, now).getSeconds();
+                            if (secondsBetween >= 60) {
+                                // 距离上次记录已超过1分钟,更新状态为看课中断
+                                // 查询 LiveWatchLog
+                                LiveWatchLog queryLog = new LiveWatchLog();
+                                queryLog.setLiveId(Long.valueOf(split[4]));
+                                queryLog.setQwUserId(String.valueOf(split[7]));
+                                queryLog.setExternalContactId(Long.valueOf(split[6]));
+                                queryLog.setLogType(1);
+                                List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                                if (logs != null && !logs.isEmpty()) {
+                                    for (LiveWatchLog log : logs) {
+                                        if (log.getLogType() != null && log.getLogType() == 2) {
+                                            continue;
+                                        }
+                                        log.setLogType(4);
+                                        updateLog.add(log);
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.error("解析缓存时间失败: cacheTime={}, error={}", cacheTime, e.getMessage());
+                        }
+                    }
+                }
+                // 批量插入回放用户数据
+                if (!updateLog.isEmpty()) {
+                    int batchSize = 500;
+                    for (int i = 0; i < updateLog.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, updateLog.size());
+                        List<LiveWatchLog> batch = updateLog.subList(i, end);
+                        liveWatchLogService.batchUpdateLiveWatchLog(batch);
+                    }
+                    for (LiveWatchLog liveWatchLog : updateLog) {
+                        redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+        }
+    }
+
     /**
      * 批量同步Redis中的观看时长到数据库
      * 每2分钟执行一次,减少数据库压力

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

@@ -56,10 +56,10 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
             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));
+            userProperties.put(AttrConstant.QW_USER_ID, Long.valueOf(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));
+            userProperties.put(AttrConstant.EXTERNAL_CONTACT_ID, Long.valueOf(parameterMap.get(AttrConstant.EXTERNAL_CONTACT_ID).get(0)));
         }
 
         // 验证token

+ 164 - 61
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

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

+ 19 - 1
fs-service/src/main/java/com/fs/course/vo/FsUserCourseComplaintRecordPageListVO.java

@@ -27,7 +27,7 @@ public class FsUserCourseComplaintRecordPageListVO extends BaseEntity{
     @Excel(name = "投诉类型")
     private String complaintTypeName;
 
-//    @Excel(name = "投诉内容")
+    @Excel(name = "投诉内容")
     private String complaintContent;
 
     @Excel(name = "投诉上传图片")
@@ -46,4 +46,22 @@ public class FsUserCourseComplaintRecordPageListVO extends BaseEntity{
     @Excel(name = "看课状态")
     private String status;
 
+    /**
+     * 销售账号
+     */
+    @Excel(name = "销售账号")
+    private String userName;
+
+    /**
+     * 销售昵称
+     */
+    @Excel(name = "销售昵称")
+    private String saleNickName;
+
+    /**
+     * 销售公司
+     */
+    @Excel(name = "销售公司")
+    private  String company;
+
 }

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

@@ -260,8 +260,8 @@ public interface FsUserMapper
             "</script>"})
     Long selectFsUserExportListVOCount(FsUserParam fsUser);
 
-    @Select("SELECT mp_open_id as openId FROM fs_user WHERE mp_open_id is not null")
-    List<UserOpenIdVO> selectOpenIdList();
+    @Select("SELECT user_id as userId, mp_open_id as openId FROM fs_user WHERE user_id > #{lastId} AND mp_open_id IS NOT NULL ORDER BY user_id LIMIT #{limit}")
+    List<UserOpenIdVO> selectOpenIdList(@Param("lastId") long lastId, @Param("limit") int limit);
 
     @Select("select * from fs_user where phone=#{phone}")
     FsUser selectFsUserByMpOpenId(@Param("phone") String phone);

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

@@ -143,7 +143,7 @@ public interface IFsUserService
      * 获取所有用户openId
      * @return
      */
-    List<UserOpenIdVO> selectOpenIdList();
+    List<UserOpenIdVO> selectOpenIdList(long lastId, int limit);
 
     FsUser selectFsUserByMpOpenId(String openId);
 

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

@@ -570,8 +570,8 @@ public class FsUserServiceImpl implements IFsUserService {
     }
 
     @Override
-    public List<UserOpenIdVO> selectOpenIdList() {
-        return fsUserMapper.selectOpenIdList();
+    public List<UserOpenIdVO> selectOpenIdList(long lastId, int limit) {
+        return fsUserMapper.selectOpenIdList(lastId, limit);
     }
 
 

+ 1 - 0
fs-service/src/main/java/com/fs/his/vo/UserOpenIdVO.java

@@ -8,4 +8,5 @@ public class UserOpenIdVO {
 
     @Excel(name = "openId")
     private String openId;
+    private  Long userId;
 }

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

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

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

@@ -29,7 +29,7 @@ public class LiveCouponUser extends BaseEntity
 
     /** 优惠券所属用户 */
     @Excel(name = "优惠券所属用户")
-    private Integer userId;
+    private Long userId;
 
     /** 优惠券名称 */
     @Excel(name = "优惠券名称")

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

@@ -96,7 +96,7 @@ public class LiveOrder extends BaseEntity {
     private String payType;
 
     /** 订单状态(-1 : 申请退款 -2 : 退货成功 0:已取消 1:待支付 2:待发货;3:待收货;4:待评价;5:已完成) */
-    @Excel(name = "订单状态", readConverterExp = "-=1,:=,申=请退款,-=2,:=,退=货成功,1=:待支付,2=:待发货;3:待收货;4:待评价;5:已完成")
+    @Excel(name = "订单状态", readConverterExp = "-=1,:=,申=请退款,-=2,:=,退=货成功,1=:待支付,2=:待发货;3:待收货;4:待评价;5:已完成;6:被拆分")
     private Integer status;
 
     /** 0 未退款 1 申请中 2 已退款 */
@@ -145,7 +145,7 @@ public class LiveOrder extends BaseEntity {
     private String isDel;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal costPrice;
 
     /** 核销码 */

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

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

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

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

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

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

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

@@ -126,6 +126,27 @@ public interface LiveOrderMapper {
     @Select("select * from live_order where order_code=#{orderCode} limit 1")
     LiveOrder selectLiveOrderByOrderCode(@Param("orderCode") String orderCode);
 
+    /**
+     * 查询状态为6(被拆分)的订单
+     */
+    @Select("select * from live_order where status = 6")
+    List<LiveOrder> selectSplitOrders();
+
+    /**
+     * 根据父订单号查询子订单(拆分订单)
+     * 通过用户ID、直播ID、公司ID等关联查找可能的拆分订单
+     */
+    @Select({"<script> " +
+            "select * from live_order " +
+            "where status != 6 " +
+            "and user_id = (select user_id from live_order where order_code = #{parentOrderCode} limit 1) " +
+            "and live_id = (select live_id from live_order where order_code = #{parentOrderCode} limit 1) " +
+            "and create_time >= (select create_time from live_order where order_code = #{parentOrderCode} limit 1) " +
+            "and order_code != #{parentOrderCode} " +
+            "order by create_time asc " +
+            "</script>"})
+    List<LiveOrder> selectChildOrdersByParentOrderCode(@Param("parentOrderCode") String parentOrderCode);
+
     List<LiveOrder> selectFsOutDateOrder();
 
 

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

@@ -74,4 +74,11 @@ public interface LiveWatchLogMapper extends BaseMapper<LiveWatchLog> {
 
     List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog);
 
+    /**
+     * 批量更新直播看课记录
+     * @param liveWatchLogs 需要更新的直播看课记录列表
+     * @return 更新的记录数
+     */
+    int batchUpdateLiveWatchLog(@Param("list") List<LiveWatchLog> liveWatchLogs);
+
 }

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

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

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

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

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

@@ -84,7 +84,7 @@ public interface ILiveAfterSalesService {
 
     R refundMoney(LiveAfterSalesRefundParam param);
 
-    R cancel(LiveAfterSalesCancelParam param);
+    R cancel(LiveAfterSalesCancelParam param)  throws ParseException;
 
     R applyForAfterSales(String userId, LiveAfterSalesParam param);
 
@@ -93,4 +93,6 @@ public interface ILiveAfterSalesService {
     Integer selectLiveAfterSalesCount(long l, int i);
 
     R handleImmediatelyRefund(Long orderId);
+
+    List<LiveAfterSalesVo> selectLiveAfterSalesVoListExport(LiveAfterSalesVo liveAfterSales);
 }

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

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

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

@@ -187,6 +187,13 @@ public interface ILiveService
      */
     LiveConfigVo asyncToCacheLiveConfig(Long liveId);
 
+    /**
+     * 清除直播间数据缓存
+     * @param liveId 直播间ID
+     * @return 结果
+     */
+    R clearLiveCache(Long liveId);
+
     List<Live> liveCompanyList(Long companyId);
 
     R subNotifyLive(LiveNotifyParam liveNotifyParam);

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

@@ -66,4 +66,18 @@ public interface ILiveWatchLogService extends IService<LiveWatchLog>{
      * @return
      */
     List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog);
+
+    /**
+     * 批量更新直播看课记录
+     * @param liveWatchLogs 需要更新的直播看课记录列表
+     * @return 更新的记录数
+     */
+    int batchUpdateLiveWatchLog(List<LiveWatchLog> liveWatchLogs);
+
+    /**
+     * 查询直播看课记录并缓存1小时
+     * @param logId 直播看课记录主键
+     * @return 直播看课记录
+     */
+    List<LiveWatchLog> selectLiveWatchLogByLogIdWithCache(LiveWatchLog logId);
 }

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

@@ -177,4 +177,6 @@ public interface ILiveWatchUserService {
      * @param liveId 直播间ID
      */
     void clearLiveFlagCache(Long liveId);
+
+    List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser);
 }

+ 64 - 23
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -192,6 +192,59 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 
 //    @Autowired
 //    private FsStoreDeliversService fsStoreDeliversService;
+
+
+    @Override
+    public List<LiveAfterSalesVo> selectLiveAfterSalesVoListExport(LiveAfterSalesVo liveAfterSales) {
+        List<LiveAfterSalesVo> liveAfterSalesVos = baseMapper.selectLiveAfterSalesVoListExport(liveAfterSales);
+        List<Long> orderIds = new ArrayList<>();
+        Map<Long, List<LiveOrderItemListUVO>> orderItemMap = new HashMap<>();
+        if(null != liveAfterSalesVos && !liveAfterSalesVos.isEmpty()){
+            orderIds = liveAfterSalesVos.stream().map(e -> e.getOrderId()).collect(Collectors.toList());
+            if(null != orderIds && !orderIds.isEmpty()){
+                List<LiveOrderItemListUVO> liveOrderItemListUVOS = liveOrderItemMapper.selectLiveOrderItemListUVOByOrderIds(orderIds);
+                orderItemMap = liveOrderItemListUVOS.stream()
+                        .collect(Collectors.groupingBy(LiveOrderItemListUVO::getOrderId));
+            }
+        }
+        boolean mapEmpty = orderItemMap.isEmpty();
+        for (LiveAfterSalesVo item : liveAfterSalesVos) {
+            if(ObjectUtil.isNotNull(item.getUserId())) {
+                FsUser fsUser = fsUserCacheService.selectFsUserById(item.getUserId());
+                if(ObjectUtil.isNotNull(fsUser)) {
+                    item.setUserName(String.format("%s_%s",fsUser.getUserId(),fsUser.getNickname()));
+                }
+            }
+
+            if(ObjectUtil.isNull(item.getCompanyUserNickName())) {
+                item.setCompanyUserNickName("-");
+            }
+
+            if(ObjectUtil.isNull(item.getCompanyName())){
+                item.setCompanyName("-");
+            }
+
+            if(!mapEmpty && orderItemMap.containsKey(item.getOrderId())){
+                List<LiveOrderItemListUVO> liveOrderItemListUVOS = orderItemMap.get(item.getOrderId());
+                for (LiveOrderItemListUVO liveOrderItemListUVO : liveOrderItemListUVOS) {
+                    try {
+                        JSONObject jsO = JSONObject.parseObject(liveOrderItemListUVO.getJsonInfo());
+                        item.setProductName(StringUtils.isNotBlank(item.getProductName()) ? item.getProductName() + "," + jsO.getString("productName") : jsO.getString("productName"));
+                        item.setProductBarCode(StringUtils.isNotBlank(item.getProductBarCode()) ? item.getProductBarCode() + "," + jsO.getString("barCode") : jsO.getString("barCode"));
+                        item.setSku(StringUtils.isNotBlank(item.getSku()) ? item.getSku() + "," + jsO.getString("sku") : jsO.getString("sku"));
+                        item.setNum(StringUtils.isNotBlank(item.getNum()) ? item.getNum() + "," + jsO.getString("num") : jsO.getString("num"));
+                        item.setPrice(StringUtils.isNotBlank(item.getPrice()) ? item.getPrice() + "," + jsO.getString("price") : jsO.getString("price"));
+                        item.setCost(StringUtils.isNotBlank(item.getCost()) ? item.getCost() + "," + liveOrderItemListUVO.getCost() : liveOrderItemListUVO.getCost());
+                        item.setCateName(StringUtils.isNotBlank(item.getCateName()) ? item.getCateName() + "," + liveOrderItemListUVO.getCateName() : liveOrderItemListUVO.getCateName());
+                    } catch (Exception ex) {
+                        log.error("售后订单商品信息转换异常",ex);
+                    }
+                }
+            }
+
+        }
+        return liveAfterSalesVos;
+    }
     /**
      * 查询售后记录列表
      *
@@ -280,8 +333,8 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
-    public R cancel(LiveAfterSalesCancelParam param) {
+    @Transactional
+    public R cancel(LiveAfterSalesCancelParam param)  throws ParseException{
         LiveAfterSales storeAfterSales = baseMapper.selectLiveAfterSalesById(param.getSalesId());
         if (storeAfterSales == null) {
             throw new CustomException("未查询到售后订单信息");
@@ -322,13 +375,8 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                     orderMap.setStatus(order.getStatus());
                     liveOrderService.updateLiveOrder(orderMap);
                     liveOrderItemMapper.updateFsStoreOrderCode(order.getOrderId(),orderSn);
-                    try {
-                        //生成新的订单
-                        liveOrderService.createOmsOrder(order.getOrderId());
-                    } catch (Exception e) {
-                        log.error("撤销售后,生成oms订单异常",e);
-                    }
-
+                    //生成新的订单
+                    liveOrderService.createOmsOrder(order.getOrderId());
                 }
             }
         }
@@ -431,7 +479,6 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         storeAfterSales.setIsDel(0);
         storeAfterSales.setOrderStatus(orderStatus);
         storeAfterSales.setUserId(Long.valueOf(userId));
-        storeAfterSales.setOrderStatus(orderStatus);
         storeAfterSales.setCompanyId(order.getCompanyId());
         storeAfterSales.setCompanyUserId(order.getCompanyUserId());
         liveAfterSalesService.insertLiveAfterSales(storeAfterSales);
@@ -557,6 +604,10 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         if(ObjectUtil.isNull(liveAfterSales)) {
             throw new IllegalArgumentException("售后单不存在!");
         }
+        if (!liveAfterSales.getStatus().equals(AfterStatusEnum.STATUS_3.getValue())) {
+            throw new CustomException("非法操作");
+        }
+        liveAfterSales.setRefundAmount(param.getRefundAmount());
         return liveOrderService.refundOrderMoney(liveAfterSales.getOrderId(),liveAfterSales);
     }
 
@@ -638,6 +689,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         if (!liveAfterSales.getStatus().equals(AfterStatusEnum.STATUS_0.getValue())) {
             throw new CustomException("非法操作");
         }
+        //仅退款
         if(liveAfterSales.getRefundType().equals(0)){
             //仅退款未发货处理
             if(liveAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_1.getValue())){
@@ -645,14 +697,11 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                 liveAfterSales.setStatus(3);
                 baseMapper.updateLiveAfterSales(liveAfterSales);
             }
-            //待收货直接退款
+            //仅退款待收货处理
             else if(liveAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_2.getValue())){
                 liveAfterSales.setStatus(2);
                 baseMapper.updateLiveAfterSales(liveAfterSales);
             //已完成 退货退款
-            } else if(liveAfterSales.getOrderStatus().equals(4)) {
-                liveAfterSales.setStatus(1);
-                baseMapper.updateLiveAfterSales(liveAfterSales);
             }
             LiveAfterSalesLogs salesLogs = new LiveAfterSalesLogs();
             salesLogs.setStoreAfterSalesId(liveAfterSales.getId());
@@ -897,7 +946,6 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         LiveOrder order = liveOrderService.selectLiveOrderByOrderId(String.valueOf(storeAfterSales.getOrderId()));
         order.setStatus(storeAfterSales.getOrderStatus());
         order.setRefundStatus(OrderInfoEnum.REFUND_STATUS_0.getValue().toString());
-        order.setIsAfterSales(0);
         liveOrderService.updateLiveOrder(order);
         //操作记录
         LiveAfterSalesLogs logs = new LiveAfterSalesLogs();
@@ -921,14 +969,6 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                 orderMap.setStatus(order.getStatus());
                 liveOrderService.updateLiveOrder(orderMap);
                 liveOrderItemService.updateFsStoreOrderCode(order.getOrderId(), orderSn);
-                //生成新的订单
-                List<LiveOrderPayment> payments = liveOrderPaymentMapper.selectLiveOrderPaymentByPay(5, order.getOrderId());
-                for (LiveOrderPayment payment : payments) {
-                    LiveOrderPayment livePayment = new LiveOrderPayment();
-                    livePayment.setPaymentId(payment.getPaymentId());
-                    livePayment.setBusinessCode(orderSn);
-                    liveOrderPaymentMapper.updateLiveOrderPayment(livePayment);
-                }
                 liveOrderService.createOmsOrder(order.getOrderId());
             }
         }
@@ -1137,4 +1177,5 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 
         return R.ok();
     }
+
 }

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

@@ -62,7 +62,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             // 1. 获取直播信息和配置
             Live live = liveService.selectLiveByLiveId(liveId);
             if (live == null) {
-                log.warn("直播间不存在, liveId={}", liveId);
+
                 return;
             }
 
@@ -71,7 +71,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             
             // 检查是否开启完课积分功能
             if (!config.isEnabled()) {
-                log.debug("直播间未开启完课积分功能, liveId={}", liveId);
+
                 return;
             }
             
@@ -80,8 +80,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             int[] pointsConfig = config.getPointsConfig();
             
             if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
-                log.warn("完课积分配置不完整, liveId={}, completionRate={}, pointsConfig={}", 
-                        liveId, completionRate, pointsConfig);
+
                 return;
             }
 
@@ -90,19 +89,18 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             if (actualWatchDuration == null) {
                 // 自动累加直播和回放的观看时长
                 actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
-                log.debug("自动累计观看时长: liveId={}, userId={}, totalDuration={}",
-                        liveId, userId, actualWatchDuration);
+
             }
 
             if (actualWatchDuration == null || actualWatchDuration <= 0) {
-                log.debug("观看时长为0, liveId={}, userId={}", liveId, userId);
+
                 return;
             }
 
             // 4. 获取视频总时长(秒)
             Long videoDuration = live.getDuration();
             if (videoDuration == null || videoDuration <= 0) {
-                log.warn("直播间视频时长无效, liveId={}, duration={}", liveId, videoDuration);
+
                 return;
             }
 
@@ -118,8 +116,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
             // 6. 判断是否达到完课标准
             if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
-                log.debug("观看时长未达到完课标准, liveId={}, userId={}, watchDuration={}, videoDuration={}, watchRate={}%, required={}%",
-                        liveId, userId, actualWatchDuration, videoDuration, watchRate, completionRate);
+
                 return;
             }
 
@@ -129,7 +126,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
             LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
             if (todayRecord != null) {
-                log.debug("今天已有完课记录, liveId={}, userId={}", liveId, userId);
+
                 return;
             }
 
@@ -145,8 +142,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
                 if (daysBetween == 0) {
                     continuousDays = latestRecord.getContinuousDays();
-                    log.debug("今天已有其他直播间完课记录,继承连续天数, liveId={}, userId={}, continuousDays={}", 
-                            liveId, userId, continuousDays);
+
                 } else if (daysBetween == 1) {
                     // 昨天完课了,连续天数+1
                     continuousDays = latestRecord.getContinuousDays() + 1;
@@ -177,8 +173,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
             recordMapper.insertRecord(record);
 
-            log.info("创建完课记录成功, liveId={}, userId={}, watchDuration={}, videoDuration={}, watchRate={}%, continuousDays={}, points={}",
-                    liveId, userId, actualWatchDuration, videoDuration, watchRate, continuousDays, points);
+
 
         } catch (Exception e) {
             log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
@@ -266,6 +261,14 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         return recordMapper.selectRecordsByUser(liveId, userId);
     }
 
+    /**
+     * 根据用户和日期查询完课记录
+     */
+    @Override
+    public LiveCompletionPointsRecord selectByUserAndDate(Long liveId, Long userId, Date date) {
+        return recordMapper.selectByUserAndDate(liveId, userId, date);
+    }
+
     /**
      * 从直播配置中获取完课积分配置
      */

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

@@ -337,7 +337,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
 
         LiveCouponUser userRecord = new LiveCouponUser();
         userRecord.setCouponId(liveCoupon.getCouponId());
-        userRecord.setUserId(Math.toIntExact(coupon.getUserId()));
+        userRecord.setUserId(coupon.getUserId());
         userRecord.setCouponTitle(liveCoupon.getTitle());
         userRecord.setCouponPrice(liveCoupon.getCouponPrice());
         userRecord.setUseMinPrice(liveCoupon.getUseMinPrice());

+ 11 - 7
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -26,6 +26,8 @@ import com.fs.his.mapper.FsUserMapper;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import java.util.stream.Collectors;
+
+import com.github.pagehelper.PageInfo;
 import io.swagger.models.auth.In;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -697,7 +699,13 @@ public class LiveDataServiceImpl implements ILiveDataService {
     @Override
     public R getLiveUserDetailListBySql(Long liveId, Long companyId, Long companyUserId ) {
         List<LiveUserDetailVo> userDetailList = liveDataMapper.selectLiveUserDetailListBySql(liveId, companyId, companyUserId);
-        return R.ok().put("data", userDetailList);
+        // 使用 PageInfo 获取分页信息
+        PageInfo<LiveUserDetailVo> pageInfo = new PageInfo<>(userDetailList);
+        R data = R.ok().put("data", userDetailList);
+        if (pageInfo != null) {
+            data.put("total", pageInfo.getTotal());
+        }
+        return data;
     }
 
     @Override
@@ -1088,6 +1096,8 @@ public class LiveDataServiceImpl implements ILiveDataService {
             return new ArrayList<>();
         }
 
+
+
         // 转换为导出VO列表
         List<LiveUserDetailExportVO> exportList = new ArrayList<>();
         for (LiveUserDetailVo userDetail : userDetailList) {
@@ -1114,12 +1124,6 @@ public class LiveDataServiceImpl implements ILiveDataService {
             exportVO.setCompanyName(userDetail.getCompanyName());
             exportVO.setSalesName(userDetail.getSalesName());
 
-            // 是否完课(根据观看时长判断,假设30分钟以上为完课)
-            if (totalSeconds >= 1800) {
-                exportVO.setIsCompleted("是");
-            } else {
-                exportVO.setIsCompleted("否");
-            }
 
             exportList.add(exportVO);
         }

+ 100 - 78
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.live.service.impl;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.sql.Timestamp;
+import java.text.DecimalFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
@@ -689,7 +690,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     }
 
     @Override
-    @Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRED)
+    @Transactional
     public String payConfirm(Integer type,Long orderId,String payCode,String tradeNo,String bankTransactionId,String bankSerialNo) {
         Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
         try {
@@ -782,21 +783,23 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(order.getLiveId());
             if (liveFlagWithCache != null && liveFlagWithCache.containsKey("liveFlag") && 1 == liveFlagWithCache.get("liveFlag")) {
                 try {
-                    LiveWatchLog queryLog = new LiveWatchLog();
-                    queryLog.setLiveId(order.getLiveId());
-                    queryLog.setUserId(Long.valueOf(order.getUserId()));
-                    queryLog.setCompanyId(order.getCompanyId());
-                    queryLog.setCompanyUserId(order.getCompanyUserId());
-
-                    List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
-                    if (logs != null && !logs.isEmpty()) {
-                        for (LiveWatchLog log : logs) {
-                            if (log.getLogType() == null || log.getLogType() != 2) {
-                                log.setLiveBuy(1);
-                                liveWatchLogService.updateLiveWatchLog(log);
+                    LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(order.getLiveId(), Long.parseLong(order.getUserId()));
+                    if (liveUserFirstEntry != null && liveUserFirstEntry.getExternalContactId()!=null && liveUserFirstEntry.getExternalContactId() > 0 &&  liveUserFirstEntry.getQwUserId()!=null && liveUserFirstEntry.getQwUserId() > 0) {
+                        LiveWatchLog queryLog = new LiveWatchLog();
+                        queryLog.setLiveId(order.getLiveId());
+                        queryLog.setQwUserId(String.valueOf(liveUserFirstEntry.getQwUserId()));
+                        queryLog.setExternalContactId(liveUserFirstEntry.getExternalContactId());
+                        List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                        if (logs != null && !logs.isEmpty()) {
+                            for (LiveWatchLog log : logs) {
+                                if (log.getLogType() == null || log.getLogType() != 2) {
+                                    log.setLiveBuy(1);
+                                    liveWatchLogService.updateLiveWatchLog(log);
+                                }
                             }
                         }
                     }
+
                 } catch (Exception e) {
                     log.error("更新 updateLiveWatchLog LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}",
                             order.getLiveId(), order.getUserId(), e.getMessage(), e);
@@ -1389,6 +1392,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public R refundOrderMoney(Long orderId, LiveAfterSales liveAfterSales) {
+        BigDecimal refundAmount = liveAfterSales.getRefundAmount();
         IErpOrderService erpOrderService = getErpService();
         FsErpConfig erpConfig = configUtil.generateStructConfigByKey("his.config", FsErpConfig.class);
         LiveOrder order = baseMapper.selectLiveOrderByOrderId(String.valueOf(orderId));
@@ -1407,43 +1411,49 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 && !CloudHostUtils.hasCloudHostName("康年堂")) {
             return R.error("暂未推送至erp,请稍后再试!");
         }
-        liveAfterSales.setRefundAmount(order.getPayPrice());
+        if (Objects.isNull(refundAmount)) {
+            throw new CustomException("退款金额不能为空");
+        }
+        if (order.getPayMoney().compareTo(refundAmount) < 0) {
+            throw new CustomException("退款金额不能大于支付金额");
+        }
+        liveAfterSales.setRefundAmount(liveAfterSales.getRefundAmount());
         liveAfterSales.setStatus(4);
         liveAfterSales.setSalesStatus(3);
         liveAfterSalesService.updateLiveAfterSales(liveAfterSales);
-        if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
-            ErpRefundUpdateRequest request = new ErpRefundUpdateRequest();
-            request.setTid(order.getOrderCode());
-            request.setOid(order.getOrderCode());
-            request.setRefund_state(1);
-            request.setOrderStatus(order.getStatus());
-            if (ObjectUtils.equals(order.getStatus(), 2)) {
-                LiveAfterSalesParam param = new LiveAfterSalesParam();
-                param.setOrderCode(order.getOrderCode());
-                param.setRefundAmount(order.getPayMoney());
-                param.setServiceType(1);
-                param.setReasons("后台手动退款流程");
-                param.setExplainImg(null);
-                List<FsStoreAfterSalesProductParam> productParams = new ArrayList<>();
-                List <LiveOrderItem> items = liveOrderItemMapper.selectLiveOrderItemByOrderId(order.getOrderId());
-                for (LiveOrderItem item : items){
-                    FsStoreAfterSalesProductParam param1 = new FsStoreAfterSalesProductParam();
-                    param1.setProductId(item.getProductId());
-                    param1.setNum(Math.toIntExact(item.getNum()));
-                    productParams.add(param1);
-                }
-                return liveAfterSalesService.applyForAfterSales(order.getUserId(), param);
-            } else {
-                ErpOrderQueryRequert queryRequest = new ErpOrderQueryRequert();
-                queryRequest.setCode(order.getExtendOrderId());
-                ErpOrderQueryResponse response = erpOrderService.getLiveOrder(queryRequest);
-                if (response.getOrders() != null && response.getOrders().size() > 0) {
-                    if (response.getOrders().get(0).getCancle() != null && !response.getOrders().get(0).getCancle()) {
-                        jSTOrderService.refundUpdateLive(request);
-                    }
-                }
-            }
-        }
+//        if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
+//            ErpRefundUpdateRequest request = new ErpRefundUpdateRequest();
+//            request.setTid(order.getOrderCode());
+//            request.setOid(order.getOrderCode());
+//            request.setRefund_state(1);
+//            request.setOrderStatus(order.getStatus());
+//            if (ObjectUtils.equals(order.getStatus(), 2)) {
+//                LiveAfterSalesParam param = new LiveAfterSalesParam();
+//                param.setOrderCode(order.getOrderCode());
+//                param.setRefundAmount(order.getPayMoney());
+//                param.setServiceType(1);
+//                param.setReasons("后台手动退款流程");
+//                param.setExplainImg(null);
+//                List<FsStoreAfterSalesProductParam> productParams = new ArrayList<>();
+//                List <LiveOrderItem> items = liveOrderItemMapper.selectLiveOrderItemByOrderId(order.getOrderId());
+//                for (LiveOrderItem item : items){
+//                    FsStoreAfterSalesProductParam param1 = new FsStoreAfterSalesProductParam();
+//                    param1.setProductId(item.getProductId());
+//                    param1.setNum(Math.toIntExact(item.getNum()));
+//                    productParams.add(param1);
+//                }
+//                return liveAfterSalesService.applyForAfterSales(order.getUserId(), param);
+//            } else {
+//                ErpOrderQueryRequert queryRequest = new ErpOrderQueryRequert();
+//                queryRequest.setCode(order.getExtendOrderId());
+//                ErpOrderQueryResponse response = erpOrderService.getLiveOrder(queryRequest);
+//                if (response.getOrders() != null && response.getOrders().size() > 0) {
+//                    if (response.getOrders().get(0).getCancle() != null && !response.getOrders().get(0).getCancle()) {
+//                        jSTOrderService.refundUpdateLive(request);
+//                    }
+//                }
+//            }
+//        }
         order.setStatus(OrderInfoEnum.STATUS_NE2.getValue());
         order.setRefundMoney(order.getPayMoney());
         order.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_2.getValue()));
@@ -1494,7 +1504,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         refundRequest.setOutTradeNo("live-" + payment.getPayCode());
                         refundRequest.setOutRefundNo("live-" + payment.getPayCode());
                         refundRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
-                        refundRequest.setRefundFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                        refundRequest.setRefundFee(WxPayUnifiedOrderRequest.yuanToFen(refundAmount.toString()));
                         try {
                             WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
                             WxPayRefundQueryResult refundQueryResult = wxPayService.refundQuery("", refundResult.getOutTradeNo(), refundResult.getOutRefundNo(), refundResult.getRefundId());
@@ -1503,14 +1513,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                                 paymentMap.setPaymentId(payment.getPaymentId());
                                 paymentMap.setStatus(-1);
                                 paymentMap.setRefundTime(DateUtils.getNowDate());
-                                paymentMap.setRefundMoney(payment.getPayMoney());
+                                paymentMap.setRefundMoney(refundAmount);
                                 liveOrderPaymentMapper.updateLiveOrderPayment(paymentMap);
                             } else {
-                                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                                 return R.error("退款请求失败" + (refundQueryResult != null ? refundQueryResult.getErrCodeDes() : ""));
                             }
                         } catch (WxPayException e) {
-                            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                             return R.error("退款请求失败" + e.getErrCodeDes());
                         }
                     } else if (payment.getPayMode() != null && "hf".equals(payment.getPayMode())) {
@@ -1518,25 +1526,26 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
                         if (payment.getAppId() != null) {
                             FsHfpayConfig fsHfpayConfig = fsHfpayConfigMapper.selectByAppId(payment.getAppId());
-                            if (fsHfpayConfig != null) {
+                            if (fsHfpayConfig == null){
+                                huifuId = fsPayConfig.getHuifuId();
+                            }else {
                                 huifuId = fsHfpayConfig.getHuifuId();
                             }
                         } else {
                             CloudHostProper cloudHostProper = SpringUtils.getBean(CloudHostProper.class);
                             if ("益善缘".equals(cloudHostProper.getCompanyName())) {
                                 FsHfpayConfig fsPayConfig2 = fsHfpayConfigMapper.selectByAppId("wx0d1a3dd485268521");
-                                if (fsPayConfig2 != null) {
-                                    huifuId = fsPayConfig2.getHuifuId();
-                                }
+                                huifuId = fsPayConfig2.getHuifuId();
                             } else {
                                 huifuId = fsPayConfig.getHuifuId();
                             }
                         }
 
                         V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
+                        DecimalFormat df = new DecimalFormat("0.00");
                         request.setOrgHfSeqId(payment.getTradeNo());
                         request.setHuifuId(huifuId);
-                        request.setOrdAmt(payment.getPayMoney().toString());
+                        request.setOrdAmt(df.format(refundAmount));
                         request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                         request.setReqSeqId("refund-" + payment.getPayCode());
                         Map<String, Object> extendInfoMap = new HashMap<>();
@@ -1547,7 +1556,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         log.info("退款:" + refund);
                         if (refund != null && ("00000000".equals(refund.getResp_code()) || "00000100".equals(refund.getResp_code()))
                                 && ("S".equals(refund.getTrans_stat()) || "P".equals(refund.getTrans_stat()))) {
-                            payment.setRefundMoney(payment.getPayMoney());
+                            payment.setRefundMoney(refundAmount);
                             payment.setStatus(-1);
                             payment.setRefundTime(new Date());
                             liveOrderPaymentMapper.updateLiveOrderPayment(payment);
@@ -3118,6 +3127,17 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     if (amount != null){
                         payMoney=amount;
                     }
+                    //运费
+                    BigDecimal payPostage = order.getPayPostage();
+                    if (payPostage == null || payPostage.compareTo(BigDecimal.ZERO) <= 0){
+                        payPostage = storeConfig.getPayPostage();
+                        if (payPostage == null){
+                            payPostage = BigDecimal.ZERO;
+                        }
+                        order.setPayPrice(order.getPayPrice().add(payPostage));
+                    }
+                    order.setPayPostage(payPostage);
+                    payMoney = payMoney.add(payPostage);
                     order.setPayMoney(payMoney);
                     order.setPayDelivery(order.getPayPrice().subtract(payMoney) );
 //                    order.setPayMoney(BigDecimal.ZERO);
@@ -3131,8 +3151,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             String payCode = OrderCodeUtils.getOrderSn();
 //            order.setOrderCode(orderCode);
 //            if(order.getPayType().equals("1")||order.getPayType().equals("2")){
-            if((order.getPayType().equals("1")||order.getPayType().equals("2")||order.getPayType().equals("3")) && order.getPayMoney().compareTo(new BigDecimal(0))>0){
-                LiveOrderPayment storePayment=new LiveOrderPayment();
+            if ((order.getPayType().equals("1") || order.getPayType().equals("2") || order.getPayType().equals("3")) && order.getPayMoney().compareTo(new BigDecimal(0)) > 0) {
+                LiveOrderPayment storePayment = new LiveOrderPayment();
                 storePayment.setCompanyId(order.getCompanyId());
                 storePayment.setCompanyUserId(order.getCompanyUserId());
                 storePayment.setPayMode(fsPayConfig.getType());
@@ -3149,35 +3169,35 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 storePayment.setAppId(param.getAppId());
                 liveOrderPaymentMapper.insertLiveOrderPayment(storePayment);
 
-                if (fsPayConfig.getType().equals("hf")){
+                if (fsPayConfig.getType().equals("hf")) {
                     HuiFuCreateOrder o = new HuiFuCreateOrder();
                     o.setTradeType("T_MINIAPP");
                     o.setOpenid(storePayment.getOpenId());
-                    o.setReqSeqId("live-"+storePayment.getPayCode());
+                    o.setReqSeqId("live-" + storePayment.getPayCode());
                     o.setTransAmt(storePayment.getPayMoney().toString());
                     o.setGoodsDesc("直播订单支付");
                     if (StringUtils.isNotBlank(param.getAppId())) {
                         o.setAppId(param.getAppId());
                     }
                     HuifuCreateOrderResult result = huiFuService.createOrder(o);
-                    if(result.getResp_code()!=null&&(result.getResp_code().equals("00000000")||result.getResp_code().equals("00000100"))){
-                        LiveOrderPayment mt=new LiveOrderPayment();
+                    if (result.getResp_code() != null && (result.getResp_code().equals("00000000") || result.getResp_code().equals("00000100"))) {
+                        LiveOrderPayment mt = new LiveOrderPayment();
                         mt.setPaymentId(storePayment.getPaymentId());
                         mt.setTradeNo(result.getHf_seq_id());
                         mt.setAppId(param.getAppId());
                         mt.setBusinessCode(order.getOrderCode());
                         liveOrderPaymentMapper.updateLiveOrderPayment(mt);
-                        redisCache.setCacheObject("isPaying:"+order.getOrderId(),order.getOrderId().toString(),1, TimeUnit.MINUTES);
+                        redisCache.setCacheObject("isPaying:" + order.getOrderId(), order.getOrderId().toString(), 1, TimeUnit.MINUTES);
                         log.info("汇付支付");
-                        Map<String, Object> resultMap = JSON.parseObject(result.getPay_info(), new TypeReference<Map<String, Object>>() {});
+                        Map<String, Object> resultMap = JSON.parseObject(result.getPay_info(), new TypeReference<Map<String, Object>>() {
+                        });
                         String s = (String) resultMap.get("package");
-                        resultMap.put("packageValue",s);
-                        return R.ok().put("payType",param.getPayType()).put("result",resultMap);
-                    }
-                    else{
+                        resultMap.put("packageValue", s);
+                        return R.ok().put("payType", param.getPayType()).put("result", resultMap).put("type", "hf");
+                    } else {
                         return R.error(result.getResp_desc());
                     }
-                }else  if (fsPayConfig.getType().equals("wx")){
+                } else if (fsPayConfig.getType().equals("wx")) {
                     WxPayConfig payConfig = new WxPayConfig();
                     payConfig.setAppId(fsPayConfig.getAppId());
                     payConfig.setMchId(fsPayConfig.getWxMchId());
@@ -3198,7 +3218,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     //调用统一下单接口,获取"预支付交易会话标识"
                     try {
                         WxPayMpOrderResult orderResult = wxPayService.createOrder(orderRequest);
-                        return R.ok().put("result", orderResult).put("type", "wx").put("isPay", 0).put("payType",param.getPayType());
+                        return R.ok().put("result", orderResult).put("type", "wx").put("isPay", 0).put("payType", param.getPayType());
                     } catch (WxPayException e) {
                         e.printStackTrace();
                         throw new CustomException("支付失败" + e.getMessage());
@@ -3206,10 +3226,10 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 }
             }
 //            else if(order.getPayType().equals("3")){
-            else if(order.getPayType().equals("3") && order.getPayMoney().compareTo(new BigDecimal(0))<=0){
+            else if (order.getPayType().equals("3") && order.getPayMoney().compareTo(new BigDecimal(0)) <= 0) {
                 //货到付款
-                this.payConfirm(2,order.getOrderId(),null,null,null,null);
-                return R.ok().put("payType",param.getPayType());
+                this.payConfirm(2, order.getOrderId(), null, null, null, null);
+                return R.ok().put("payType", param.getPayType());
             }
             return R.error();
         }
@@ -3512,6 +3532,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                     request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                     request.setOrgHfSeqId(payment.getTradeNo());
+                    request.setAppId(payment.getAppId());
                     HuiFuQueryOrderResult queryOrderResult = null;
                     try {
                         queryOrderResult = huiFuService.queryOrder(request);
@@ -3604,9 +3625,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
 
         LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
-        liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
-        liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
-        liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        if (ObjectUtil.isNotEmpty(liveUserFirstEntry)) {
+            liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
+            liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
+            liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        }
         String orderSn = OrderCodeUtils.getOrderSn();
 //        String orderSn = "123"; // todo yhq
         log.info("订单生成:"+orderSn);
@@ -3661,7 +3684,6 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         liveOrder.setStatus(OrderInfoEnum.STATUS_0.getValue());
         liveOrder.setPayType("1");
         liveOrder.setTotalPrice(payPrice);
-        liveOrder.setPayMoney(BigDecimal.ZERO);
         liveOrder.setPayPrice(payPrice.subtract(liveOrder.getDiscountMoney()));
         try {
             if (baseMapper.insertLiveOrder(liveOrder) > 0) {

+ 63 - 17
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.live.service.impl;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -319,14 +320,17 @@ public class LiveServiceImpl implements ILiveService
 
     @Override
     public R subNotifyLive(LiveNotifyParam param) {
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("小程序订阅失败:appId为空!");
+        }
         LiveMiniprogramSubNotifyTask notifyTask = new LiveMiniprogramSubNotifyTask();
-        notifyTask.setPage("/pages_course/living.html?liveId=" + param.getLiveId());
+        notifyTask.setPage("pages_course/living?liveId=" + param.getLiveId());
         notifyTask.setTaskName("直播间预约提醒");
         notifyTask.setTemplateId(param.getTemplateId());
         Long userId = param.getUserId();
         Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery()
                 .eq(FsUserWx::getFsUserId, userId)
-                .eq(FsUserWx::getAppId, StringUtils.isEmpty(param.getAppId()) ? "wx44beed5640bcb1ba" : param.getAppId()); // 卓美小程序
+                .eq(FsUserWx::getAppId, StringUtils.isEmpty(param.getAppId()) ? "wxd791d5933ed42218" : param.getAppId()); // 卓美小程序
         FsUserWx fsUserWx = fsUserWxMapper.selectOne(queryWrapper);
         String maOpenId = "";
         if (fsUserWx == null) {
@@ -341,7 +345,6 @@ public class LiveServiceImpl implements ILiveService
         }
 
         notifyTask.setTouser(maOpenId);
-        notifyTask.setPage(String.valueOf(1));
 
         notifyTask.setCreateTime(LocalDateTime.now());
         // 状态等待执行
@@ -973,7 +976,38 @@ public class LiveServiceImpl implements ILiveService
             redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
             redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
         });
+        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey);
+        String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey2);
 
+        // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
+        try {
+            // 获取视频时长
+            Long videoDuration = 0L;
+            List<LiveVideo> videos = liveVideoService.listByLiveId(live.getLiveId(), 1);
+            if (CollUtil.isNotEmpty(videos)) {
+                videoDuration = videos.stream()
+                        .filter(v -> v.getDuration() != null)
+                        .mapToLong(LiveVideo::getDuration)
+                        .sum();
+            }
+
+            // 如果视频时长大于0,将直播间信息存入Redis
+            if (videoDuration > 0 && live.getStartTime() != null) {
+                Map<String, Object> tagMarkInfo = new HashMap<>();
+                tagMarkInfo.put("liveId", live.getLiveId());
+                tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
+                tagMarkInfo.put("videoDuration", videoDuration);
+
+                String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
+                log.info("手动开直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}",
+                        live.getLiveId(), live.getStartTime(), videoDuration);
+            }
+        } catch (Exception e) {
+            log.error("手动开写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+        }
 
         return R.ok();
     }
@@ -1224,9 +1258,9 @@ public class LiveServiceImpl implements ILiveService
         Map<Long, LiveAutoTask> goodsTaskMap = goodsTasks.stream()
                 .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "goodsId"),
                         Function.identity(), (existing, replacement) -> existing));
-        Map<Long, LiveAutoTask> shelfTaskMap = shelfTasks.stream()
-                .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "goodsId"),
-                        Function.identity(), (existing, replacement) -> existing));
+        // 使用 groupingBy 支持同一个商品有多个上下架任务(不同时间点的上架/下架操作)
+        Map<Long, List<LiveAutoTask>> shelfTaskMap = shelfTasks.stream()
+                .collect(Collectors.groupingBy(task -> parseIdFromContent(task.getContent(), "goodsId")));
 
         LiveGoods queryParam = new LiveGoods();
         queryParam.setLiveId(existLiveId);
@@ -1266,14 +1300,16 @@ public class LiveServiceImpl implements ILiveService
                     liveAutoTaskService.directInsertLiveAutoTask(newTask);
                 }
 
-                // 复制上下架任务(taskType=6)
-                LiveAutoTask shelfTask = shelfTaskMap.get(liveGoods.getGoodsId());
-                if (shelfTask != null) {
-                    JSONObject contentJson = JSON.parseObject(shelfTask.getContent());
-                    contentJson.put("goodsId", newGoods.getGoodsId());
-                    LiveAutoTask newTask = createAutoTaskEntity(shelfTask, newLiveId, now,
-                            contentJson.toJSONString());
-                    liveAutoTaskService.directInsertLiveAutoTask(newTask);
+                // 复制上下架任务(taskType=6)- 支持同一个商品有多个上下架任务
+                List<LiveAutoTask> shelfTaskList = shelfTaskMap.get(liveGoods.getGoodsId());
+                if (shelfTaskList != null && !shelfTaskList.isEmpty()) {
+                    for (LiveAutoTask shelfTask : shelfTaskList) {
+                        JSONObject contentJson = JSON.parseObject(shelfTask.getContent());
+                        contentJson.put("goodsId", newGoods.getGoodsId());
+                        LiveAutoTask newTask = createAutoTaskEntity(shelfTask, newLiveId, now,
+                                contentJson.toJSONString());
+                        liveAutoTaskService.directInsertLiveAutoTask(newTask);
+                    }
                 }
             }
         }
@@ -1348,15 +1384,25 @@ public class LiveServiceImpl implements ILiveService
         return jsonObject.getLong(key);
     }
 
+
     /**
-     * 清除直播间数据缓存
+     * 清除直播间数据缓存(公开方法)
      * @param liveId 直播间ID
+     * @return 结果
      */
-    private void clearLiveCache(Long liveId) {
-        if (liveId != null) {
+    @Override
+    public R clearLiveCache(Long liveId) {
+        if (liveId == null) {
+            return R.error("直播间ID不能为空");
+        }
+        try {
             String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId);
             redisCache.deleteObject(cacheKey);
             log.debug("清除直播间缓存: liveId={}", liveId);
+            return R.ok("缓存清理成功");
+        } catch (Exception e) {
+            log.error("清除直播间缓存失败: liveId={}", liveId, e);
+            return R.error("缓存清理失败: " + e.getMessage());
         }
     }
 

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

@@ -33,7 +33,7 @@ public class LiveVideoServiceImpl implements ILiveVideoService
 {
     @Autowired
     private LiveVideoMapper liveVideoMapper;
-    
+
     @Autowired
     private RedisCache redisCache;
     @Autowired
@@ -84,13 +84,13 @@ public class LiveVideoServiceImpl implements ILiveVideoService
     @Override
     public int insertLiveVideo(LiveVideo liveVideo)
     {
-        if (LiveEnum.contains(cloudHostProper.getCompanyName())) {
-            liveVideo.setVideoUrl(liveVideo.getLineOne());
-            liveVideo.setFinishStatus(1);
-        }else {
+//        if (LiveEnum.contains(cloudHostProper.getCompanyName())) {
+//            liveVideo.setVideoUrl(liveVideo.getLineOne());
+//            liveVideo.setFinishStatus(1);
+//        }else {
             // 直播ID为-1,则新增 直播视频库
             liveVideo.setFinishStatus(0);
-        }
+//        }
 
         if (liveVideo.getLiveId() == -1) {
             liveVideo.setCreateTime(DateUtils.getNowDate());
@@ -198,21 +198,21 @@ public class LiveVideoServiceImpl implements ILiveVideoService
     public List<LiveVideo> listByLiveIdWithCache(Long liveId, Integer type) {
         // Redis缓存键
         String cacheKey = "live:video:list:" + liveId + ":" + type;
-        
+
         // 先从缓存中获取
         List<LiveVideo> cachedVideos = redisCache.getCacheObject(cacheKey);
         if (cachedVideos != null && !cachedVideos.isEmpty()) {
             return cachedVideos;
         }
-        
+
         // 缓存未命中,从数据库查询
         List<LiveVideo> videos = liveVideoMapper.selectByIdAndType(liveId, type);
-        
+
         // 将查询结果存入缓存,缓存时间1小时
         if (videos != null) {
             redisCache.setCacheObject(cacheKey, videos, 1, TimeUnit.HOURS);
         }
-        
+
         return videos;
     }
 

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

@@ -1,7 +1,11 @@
 package com.fs.live.service.impl;
 
+import java.util.Date;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.live.vo.LiveWatchLogListVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,6 +25,10 @@ public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, Liv
 
     @Autowired
     private LiveWatchLogMapper liveWatchLogMapper;
+    
+    private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
+    
+    private static final String LIVE_WATCH_LOG_CACHE_KEY = "live:watch:log:cache:%s"; // logId
 
     /**
      * 查询直播看课记录
@@ -101,4 +109,62 @@ public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, Liv
     {
         return baseMapper.deleteLiveWatchLogByLogId(logId);
     }
+
+    /**
+     * 批量更新直播看课记录
+     * @param liveWatchLogs 需要更新的直播看课记录列表
+     * @return 更新的记录数
+     */
+    @Override
+    public int batchUpdateLiveWatchLog(List<LiveWatchLog> liveWatchLogs) {
+        if (liveWatchLogs == null || liveWatchLogs.isEmpty()) {
+            return 0;
+        }
+        Date now = DateUtils.getNowDate();
+        // 设置统一的更新时间
+        for (LiveWatchLog log : liveWatchLogs) {
+            if (log.getUpdateTime() == null) {
+                log.setUpdateTime(now);
+            }
+        }
+        int result = baseMapper.batchUpdateLiveWatchLog(liveWatchLogs);
+        
+        // 更新后清除相关缓存
+        for (LiveWatchLog log : liveWatchLogs) {
+            if (log.getLogId() != null) {
+                String cacheKey = String.format(LIVE_WATCH_LOG_CACHE_KEY, log.getLogId());
+                redisCache.deleteObject(cacheKey);
+            }
+        }
+        
+        return result;
+    }
+
+    /**
+     * 查询直播看课记录并缓存1小时
+     * @param logId 直播看课记录主键
+     * @return 直播看课记录
+     */
+    @Override
+    public List<LiveWatchLog> selectLiveWatchLogByLogIdWithCache(LiveWatchLog logId) {
+        if (logId == null) {
+            return null;
+        }
+        
+        // 先从缓存中获取
+        String cacheKey = String.format(LIVE_WATCH_LOG_CACHE_KEY, logId);
+        List<LiveWatchLog> cachedLog = redisCache.getCacheObject(cacheKey);
+        if (cachedLog != null) {
+            return cachedLog;
+        }
+        
+        // 缓存中没有,从数据库查询
+        List<LiveWatchLog> log = baseMapper.selectLiveWatchLogList(logId);
+        if (log != null) {
+            // 将查询结果缓存1小时
+            redisCache.setCacheObject(cacheKey, log, 1, TimeUnit.HOURS);
+        }
+        
+        return log;
+    }
 }

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

@@ -11,15 +11,11 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
-import com.fs.course.domain.FsCourseLink;
-import com.fs.course.service.impl.FsUserCourseVideoServiceImpl;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
-import com.fs.his.service.IFsUserService;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.domain.Live;
-import com.fs.live.domain.LiveVideo;
 import com.fs.live.domain.LiveWatchLog;
 import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.mapper.*;
@@ -220,6 +216,11 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         redisCache.deleteObject(cacheKey);
     }
 
+    @Override
+    public List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser) {
+        return baseMapper.selectLiveWatchUserList(queryUser);
+    }
+
     /**
      * 批量删除直播间观看用户
      *
@@ -854,9 +855,13 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     @Override
     @Async
     public void qwTagMarkByLiveWatchLog(Long liveId) {
+        log.info("处理直播间打标签: liveId={}", liveId);
         //查询直播间的标签配置
         List<LiveTagItemVO> liveTagConfig = liveTagConfigMapper.getLiveTagListByliveId(liveId);
-
+        log.info("处理直播间打标签: liveTagConfig={}", liveTagConfig);
+        if(null == liveTagConfig || liveTagConfig.isEmpty()){
+            return;
+        }
         /**
          * 8	回放已下单
          * 7	直播已下单
@@ -875,7 +880,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 ));
         //查询直播间的看课记录
         List<LiveWatchLog> liveWatchLogs = liveWatchLogMapper.selectLiveWatchLogByLiveId(liveId);
-
+        log.info("处理直播间打标签: liveWatchLogs={}", liveWatchLogs);
+        if(null == liveWatchLogs || liveWatchLogs.isEmpty()){
+            return;
+        }
         //根据配置给每位用户打上标签
         List<HandleUserTagVO> handleUserTagVOS = new ArrayList<>();
         liveWatchLogs.forEach(liveLog -> {
@@ -921,6 +929,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 default:
                     break;
             }
+            addItem.setTags(tags);
             if (null != liveLog.getLiveBuy() && liveLog.getLiveBuy().equals(1)) {
                 liveTagItemVO = liveTagMp.get(7);
                 if (null != liveTagItemVO) {
@@ -929,6 +938,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             }
             handleUserTagVOS.add(addItem);
         });
+        log.info("处理直播间打标签最终打标签:{}",handleUserTagVOS);
         handleUserTags2Qw(handleUserTagVOS);
     }
 

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

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

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

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

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

@@ -49,7 +49,7 @@ public class LiveUserDetailExportVO {
     private String salesName;
 
     /** 是否完课 */
-    @Excel(name = "是否完课")
+//    @Excel(name = "是否完课")
     private String isCompleted;
 
 }

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

@@ -19,6 +19,10 @@ public class MergedOrderExportVO implements Serializable
 {
     private static final long serialVersionUID = 1L;
 
+    /** 订单类型 */
+    @Excel(name = "订单类型")
+    private String orderTypeName;
+
     /** 订单号 */
     @Excel(name = "订单号")
     private String orderCode;
@@ -48,13 +52,25 @@ public class MergedOrderExportVO implements Serializable
     private Integer totalNum;
 
     /** 产品价格 */
-    @Excel(name = "产品价格")
+//    @Excel(name = "产品价格")
     private BigDecimal price;
 
     /** 成本价 */
-    @Excel(name = "成本价")
+    @Excel(name = "成本价")
     private BigDecimal cost;
 
+    /** 商品金额 */
+    @Excel(name = "商品金额")
+    private BigDecimal totalPrice;
+
+    /** 应付金额 */
+    @Excel(name = "应付金额")
+    private BigDecimal payPrice;
+
+    /** 实付金额 */
+    @Excel(name = "实付金额")
+    private BigDecimal payMoney;
+
     /** 结算价 */
     @Excel(name = "结算价")
     private BigDecimal FPrice;
@@ -137,18 +153,11 @@ public class MergedOrderExportVO implements Serializable
     /** 银行交易流水号 */
     @Excel(name = "银行交易流水号")
     private String bankTransactionId;
+    /** 汇付商户订单号 */
+    @Excel(name = "汇付商户订单号")
+    private String hfshh;
 
-    /** 商品金额 */
-    @Excel(name = "商品金额")
-    private BigDecimal totalPrice;
 
-    /** 应付金额 */
-    @Excel(name = "应付金额")
-    private BigDecimal payPrice;
-
-    /** 实付金额 */
-    @Excel(name = "实付金额")
-    private BigDecimal payMoney;
 
 
 }

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

@@ -109,6 +109,8 @@ public class MergedOrderVO implements Serializable
 
     /** 订单类型:1-销售订单,2-商城订单,3-直播订单 */
     private Integer orderType;
+//    汇付商户订单号
+    private String hfshh;
 
     /** 订单类型名称 */
     private String orderTypeName;

+ 5 - 0
fs-service/src/main/java/com/fs/qw/vo/QwExternalContactComplaintVO.java

@@ -35,6 +35,11 @@ public class QwExternalContactComplaintVO extends BaseEntity {
     */
     private String nickName;
 
+    /**
+     * 销售公司
+     */
+    private  String company;
+
 
     /**
     * 客户小程序id

+ 7 - 1
fs-service/src/main/resources/mapper/course/FsUserCourseComplaintRecordMapper.xml

@@ -36,8 +36,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ct.complaint_type_name,
         uc.course_name,
         ucv.title,
-        fs_user.nick_name
+        fs_user.nick_name,
 --         if(ec.comment_status = 1,'已拉黑','正常') as status
+        cu.user_name userName,
+        cu.nick_name saleNickName,
+        c.company_name as company
         FROM
         fs_user_course_complaint_record cr
         LEFT JOIN fs_user_course_complaint_type ct ON ct.complaint_type_id = cr.complaint_type_id
@@ -45,6 +48,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         LEFT JOIN fs_user_course_video ucv ON ucv.video_id = cr.video_id
         LEFT JOIN fs_user ON fs_user.user_id = cr.user_id
 --         left join qw_external_contact ec on cr.user_id = ec.fs_user_id
+        left join  fs_user_company_user fucu on fs_user.user_id = fucu.user_id
+        left join  company_user cu on cu.user_id = fucu.company_user_id
+        left join company c on c.company_id = cu.company_id
         <where>
             <if test="userId != null">
                 and cr.user_id like concat('%', #{userId}, '%')

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

@@ -115,6 +115,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </if>
         order by las.create_time desc
     </select>
+    <select id="selectLiveAfterSalesVoListExport" parameterType="com.fs.live.vo.LiveAfterSalesVo" resultType="com.fs.live.vo.LiveAfterSalesVo">
+        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,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
+        left join live_order lo on lo.order_id = las.order_id
+        left join company_user cu on cu.user_id = las.company_user_id
+        left join company c on c.company_id = cu.company_id
+        left join live_order_payment lop on lop.business_id = lo.order_id and lop.status in (1,-1)
+        <if test="productName != null and productName != ''">
+        left join live_order_item loi on loi.order_id = lo.order_id
+        </if>
+
+        where 1=1 and las.status =4 and lop.bank_transaction_id is not null
+            <if test="hfOrderCode != null and hfOrderCode != ''"> and lop.pay_code = #{hfOrderCode}</if>
+            <if test="liveId != null and liveId != ''"> and las.live_id = #{liveId}</if>
+            <if test="companyUserNickName != null and companyUserNickName != ''"> and cu.nick_name like concat(#{companyUserNickName},'%')</if>
+            <if test="storeId != null and storeId != ''"> and las.store_id = #{storeId}</if>
+            <if test="orderCode != null and orderCode != ''"> and lo.order_code = #{orderCode}</if>
+            <if test="productNameQuery != null and productNameQuery != ''"> and JSON_UNQUOTE(JSON_EXTRACT(loi.json_info, '$.productName')) like concat('%', #{productNameQuery}, '%')</if>
+            <if test="refundAmount != null "> and las.refund_amount = #{refundAmount}</if>
+            <if test="refundType != null "> and las.refund_type = #{refundType}</if>
+            <if test="status != null "> and las.status = #{status}</if>
+            <if test="salesStatus != null "> and las.sales_status = #{salesStatus}</if>
+            <if test="orderStatus != null "> and las.order_status = #{orderStatus}</if>
+            <if test="reasons != null  and reasons != ''"> and las.reasons = #{reasons}</if>
+            <if test="explains != null  and explains != ''"> and las.explains = #{explains}</if>
+            <if test="explainImg != null  and explainImg != ''"> and las.explain_img = #{explainImg}</if>
+            <if test="deliveryCode != null  and deliveryCode != ''"> and las.delivery_code = #{deliveryCode}</if>
+            <if test="deliverySn != null  and deliverySn != ''"> and las.delivery_sn = #{deliverySn}</if>
+            <if test="deliveryName != null  and deliveryName != ''"> and las.delivery_name like concat('%', #{deliveryName}, '%')</if>
+            <if test="status != null "> and las.status = #{status}</if>
+            <if test="salesStatus != null "> and las.sales_status = #{salesStatus}</if>
+            <if test="orderStatus != null "> and las.order_status = #{orderStatus}</if>
+            <if test="deliveryStatus != null and deliveryStatus!= ''"> and las.order_status = #{deliveryStatus}</if>
+            <if test="isDel != null  and isDel != ''"> and las.is_del = #{isDel}</if>
+            <if test="userId != null "> and las.user_id = #{userId}</if>
+            <if test="consignee != null  and consignee != ''"> and las.consignee = #{consignee}</if>
+            <if test="phoneNumber != null  and phoneNumber != ''"> and las.phone_number = #{phoneNumber}</if>
+            <if test="address != null  and address != ''"> and las.address = #{address}</if>
+            <if test="companyId != null "> and las.company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and las.company_user_id = #{companyUserId}</if>
+            <if test="deptId != null "> and cu.dept_id = #{deptId}</if>
+            <if test="userPhone != null "> and lo.user_phone like concat(#{userPhone},'%')</if>
+
+        <if test="productName != null and productName != ''">
+        group by las.id
+        </if>
+        order by las.create_time desc
+    </select>
 
     <select id="selectLiveAfterSalesById" parameterType="Long" resultMap="LiveAfterSalesResult">
         <include refid="selectLiveAfterSalesVo"/>

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

@@ -89,6 +89,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         LIMIT 1
     </select>
 
+    <!-- 查询用户在某直播间最近一次完课记录(不限制日期) -->
+    <select id="selectLatestByUserAndLiveId" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC, id DESC
+        LIMIT 1
+    </select>
+
     <!-- 查询用户未领取的完课记录列表 -->
     <select id="selectUnreceivedByUser" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record
@@ -101,14 +110,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <!-- 查询用户的完课积分领取记录列表 -->
     <select id="selectRecordsByUser" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record
-        WHERE user_id = #{userId}
+        WHERE user_id = #{userId} and receive_status = 1
         ORDER BY current_completion_date DESC
     </select>
 
     <!-- 根据ID查询 -->
     <select id="selectById" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record
-        WHERE id = #{id}
+        WHERE id = #{id} for update
     </select>
 
 </mapper>

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

@@ -414,7 +414,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             COALESCE(video_duration.total_duration, 0) AS videoDuration,
             COUNT(DISTINCT lwu.user_id) AS totalViewers,
             COUNT(DISTINCT CASE
-                WHEN COALESCE(user_duration.total_duration, 0) >= 1800
+                WHEN COALESCE(user_duration.total_duration, 0) >= 1200
                 THEN lwu.user_id
             END) AS totalCompletedCourses,
             CASE
@@ -425,30 +425,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                     END) * 100.0 / COUNT(DISTINCT lwu.user_id), 2)
                 ELSE 0
             END AS totalCompletionRate,
-            COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END) AS liveViewers,
+            COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 and lwu.online_seconds > 0 THEN lwu.user_id END) AS liveViewers,
             COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) AS liveOver20Minutes,
             COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) AS liveOver30Minutes,
             CASE
                 WHEN COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END) > 0 THEN
-                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END), 2)
+                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 and lwu.online_seconds > 0  THEN lwu.user_id END), 2)
                 ELSE 0
             END AS liveCompletionRate20,
             CASE
                 WHEN COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END) > 0 THEN
-                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END), 2)
+                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 and lwu.online_seconds > 0  THEN lwu.user_id END), 2)
                 ELSE 0
             END AS liveCompletionRate30,
-            COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END) AS playbackViewers,
+            COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 and lwu.online_seconds > 0 THEN lwu.user_id END) AS playbackViewers,
             COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) AS playbackOver20Minutes,
             COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) AS playbackOver30Minutes,
             CASE
                 WHEN COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END) > 0 THEN
-                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END), 2)
+                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1200 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 and lwu.online_seconds > 0  THEN lwu.user_id END), 2)
                 ELSE 0
             END AS playbackCompletionRate20,
             CASE
                 WHEN COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END) > 0 THEN
-                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END), 2)
+                    ROUND(COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 AND lwu.online_seconds >= 1800 THEN lwu.user_id END) * 100.0 / COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 and lwu.online_seconds > 0  THEN lwu.user_id END), 2)
                 ELSE 0
             END AS playbackCompletionRate30,
             COALESCE(ld.peak_concurrent_viewers, 0) AS livePeak,

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

@@ -27,6 +27,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="userId != null "> and user_id = #{userId}</if>
             <if test="createTime != null "> and create_time = #{createTime}</if>
         </where>
+
+        order by create_time desc
     </select>
 
     <select id="selectLiveUserRedRecordById" parameterType="Long" resultMap="LiveUserRedRecordResult">

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

@@ -245,4 +245,95 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replayBuy != null">and t1.replay_buy = #{replayBuy} </if>
         </where>
     </select>
+
+    <!-- 批量更新直播看课记录 -->
+    <update id="batchUpdateLiveWatchLog" parameterType="java.util.List">
+        UPDATE live_watch_log
+        <set>
+            <if test="list != null and list.size() > 0 and list[0].logType != null">
+                log_type = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.logType}
+                </foreach>
+                ELSE log_type
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].updateTime != null">
+                update_time = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.updateTime}
+                </foreach>
+                ELSE update_time
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].finishTime != null">
+                finish_time = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.finishTime}
+                </foreach>
+                ELSE finish_time
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].sopCreateTime != null">
+                sop_create_time = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.sopCreateTime}
+                </foreach>
+                ELSE sop_create_time
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].sendAppId != null">
+                send_app_id = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.sendAppId}
+                </foreach>
+                ELSE send_app_id
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].logSource != null">
+                log_source = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.logSource}
+                </foreach>
+                ELSE log_source
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].watchType != null">
+                watch_type = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.watchType}
+                </foreach>
+                ELSE watch_type
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].corpId != null">
+                corp_id = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.corpId}
+                </foreach>
+                ELSE corp_id
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].liveBuy != null">
+                live_buy = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.liveBuy}
+                </foreach>
+                ELSE live_buy
+                END,
+            </if>
+            <if test="list != null and list.size() > 0 and list[0].replayBuy != null">
+                replay_buy = CASE log_id
+                <foreach collection="list" item="item">
+                    WHEN #{item.logId} THEN #{item.replayBuy}
+                </foreach>
+                ELSE replay_buy
+                END
+            </if>
+        </set>
+        WHERE log_id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.logId}
+        </foreach>
+    </update>
 </mapper>

+ 2 - 1
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -619,10 +619,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
     <select id="selectFsUserCourseComplaintRecordByUserId" resultType="com.fs.qw.vo.QwExternalContactComplaintVO">
         select qec.id,qec.qw_user_id,qec.status,qec.user_id,qec.name,qec.avatar,qec.remark,qec.description,qec.fs_user_id,
-               qu.qw_user_name,cu.user_name ,cu.nick_name
+               qu.qw_user_name,cu.user_name ,cu.nick_name,c.company_name as company
         from qw_external_contact qec
                left join qw_user qu on qu.id=qec.qw_user_id
                left join company_user cu on cu.user_id=qu.company_user_id
+               left join company c on c.company_id=cu.company_id
         where fs_user_id = #{userId}
     </select>
 

+ 48 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedController.java

@@ -4,13 +4,24 @@ import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.app.facade.LiveFacadeService;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.live.domain.LiveUserRedRecord;
 import com.fs.live.param.RedPO;
+import com.fs.live.service.ILiveUserRedRecordService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
+@Slf4j
 @RestController
 @RequestMapping("/app/live/liveRed")
 public class LiveRedController extends AppBaseController {
@@ -18,6 +29,9 @@ public class LiveRedController extends AppBaseController {
     @Autowired
     private LiveFacadeService liveFacadeService;
 
+    @Autowired
+    private ILiveUserRedRecordService liveUserRedRecordService;
+
     /**
      * 领取红包
      */
@@ -28,4 +42,38 @@ public class LiveRedController extends AppBaseController {
         return liveFacadeService.redClaim(red);
     }
 
+    /**
+     * 查询用户自己领取直播间的红包记录
+     * @return 红包记录列表
+     */
+    @Login
+    @GetMapping("/list")
+    public R list() {
+        try {
+            String userId = getUserId();
+            if (userId == null || userId.isEmpty()) {
+                return R.error("用户未登录");
+            }
+            // 分页参数
+            String pageNum = ServletUtils.getParameter("pageNum");
+            String pageSize = ServletUtils.getParameter("pageSize");
+            int page = pageNum != null && !pageNum.isEmpty() ? Integer.parseInt(pageNum) : 1;
+            int size = pageSize != null && !pageSize.isEmpty() ? Integer.parseInt(pageSize) : 10;
+
+            // 构建查询条件
+            LiveUserRedRecord query = new LiveUserRedRecord();
+            query.setUserId(Long.parseLong(userId));
+
+            // 分页查询
+            PageHelper.startPage(page, size);
+            List<LiveUserRedRecord> list = liveUserRedRecordService.selectLiveUserRedRecordList(query);
+            PageInfo<LiveUserRedRecord> pageInfo = new PageInfo<>(list);
+
+            return R.ok().put("data", pageInfo);
+        } catch (Exception e) {
+            log.error("查询用户红包记录失败", e);
+            return R.error("查询失败:" + e.getMessage());
+        }
+    }
+
 }

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

@@ -233,7 +233,8 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
     }
 
     @Override
-    @DistributeLock(keyExpression = "#coupon.couponIssueId +'_'+#coupon.userId", scene = "coupon_claim", waitTime = 1000, errorMsg = "优惠券领取失败")
+    @DistributeLock(keyExpression = "(#coupon?.couponIssueId?:'default') + '_' + (#coupon?.userId?:'default')"
+, scene = "coupon_claim", waitTime = 1000, errorMsg = "优惠券领取失败")
     public R couponClaim(CouponPO coupon) {
         return iLiveCouponService.claimCoupon(coupon);
     }