2 Commits 6c565f9a85 ... 4679685f85

Autore SHA1 Messaggio Data
  yys 4679685f85 1、调整登录创建用户头像不对 4 giorni fa
  yys ba4a44a221 1、优化定时任务打印日志 4 giorni fa
32 ha cambiato i file con 1008 aggiunte e 254 eliminazioni
  1. 21 1
      fs-admin/src/main/java/com/fs/live/controller/LiveLotteryConfController.java
  2. 27 1
      fs-admin/src/main/java/com/fs/live/controller/LiveRedConfController.java
  3. 36 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  4. 21 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveLotteryConfController.java
  5. 27 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveRedConfController.java
  6. 37 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  7. 8 3
      fs-ipad-task/src/main/java/com/fs/app/task/SendAppMsg.java
  8. 8 0
      fs-live-app/src/main/java/com/fs/live/redis/LiveWsBroadcastSubscriber.java
  9. 40 11
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  10. 210 95
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  11. 122 68
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  12. 64 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  13. 22 0
      fs-service/src/main/java/com/fs/gtPush/service/impl/uniPush2ServiceImpl.java
  14. 1 0
      fs-service/src/main/java/com/fs/gtPush/service/uniPush2Service.java
  15. 1 0
      fs-service/src/main/java/com/fs/im/service/OpenIMService.java
  16. 38 0
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  17. 5 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  18. 12 0
      fs-service/src/main/java/com/fs/live/service/ILiveConsoleOpLogService.java
  19. 5 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  20. 81 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveConsoleOpLogServiceImpl.java
  21. 4 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java
  22. 0 13
      fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java
  23. 1 22
      fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
  24. 1 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveUserFirstEntryServiceImpl.java
  25. 7 9
      fs-service/src/main/java/com/fs/live/service/impl/LiveVideoServiceImpl.java
  26. 2 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchLogServiceImpl.java
  27. 19 17
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  28. 3 0
      fs-service/src/main/java/com/fs/live/vo/LiveConsoleOpLogRecordVo.java
  29. 42 0
      fs-service/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java
  30. 121 0
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  31. 18 0
      fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
  32. 4 4
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

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

@@ -8,9 +8,12 @@ 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.common.utils.StringUtils;
 import com.fs.live.domain.LiveLotteryConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.service.ILiveLotteryConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -29,6 +32,9 @@ public class LiveLotteryConfController extends BaseController
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
      * 查询直播抽奖配置列表
      */
@@ -90,7 +96,21 @@ public class LiveLotteryConfController extends BaseController
     @PutMapping
     public R edit(@RequestBody LiveLotteryConf liveLotteryConf)
     {
-        return liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
+        R result = liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
+        if ("1".equals(liveLotteryConf.getLotteryStatus())) {
+            LiveLotteryConf persisted = liveLotteryConfService.getById(liveLotteryConf.getLotteryId());
+            String bizName = persisted != null && StringUtils.isNotEmpty(persisted.getDesc())
+                    ? persisted.getDesc() : "抽奖 #" + liveLotteryConf.getLotteryId();
+            LiveConsoleOpLog sendOpLog = liveConsoleOpLogService.saveLotterySendLog(
+                    liveLotteryConf.getLiveId(),
+                    LiveConsoleOpLog.HANDLE_CONSOLE,
+                    liveLotteryConf.getLotteryId(),
+                    bizName);
+            if (sendOpLog != null && sendOpLog.getId() != null) {
+                result.put("opLogId", sendOpLog.getId());
+            }
+        }
+        return result;
     }
 
     /**

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

@@ -8,8 +8,11 @@ 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.common.utils.StringUtils;
 import com.fs.live.domain.LiveRedConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -28,6 +31,9 @@ public class LiveRedConfController extends BaseController
     @Autowired
     private ILiveRedConfService liveRedConfService;
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
      * 查询直播红包记录配置列表
      */
@@ -91,7 +97,27 @@ public class LiveRedConfController extends BaseController
     public R edit(@RequestBody LiveRedConf liveRedConf)
     {
         liveRedConfService.updateLiveRedConf(liveRedConf);
-        return R.ok(liveRedConf.getRedStatus().toString());
+        R result = R.ok(liveRedConf.getRedStatus().toString());
+        Long opLogId = resolveConsoleRedOpLogId(liveRedConf);
+        if (opLogId != null) {
+            result.put("opLogId", opLogId);
+        }
+        return result;
+    }
+
+    private Long resolveConsoleRedOpLogId(LiveRedConf conf) {
+        if (conf == null || conf.getRedStatus() == null || conf.getRedId() == null) {
+            return null;
+        }
+        LiveRedConf persisted = liveRedConfService.getById(conf.getRedId());
+        String bizName = persisted != null && StringUtils.isNotEmpty(persisted.getDesc())
+                ? persisted.getDesc() : "红包 #" + conf.getRedId();
+        LiveConsoleOpLog opLog = null;
+        if (conf.getRedStatus() == 1L) {
+            opLog = liveConsoleOpLogService.saveRedSendLog(
+                    conf.getLiveId(), LiveConsoleOpLog.HANDLE_CONSOLE, conf.getRedId(), bizName);
+        }
+        return opLog != null ? opLog.getId() : null;
     }
 
     /**

+ 36 - 0
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -116,6 +116,42 @@ public class RedisCache
         return JSON.parseObject(JSON.toJSONString(cached), clazz);
     }
 
+    /**
+     * 将 Redis 返回的 List 安全转换为目标元素类型(兼容 FastJson 反序列化为 JSONObject 的情况)
+     */
+    public static <T> List<T> convertCacheList(Object cached, Class<T> clazz)
+    {
+        if (cached == null || clazz == null)
+        {
+            return null;
+        }
+        if (!(cached instanceof List))
+        {
+            return JSON.parseArray(JSON.toJSONString(cached), clazz);
+        }
+        List<?> list = (List<?>) cached;
+        if (list.isEmpty())
+        {
+            return new ArrayList<>();
+        }
+        for (Object item : list)
+        {
+            if (item != null && !clazz.isInstance(item))
+            {
+                return JSON.parseArray(JSON.toJSONString(list), clazz);
+            }
+        }
+        List<T> result = new ArrayList<>(list.size());
+        for (Object item : list)
+        {
+            if (item != null)
+            {
+                result.add(clazz.cast(item));
+            }
+        }
+        return result;
+    }
+
     /**
      * 删除单个对象
      *

+ 21 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveLotteryConfController.java

@@ -8,9 +8,12 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveLotteryConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.service.ILiveLotteryConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -29,6 +32,9 @@ public class LiveLotteryConfController extends BaseController
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
      * 查询直播抽奖配置列表
      */
@@ -91,7 +97,21 @@ public class LiveLotteryConfController extends BaseController
     @PutMapping
     public R edit(@RequestBody LiveLotteryConf liveLotteryConf)
     {
-        return liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
+        R result = liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
+        if ("1".equals(liveLotteryConf.getLotteryStatus())) {
+            LiveLotteryConf persisted = liveLotteryConfService.getById(liveLotteryConf.getLotteryId());
+            String bizName = persisted != null && StringUtils.isNotEmpty(persisted.getDesc())
+                    ? persisted.getDesc() : "抽奖 #" + liveLotteryConf.getLotteryId();
+            LiveConsoleOpLog sendOpLog = liveConsoleOpLogService.saveLotterySendLog(
+                    liveLotteryConf.getLiveId(),
+                    LiveConsoleOpLog.HANDLE_CONSOLE,
+                    liveLotteryConf.getLotteryId(),
+                    bizName);
+            if (sendOpLog != null && sendOpLog.getId() != null) {
+                result.put("opLogId", sendOpLog.getId());
+            }
+        }
+        return result;
     }
 
     /**

+ 27 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveRedConfController.java

@@ -8,8 +8,11 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveRedConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -28,6 +31,9 @@ public class LiveRedConfController extends BaseController
     @Autowired
     private ILiveRedConfService liveRedConfService;
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
      * 查询直播红包记录配置列表
      */
@@ -91,7 +97,27 @@ public class LiveRedConfController extends BaseController
     public R edit(@RequestBody LiveRedConf liveRedConf)
     {
         liveRedConfService.updateLiveRedConf(liveRedConf);
-        return R.ok(liveRedConf.getRedStatus().toString());
+        R result = R.ok(liveRedConf.getRedStatus().toString());
+        Long opLogId = resolveConsoleRedOpLogId(liveRedConf);
+        if (opLogId != null) {
+            result.put("opLogId", opLogId);
+        }
+        return result;
+    }
+
+    private Long resolveConsoleRedOpLogId(LiveRedConf conf) {
+        if (conf == null || conf.getRedStatus() == null || conf.getRedId() == null) {
+            return null;
+        }
+        LiveRedConf persisted = liveRedConfService.getById(conf.getRedId());
+        String bizName = persisted != null && StringUtils.isNotEmpty(persisted.getDesc())
+                ? persisted.getDesc() : "红包 #" + conf.getRedId();
+        LiveConsoleOpLog opLog = null;
+        if (conf.getRedStatus() == 1L) {
+            opLog = liveConsoleOpLogService.saveRedSendLog(
+                    conf.getLiveId(), LiveConsoleOpLog.HANDLE_CONSOLE, conf.getRedId(), bizName);
+        }
+        return opLog != null ? opLog.getId() : null;
     }
 
     /**

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

@@ -803,6 +803,11 @@ public class IpadSendServer {
                 case "24":
                     sendAppShortLink(vo, content, miniMap);
                     break;
+                case "25":
+                    // APP直播卡片走 OpenIM WebSocket,企微侧跳过
+                    content.setSendStatus(0);
+                    content.setSendRemarks("APP待发送");
+                    break;
                 case "99":
                     // 群发
                     sendTxtAtMsg(vo);
@@ -1317,6 +1322,7 @@ public class IpadSendServer {
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "模板未选消息类型,不发送");
             return false;
         }
+        if (setting.getVideoId() != null) {
         Integer cacheValue = redisCache.getCacheObject("sopCourse:video:isPause:" + setting.getVideoId());
         int isPause = (cacheValue != null) ? cacheValue : 0;
         log.info("SOP_LOG_ID:{},判断课程({})当前状态:{}", qwSopLogs.getId(), setting.getVideoId(), isPause);
@@ -1325,6 +1331,37 @@ public class IpadSendServer {
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "课程暂停,AI不发送");
             return false;
         }
+        }
+
+        boolean hasAppLiveCard = setting.getSetting() != null
+                && setting.getSetting().stream().anyMatch(s -> "25".equals(s.getContentType()));
+        if (hasAppLiveCard) {
+            Long queryLiveId = setting.getLiveId();
+            if (queryLiveId == null && setting.getSetting() != null) {
+                for (QwSopCourseFinishTempSetting.Setting a : setting.getSetting()) {
+                    if (StringUtils.isNotBlank(a.getLiveId())) {
+                        queryLiveId = Long.valueOf(a.getLiveId());
+                        break;
+                    }
+                }
+            }
+            if (queryLiveId != null) {
+                LiveWatchLog liveWatchLog = liveWatchLogMapper.selectOneLogByLiveIdAndQwUserIdAndExternalId(
+                        queryLiveId, String.valueOf(qwUser.getId()), qwSopLogs.getExternalId());
+                if (liveWatchLog == null) {
+                    log.warn("SOP_LOG_ID:{}, APP直播卡片无观看记录,不发送", qwSopLogs.getId());
+                    qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "无观看记录,不发送");
+                    return false;
+                }
+                Integer courseType = setting.getCourseType();
+                if (courseType != null && courseType != 0
+                        && !QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
+                    qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "看课状态未满足,不发送");
+                    return false;
+                }
+            }
+            return true;
+        }
 
         if (qwSopLogs.getSendType() != 12 && noSop) {
             // 客户的信息

+ 8 - 3
fs-ipad-task/src/main/java/com/fs/app/task/SendAppMsg.java

@@ -136,7 +136,7 @@ public class SendAppMsg {
         if (qwSopLogList.isEmpty()) {
             return;
         }
-        List<String> typeList = Arrays.asList("9", "15", "16");
+        List<String> typeList = Arrays.asList("9", "15", "16", "25");
         // 获取企微用户
         QwUser user = qwUserMapper.selectById(qwUser.getId());
         long end1 = System.currentTimeMillis();
@@ -171,6 +171,7 @@ public class SendAppMsg {
                 boolean txtSendStatus = true;
                 boolean mp3SendStatus = true;
                 boolean courseSendStatus = true;
+                boolean liveCardSendStatus = true;
 //                for (QwSopCourseFinishTempSetting.Setting content : allContent) {
 //                    String contentType = content.getContentType();
 //                    if (!typeList.contains(contentType)) {
@@ -226,6 +227,10 @@ public class SendAppMsg {
                         if (!voiceList.isEmpty()) {
                             mp3SendStatus = asyncSopTestService.asyncSendMsgBySopAppMP3NormalIM(voiceList, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
                         }
+                        List<QwSopCourseFinishTempSetting.Setting> liveCardList = allContent.stream().filter(e -> "25".equals(e.getContentType())).collect(Collectors.toList());
+                        if (!liveCardList.isEmpty()) {
+                            liveCardSendStatus = asyncSopTestService.asyncSendMsgBySopAppLiveCardNormalIM(liveCardList, qwSopLogs.getCorpId(), user.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
+                        }
                         // 发送成功后记录次数
                         // 发送成功后记录次数(只记录真正发送的 content)
 //                        for (QwSopCourseFinishTempSetting.Setting content : allContent) {
@@ -255,10 +260,10 @@ public class SendAppMsg {
 //                qwSopLogs.setSend(true);
                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                 QwSopLogs updateQwSop = new QwSopLogs();
-                if (courseSendStatus&&txtSendStatus&&mp3SendStatus){
+                if (courseSendStatus&&txtSendStatus&&mp3SendStatus&&liveCardSendStatus){
                     updateQwSop.setAppSendRemark("APP全部发送成功");
                     updateQwSop.setAppSendStatus(1);
-                }else if(!courseSendStatus&&!txtSendStatus&&!mp3SendStatus){
+                }else if(!courseSendStatus&&!txtSendStatus&&!mp3SendStatus&&!liveCardSendStatus){
                     updateQwSop.setAppSendRemark("APP全部发送失败");
                     updateQwSop.setAppSendStatus(2);
                 }else {

+ 8 - 0
fs-live-app/src/main/java/com/fs/live/redis/LiveWsBroadcastSubscriber.java

@@ -52,6 +52,14 @@ public class LiveWsBroadcastSubscriber implements MessageListener {
             if (payload.containsKey("data")) {
                 sendMsgVo.setData(payload.getString("data"));
             }
+            if (payload.containsKey("opLogId") && StringUtils.isNotEmpty(sendMsgVo.getData())) {
+                try {
+                    JSONObject dataObj = JSONObject.parseObject(sendMsgVo.getData());
+                    dataObj.put("opLogId", payload.getLong("opLogId"));
+                    sendMsgVo.setData(dataObj.toJSONString());
+                } catch (Exception ignored) {
+                }
+            }
             WebSocketServer webSocketServer = SpringUtils.getBean(WebSocketServer.class);
             webSocketServer.broadcastLiveCmd(sendMsgVo);
             log.info("[LiveWsBroadcast] 已推送指令, liveId={}, cmd={}, status={}",

+ 40 - 11
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -7,10 +7,12 @@ import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.domain.LiveConsoleOpLog;
+import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.service.ILiveCompletionCouponService;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.LiveCompletionCouponInfoVO;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.websocket.bean.SendMsgVo;
@@ -20,8 +22,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
+
+import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 
 /**
  * 直播完课奖励定时任务(积分 / 优惠券)
@@ -30,11 +35,12 @@ import java.util.Map;
 @Component
 public class LiveCompletionPointsTask {
 
-    private static final String WATCH_DURATION_HASH_PREFIX = "live:watch:duration:hash:";
-
     @Autowired
     private RedisCache redisCache;
 
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
     @Autowired
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
@@ -170,20 +176,29 @@ public class LiveCompletionPointsTask {
         for (Live live : activeLives) {
             try {
                 Long liveId = live.getLiveId();
-                String hashKey = WATCH_DURATION_HASH_PREFIX + liveId;
-                Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
-
-                if (userDurations == null || userDurations.isEmpty()) {
+                LiveWatchUser queryUser = new LiveWatchUser();
+                queryUser.setLiveId(liveId);
+                List<LiveWatchUser> watchUsers = liveWatchUserService.selectAllWatchUser(queryUser);
+                if (watchUsers == null || watchUsers.isEmpty()) {
                     continue;
                 }
 
-                for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                Set<Long> userIds = new HashSet<>();
+                for (LiveWatchUser watchUser : watchUsers) {
+                    if (watchUser.getUserId() != null) {
+                        userIds.add(watchUser.getUserId());
+                    }
+                }
+
+                for (Long userId : userIds) {
                     try {
-                        Long userId = Long.parseLong(entry.getKey().toString());
-                        Long duration = Long.parseLong(entry.getValue().toString());
+                        long duration = resolveEffectiveWatchDuration(liveId, userId);
+                        if (duration <= 0) {
+                            continue;
+                        }
                         handler.handle(liveId, userId, duration);
                     } catch (Exception e) {
-                        log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
+                        log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, userId, e);
                     }
                 }
             } catch (Exception e) {
@@ -192,6 +207,20 @@ public class LiveCompletionPointsTask {
         }
     }
 
+    /**
+     * 与 scanLiveWatchUserStatus 一致:DB 累计时长 + 当前在线会话未落库部分
+     */
+    private long resolveEffectiveWatchDuration(Long liveId, Long userId) {
+        Long total = liveWatchUserService.getTotalWatchDuration(liveId, userId);
+        long duration = total != null ? total : 0L;
+        String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+        Long entryTime = redisCache.getCacheObject(entryTimeKey);
+        if (entryTime != null) {
+            duration += (System.currentTimeMillis() - entryTime) / 1000;
+        }
+        return duration;
+    }
+
     @FunctionalInterface
     private interface CompletionHandler {
         void handle(Long liveId, Long userId, Long duration);

+ 210 - 95
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -47,6 +47,17 @@ import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 public class Task {
 
     private static final Logger log = LoggerFactory.getLogger(Task.class);
+    private static final String LOG_PREFIX = "[LiveScheduled]";
+
+
+    private void logTaskFinish(String taskName,  String summary) {
+        log.info("{} {} 完成, {}", LOG_PREFIX, taskName, summary);
+    }
+
+    private void logTaskError(String taskName, Exception e) {
+        log.error("{} {} 异常: {}", LOG_PREFIX, taskName, e.getMessage(), e);
+    }
+
     private final ILiveService liveService;
 
     private final ILiveDataService liveDataService;
@@ -93,9 +104,12 @@ public class Task {
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveStatusByTime", scene = "task")
     public void updateLiveStatusByTime() {
+        try {
         List<Live> list = liveService.selectNoEndLiveList();
-        if (list.isEmpty())
+        if (list.isEmpty()) {
+            logTaskFinish("updateLiveStatusByTime", "无未结束直播间,跳过");
             return;
+        }
         List<Long> liveIdLists = list.stream().map(Live::getLiveId).collect(Collectors.toList());
         List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectLiveAutoTaskByLiveIds(liveIdLists);
         List<Live> liveList = new ArrayList<>();
@@ -185,14 +199,7 @@ public class Task {
                 // 将开启的直播间信息写入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();
-                    }
+                    Long videoDuration = sumLiveVideoDurationSeconds(live.getLiveId());
 
                     // 如果视频时长大于0,将直播间信息存入Redis
                     if (videoDuration > 0 && live.getStartTime() != null) {
@@ -245,11 +252,23 @@ public class Task {
             liveService.asyncToCache();
         }
 
+        logTaskFinish("updateLiveStatusByTime",
+                String.format("扫描=%d, 状态更新=%d, 开播=%d, 结束=%d, 加载自动任务缓存=%d",
+                        list.size(), liveList.size(), startLiveList.size(), endLiveList.size(),
+                        startLiveList.stream().mapToInt(live -> (int) liveAutoTasks.stream()
+                                .filter(t -> t.getLiveId().equals(live.getLiveId())).count()).sum()));
+        } catch (Exception e) {
+            logTaskError("updateLiveStatusByTime", e);
+        }
     }
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
     public void liveLotteryTask() {
-        long currentTime = Instant.now().toEpochMilli(); // 当前时间戳(毫秒)
+        long startMs = System.currentTimeMillis();
+        int lotteryProcessed = 0;
+        int redProcessed = 0;
+        try {
+        long currentTime = Instant.now().toEpochMilli();
         String lotteryKey = "live:lottery_task:*";
         Set<String> allLiveKeys = redisCache.redisTemplate.keys(lotteryKey);
         if (allLiveKeys != null && !allLiveKeys.isEmpty()) {
@@ -258,7 +277,9 @@ public class Task {
                 if (range == null || range.isEmpty()) {
                     continue;
                 }
+                log.info("{} liveLotteryTask 处理抽奖: liveKey={}, 待执行数={}", LOG_PREFIX, liveKey, range.size());
                 processLotteryTask(range);
+                lotteryProcessed += range.size();
                 redisCache.redisTemplate.opsForZSet()
                         .removeRangeByScore(liveKey, 0, currentTime);
             }
@@ -266,34 +287,34 @@ public class Task {
 
         String redKey = "live:red_task:*";
         allLiveKeys = redisCache.redisTemplate.keys(redKey);
-        if (allLiveKeys == null || allLiveKeys.isEmpty()) {
-            return;
-        }
-        for (String liveKey : allLiveKeys) {
-            Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
-            if (range == null || range.isEmpty()) {
-                continue;
-            }
-
-            List<LiveConsoleOpLog> opLogs = updateRedStatus(range);
-            redisCache.redisTemplate.opsForZSet()
-                    .removeRangeByScore(liveKey, 0, currentTime);
-            try {
-                Long liveId = Long.parseLong(liveKey.substring("live:red_task:".length()));
-                // 广播红包关闭消息
-                SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(liveId);
-                sendMsgVo.setCmd("red");
-                sendMsgVo.setStatus(-1);
-                liveService.asyncToCacheLiveConfig(liveId);
-                if (!opLogs.isEmpty()) {
-                    WebSocketServer.attachOpLog(sendMsgVo, opLogs.get(opLogs.size() - 1));
+        if (allLiveKeys != null && !allLiveKeys.isEmpty()) {
+            for (String liveKey : allLiveKeys) {
+                Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
+                if (range == null || range.isEmpty()) {
+                    continue;
+                }
+                log.info("{} liveLotteryTask 处理红包: liveKey={}, 待执行数={}", LOG_PREFIX, liveKey, range.size());
+                updateRedStatus(range);
+                redProcessed += range.size();
+                redisCache.redisTemplate.opsForZSet()
+                        .removeRangeByScore(liveKey, 0, currentTime);
+                try {
+                    Long liveId = Long.parseLong(liveKey.substring("live:red_task:".length()));
+                    SendMsgVo sendMsgVo = new SendMsgVo();
+                    sendMsgVo.setLiveId(liveId);
+                    sendMsgVo.setCmd("red");
+                    sendMsgVo.setStatus(-1);
+                    liveService.asyncToCacheLiveConfig(liveId);
+                    webSocketServer.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                    log.info("{} liveLotteryTask 红包关闭广播完成: liveId={}, redIds={}", LOG_PREFIX, liveId, range);
+                } catch (Exception e) {
+                    log.error("{} liveLotteryTask 红包关闭广播失败: liveKey={}, error={}", LOG_PREFIX, liveKey, e.getMessage(), e);
                 }
-                webSocketServer.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
-            } catch (Exception e) {
-                log.error("更新红包状态异常", e);
             }
         }
+        } catch (Exception e) {
+            logTaskError("liveLotteryTask", e);
+        }
     }
 
     private List<LiveConsoleOpLog> updateRedStatus(Set<String> range) {
@@ -302,13 +323,21 @@ public class Task {
 
     private void processLotteryTask(Set<String> range) {
         List<LiveLotteryConfVo> liveLotteries = liveLotteryConfService.selectVoListByLotteryIds(range);
-        if(liveLotteries.isEmpty()) return;
+        if (liveLotteries.isEmpty()) {
+            log.warn("{} processLotteryTask 未查到抽奖配置: lotteryIds={}", LOG_PREFIX, range);
+            return;
+        }
         Date now = new Date();
         for (LiveLotteryConfVo liveLottery : liveLotteries) {
+            log.info("{} processLotteryTask 开始开奖: lotteryId={}, liveId={}",
+                    LOG_PREFIX, liveLottery.getLotteryId(), liveLottery.getLiveId());
             // 查询抽奖数量
             List<LiveLotteryProductListVo> products = liveLottery.getProducts();
             Integer totalLots = products.stream().mapToInt(liveLotteryProductListVo -> Math.toIntExact(liveLotteryProductListVo.getTotalLots())).sum();
-            if(totalLots <= 0) continue;
+            if (totalLots <= 0) {
+                log.warn("{} processLotteryTask 奖品数量为0,跳过: lotteryId={}", LOG_PREFIX, liveLottery.getLotteryId());
+                continue;
+            }
             // 先将参与记录插入数据库
             String hashKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveLottery.getLiveId(), liveLottery.getLotteryId());
             Map<Object, Object> hashEntries = redisCache.hashEntries(hashKey);
@@ -322,7 +351,11 @@ public class Task {
 
             // 查询在线用户 并且参与了抽奖的用户
             List<LiveWatchUser> liveWatchUsers = liveWatchUserService.selectLiveWatchAndRegisterUser(liveLottery.getLiveId(),liveLottery.getLotteryId());
-            if(liveWatchUsers.isEmpty()) continue;
+            if (liveWatchUsers.isEmpty()) {
+                log.warn("{} processLotteryTask 无在线参与用户,跳过: lotteryId={}, liveId={}",
+                        LOG_PREFIX, liveLottery.getLotteryId(), liveLottery.getLiveId());
+                continue;
+            }
             LiveLotteryRegistration liveLotteryRegistration;
             // 收集中奖信息
             List<LotteryVo> lotteryVos = new ArrayList<>();
@@ -370,63 +403,67 @@ public class Task {
             sendMsgVo.setLiveId(liveLottery.getLiveId());
             sendMsgVo.setCmd("LotteryDetail");
             sendMsgVo.setData(JSON.toJSONString(lotteryVos));
-            WebSocketServer.attachOpLog(sendMsgVo, liveConsoleOpLogService.saveLotterySettleLog(
-                    liveLottery.getLiveId(),
-                    LiveConsoleOpLog.HANDLE_AUTO,
-                    liveLottery.getLotteryId(),
-                    resolveLotteryBizName(liveLottery.getLotteryId(), liveLottery.getDesc())
-            ));
             webSocketServer.broadcastMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
 
             liveService.asyncToCacheLiveConfig(liveLottery.getLiveId());
             // 删除缓存 同步抽奖记录
             redisCache.deleteObject(hashKey);
+            log.info("{} processLotteryTask 开奖完成: lotteryId={}, liveId={}, 中奖人数={}",
+                    LOG_PREFIX, liveLottery.getLotteryId(), liveLottery.getLiveId(), lotteryVos.size());
         }
 
         List<Long> collect = liveLotteries.stream().map(LiveLotteryConfVo::getLotteryId).collect(Collectors.toList());
         liveLotteryConfService.finishStatusByLotteryIds(collect);
+        log.info("{} processLotteryTask 更新抽奖状态完成: lotteryIds={}", LOG_PREFIX, collect);
     }
 
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveAutoTask", scene = "task")
     public void liveAutoTask() {
-        long currentTime = Instant.now().toEpochMilli(); // 当前时间戳(毫秒)
-        log.info("定时任务执行 - 当前时间戳: {}, 当前时间: {}", currentTime, new Date(currentTime));
+        long startMs = System.currentTimeMillis();
+        int totalProcessed = 0;
+        try {
+        long currentTime = Instant.now().toEpochMilli();
 
         Set<String> allLiveKeys = redisCache.redisTemplate.keys("live:auto_task:*");
         if (allLiveKeys == null || allLiveKeys.isEmpty()) {
-            return; // 没有数据,直接返回
+            return;
         }
-        // 2. 遍历每个直播间的ZSet键
         for (String liveKey : allLiveKeys) {
-            // 3. 获取当前直播间ZSet中所有元素(按score排序)
-            // range方法:0表示第一个元素,-1表示最后一个元素,即获取全部
             Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
             if (range == null || range.isEmpty()) {
-                log.info("当前直播间没有数据,跳过处理");
-                continue; // 没有数据,直接返回
+                continue;
             }
+            log.info("{} liveAutoTask 处理直播间任务: liveKey={}, 待执行数={}, 截止时间戳={}",
+                    LOG_PREFIX, liveKey, range.size(), currentTime);
             redisCache.redisTemplate.opsForZSet()
                     .removeRangeByScore(liveKey, 0, currentTime);
             processAutoTask(range);
+            totalProcessed += range.size();
+        }
+        if (totalProcessed > 0) {
+            logTaskFinish("liveAutoTask", "执行自动任务数=" + totalProcessed);
+        }
+        } catch (Exception e) {
+            logTaskError("liveAutoTask", e);
         }
-    }
-
-    public static void main(String[] args) {
-        long currentTime = Instant.now().toEpochMilli();
-        System.out.println(currentTime);
-        long startTime = 1776219541000L;
-        System.out.println(new Date(startTime));
-        System.out.println(new Date(currentTime));
-
     }
 
     private void processAutoTask(Set<String> range) {
-        for (String liveAutoTask : range) {
-            LiveAutoTask task = JSON.parseObject(liveAutoTask, LiveAutoTask.class);
-            webSocketServer.handleAutoTask(task);
-            task.setFinishStatus(1L);
-            liveAutoTaskService.finishLiveAutoTask(task);
+        for (String liveAutoTaskJson : range) {
+            try {
+                LiveAutoTask task = JSON.parseObject(liveAutoTaskJson, LiveAutoTask.class);
+                log.info("{} processAutoTask 开始执行: taskId={}, liveId={}, taskType={}, absValue={}",
+                        LOG_PREFIX, task.getId(), task.getLiveId(), task.getTaskType(), task.getAbsValue());
+                webSocketServer.handleAutoTask(task);
+                task.setFinishStatus(1L);
+                liveAutoTaskService.finishLiveAutoTask(task);
+                log.info("{} processAutoTask 执行成功: taskId={}, liveId={}, taskType={}",
+                        LOG_PREFIX, task.getId(), task.getLiveId(), task.getTaskType());
+            } catch (Exception e) {
+                log.error("{} processAutoTask 执行失败: content={}, error={}",
+                        LOG_PREFIX, liveAutoTaskJson, e.getMessage(), e);
+            }
         }
     }
 
@@ -434,37 +471,56 @@ public class Task {
     @DistributeLock(key = "autoUpdateWatchReward", scene = "task")
     @Transactional
     public void autoUpdateWatchReward() {
+        int rewardLiveCount = 0;
+        int rewardedUserCount = 0;
+        try {
 
         // 1.查询所有直播中的直播间
         List<Live> lives = liveService.liveList();
-
+        if (lives == null || lives.isEmpty()) {
+            logTaskFinish("autoUpdateWatchReward", "无直播中直播间,跳过");
+            return;
+        }
 
         // 2.检查是否开启观看奖励
         List<Live> openRewardLives = lives.stream().filter(live -> StringUtils.isNotEmpty(live.getConfigJson())).collect(Collectors.toList());
+        if (openRewardLives.isEmpty()) {
+            logTaskFinish("autoUpdateWatchReward", "无观看奖励配置,跳过");
+            return;
+        }
         Date now = new Date();
 
         for (Live openRewardLive : openRewardLives) {
             String configJson = openRewardLive.getConfigJson();
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
             if (!config.getEnabled() || config.getParticipateCondition() == null || config.getAction() == null) {
+                log.info("{} autoUpdateWatchReward 配置未启用或缺失: liveId={}", LOG_PREFIX, openRewardLive.getLiveId());
                 continue;
             }
             // 只处理 "达到指定观看时长" 的参与条件
             if (1 != config.getParticipateCondition()) {
+                log.info("{} autoUpdateWatchReward 参与条件非观看时长: liveId={}, condition={}",
+                        LOG_PREFIX, openRewardLive.getLiveId(), config.getParticipateCondition());
                 continue;
             }
 
             List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
             if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+                log.info("{} autoUpdateWatchReward 无待发放用户: liveId={}", LOG_PREFIX, openRewardLive.getLiveId());
                 continue;
             }
             // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
             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;
+            if (onlineUser.isEmpty()) {
+                log.info("{} autoUpdateWatchReward 无达到观看时长用户: liveId={}", LOG_PREFIX, openRewardLive.getLiveId());
+                continue;
+            }
 
             List<Long> userIds = onlineUser.stream().map(LiveWatchUser::getUserId).collect(Collectors.toList());
+            log.info("{} autoUpdateWatchReward 准备发放: liveId={}, action={}, 用户数={}",
+                    LOG_PREFIX, openRewardLive.getLiveId(), config.getAction(), userIds.size());
 
             // 根据 action 类型处理不同的奖励
             Long action = config.getAction();
@@ -488,6 +544,10 @@ public class Task {
                     );
                     userIds.forEach(userId -> webSocketServer.sendIntegralMessage(
                             openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog));
+                    rewardLiveCount++;
+                    rewardedUserCount += userIds.size();
+                    log.info("{} autoUpdateWatchReward 积分发放完成: liveId={}, 用户数={}",
+                            LOG_PREFIX, openRewardLive.getLiveId(), userIds.size());
                     break;
 
                 case 3: // 优惠券
@@ -512,15 +572,28 @@ public class Task {
                                 watchCouponOpLog.getId(), openRewardLive.getLiveId(), couponRelations);
                         couponRelations.forEach(relation -> sendCouponRewardMessage(
                                 openRewardLive.getLiveId(), relation.getUserId(), watchRewardCoupon, watchCouponOpLog));
+                        rewardLiveCount++;
+                        rewardedUserCount += couponRelations.size();
+                        log.info("{} autoUpdateWatchReward 优惠券发放完成: liveId={}, 用户数={}",
+                                LOG_PREFIX, openRewardLive.getLiveId(), couponRelations.size());
+                    } else {
+                        log.warn("{} autoUpdateWatchReward 优惠券发放无成功用户: liveId={}, couponId={}",
+                                LOG_PREFIX, openRewardLive.getLiveId(), actionCouponId);
                     }
                     break;
 
                 case 1: // 现金红包 - 暂不处理(现有逻辑)
                 default:
-                    log.info("观看奖励类型 {} 暂不处理,liveId={}", action, openRewardLive.getLiveId());
+                    log.info("{} autoUpdateWatchReward 奖励类型暂不处理: action={}, liveId={}",
+                            LOG_PREFIX, action, openRewardLive.getLiveId());
                     break;
             }
         }
+        logTaskFinish("autoUpdateWatchReward",
+                String.format("发放直播间数=%d, 发放用户数=%d", rewardLiveCount, rewardedUserCount));
+        } catch (Exception e) {
+            logTaskError("autoUpdateWatchReward", e);
+        }
     }
 
     /**
@@ -764,9 +837,14 @@ public class Task {
      */
     @Scheduled(cron = "0 0/1 * * * ?")// 每分钟执行一次
     public void syncLiveDataToDB() {
+        int syncCount = 0;
+        int couponSyncCount = 0;
+        try {
         List<LiveData> liveDatas = liveDataService.getAllLiveDatas(); // 获取所有正在直播的直播间数据
-        if(liveDatas == null)
+        if (liveDatas == null || liveDatas.isEmpty()) {
+            logTaskFinish("syncLiveDataToDB", "无直播数据,跳过");
             return;
+        }
         liveDatas.forEach(liveData ->{
 
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveData.getLiveId());
@@ -862,9 +940,8 @@ public class Task {
         if(!liveDatas.isEmpty())
             for (LiveData liveData : liveDatas) {
                 liveDataService.updateLiveData(liveData);
+                syncCount++;
             }
-            /*// 更新数据库
-            liveDataService.updateLiveData(liveData);*/
         Set<String> keys = redisCache.redisTemplate.keys(String.format(LIVE_COUPON_NUM, "*"));
         if (keys != null && !keys.isEmpty()) {
             for (String key : keys) {
@@ -874,9 +951,15 @@ public class Task {
                     updateEntity.setId(Long.valueOf(key));
                     updateEntity.setRemainCount(Long.parseLong(o.toString()));
                     liveCouponIssueService.updateLiveCouponIssue(updateEntity);
+                    couponSyncCount++;
                 }
             }
         }
+        logTaskFinish("syncLiveDataToDB",
+                String.format("同步直播数据=%d, 同步优惠券余量=%d", syncCount, couponSyncCount));
+        } catch (Exception e) {
+            logTaskError("syncLiveDataToDB", e);
+        }
     }
 
     /**
@@ -885,7 +968,11 @@ public class Task {
     @Scheduled(cron = "0/5 * * * * ?")
     @DistributeLock(key = "updateRedQuantityNum", scene = "task")
     public void updateRedQuantityNum() {
-        liveRedConfService.updateRedQuantityNum();
+        try {
+            liveRedConfService.updateRedQuantityNum();
+        } catch (Exception e) {
+            logTaskError("updateRedQuantityNum", e);
+        }
     }
 
     /**
@@ -895,6 +982,7 @@ public class Task {
     @Scheduled(cron = "0/10 * * * * ?")
     @DistributeLock(key = "scanLiveTagMark", scene = "task")
     public void scanLiveTagMark() {
+        int processedCount = 0;
         try {
 
             // 获取所有打标签缓存的key
@@ -1040,9 +1128,11 @@ public class Task {
                         processedLiveIds.add(liveId);
                         // 调用打标签方法
                         liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+                        processedCount++;
+                        log.info("{} scanLiveTagMark 打标签完成: liveId={}", LOG_PREFIX, liveId);
                     }
                 } catch (Exception e) {
-                    log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
+                    log.error("{} scanLiveTagMark 处理缓存异常: key={}, error={}", LOG_PREFIX, key, e.getMessage(), e);
                 }
             }
 
@@ -1052,11 +1142,13 @@ public class Task {
                     String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, liveId);
                     redisCache.deleteObject(tagMarkKey);
                 } catch (Exception e) {
-                    log.error("删除直播间打标签缓存失败: liveId={}, error={}", liveId, e.getMessage(), e);
+                    log.error("{} scanLiveTagMark 删除缓存失败: liveId={}, error={}", LOG_PREFIX, liveId, e.getMessage(), e);
                 }
             }
+            logTaskFinish("scanLiveTagMark",
+                    String.format("扫描缓存=%d, 打标签完成=%d", keys.size(), processedCount));
         } catch (Exception e) {
-            log.error("扫描直播间打标签任务异常: error={}", e.getMessage(), e);
+            logTaskError("scanLiveTagMark", e);
         }
     }
 
@@ -1067,11 +1159,14 @@ public class Task {
     @Scheduled(cron = "0/30 * * * * ?")
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
+        int processedLiveCount = 0;
+        int updatedLogCount = 0;
         try {
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
+                logTaskFinish("scanLiveWatchUserStatus", "无活跃直播间,跳过");
                 return;
             }
             for (Live live : activeLives) {
@@ -1096,15 +1191,7 @@ public class Task {
                     if (onlineUsers == null || onlineUsers.isEmpty()) {
                         continue;
                     }
-                    // 获取直播视频总时长
-                    List<LiveVideo> videos = liveVideoService.listByLiveIdWithCache(liveId, 1);
-                    long totalVideoDuration = 0L;
-                    if (videos != null && !videos.isEmpty()) {
-                        totalVideoDuration = videos.stream()
-                                .filter(v -> v.getDuration() != null)
-                                .mapToLong(LiveVideo::getDuration)
-                                .sum();
-                    }
+                    long totalVideoDuration = sumLiveVideoDurationSeconds(liveId);
 
                     // 处理每个在线用户
                     List<LiveWatchLog> updateLog = new ArrayList<>();
@@ -1163,15 +1250,22 @@ public class Task {
                         for (LiveWatchLog liveWatchLog : updateLog) {
                             redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
                         }
+                        updatedLogCount += updateLog.size();
+                        processedLiveCount++;
+                        log.info("{} scanLiveWatchUserStatus 更新观看记录: liveId={}, 记录数={}",
+                                LOG_PREFIX, liveId, updateLog.size());
                     }
 
                 } catch (Exception e) {
-                    log.error("处理直播间观看记录状态异常: liveId={}, error={}",
-                            live.getLiveId(), e.getMessage(), e);
+                    log.error("{} scanLiveWatchUserStatus 处理直播间异常: liveId={}, error={}",
+                            LOG_PREFIX, live.getLiveId(), e.getMessage(), e);
                 }
             }
+            logTaskFinish("scanLiveWatchUserStatus",
+                    String.format("扫描直播间=%d, 更新记录直播间=%d, 更新记录数=%d",
+                            activeLives.size(), processedLiveCount, updatedLogCount));
         } catch (Exception e) {
-            log.error("实时扫描用户直播数据任务异常: error={}", e.getMessage(), e);
+            logTaskError("scanLiveWatchUserStatus", e);
         }
     }
 
@@ -1231,8 +1325,8 @@ public class Task {
                 }
             }
         } catch (Exception e) {
-            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}",
-                    liveId, userId, e.getMessage(), e);
+            log.error("{} updateLiveWatchLogTypeByDuration 异常: liveId={}, userId={}, error={}",
+                    LOG_PREFIX, liveId, userId, e.getMessage(), e);
         }
     }
 
@@ -1242,12 +1336,16 @@ public class Task {
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
     public void updateLiveWatchUserStatus() {
+        int updatedLogCount = 0;
         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()) {
+            if (keys == null || keys.isEmpty()) {
+                logTaskFinish("updateLiveWatchUserStatus", "无用户活跃缓存,跳过");
+                return;
+            }
                 for (String key : keys) {
                     String[] split = key.split(":");
                     String cacheTime = redisCache.getCacheObject(key);
@@ -1277,7 +1375,8 @@ public class Task {
                                 }
                             }
                         } catch (Exception e) {
-                            log.error("解析缓存时间失败: cacheTime={}, error={}", cacheTime, e.getMessage());
+                            log.error("{} updateLiveWatchUserStatus 解析缓存时间失败: cacheTime={}, error={}",
+                                    LOG_PREFIX, cacheTime, e.getMessage());
                         }
                     }
                 }
@@ -1292,10 +1391,12 @@ public class Task {
                     for (LiveWatchLog liveWatchLog : updateLog) {
                         redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
                     }
+                    updatedLogCount = updateLog.size();
                 }
-            }
+            logTaskFinish("updateLiveWatchUserStatus",
+                    String.format("扫描缓存=%d, 更新看课中断记录=%d", keys.size(), updatedLogCount));
         } catch (Exception ex) {
-            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+            logTaskError("updateLiveWatchUserStatus", ex);
         }
     }
 
@@ -1313,7 +1414,7 @@ public class Task {
 //            List<Live> activeLives = liveService.selectNoEndLiveList();
 //
 //            if (activeLives == null || activeLives.isEmpty()) {
-//                log.debug("当前没有活跃的直播间");
+//                log.info("当前没有活跃的直播间");
 //                return;
 //            }
 //
@@ -1405,4 +1506,18 @@ public class Task {
         }
         return title + ",发放" + userCount + "人";
     }
+
+    private long sumLiveVideoDurationSeconds(Long liveId) {
+        List<LiveVideo> videos = liveVideoService.listByLiveId(liveId, 1);
+        if (videos == null || videos.isEmpty()) {
+            return 0L;
+        }
+        long total = 0L;
+        for (LiveVideo video : videos) {
+            if (video != null && video.getDuration() != null) {
+                total += video.getDuration();
+            }
+        }
+        return total;
+    }
 }

+ 122 - 68
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -659,7 +659,7 @@ public class WebSocketServer {
         msg.setStatus(status);
         Long couponIssueId = jsonObject.getLong("couponIssueId");
         // ①  检查  缓存是否存在  ② 如果是发布 放入缓存 ③ 删除缓存
-        if (status == 1) {
+        if (status != null && status == 1) {
             Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
             if (cacheObject == null) {
                 LiveCouponIssue liveCoupon = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
@@ -671,7 +671,8 @@ public class WebSocketServer {
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
         }
         LiveConsoleOpLog opLog = saveConsoleCouponOpLog(liveId, couponIssueId, status);
-        attachOpLog(msg, opLog);
+        attachConsoleOpLog(msg, jsonObject, opLog);
+        embedOpLogIdInMessageData(msg);
         // 管理员消息插队
         enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
@@ -698,13 +699,14 @@ public class WebSocketServer {
     private void processRed(Long liveId, SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
-        msg.setStatus( status);
+        msg.setStatus(status);
         LiveRedConf liveRedConf = liveRedConfService.selectLiveRedConfByRedId(jsonObject.getLong("redId"));
         if (Objects.nonNull(liveRedConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveRedConf));
             LiveConsoleOpLog opLog = saveConsoleRedOpLog(liveId, liveRedConf, status);
-            attachOpLog(msg, opLog);
+            attachConsoleOpLog(msg, jsonObject, opLog);
+            embedOpLogIdInMessageData(msg);
             // 管理员消息插队
             enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
@@ -716,90 +718,59 @@ public class WebSocketServer {
     private void processLottery(Long liveId, SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
-        msg.setStatus( status);
+        msg.setStatus(status);
         LiveLotteryConf liveLotteryConf = liveLotteryConfService.selectLiveLotteryConfByLotteryId(jsonObject.getLong("lotteryId"));
         if (Objects.nonNull(liveLotteryConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveLotteryConf));
             LiveConsoleOpLog opLog = saveConsoleLotteryOpLog(liveId, liveLotteryConf, status);
-            attachOpLog(msg, opLog);
+            attachConsoleOpLog(msg, jsonObject, opLog);
+            embedOpLogIdInMessageData(msg);
             // 管理员消息插队
             enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
     }
 
     /**
-     * 中控台红包:仅结算时挂载 REST/定时任务已写入的留存,不在 WebSocket 侧新建记录
+     * 中控台红包:仅开始(含暂停后再开始)写入发放留存
      */
     private LiveConsoleOpLog saveConsoleRedOpLog(Long liveId, LiveRedConf liveRedConf, Integer status) {
-        if (liveRedConf == null || status == null) {
+        if (liveRedConf == null || status == null || status != 1) {
             return null;
         }
-        if (status == 2 || status == -1) {
-            return findRecentRedSettleOpLog(liveId, liveRedConf.getRedId());
-        }
-        return null;
+        return liveConsoleOpLogService.saveRedSendLog(
+                liveId,
+                LiveConsoleOpLog.HANDLE_CONSOLE,
+                liveRedConf.getRedId(),
+                resolveRedBizName(liveRedConf));
     }
 
-    /**
-     * 查询 REST 结算接口刚写入的红包结算留存,供 WebSocket 挂载 opLog
-     */
-    private LiveConsoleOpLog findRecentRedSettleOpLog(Long liveId, Long redId) {
-        LiveConsoleOpLog query = new LiveConsoleOpLog();
-        query.setLiveId(liveId);
-        query.setBizId(redId);
-        List<LiveConsoleOpLog> list = liveConsoleOpLogService.selectLiveConsoleOpLogList(query);
-        if (list == null || list.isEmpty()) {
-            return null;
+    private String resolveRedBizName(LiveRedConf conf) {
+        if (conf != null && StringUtils.isNotEmpty(conf.getDesc())) {
+            return conf.getDesc();
         }
-        long now = System.currentTimeMillis();
-        for (LiveConsoleOpLog item : list) {
-            if (item.getCreateTime() == null
-                    || !Objects.equals(item.getOpType(), LiveConsoleOpLog.OP_RED_SETTLE)) {
-                continue;
-            }
-            if (now - item.getCreateTime().getTime() <= 30000) {
-                return item;
-            }
-        }
-        return null;
+        return conf != null && conf.getRedId() != null ? "红包 #" + conf.getRedId() : "红包";
     }
 
     /**
-     * 中控台抽奖:仅结算时挂载 REST 已写入的留存,不在 WebSocket 侧新建记录
+     * 中控台抽奖:仅开始(含暂停后再开始)写入发放留存
      */
     private LiveConsoleOpLog saveConsoleLotteryOpLog(Long liveId, LiveLotteryConf liveLotteryConf, Integer status) {
-        if (liveLotteryConf == null || status == null) {
+        if (liveLotteryConf == null || status == null || status != 1) {
             return null;
         }
-        if (status == 2 || status == -1) {
-            return findRecentLotterySettleOpLog(liveId, liveLotteryConf.getLotteryId());
-        }
-        return null;
+        return liveConsoleOpLogService.saveLotterySendLog(
+                liveId,
+                LiveConsoleOpLog.HANDLE_CONSOLE,
+                liveLotteryConf.getLotteryId(),
+                resolveLotteryBizName(liveLotteryConf));
     }
 
-    /**
-     * 查询 REST 结算接口刚写入的抽奖结算留存,供 WebSocket 挂载 opLog
-     */
-    private LiveConsoleOpLog findRecentLotterySettleOpLog(Long liveId, Long lotteryId) {
-        LiveConsoleOpLog query = new LiveConsoleOpLog();
-        query.setLiveId(liveId);
-        query.setBizId(lotteryId);
-        List<LiveConsoleOpLog> list = liveConsoleOpLogService.selectLiveConsoleOpLogList(query);
-        if (list == null || list.isEmpty()) {
-            return null;
+    private String resolveLotteryBizName(LiveLotteryConf conf) {
+        if (conf != null && StringUtils.isNotEmpty(conf.getDesc())) {
+            return conf.getDesc();
         }
-        long now = System.currentTimeMillis();
-        for (LiveConsoleOpLog item : list) {
-            if (item.getCreateTime() == null
-                    || !Objects.equals(item.getOpType(), LiveConsoleOpLog.OP_LOTTERY_SETTLE)) {
-                continue;
-            }
-            if (now - item.getCreateTime().getTime() <= 30000) {
-                return item;
-            }
-        }
-        return null;
+        return conf != null && conf.getLotteryId() != null ? "抽奖 #" + conf.getLotteryId() : "抽奖";
     }
 
     /**
@@ -876,6 +847,7 @@ public class WebSocketServer {
             try {
                 SendMsgVo msg = JSONObject.parseObject(text, SendMsgVo.class);
                 if (msg != null) {
+                    normalizeNestedMessageData(text, msg);
                     fillMessageDefaults(msg, liveId, userId, userType);
                     if (StringUtils.isEmpty(msg.getCmd()) && StringUtils.isNotEmpty(msg.getMsg())) {
                         msg.setCmd("sendMsg");
@@ -1057,6 +1029,65 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 优先使用 REST 下发的 opLogId,否则使用 WebSocket 侧查到的留存
+     */
+    private void attachConsoleOpLog(SendMsgVo msg, JSONObject dataJson, LiveConsoleOpLog fallbackOpLog) {
+        LiveConsoleOpLog opLog = resolveOpLogFromData(dataJson);
+        if (opLog == null) {
+            opLog = fallbackOpLog;
+        }
+        attachOpLog(msg, opLog);
+    }
+
+    private LiveConsoleOpLog resolveOpLogFromData(JSONObject dataJson) {
+        if (dataJson == null) {
+            return null;
+        }
+        Long opLogId = dataJson.getLong("opLogId");
+        if (opLogId == null) {
+            return null;
+        }
+        return liveConsoleOpLogService.selectLiveConsoleOpLogById(opLogId);
+    }
+
+    /**
+     * 确保嵌套 data 对象被序列化为 JSON 字符串,避免 Fastjson 反序列化异常
+     */
+    private void normalizeNestedMessageData(String raw, SendMsgVo msg) {
+        if (msg == null || StringUtils.isEmpty(raw)) {
+            return;
+        }
+        try {
+            JSONObject root = JSONObject.parseObject(raw);
+            Object dataNode = root.get("data");
+            if (dataNode != null && !(dataNode instanceof String)) {
+                msg.setData(JSON.toJSONString(dataNode));
+            }
+        } catch (Exception ignored) {
+        }
+    }
+
+    /**
+     * 将留存 ID 写入 data,与 opLog 字段一并下发给 App
+     */
+    private void embedOpLogIdInMessageData(SendMsgVo msg) {
+        if (msg == null || msg.getOpLog() == null || msg.getOpLog().getId() == null) {
+            return;
+        }
+        try {
+            JSONObject dataObj = StringUtils.isEmpty(msg.getData())
+                    ? new JSONObject()
+                    : JSON.parseObject(msg.getData());
+            if (dataObj != null) {
+                dataObj.put("opLogId", msg.getOpLog().getId());
+                msg.setData(dataObj.toJSONString());
+            }
+        } catch (Exception e) {
+            log.warn("嵌入 opLogId 到 WebSocket data 失败, cmd={}", msg.getCmd());
+        }
+    }
+
     public void sendIntegralMessage(Long liveId, Long userId, Long scoreAmount) {
         sendIntegralMessage(liveId, userId, scoreAmount, null);
     }
@@ -1444,6 +1475,14 @@ public class WebSocketServer {
                 msg.setData(JSON.toJSONString(liveRedConf));
                 liveRedConfService.updateLiveRedConf(liveRedConf);
                 liveService.asyncToCacheLiveConfig(task.getLiveId());
+                LiveConsoleOpLog redSendOpLog = liveConsoleOpLogService.saveRedSendLog(
+                        task.getLiveId(),
+                        LiveConsoleOpLog.HANDLE_AUTO,
+                        liveRedConf.getRedId(),
+                        StringUtils.isNotEmpty(liveRedConf.getDesc())
+                                ? liveRedConf.getDesc() : "红包 #" + liveRedConf.getRedId());
+                attachOpLog(msg, redSendOpLog);
+                embedOpLogIdInMessageData(msg);
             }else if (task.getTaskType() == 4L) {
                 msg.setCmd("lottery");
                 LiveLotteryConf liveLotteryConf = JSON.parseObject(task.getContent(), LiveLotteryConf.class);
@@ -1456,6 +1495,14 @@ public class WebSocketServer {
                 msg.setData(JSON.toJSONString(liveLotteryConf));
                 liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
                 liveService.asyncToCacheLiveConfig(task.getLiveId());
+                LiveConsoleOpLog lotterySendOpLog = liveConsoleOpLogService.saveLotterySendLog(
+                        task.getLiveId(),
+                        LiveConsoleOpLog.HANDLE_AUTO,
+                        liveLotteryConf.getLotteryId(),
+                        StringUtils.isNotEmpty(liveLotteryConf.getDesc())
+                                ? liveLotteryConf.getDesc() : "抽奖 #" + liveLotteryConf.getLotteryId());
+                attachOpLog(msg, lotterySendOpLog);
+                embedOpLogIdInMessageData(msg);
             }else if (task.getTaskType() == 3L) {
                 msg.setCmd("sendMsg");
                 msg.setMsg(task.getContent());
@@ -1729,14 +1776,7 @@ public class WebSocketServer {
                                                    Long companyUserId, Long onlineSeconds) {
         try {
             // 获取直播视频总时长(videoType = 1 的视频,使用带缓存的查询方法)
-            List<LiveVideo> videos = liveVideoService.listByLiveIdWithCache(liveId, 1);
-            long totalVideoDuration = 0L;
-            if (videos != null && !videos.isEmpty()) {
-                totalVideoDuration = videos.stream()
-                        .filter(v -> v.getDuration() != null)
-                        .mapToLong(LiveVideo::getDuration)
-                        .sum();
-            }
+            long totalVideoDuration = sumLiveVideoDurationSeconds(liveId);
 
             // 查询 LiveWatchLog
             LiveWatchLog queryLog = new LiveWatchLog();
@@ -2222,5 +2262,19 @@ public class WebSocketServer {
         }
     }
 
+    private long sumLiveVideoDurationSeconds(Long liveId) {
+        List<LiveVideo> videos = liveVideoService.listByLiveId(liveId, 1);
+        if (videos == null || videos.isEmpty()) {
+            return 0L;
+        }
+        long total = 0L;
+        for (LiveVideo video : videos) {
+            if (video != null && video.getDuration() != null) {
+                total += video.getDuration();
+            }
+        }
+        return total;
+    }
+
 }
 

+ 64 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -89,6 +89,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private static final String appActivitlLink = "/pages_course/activity.html?link=";
     private static final String registeredRealLink = "/pages_course/register.html?link=";
     private static final String h5LiveShortLink = "/pages_course/livingInvite.html?s=";
+    private static final String appLiveShortLink = "/pages_live/livingList?link=";
 
 
 //    private static final String miniappRealLink = "/pages/index/index?course=";
@@ -1219,6 +1220,19 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     setting.setMiniprogramPage(recordShortH5Link);
                     break;
                 }
+                case "25": {
+                    String recordCorpId = logVo.getCorpId();
+                    String appLiveLink = createAppLiveShortLink(setting, recordCorpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    setting.setMiniprogramPage(appLiveLink);
+                    setting.setAppLinkUrl(appLiveLink);
+                    sopLogs.setIsHaveApp(1);
+                    sopLogs.setAppSendStatus(0);
+                    break;
+                }
                 default:
                     break;
             }
@@ -1477,6 +1491,19 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     setting.setMiniprogramPage(liveShortH5Link);
                     break;
                 }
+                case "25": {
+                    String liveCorpId = logVo.getCorpId();
+                    String appLiveLink = createAppLiveShortLink(setting, liveCorpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    setting.setMiniprogramPage(appLiveLink);
+                    setting.setAppLinkUrl(appLiveLink);
+                    sopLogs.setIsHaveApp(1);
+                    sopLogs.setAppSendStatus(0);
+                    break;
+                }
                 default:
                     break;
             }
@@ -1837,6 +1864,18 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                     setting.setMiniprogramPage(shortH5Link);
 
+                    break;
+                case "25":
+                    String appLiveCorpId = logVo.getCorpId();
+                    String appLiveLink = createAppLiveShortLink(setting, appLiveCorpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    setting.setMiniprogramPage(appLiveLink);
+                    setting.setAppLinkUrl(appLiveLink);
+                    sopLogs.setIsHaveApp(1);
+                    sopLogs.setAppSendStatus(0);
                     break;
                 default:
                     break;
@@ -1883,6 +1922,31 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         return link.getRealLink();
 
     }
+
+    private String createAppLiveShortLink(QwSopTempSetting.Content.Setting setting, String corpId, String qwUserId, String companyUserId, String companyId) {
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setLiveId(setting.getLiveId());
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = appLiveShortLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
     private String getAppIdFromMiniMap(Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                        String companyId,
                                        int sendMsgType,

+ 22 - 0
fs-service/src/main/java/com/fs/gtPush/service/impl/uniPush2ServiceImpl.java

@@ -104,6 +104,28 @@ public class uniPush2ServiceImpl implements uniPush2Service {
 
     }
 
+    @Override
+    public OpenImResponseDTO pushSopAppLiveCardMsgByExternalIM(String cropId, String title, String linkImageUrl, String link, Long companyUserId, Long fsUserId) throws JsonProcessingException {
+        if (companyUserId == null || fsUserId == null || fsUserId == 0) {
+            OpenImResponseDTO errorResponse = new OpenImResponseDTO();
+            errorResponse.setErrCode(-1);
+            errorResponse.setErrMsg("参数错误:用户未绑定销售");
+            errorResponse.setErrDlt("缺少必要参数");
+            return errorResponse;
+        }
+
+        FsUser fsUser = userService.selectFsUserByUserId(fsUserId);
+        if (fsUser == null) {
+            OpenImResponseDTO errorResponse = new OpenImResponseDTO();
+            errorResponse.setErrCode(-2);
+            errorResponse.setErrMsg("未找到对应的用户信息");
+            errorResponse.setErrDlt("用户ID: " + fsUserId);
+            return errorResponse;
+        }
+
+        return openIMService.sendLive(fsUserId, companyUserId, link, title, linkImageUrl, cropId);
+    }
+
     /**
      *
      * @param userId 接收人id

+ 1 - 0
fs-service/src/main/java/com/fs/gtPush/service/uniPush2Service.java

@@ -14,5 +14,6 @@ public interface uniPush2Service {
     PushReqBean getParam(Long userId,String purl,String title,String content,Float type,Integer desType,String imJsonString);
 //    void pushSopAppLinkMsgByExternalIM(String cropId,String linkTile,String linkDescribe,String linkImageUrl,String link,Long companyUserId,Long fsUserId) throws JsonProcessingException;
     OpenImResponseDTO pushSopAppLinkMsgByExternalIM(String cropId, String linkTile, String linkDescribe, String linkImageUrl, String link, Long companyUserId, Long fsUserId) throws JsonProcessingException;
+    OpenImResponseDTO pushSopAppLiveCardMsgByExternalIM(String cropId, String title, String linkImageUrl, String link, Long companyUserId, Long fsUserId) throws JsonProcessingException;
     void pushIm(Long userId, Long businessId, String purl, String title, String content, Float type, Integer desType,String imJsonString);
 }

+ 1 - 0
fs-service/src/main/java/com/fs/im/service/OpenIMService.java

@@ -34,6 +34,7 @@ public interface OpenIMService {
     R accountCheck(String userId, String type);
     void checkAndImportFriend(Long companyUserId,String fsUserId);
     OpenImResponseDTO sendCourse(Long userId,Long companyUserId,String url,String title,String linkImageUrl,String cropId) throws JsonProcessingException;
+    OpenImResponseDTO sendLive(Long userId, Long companyUserId, String url, String title, String linkImageUrl, String cropId) throws JsonProcessingException;
     void checkAndImportFriendByDianBo(Long companyUserId,String fsUserId,String cropId, boolean isUpdate);
 
     OpenImResponseDTO updateUserInfo(CompanyUser companyUser);

+ 38 - 0
fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java

@@ -558,6 +558,44 @@ public class OpenIMServiceImpl implements OpenIMService {
         content = null;
         return openImResponseDTO;
     }
+
+    @Override
+    public OpenImResponseDTO sendLive(Long userId, Long companyUserId, String url, String title, String linkImageUrl, String cropId) throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        checkAndImportFriendByDianBo(companyUserId, userId.toString(), cropId, true);
+        OpenImMsgDTO.Content content = new OpenImMsgDTO.Content();
+        OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
+        PayloadDTO payload = new PayloadDTO();
+        PayloadDTO.Extension extension = new PayloadDTO.Extension();
+        payload.setData("live");
+        extension.setTitle(title);
+        extension.setAppRealLink(url);
+        extension.setSendTime(new Date());
+        extension.setCourseUrl(linkImageUrl);
+        payload.setExtension(extension);
+        imData.setPayload(payload);
+        String imJson = objectMapper.writeValueAsString(imData);
+        content.setData(imJson);
+
+        OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
+        offlinePushInfo.setDesc(title);
+        CompanyUser companyUser = companyUserMapper.selectCompanyUserById(companyUserId);
+        offlinePushInfo.setTitle(StringUtils.isNotEmpty(companyUser.getImNickName()) ? companyUser.getImNickName() : companyUser.getNickName());
+        offlinePushInfo.setIOSBadgeCount(true);
+        offlinePushInfo.setIOSPushSound("");
+
+        OpenImMsgDTO openImMsgDTO = new OpenImMsgDTO();
+        openImMsgDTO.setOfflinePushInfo(offlinePushInfo);
+        openImMsgDTO.setContent(content);
+        openImMsgDTO.setSendID("C" + companyUserId);
+        openImMsgDTO.setRecvID("U" + userId);
+        openImMsgDTO.setContentType(110);
+        openImMsgDTO.setSessionType(1);
+        log.info("直播卡片消息: {}", JSON.toJSONString(openImMsgDTO));
+        return openIMSendMsg(openImMsgDTO);
+    }
+
     @Override
     public OpenImResponseDTO sendPackageUtil(String sendID, String recvID, Integer contentType, String payloadData,String packageName,String packageId){
         try {

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

@@ -49,6 +49,11 @@ public interface LiveWatchUserMapper {
      */
     List<LiveWatchUser> selectLiveWatchUserList(LiveWatchUser liveWatchUser);
 
+    /**
+     * 查询有效用户(fs_user.status=1 且未注销)的观看记录
+     */
+    List<LiveWatchUser> selectActiveLiveWatchUserList(LiveWatchUser liveWatchUser);
+
     /**
      * 新增直播间观看用户
      *

+ 12 - 0
fs-service/src/main/java/com/fs/live/service/ILiveConsoleOpLogService.java

@@ -14,6 +14,8 @@ public interface ILiveConsoleOpLogService {
 
     List<LiveConsoleOpLog> selectLiveConsoleOpLogList(LiveConsoleOpLog liveConsoleOpLog);
 
+    LiveConsoleOpLog selectLiveConsoleOpLogById(Long id);
+
     LiveConsoleOpLog saveLog(Long liveId, Integer opType, Integer handleType, Long bizId, String bizName);
 
     /**
@@ -26,6 +28,16 @@ public interface ILiveConsoleOpLogService {
      */
     LiveConsoleOpLog saveRedSettleLog(Long liveId, Integer handleType, Long redId, String bizName);
 
+    /**
+     * 红包发放留存:同一红包仅写入一条,暂停后再次开始不重复生成
+     */
+    LiveConsoleOpLog saveRedSendLog(Long liveId, Integer handleType, Long redId, String bizName);
+
+    /**
+     * 抽奖发放留存:同一抽奖仅写入一条,暂停后再次开始不重复生成
+     */
+    LiveConsoleOpLog saveLotterySendLog(Long liveId, Integer handleType, Long lotteryId, String bizName);
+
     LiveConsoleOpLog saveCouponShowLog(Long liveId, Long couponIssueId, LiveCoupon coupon, Integer handleType);
 
     void bindOpLogUsers(Long opLogId, Long liveId, List<LiveConsoleOpLogUser> relations);

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

@@ -184,4 +184,9 @@ public interface ILiveWatchUserService {
     void clearLiveFlagCache(Long liveId);
 
     List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser);
+
+    /**
+     * 查询有效用户(未注销/禁用)的观看记录
+     */
+    List<LiveWatchUser> selectActiveWatchUser(LiveWatchUser queryUser);
 }

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

@@ -28,7 +28,9 @@ import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -68,6 +70,14 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         return liveConsoleOpLogMapper.selectLiveConsoleOpLogList(liveConsoleOpLog);
     }
 
+    @Override
+    public LiveConsoleOpLog selectLiveConsoleOpLogById(Long id) {
+        if (id == null) {
+            return null;
+        }
+        return liveConsoleOpLogMapper.selectLiveConsoleOpLogById(id);
+    }
+
     @Override
     public LiveConsoleOpLog saveLog(Long liveId, Integer opType, Integer handleType, Long bizId, String bizName) {
         LiveConsoleOpLog log = new LiveConsoleOpLog();
@@ -128,6 +138,38 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         return saveLog(liveId, LiveConsoleOpLog.OP_RED_SETTLE, handleType, redId, bizName);
     }
 
+    @Override
+    public LiveConsoleOpLog saveRedSendLog(Long liveId, Integer handleType, Long redId, String bizName) {
+        if (liveId == null || redId == null) {
+            return null;
+        }
+        LiveConsoleOpLog query = new LiveConsoleOpLog();
+        query.setLiveId(liveId);
+        query.setBizId(redId);
+        query.setOpType(LiveConsoleOpLog.OP_RED_SEND);
+        List<LiveConsoleOpLog> existing = liveConsoleOpLogMapper.selectLiveConsoleOpLogList(query);
+        if (!CollectionUtils.isEmpty(existing)) {
+            return existing.get(0);
+        }
+        return saveLog(liveId, LiveConsoleOpLog.OP_RED_SEND, handleType, redId, bizName);
+    }
+
+    @Override
+    public LiveConsoleOpLog saveLotterySendLog(Long liveId, Integer handleType, Long lotteryId, String bizName) {
+        if (liveId == null || lotteryId == null) {
+            return null;
+        }
+        LiveConsoleOpLog query = new LiveConsoleOpLog();
+        query.setLiveId(liveId);
+        query.setBizId(lotteryId);
+        query.setOpType(LiveConsoleOpLog.OP_LOTTERY_SEND);
+        List<LiveConsoleOpLog> existing = liveConsoleOpLogMapper.selectLiveConsoleOpLogList(query);
+        if (!CollectionUtils.isEmpty(existing)) {
+            return existing.get(0);
+        }
+        return saveLog(liveId, LiveConsoleOpLog.OP_LOTTERY_SEND, handleType, lotteryId, bizName);
+    }
+
     @Override
     public LiveConsoleOpLog saveCouponShowLog(Long liveId, Long couponIssueId, LiveCoupon coupon, Integer handleType) {
         int opType = isVerifyCouponType(coupon)
@@ -201,15 +243,53 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         }
 
         Date now = DateUtils.getNowDate();
+        Map<Long, Long> couponTypeMap = resolveCouponTypeMap(opLogs);
         List<LiveConsoleOpLogRecordVo> result = new ArrayList<>(opLogs.size());
         for (LiveConsoleOpLog opLog : opLogs) {
             boolean claimed = opLog.getId() != null && claimedOpLogIds.contains(opLog.getId());
             int status = resolveOpLogStatus(opLog, claimed, now);
-            result.add(LiveConsoleOpLogRecordVo.from(opLog, status));
+            LiveConsoleOpLogRecordVo recordVo = LiveConsoleOpLogRecordVo.from(opLog, status);
+            fillCouponType(recordVo, opLog, couponTypeMap);
+            result.add(recordVo);
         }
         return result;
     }
 
+    private Map<Long, Long> resolveCouponTypeMap(List<LiveConsoleOpLog> opLogs) {
+        List<Long> couponIds = opLogs.stream()
+                .filter(log -> log.getOpType() != null && log.getBizId() != null
+                        && (log.getOpType() == LiveConsoleOpLog.OP_COMPLETION_COUPON
+                        || log.getOpType() == LiveConsoleOpLog.OP_WATCH_REWARD_COUPON))
+                .map(LiveConsoleOpLog::getBizId)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<Long, Long> couponTypeMap = new HashMap<>();
+        if (couponIds.isEmpty()) {
+            return couponTypeMap;
+        }
+        String ids = couponIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+        List<LiveCoupon> coupons = liveCouponMapper.selectLiveCouponByIds(ids);
+        if (coupons != null) {
+            for (LiveCoupon coupon : coupons) {
+                if (coupon.getCouponId() != null) {
+                    couponTypeMap.put(coupon.getCouponId(), coupon.getType());
+                }
+            }
+        }
+        return couponTypeMap;
+    }
+
+    private void fillCouponType(LiveConsoleOpLogRecordVo recordVo, LiveConsoleOpLog opLog, Map<Long, Long> couponTypeMap) {
+        if (recordVo == null || opLog == null || opLog.getOpType() == null || opLog.getBizId() == null) {
+            return;
+        }
+        if (opLog.getOpType() != LiveConsoleOpLog.OP_COMPLETION_COUPON
+                && opLog.getOpType() != LiveConsoleOpLog.OP_WATCH_REWARD_COUPON) {
+            return;
+        }
+        recordVo.setCouponType(couponTypeMap.get(opLog.getBizId()));
+    }
+
     /**
      * 状态优先级:已领取 > 已结束 > 待领取
      */

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

@@ -204,7 +204,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
 
     @Override
     public int insertLiveCouponEntity(Map<String, Object> payload) {
-        Long liveId = Long.valueOf((String) payload.get("liveId"));
+        Long liveId = Long.valueOf(payload.get("liveId").toString());
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if(live == null) return -1;
         String s = String.valueOf(payload.get("couponIds"));
@@ -221,7 +221,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
 
     @Override
     public R handleIsShowChange(Map<String, Object> payload) {
-        Long liveId = Long.valueOf((String) payload.get("liveId"));
+        Long liveId = Long.valueOf(payload.get("liveId").toString());
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if(live == null) return R.error("直播间不存在");
         boolean isShow = Boolean.parseBoolean(payload.get("isShow").toString());
@@ -245,8 +245,9 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         if (isShow) {
             // updateChangeShow 会自动收起其它券,仅保留当前展示的一张
             liveCouponMapper.updateChangeShow(liveId, couponIssueId);
-            liveConsoleOpLogService.saveCouponShowLog(
+            LiveConsoleOpLog opLog = liveConsoleOpLogService.saveCouponShowLog(
                     liveId, couponIssueId, liveCoupon, LiveConsoleOpLog.HANDLE_CONSOLE);
+            return R.ok("操作成功").put("opLogId", opLog != null ? opLog.getId() : null);
         } else {
             liveCouponMapper.updateShow(liveId, couponIssueId, 0);
         }

+ 0 - 13
fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java

@@ -5,7 +5,6 @@ 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.live.domain.LiveConsoleOpLog;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryRegistration;
 import com.fs.live.mapper.LiveLotteryConfMapper;
@@ -16,7 +15,6 @@ import com.fs.live.param.LiveLotteryProduct;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.service.ILiveAutoTaskService;
-import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.vo.LiveLotteryConfVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
@@ -56,8 +54,6 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
     @Autowired
     private ILiveAutoTaskService liveAutoTaskService;
-    @Autowired
-    private ILiveConsoleOpLogService liveConsoleOpLogService;
     /**
      * 查询直播抽奖配置
      *
@@ -120,15 +116,6 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
             redisCache.deleteObject(cacheKey);
         }
         int rows = baseMapper.updateLiveLotteryConf(liveLotteryConf);
-        if ("2".equals(liveLotteryConf.getLotteryStatus())) {
-            LiveLotteryConf persisted = baseMapper.selectLiveLotteryConfByLotteryId(liveLotteryConf.getLotteryId());
-            liveConsoleOpLogService.saveLotterySettleLog(
-                    liveLotteryConf.getLiveId(),
-                    LiveConsoleOpLog.HANDLE_CONSOLE,
-                    liveLotteryConf.getLotteryId(),
-                    resolveLotteryBizName(persisted)
-            );
-        }
         return R.ok().put("data", rows);
     }
 

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

@@ -149,15 +149,6 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
             redStatusUpdate(CollUtil.newHashSet(liveRedConf.getRedId()));
         }
         int rows = baseMapper.updateLiveRedConf(liveRedConf);
-        if (liveRedConf.getRedStatus() != null && liveRedConf.getRedStatus() == 2L) {
-            LiveRedConf persisted = baseMapper.selectLiveRedConfByRedId(liveRedConf.getRedId());
-            liveConsoleOpLogService.saveRedSettleLog(
-                    liveRedConf.getLiveId(),
-                    LiveConsoleOpLog.HANDLE_CONSOLE,
-                    liveRedConf.getRedId(),
-                    resolveRedBizName(persisted)
-            );
-        }
         return rows;
     }
 
@@ -426,27 +417,15 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
 
     @Override
     public List<LiveConsoleOpLog> finishRedStatusBySetIds(Set<String> range) {
-        List<LiveConsoleOpLog> opLogs = new ArrayList<>();
         try {
             log.info("开始结束红包状态:{}",range);
-            for (String redIdStr : range) {
-                LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(Long.valueOf(redIdStr));
-                if (conf != null) {
-                    opLogs.add(liveConsoleOpLogService.saveRedSettleLog(
-                            conf.getLiveId(),
-                            LiveConsoleOpLog.HANDLE_AUTO,
-                            conf.getRedId(),
-                            resolveRedBizName(conf)
-                    ));
-                }
-            }
             baseMapper.finishRedStatusBySetIds(range);
             redStatusUpdate(range.stream().map(Long::valueOf).collect(Collectors.toSet()));
             log.info("结束红包状态完成");
         }catch (Exception e){
             log.info("红包状态结束异常",e);
         }
-        return opLogs;
+        return Collections.emptyList();
     }
 
     @Override

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

@@ -124,8 +124,7 @@ public class LiveUserFirstEntryServiceImpl implements ILiveUserFirstEntryService
         // Redis缓存键
         String cacheKey = "live:user:first:entry:" + liveId + ":" + userId;
         
-        // 先从缓存中获取
-        LiveUserFirstEntry cachedEntry = redisCache.getCacheObject(cacheKey);
+        LiveUserFirstEntry cachedEntry = redisCache.getCacheObject(cacheKey, LiveUserFirstEntry.class);
         if (cachedEntry != null) {
             return cachedEntry;
         }

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

@@ -196,24 +196,22 @@ public class LiveVideoServiceImpl implements ILiveVideoService
 
     @Override
     public List<LiveVideo> listByLiveIdWithCache(Long liveId, Integer type) {
-        // Redis缓存键
         String cacheKey = "live:video:list:" + liveId + ":" + type;
 
-        // 先从缓存中获取
-        List<LiveVideo> cachedVideos = redisCache.getCacheObject(cacheKey);
+        List<LiveVideo> cachedVideos = RedisCache.convertCacheList(
+                redisCache.getCacheObject(cacheKey), LiveVideo.class);
         if (cachedVideos != null && !cachedVideos.isEmpty()) {
             return cachedVideos;
         }
+        if (redisCache.hasKey(cacheKey)) {
+            redisCache.deleteObject(cacheKey);
+        }
 
-        // 缓存未命中,从数据库查询
         List<LiveVideo> videos = liveVideoMapper.selectByIdAndType(liveId, type);
-
-        // 将查询结果存入缓存,缓存时间1小时
-        if (videos != null) {
+        if (videos != null && !videos.isEmpty()) {
             redisCache.setCacheObject(cacheKey, videos, 1, TimeUnit.HOURS);
         }
-
-        return videos;
+        return videos != null ? videos : Collections.emptyList();
     }
 
 }

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

@@ -151,9 +151,9 @@ public class LiveWatchLogServiceImpl extends ServiceImpl<LiveWatchLogMapper, Liv
             return null;
         }
         
-        // 先从缓存中获取
         String cacheKey = String.format(LIVE_WATCH_LOG_CACHE_KEY, logId);
-        List<LiveWatchLog> cachedLog = redisCache.getCacheObject(cacheKey);
+        List<LiveWatchLog> cachedLog = RedisCache.convertCacheList(
+                redisCache.getCacheObject(cacheKey), LiveWatchLog.class);
         if (cachedLog != null) {
             return cachedLog;
         }

+ 19 - 17
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -233,6 +233,11 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return baseMapper.selectLiveWatchUserList(queryUser);
     }
 
+    @Override
+    public List<LiveWatchUser> selectActiveWatchUser(LiveWatchUser queryUser) {
+        return baseMapper.selectActiveLiveWatchUserList(queryUser);
+    }
+
     /**
      * 批量删除直播间观看用户
      *
@@ -1292,26 +1297,16 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
      */
     @Override
     public Long getTotalWatchDuration(Long liveId, Long userId) {
+        if (liveId == null || userId == null) {
+            return 0L;
+        }
         try {
-            long totalDuration = 0L;
-
-            // 1. 查询直播观看记录(liveFlag=1, replayFlag=0)
-            LiveWatchUser liveRecord = baseMapper.selectByUniqueIndex(liveId, userId, 1, 0);
-            if (liveRecord != null && liveRecord.getOnlineSeconds() != null) {
-                totalDuration += liveRecord.getOnlineSeconds();
-            }
-
-            // 2. 查询回放观看记录(liveFlag=0, replayFlag=1)
-            LiveWatchUser replayRecord = baseMapper.selectByUniqueIndex(liveId, userId, 0, 1);
-            if (replayRecord != null && replayRecord.getOnlineSeconds() != null) {
-                totalDuration += replayRecord.getOnlineSeconds();
-            }
+            long liveDuration = safeOnlineSeconds(baseMapper.selectByUniqueIndex(liveId, userId, 1, 0));
+            long replayDuration = safeOnlineSeconds(baseMapper.selectByUniqueIndex(liveId, userId, 0, 1));
+            long totalDuration = liveDuration + replayDuration;
 
             log.debug("查询总观看时长: liveId={}, userId={}, liveDuration={}, replayDuration={}, total={}",
-                    liveId, userId,
-                    liveRecord != null ? liveRecord.getOnlineSeconds() : 0,
-                    replayRecord != null ? replayRecord.getOnlineSeconds() : 0,
-                    totalDuration);
+                    liveId, userId, liveDuration, replayDuration, totalDuration);
 
             return totalDuration;
         } catch (Exception e) {
@@ -1320,6 +1315,13 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         }
     }
 
+    private static long safeOnlineSeconds(LiveWatchUser record) {
+        if (record == null || record.getOnlineSeconds() == null) {
+            return 0L;
+        }
+        return record.getOnlineSeconds();
+    }
+
     @Override
     public Long getUserWatchDuration(Long liveId, Long userId) {
         Long total = getTotalWatchDuration(liveId, userId);

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

@@ -35,6 +35,9 @@ public class LiveConsoleOpLogRecordVo {
     /** 状态名称 */
     private String statusName;
 
+    /** 优惠券类型(opType=8完课优惠券、10观看奖励优惠券时返回,对应 live_coupon.type:0普通 1套餐 2制单 3无门槛) */
+    private Long couponType;
+
     public static LiveConsoleOpLogRecordVo from(LiveConsoleOpLog log, int status) {
         LiveConsoleOpLogRecordVo vo = new LiveConsoleOpLogRecordVo();
         if (log == null) {

+ 42 - 0
fs-service/src/main/java/com/fs/qw/service/impl/AsyncSopTestService.java

@@ -562,6 +562,48 @@ public class AsyncSopTestService {
         return success;
     }
 
+    public boolean asyncSendMsgBySopAppLiveCardNormalIM(
+            List<QwSopCourseFinishTempSetting.Setting> setting,
+            String cropId,
+            Long companyUserId,
+            Long fsUserId,
+            String logId) {
+
+        boolean success = true;
+        String remark = "APP直播卡片发送成功";
+
+        for (QwSopCourseFinishTempSetting.Setting item : setting) {
+            item.setSendStatus(2);
+            item.setSendRemarks("APP直播卡片发送失败");
+
+            try {
+                String liveLink = StringUtils.isNotEmpty(item.getAppLinkUrl()) ? item.getAppLinkUrl() : item.getMiniprogramPage();
+                String title = item.getMiniprogramTitle();
+                String coverUrl = item.getMiniprogramPicUrl();
+                OpenImResponseDTO resp = push2Service.pushSopAppLiveCardMsgByExternalIM(
+                        cropId, title, coverUrl, liveLink, companyUserId, fsUserId);
+                if (resp != null && resp.getErrCode() != null && resp.getErrCode() == 0) {
+                    item.setSendStatus(1);
+                    item.setSendRemarks("发送成功");
+                } else {
+                    success = false;
+                    remark = "APP直播卡片发送失败";
+                    if (resp != null) {
+                        item.setSendRemarks(resp.getErrMsg());
+                    }
+                }
+            } catch (Exception e) {
+                success = false;
+                remark = "APP直播卡片发送失败";
+                item.setSendRemarks("异常:" + e.getMessage());
+                log.error("APP直播卡片发送异常 logId={}", logId, e);
+            }
+        }
+
+        log.info("APP直播卡片发送完成,logId={},结果={}", logId, remark);
+        return success;
+    }
+
 
     /**
      * 异步录入 发送有app的客户 之 正常sop版

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

@@ -820,6 +820,22 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                     log.error("APP直播模板解析失败:" + e);
                                 }
                                 break;
+                            case "24":
+                            case "25": {
+                                if (vo == null || vo.getId() == null || StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+                                    break;
+                                }
+                                String appLiveLink = createAppLiveShortLink(st, param.getCorpId(), qwUser.getId(),
+                                        companyUserId, companyId, vo.getId(), createTime);
+                                st.setMiniprogramPage(appLiveLink);
+                                fillAppLiveSetting(st, param.getCorpId(), qwUser, vo.getId());
+                                if ("25".equals(st.getContentType())) {
+                                    st.setAppLinkUrl(appLiveLink);
+                                    sopLogs.setIsHaveApp(1);
+                                    sopLogs.setAppSendStatus(0);
+                                }
+                                break;
+                            }
                             default:
                                 break;
                         }
@@ -1423,6 +1439,29 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             }
 
                             break;
+                        case "24": {
+                            if (StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+                                throw new BaseException("APP直播短链配置缺少直播间");
+                            }
+                            String appLiveLink = createAppLiveShortLink(st, param.getCorpId(), qwUser.getId(),
+                                    companyUserId, companyId, item.getExternalId(), createTime);
+                            st.setMiniprogramPage(appLiveLink);
+                            fillAppLiveSetting(st, param.getCorpId(), qwUser, item.getExternalId());
+                            break;
+                        }
+                        case "25": {
+                            if (StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+                                throw new BaseException("APP直播卡片配置缺少直播间");
+                            }
+                            String appLiveLink = createAppLiveShortLink(st, param.getCorpId(), qwUser.getId(),
+                                    companyUserId, companyId, item.getExternalId(), createTime);
+                            st.setMiniprogramPage(appLiveLink);
+                            st.setAppLinkUrl(appLiveLink);
+                            fillAppLiveSetting(st, param.getCorpId(), qwUser, item.getExternalId());
+                            sopLogs.setIsHaveApp(1);
+                            sopLogs.setAppSendStatus(0);
+                            break;
+                        }
                         default:
                             break;
 
@@ -2088,6 +2127,29 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         log.error("浏览器看课模板解析失败:" + e);
                     }
                     break;
+                case "24": {
+                    if (StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+                        throw new BaseException("APP直播短链配置缺少直播间");
+                    }
+                    String appLiveLink = createAppLiveShortLink(st, param.getCorpId(), qwUser.getId(),
+                            companyUserId, companyId, item.getExternalId(), dataTime);
+                    st.setMiniprogramPage(appLiveLink);
+                    fillAppLiveSetting(st, param.getCorpId(), qwUser, item);
+                    break;
+                }
+                case "25": {
+                    if (StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+                        throw new BaseException("APP直播卡片配置缺少直播间");
+                    }
+                    String appLiveLink = createAppLiveShortLink(st, param.getCorpId(), qwUser.getId(),
+                            companyUserId, companyId, item.getExternalId(), dataTime);
+                    st.setMiniprogramPage(appLiveLink);
+                    st.setAppLinkUrl(appLiveLink);
+                    fillAppLiveSetting(st, param.getCorpId(), qwUser, item);
+                    sopLogs.setIsHaveApp(1);
+                    sopLogs.setAppSendStatus(0);
+                    break;
+                }
                 default:
                     break;
 
@@ -2357,6 +2419,65 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return updateTime;
     }
 
+    private void fillAppLiveSetting(QwSopCourseFinishTempSetting.Setting st, String corpId, QwUser qwUser,
+                                    SopUserLogsInfo item) {
+        fillAppLiveSetting(st, corpId, qwUser, item != null ? item.getExternalId() : null);
+    }
+
+    private void fillAppLiveSetting(QwSopCourseFinishTempSetting.Setting st, String corpId, QwUser qwUser,
+                                    Long externalId) {
+        String liveConfigJson = configService.selectConfigByKey("his.config");
+        FSSysConfig liveSysConfig = JSON.parseObject(liveConfigJson, FSSysConfig.class);
+        if (liveSysConfig != null && !StringUtil.strIsNullOrEmpty(liveSysConfig.getAppId())) {
+            st.setMiniprogramAppid(liveSysConfig.getAppId());
+        }
+
+        String liveTitle = st.getMiniprogramTitle();
+        int liveTitleMaxLength = 17;
+        if (!StringUtil.strIsNullOrEmpty(liveTitle) && liveTitle.length() > liveTitleMaxLength) {
+            st.setMiniprogramTitle(liveTitle.substring(0, liveTitleMaxLength) + "...");
+        }
+        if (liveSysConfig != null && externalId != null && !StringUtil.strIsNullOrEmpty(st.getLiveId())) {
+            try {
+                createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),
+                        externalId.toString(), Long.valueOf(st.getLiveId()), liveSysConfig.getAppId(), 2,
+                        String.valueOf(qwUser.getId()), corpId);
+            } catch (Exception e) {
+                log.error("APP直播一键群发创建看课记录失败", e);
+            }
+        }
+        st.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(st.getMiniprogramPicUrl())
+                ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png"
+                : st.getMiniprogramPicUrl());
+    }
+
+    private String createAppLiveShortLink(QwSopCourseFinishTempSetting.Setting st, String corpId, Long qwUserId,
+                                          String companyUserId, String companyId, Long externalId, Date sendTime) {
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId);
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setLiveId(Long.valueOf(st.getLiveId()));
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
+        link.setQwExternalId(externalId);
+        link.setCreateTime(sendTime);
+        link.setProjectCode(cloudHostProper.getProjectCode());
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = appLiveShortLink + courseJson;
+        link.setRealLink(realLinkFull);
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return realLinkFull;
+    }
+
     public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                            String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
         // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties

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

@@ -40,6 +40,24 @@
         </where>
     </select>
 
+    <select id="selectActiveLiveWatchUserList" parameterType="LiveWatchUser" resultMap="LiveWatchUserResult">
+        select lwu.id, lwu.live_id, lwu.user_id, lwu.msg_status, lwu.online, lwu.create_time, lwu.create_by,
+               lwu.update_by, lwu.update_time, lwu.remark, lwu.online_seconds, lwu.global_visible, lwu.single_visible,
+               lwu.live_flag, lwu.replay_flag, lwu.location
+        from live_watch_user lwu
+        inner join fs_user fu on lwu.user_id = fu.user_id
+            and fu.status = 1
+            and (fu.is_del = 0 or fu.is_del is null)
+        <where>
+            <if test="liveId != null "> and lwu.live_id = #{liveId}</if>
+            <if test="userId != null "> and lwu.user_id = #{userId}</if>
+            <if test="liveFlag != null "> and lwu.live_flag = #{liveFlag}</if>
+            <if test="replayFlag != null "> and lwu.replay_flag = #{replayFlag}</if>
+            <if test="msgStatus != null "> and lwu.msg_status = #{msgStatus}</if>
+            <if test="online != null "> and lwu.online = #{online}</if>
+        </where>
+    </select>
+
     <select id="selectLiveWatchUserById" parameterType="Long" resultMap="LiveWatchUserResult">
         <include refid="selectLiveWatchUserVo"/>
         where id = #{id}

+ 4 - 4
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -104,7 +104,7 @@ public class AppLoginController extends AppBaseController{
             user.setPhone(param.getPhone());
             user.setNickName("app用户" + param.getPhone().substring(param.getPhone().length() - 4));
             user.setStatus(1);
-            user.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+            user.setAvatar("https://obs.cqtyt.com/fs/20260615/4e7c1c2de4b54ec4aaf1047394592a42.png");
             user.setPassword(Md5Utils.hash(param.getPassword()));
             user.setCreateTime(new Date());
 
@@ -197,7 +197,7 @@ public class AppLoginController extends AppBaseController{
         user.setSource(map.get("source"));
         user.setNickName("app用户" + phone.substring(phone.length() - 4));
         user.setStatus(1);
-        user.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+        user.setAvatar("https://obs.cqtyt.com/fs/20260615/4e7c1c2de4b54ec4aaf1047394592a42.png");
         user.setPassword(Md5Utils.hash(password));
         user.setCreateTime(new Date());
         if (userService.insertFsUser(user) > 0) {
@@ -372,7 +372,7 @@ public class AppLoginController extends AppBaseController{
         newUser.setNickName("苹果用户" + param.getPhone().substring(param.getPhone().length() - 4));
         newUser.setCreateTime(new Date());
         newUser.setStatus(1);
-        newUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+        newUser.setAvatar("https://obs.cqtyt.com/fs/20260615/4e7c1c2de4b54ec4aaf1047394592a42.png");
         if (StringUtils.isNotEmpty(param.getJpushId())) {
             newUser.setJpushId(param.getJpushId());
         }
@@ -883,7 +883,7 @@ public class AppLoginController extends AppBaseController{
         newUser.setPhone(param.getPhone());
         newUser.setCreateTime(new Date());
         newUser.setStatus(1);
-        newUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+        newUser.setAvatar("https://obs.cqtyt.com/fs/20260615/4e7c1c2de4b54ec4aaf1047394592a42.png");
         if (StringUtils.isNotEmpty(param.getJpushId())) {
             newUser.setJpushId(param.getJpushId());
         }