wangxy před 1 týdnem
rodič
revize
16f94b84f6
30 změnil soubory, kde provedl 472 přidání a 195 odebrání
  1. 9 0
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  2. 15 1
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  3. 2 1
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  4. 10 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  5. 12 3
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  6. 1 0
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  7. 1 8
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  8. 37 14
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  9. 2 2
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  10. 9 5
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  11. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  12. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java
  13. 1 1
      fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  14. 7 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  15. 14 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchLogService.java
  16. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  17. 10 23
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  18. 10 15
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  19. 11 7
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  20. 94 75
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  21. 14 5
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  22. 10 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveVideoServiceImpl.java
  23. 66 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java
  24. 8 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  25. 1 1
      fs-service/src/main/java/com/fs/live/vo/LiveUserDetailExportVO.java
  26. 16 11
      fs-service/src/main/java/com/fs/live/vo/MergedOrderExportVO.java
  27. 2 0
      fs-service/src/main/java/com/fs/live/vo/MergedOrderVO.java
  28. 2 2
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  29. 7 7
      fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
  30. 91 0
      fs-service/src/main/resources/mapper/live/LiveWatchLogMapper.xml

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

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

@@ -262,7 +262,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()); // 快递公司,合并订单暂无此字段

+ 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());    }
 
     /**
      * 查询直播间详情数据(查询数据服务器处理方式)

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

@@ -270,6 +270,7 @@ public class OrderController extends BaseController
             // 公司和销售信息
             exportVO.setCompanyName(vo.getCompanyName());
             exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
+            exportVO.setHfshh(vo.getHfshh());
 
             // 套餐信息
             exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段

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

+ 37 - 14
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -169,7 +169,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 +219,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 +415,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 +633,7 @@ public class Task {
     }
 
     /**
-     * 定时扫描开启的直播间,检查是否到了打标签的时间
+     * 定时扫描开启的直播间,检查是否到了打标签的时间,然后把正在看直播的用户拆分为 直播用户和回放用户
      * 每10秒执行一次
      */
     @Scheduled(cron = "0/10 * * * * ?")
@@ -679,7 +688,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 +735,7 @@ public class Task {
                                 // 更新直播用户的在线时长
                                 liveUser.setOnlineSeconds(totalOnlineSeconds);
                                 liveUser.setUpdateTime(nowDate);
+                                liveUser.setOnline(1);
                                 updateLiveUsers.add(liveUser);
 
                                 // 2. 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始
@@ -732,7 +743,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 +753,7 @@ public class Task {
                                 replayUser.setCreateTime(nowDate);
                                 replayUser.setUpdateTime(nowDate);
                                 replayUsers.add(replayUser);
+                                redisCache.setCacheObject(entryTimeKey,now);
                             }
 
                             // 批量更新直播用户的在线时长
@@ -824,8 +836,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 +851,7 @@ public class Task {
                     }
 
                     // 处理每个在线用户
+                    List<LiveWatchLog> updateLog = new ArrayList<>();
                     for (LiveWatchUser user : onlineUsers) {
                         try {
                             Long userId = user.getUserId();
@@ -869,13 +881,25 @@ public class Task {
 
                             // 使用 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 +921,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 +963,7 @@ public class Task {
                 // 如果 logType 已经是 2(完课),不再更新
                 if (needUpdate) {
                     log.setLogType(newLogType);
-                    liveWatchLogService.updateLiveWatchLog(log);
+                    updateLog.add(log);
                 }
             }
         } catch (Exception e) {

+ 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

+ 9 - 5
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -128,7 +128,7 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserById(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -137,8 +137,14 @@ public class WebSocketServer {
             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);
@@ -275,7 +281,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("用户信息错误");
             }
@@ -1178,7 +1184,6 @@ public class WebSocketServer {
         try {
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
-            queryLog.setUserId(userId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(externalContactId);
             
@@ -1302,7 +1307,6 @@ public class WebSocketServer {
             // 查询 LiveWatchLog
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
-            queryLog.setUserId(userId);
             queryLog.setCompanyId(companyId);
             queryLog.setCompanyUserId(companyUserId);
             

+ 1 - 1
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 已退款 */

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

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

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

@@ -280,8 +280,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 +322,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 +426,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 +551,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 +636,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 +644,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 +893,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 +916,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());
             }
         }

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

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

+ 94 - 75
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);
+                    } 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();
         }
@@ -3661,7 +3681,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) {

+ 14 - 5
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -320,7 +320,7 @@ public class LiveServiceImpl implements ILiveService
     @Override
     public R subNotifyLive(LiveNotifyParam param) {
         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();
@@ -341,7 +341,6 @@ public class LiveServiceImpl implements ILiveService
         }
 
         notifyTask.setTouser(maOpenId);
-        notifyTask.setPage(String.valueOf(1));
 
         notifyTask.setCreateTime(LocalDateTime.now());
         // 状态等待执行
@@ -1348,15 +1347,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;
+    }
 }

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

@@ -220,6 +220,11 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         redisCache.deleteObject(cacheKey);
     }
 
+    @Override
+    public List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser) {
+        return baseMapper.selectLiveWatchUserList(queryUser);
+    }
+
     /**
      * 批量删除直播间观看用户
      *
@@ -854,9 +859,10 @@ 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);
         /**
          * 8	回放已下单
          * 7	直播已下单
@@ -921,6 +927,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) {

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

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

@@ -48,13 +48,25 @@ public class MergedOrderExportVO implements Serializable
     private Integer totalNum;
 
     /** 产品价格 */
-    @Excel(name = "产品价格")
+//    @Excel(name = "产品价格")
     private BigDecimal price;
 
     /** 成本价 */
     @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 +149,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;

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

@@ -101,14 +101,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,

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