2 Commit-ok 6c565f9a85 ... 4679685f85

Szerző SHA1 Üzenet Dátum
  yys 4679685f85 1、调整登录创建用户头像不对 4 napja
  yys ba4a44a221 1、优化定时任务打印日志 4 napja
32 módosított fájl, 1008 hozzáadás és 254 törlés
  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.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.service.ILiveLotteryConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
@@ -29,6 +32,9 @@ public class LiveLotteryConfController extends BaseController
     @Autowired
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
     private ILiveLotteryConfService liveLotteryConfService;
 
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
     /**
      * 查询直播抽奖配置列表
      * 查询直播抽奖配置列表
      */
      */
@@ -90,7 +96,21 @@ public class LiveLotteryConfController extends BaseController
     @PutMapping
     @PutMapping
     public R edit(@RequestBody LiveLotteryConf liveLotteryConf)
     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.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveRedConf;
 import com.fs.live.domain.LiveRedConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.service.ILiveRedConfService;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
@@ -28,6 +31,9 @@ public class LiveRedConfController extends BaseController
     @Autowired
     @Autowired
     private ILiveRedConfService liveRedConfService;
     private ILiveRedConfService liveRedConfService;
 
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
     /**
      * 查询直播红包记录配置列表
      * 查询直播红包记录配置列表
      */
      */
@@ -91,7 +97,27 @@ public class LiveRedConfController extends BaseController
     public R edit(@RequestBody LiveRedConf liveRedConf)
     public R edit(@RequestBody LiveRedConf liveRedConf)
     {
     {
         liveRedConfService.updateLiveRedConf(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);
         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.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.service.ILiveLotteryConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
@@ -29,6 +32,9 @@ public class LiveLotteryConfController extends BaseController
     @Autowired
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
     private ILiveLotteryConfService liveLotteryConfService;
 
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
     /**
      * 查询直播抽奖配置列表
      * 查询直播抽奖配置列表
      */
      */
@@ -91,7 +97,21 @@ public class LiveLotteryConfController extends BaseController
     @PutMapping
     @PutMapping
     public R edit(@RequestBody LiveLotteryConf liveLotteryConf)
     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.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.LiveRedConf;
 import com.fs.live.domain.LiveRedConf;
+import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.service.ILiveRedConfService;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.service.ILiveConsoleOpLogService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
@@ -28,6 +31,9 @@ public class LiveRedConfController extends BaseController
     @Autowired
     @Autowired
     private ILiveRedConfService liveRedConfService;
     private ILiveRedConfService liveRedConfService;
 
 
+    @Autowired
+    private ILiveConsoleOpLogService liveConsoleOpLogService;
+
     /**
     /**
      * 查询直播红包记录配置列表
      * 查询直播红包记录配置列表
      */
      */
@@ -91,7 +97,27 @@ public class LiveRedConfController extends BaseController
     public R edit(@RequestBody LiveRedConf liveRedConf)
     public R edit(@RequestBody LiveRedConf liveRedConf)
     {
     {
         liveRedConfService.updateLiveRedConf(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":
                 case "24":
                     sendAppShortLink(vo, content, miniMap);
                     sendAppShortLink(vo, content, miniMap);
                     break;
                     break;
+                case "25":
+                    // APP直播卡片走 OpenIM WebSocket,企微侧跳过
+                    content.setSendStatus(0);
+                    content.setSendRemarks("APP待发送");
+                    break;
                 case "99":
                 case "99":
                     // 群发
                     // 群发
                     sendTxtAtMsg(vo);
                     sendTxtAtMsg(vo);
@@ -1317,6 +1322,7 @@ public class IpadSendServer {
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "模板未选消息类型,不发送");
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "模板未选消息类型,不发送");
             return false;
             return false;
         }
         }
+        if (setting.getVideoId() != null) {
         Integer cacheValue = redisCache.getCacheObject("sopCourse:video:isPause:" + setting.getVideoId());
         Integer cacheValue = redisCache.getCacheObject("sopCourse:video:isPause:" + setting.getVideoId());
         int isPause = (cacheValue != null) ? cacheValue : 0;
         int isPause = (cacheValue != null) ? cacheValue : 0;
         log.info("SOP_LOG_ID:{},判断课程({})当前状态:{}", qwSopLogs.getId(), setting.getVideoId(), isPause);
         log.info("SOP_LOG_ID:{},判断课程({})当前状态:{}", qwSopLogs.getId(), setting.getVideoId(), isPause);
@@ -1325,6 +1331,37 @@ public class IpadSendServer {
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "课程暂停,AI不发送");
             qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "课程暂停,AI不发送");
             return false;
             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) {
         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()) {
         if (qwSopLogList.isEmpty()) {
             return;
             return;
         }
         }
-        List<String> typeList = Arrays.asList("9", "15", "16");
+        List<String> typeList = Arrays.asList("9", "15", "16", "25");
         // 获取企微用户
         // 获取企微用户
         QwUser user = qwUserMapper.selectById(qwUser.getId());
         QwUser user = qwUserMapper.selectById(qwUser.getId());
         long end1 = System.currentTimeMillis();
         long end1 = System.currentTimeMillis();
@@ -171,6 +171,7 @@ public class SendAppMsg {
                 boolean txtSendStatus = true;
                 boolean txtSendStatus = true;
                 boolean mp3SendStatus = true;
                 boolean mp3SendStatus = true;
                 boolean courseSendStatus = true;
                 boolean courseSendStatus = true;
+                boolean liveCardSendStatus = true;
 //                for (QwSopCourseFinishTempSetting.Setting content : allContent) {
 //                for (QwSopCourseFinishTempSetting.Setting content : allContent) {
 //                    String contentType = content.getContentType();
 //                    String contentType = content.getContentType();
 //                    if (!typeList.contains(contentType)) {
 //                    if (!typeList.contains(contentType)) {
@@ -226,6 +227,10 @@ public class SendAppMsg {
                         if (!voiceList.isEmpty()) {
                         if (!voiceList.isEmpty()) {
                             mp3SendStatus = asyncSopTestService.asyncSendMsgBySopAppMP3NormalIM(voiceList, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
                             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)
                         // 发送成功后记录次数(只记录真正发送的 content)
 //                        for (QwSopCourseFinishTempSetting.Setting content : allContent) {
 //                        for (QwSopCourseFinishTempSetting.Setting content : allContent) {
@@ -255,10 +260,10 @@ public class SendAppMsg {
 //                qwSopLogs.setSend(true);
 //                qwSopLogs.setSend(true);
                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                 QwSopLogs updateQwSop = new QwSopLogs();
                 QwSopLogs updateQwSop = new QwSopLogs();
-                if (courseSendStatus&&txtSendStatus&&mp3SendStatus){
+                if (courseSendStatus&&txtSendStatus&&mp3SendStatus&&liveCardSendStatus){
                     updateQwSop.setAppSendRemark("APP全部发送成功");
                     updateQwSop.setAppSendRemark("APP全部发送成功");
                     updateQwSop.setAppSendStatus(1);
                     updateQwSop.setAppSendStatus(1);
-                }else if(!courseSendStatus&&!txtSendStatus&&!mp3SendStatus){
+                }else if(!courseSendStatus&&!txtSendStatus&&!mp3SendStatus&&!liveCardSendStatus){
                     updateQwSop.setAppSendRemark("APP全部发送失败");
                     updateQwSop.setAppSendRemark("APP全部发送失败");
                     updateQwSop.setAppSendStatus(2);
                     updateQwSop.setAppSendStatus(2);
                 }else {
                 }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")) {
             if (payload.containsKey("data")) {
                 sendMsgVo.setData(payload.getString("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 webSocketServer = SpringUtils.getBean(WebSocketServer.class);
             webSocketServer.broadcastLiveCmd(sendMsgVo);
             webSocketServer.broadcastLiveCmd(sendMsgVo);
             log.info("[LiveWsBroadcast] 已推送指令, liveId={}, cmd={}, status={}",
             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.Live;
 import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.domain.LiveConsoleOpLog;
+import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.service.ILiveCompletionCouponService;
 import com.fs.live.service.ILiveCompletionCouponService;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.LiveCompletionCouponInfoVO;
 import com.fs.live.vo.LiveCompletionCouponInfoVO;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.websocket.bean.SendMsgVo;
 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.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
+import java.util.HashSet;
 import java.util.List;
 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
 @Component
 public class LiveCompletionPointsTask {
 public class LiveCompletionPointsTask {
 
 
-    private static final String WATCH_DURATION_HASH_PREFIX = "live:watch:duration:hash:";
-
     @Autowired
     @Autowired
     private RedisCache redisCache;
     private RedisCache redisCache;
 
 
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
     @Autowired
     @Autowired
     private ILiveCompletionPointsRecordService completionPointsRecordService;
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
 
@@ -170,20 +176,29 @@ public class LiveCompletionPointsTask {
         for (Live live : activeLives) {
         for (Live live : activeLives) {
             try {
             try {
                 Long liveId = live.getLiveId();
                 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;
                     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 {
                     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);
                         handler.handle(liveId, userId, duration);
                     } catch (Exception e) {
                     } catch (Exception e) {
-                        log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
+                        log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, userId, e);
                     }
                     }
                 }
                 }
             } catch (Exception 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
     @FunctionalInterface
     private interface CompletionHandler {
     private interface CompletionHandler {
         void handle(Long liveId, Long userId, Long duration);
         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 {
 public class Task {
 
 
     private static final Logger log = LoggerFactory.getLogger(Task.class);
     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 ILiveService liveService;
 
 
     private final ILiveDataService liveDataService;
     private final ILiveDataService liveDataService;
@@ -93,9 +104,12 @@ public class Task {
     @Scheduled(cron = "0 0/1 * * * ?")
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveStatusByTime", scene = "task")
     @DistributeLock(key = "updateLiveStatusByTime", scene = "task")
     public void updateLiveStatusByTime() {
     public void updateLiveStatusByTime() {
+        try {
         List<Live> list = liveService.selectNoEndLiveList();
         List<Live> list = liveService.selectNoEndLiveList();
-        if (list.isEmpty())
+        if (list.isEmpty()) {
+            logTaskFinish("updateLiveStatusByTime", "无未结束直播间,跳过");
             return;
             return;
+        }
         List<Long> liveIdLists = list.stream().map(Live::getLiveId).collect(Collectors.toList());
         List<Long> liveIdLists = list.stream().map(Live::getLiveId).collect(Collectors.toList());
         List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectLiveAutoTaskByLiveIds(liveIdLists);
         List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectLiveAutoTaskByLiveIds(liveIdLists);
         List<Live> liveList = new ArrayList<>();
         List<Live> liveList = new ArrayList<>();
@@ -185,14 +199,7 @@ public class Task {
                 // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
                 // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
                 try {
                 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
                     // 如果视频时长大于0,将直播间信息存入Redis
                     if (videoDuration > 0 && live.getStartTime() != null) {
                     if (videoDuration > 0 && live.getStartTime() != null) {
@@ -245,11 +252,23 @@ public class Task {
             liveService.asyncToCache();
             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 * * * * ?")
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
     public void liveLotteryTask() {
     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:*";
         String lotteryKey = "live:lottery_task:*";
         Set<String> allLiveKeys = redisCache.redisTemplate.keys(lotteryKey);
         Set<String> allLiveKeys = redisCache.redisTemplate.keys(lotteryKey);
         if (allLiveKeys != null && !allLiveKeys.isEmpty()) {
         if (allLiveKeys != null && !allLiveKeys.isEmpty()) {
@@ -258,7 +277,9 @@ public class Task {
                 if (range == null || range.isEmpty()) {
                 if (range == null || range.isEmpty()) {
                     continue;
                     continue;
                 }
                 }
+                log.info("{} liveLotteryTask 处理抽奖: liveKey={}, 待执行数={}", LOG_PREFIX, liveKey, range.size());
                 processLotteryTask(range);
                 processLotteryTask(range);
+                lotteryProcessed += range.size();
                 redisCache.redisTemplate.opsForZSet()
                 redisCache.redisTemplate.opsForZSet()
                         .removeRangeByScore(liveKey, 0, currentTime);
                         .removeRangeByScore(liveKey, 0, currentTime);
             }
             }
@@ -266,34 +287,34 @@ public class Task {
 
 
         String redKey = "live:red_task:*";
         String redKey = "live:red_task:*";
         allLiveKeys = redisCache.redisTemplate.keys(redKey);
         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) {
     private List<LiveConsoleOpLog> updateRedStatus(Set<String> range) {
@@ -302,13 +323,21 @@ public class Task {
 
 
     private void processLotteryTask(Set<String> range) {
     private void processLotteryTask(Set<String> range) {
         List<LiveLotteryConfVo> liveLotteries = liveLotteryConfService.selectVoListByLotteryIds(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();
         Date now = new Date();
         for (LiveLotteryConfVo liveLottery : liveLotteries) {
         for (LiveLotteryConfVo liveLottery : liveLotteries) {
+            log.info("{} processLotteryTask 开始开奖: lotteryId={}, liveId={}",
+                    LOG_PREFIX, liveLottery.getLotteryId(), liveLottery.getLiveId());
             // 查询抽奖数量
             // 查询抽奖数量
             List<LiveLotteryProductListVo> products = liveLottery.getProducts();
             List<LiveLotteryProductListVo> products = liveLottery.getProducts();
             Integer totalLots = products.stream().mapToInt(liveLotteryProductListVo -> Math.toIntExact(liveLotteryProductListVo.getTotalLots())).sum();
             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());
             String hashKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveLottery.getLiveId(), liveLottery.getLotteryId());
             Map<Object, Object> hashEntries = redisCache.hashEntries(hashKey);
             Map<Object, Object> hashEntries = redisCache.hashEntries(hashKey);
@@ -322,7 +351,11 @@ public class Task {
 
 
             // 查询在线用户 并且参与了抽奖的用户
             // 查询在线用户 并且参与了抽奖的用户
             List<LiveWatchUser> liveWatchUsers = liveWatchUserService.selectLiveWatchAndRegisterUser(liveLottery.getLiveId(),liveLottery.getLotteryId());
             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;
             LiveLotteryRegistration liveLotteryRegistration;
             // 收集中奖信息
             // 收集中奖信息
             List<LotteryVo> lotteryVos = new ArrayList<>();
             List<LotteryVo> lotteryVos = new ArrayList<>();
@@ -370,63 +403,67 @@ public class Task {
             sendMsgVo.setLiveId(liveLottery.getLiveId());
             sendMsgVo.setLiveId(liveLottery.getLiveId());
             sendMsgVo.setCmd("LotteryDetail");
             sendMsgVo.setCmd("LotteryDetail");
             sendMsgVo.setData(JSON.toJSONString(lotteryVos));
             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)));
             webSocketServer.broadcastMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
 
 
             liveService.asyncToCacheLiveConfig(liveLottery.getLiveId());
             liveService.asyncToCacheLiveConfig(liveLottery.getLiveId());
             // 删除缓存 同步抽奖记录
             // 删除缓存 同步抽奖记录
             redisCache.deleteObject(hashKey);
             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());
         List<Long> collect = liveLotteries.stream().map(LiveLotteryConfVo::getLotteryId).collect(Collectors.toList());
         liveLotteryConfService.finishStatusByLotteryIds(collect);
         liveLotteryConfService.finishStatusByLotteryIds(collect);
+        log.info("{} processLotteryTask 更新抽奖状态完成: lotteryIds={}", LOG_PREFIX, collect);
     }
     }
 
 
     @Scheduled(cron = "0/1 * * * * ?")
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveAutoTask", scene = "task")
     @DistributeLock(key = "liveAutoTask", scene = "task")
     public void liveAutoTask() {
     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:*");
         Set<String> allLiveKeys = redisCache.redisTemplate.keys("live:auto_task:*");
         if (allLiveKeys == null || allLiveKeys.isEmpty()) {
         if (allLiveKeys == null || allLiveKeys.isEmpty()) {
-            return; // 没有数据,直接返回
+            return;
         }
         }
-        // 2. 遍历每个直播间的ZSet键
         for (String liveKey : allLiveKeys) {
         for (String liveKey : allLiveKeys) {
-            // 3. 获取当前直播间ZSet中所有元素(按score排序)
-            // range方法:0表示第一个元素,-1表示最后一个元素,即获取全部
             Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
             Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
             if (range == null || range.isEmpty()) {
             if (range == null || range.isEmpty()) {
-                log.info("当前直播间没有数据,跳过处理");
-                continue; // 没有数据,直接返回
+                continue;
             }
             }
+            log.info("{} liveAutoTask 处理直播间任务: liveKey={}, 待执行数={}, 截止时间戳={}",
+                    LOG_PREFIX, liveKey, range.size(), currentTime);
             redisCache.redisTemplate.opsForZSet()
             redisCache.redisTemplate.opsForZSet()
                     .removeRangeByScore(liveKey, 0, currentTime);
                     .removeRangeByScore(liveKey, 0, currentTime);
             processAutoTask(range);
             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) {
     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")
     @DistributeLock(key = "autoUpdateWatchReward", scene = "task")
     @Transactional
     @Transactional
     public void autoUpdateWatchReward() {
     public void autoUpdateWatchReward() {
+        int rewardLiveCount = 0;
+        int rewardedUserCount = 0;
+        try {
 
 
         // 1.查询所有直播中的直播间
         // 1.查询所有直播中的直播间
         List<Live> lives = liveService.liveList();
         List<Live> lives = liveService.liveList();
-
+        if (lives == null || lives.isEmpty()) {
+            logTaskFinish("autoUpdateWatchReward", "无直播中直播间,跳过");
+            return;
+        }
 
 
         // 2.检查是否开启观看奖励
         // 2.检查是否开启观看奖励
         List<Live> openRewardLives = lives.stream().filter(live -> StringUtils.isNotEmpty(live.getConfigJson())).collect(Collectors.toList());
         List<Live> openRewardLives = lives.stream().filter(live -> StringUtils.isNotEmpty(live.getConfigJson())).collect(Collectors.toList());
+        if (openRewardLives.isEmpty()) {
+            logTaskFinish("autoUpdateWatchReward", "无观看奖励配置,跳过");
+            return;
+        }
         Date now = new Date();
         Date now = new Date();
 
 
         for (Live openRewardLive : openRewardLives) {
         for (Live openRewardLive : openRewardLives) {
             String configJson = openRewardLive.getConfigJson();
             String configJson = openRewardLive.getConfigJson();
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
             if (!config.getEnabled() || config.getParticipateCondition() == null || config.getAction() == null) {
             if (!config.getEnabled() || config.getParticipateCondition() == null || config.getAction() == null) {
+                log.info("{} autoUpdateWatchReward 配置未启用或缺失: liveId={}", LOG_PREFIX, openRewardLive.getLiveId());
                 continue;
                 continue;
             }
             }
             // 只处理 "达到指定观看时长" 的参与条件
             // 只处理 "达到指定观看时长" 的参与条件
             if (1 != config.getParticipateCondition()) {
             if (1 != config.getParticipateCondition()) {
+                log.info("{} autoUpdateWatchReward 参与条件非观看时长: liveId={}, condition={}",
+                        LOG_PREFIX, openRewardLive.getLiveId(), config.getParticipateCondition());
                 continue;
                 continue;
             }
             }
 
 
             List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
             List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
             if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
             if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+                log.info("{} autoUpdateWatchReward 无待发放用户: liveId={}", LOG_PREFIX, openRewardLive.getLiveId());
                 continue;
                 continue;
             }
             }
             // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
             // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
             List<LiveWatchUser> onlineUser = liveWatchUsers
             List<LiveWatchUser> onlineUser = liveWatchUsers
                     .stream().filter(user -> (now.getTime() - user.getUpdateTime().getTime() + (user.getOnlineSeconds() == null ? 0L : user.getOnlineSeconds())) > config.getWatchDuration() * 60 * 1000)
                     .stream().filter(user -> (now.getTime() - user.getUpdateTime().getTime() + (user.getOnlineSeconds() == null ? 0L : user.getOnlineSeconds())) > config.getWatchDuration() * 60 * 1000)
                     .collect(Collectors.toList());
                     .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());
             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 类型处理不同的奖励
             // 根据 action 类型处理不同的奖励
             Long action = config.getAction();
             Long action = config.getAction();
@@ -488,6 +544,10 @@ public class Task {
                     );
                     );
                     userIds.forEach(userId -> webSocketServer.sendIntegralMessage(
                     userIds.forEach(userId -> webSocketServer.sendIntegralMessage(
                             openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog));
                             openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog));
+                    rewardLiveCount++;
+                    rewardedUserCount += userIds.size();
+                    log.info("{} autoUpdateWatchReward 积分发放完成: liveId={}, 用户数={}",
+                            LOG_PREFIX, openRewardLive.getLiveId(), userIds.size());
                     break;
                     break;
 
 
                 case 3: // 优惠券
                 case 3: // 优惠券
@@ -512,15 +572,28 @@ public class Task {
                                 watchCouponOpLog.getId(), openRewardLive.getLiveId(), couponRelations);
                                 watchCouponOpLog.getId(), openRewardLive.getLiveId(), couponRelations);
                         couponRelations.forEach(relation -> sendCouponRewardMessage(
                         couponRelations.forEach(relation -> sendCouponRewardMessage(
                                 openRewardLive.getLiveId(), relation.getUserId(), watchRewardCoupon, watchCouponOpLog));
                                 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;
                     break;
 
 
                 case 1: // 现金红包 - 暂不处理(现有逻辑)
                 case 1: // 现金红包 - 暂不处理(现有逻辑)
                 default:
                 default:
-                    log.info("观看奖励类型 {} 暂不处理,liveId={}", action, openRewardLive.getLiveId());
+                    log.info("{} autoUpdateWatchReward 奖励类型暂不处理: action={}, liveId={}",
+                            LOG_PREFIX, action, openRewardLive.getLiveId());
                     break;
                     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 * * * ?")// 每分钟执行一次
     @Scheduled(cron = "0 0/1 * * * ?")// 每分钟执行一次
     public void syncLiveDataToDB() {
     public void syncLiveDataToDB() {
+        int syncCount = 0;
+        int couponSyncCount = 0;
+        try {
         List<LiveData> liveDatas = liveDataService.getAllLiveDatas(); // 获取所有正在直播的直播间数据
         List<LiveData> liveDatas = liveDataService.getAllLiveDatas(); // 获取所有正在直播的直播间数据
-        if(liveDatas == null)
+        if (liveDatas == null || liveDatas.isEmpty()) {
+            logTaskFinish("syncLiveDataToDB", "无直播数据,跳过");
             return;
             return;
+        }
         liveDatas.forEach(liveData ->{
         liveDatas.forEach(liveData ->{
 
 
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveData.getLiveId());
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveData.getLiveId());
@@ -862,9 +940,8 @@ public class Task {
         if(!liveDatas.isEmpty())
         if(!liveDatas.isEmpty())
             for (LiveData liveData : liveDatas) {
             for (LiveData liveData : liveDatas) {
                 liveDataService.updateLiveData(liveData);
                 liveDataService.updateLiveData(liveData);
+                syncCount++;
             }
             }
-            /*// 更新数据库
-            liveDataService.updateLiveData(liveData);*/
         Set<String> keys = redisCache.redisTemplate.keys(String.format(LIVE_COUPON_NUM, "*"));
         Set<String> keys = redisCache.redisTemplate.keys(String.format(LIVE_COUPON_NUM, "*"));
         if (keys != null && !keys.isEmpty()) {
         if (keys != null && !keys.isEmpty()) {
             for (String key : keys) {
             for (String key : keys) {
@@ -874,9 +951,15 @@ public class Task {
                     updateEntity.setId(Long.valueOf(key));
                     updateEntity.setId(Long.valueOf(key));
                     updateEntity.setRemainCount(Long.parseLong(o.toString()));
                     updateEntity.setRemainCount(Long.parseLong(o.toString()));
                     liveCouponIssueService.updateLiveCouponIssue(updateEntity);
                     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 * * * * ?")
     @Scheduled(cron = "0/5 * * * * ?")
     @DistributeLock(key = "updateRedQuantityNum", scene = "task")
     @DistributeLock(key = "updateRedQuantityNum", scene = "task")
     public void updateRedQuantityNum() {
     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 * * * * ?")
     @Scheduled(cron = "0/10 * * * * ?")
     @DistributeLock(key = "scanLiveTagMark", scene = "task")
     @DistributeLock(key = "scanLiveTagMark", scene = "task")
     public void scanLiveTagMark() {
     public void scanLiveTagMark() {
+        int processedCount = 0;
         try {
         try {
 
 
             // 获取所有打标签缓存的key
             // 获取所有打标签缓存的key
@@ -1040,9 +1128,11 @@ public class Task {
                         processedLiveIds.add(liveId);
                         processedLiveIds.add(liveId);
                         // 调用打标签方法
                         // 调用打标签方法
                         liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
                         liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+                        processedCount++;
+                        log.info("{} scanLiveTagMark 打标签完成: liveId={}", LOG_PREFIX, liveId);
                     }
                     }
                 } catch (Exception e) {
                 } 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);
                     String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, liveId);
                     redisCache.deleteObject(tagMarkKey);
                     redisCache.deleteObject(tagMarkKey);
                 } catch (Exception e) {
                 } 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) {
         } catch (Exception e) {
-            log.error("扫描直播间打标签任务异常: error={}", e.getMessage(), e);
+            logTaskError("scanLiveTagMark", e);
         }
         }
     }
     }
 
 
@@ -1067,11 +1159,14 @@ public class Task {
     @Scheduled(cron = "0/30 * * * * ?")
     @Scheduled(cron = "0/30 * * * * ?")
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
     public void scanLiveWatchUserStatus() {
+        int processedLiveCount = 0;
+        int updatedLogCount = 0;
         try {
         try {
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
             if (activeLives == null || activeLives.isEmpty()) {
+                logTaskFinish("scanLiveWatchUserStatus", "无活跃直播间,跳过");
                 return;
                 return;
             }
             }
             for (Live live : activeLives) {
             for (Live live : activeLives) {
@@ -1096,15 +1191,7 @@ public class Task {
                     if (onlineUsers == null || onlineUsers.isEmpty()) {
                     if (onlineUsers == null || onlineUsers.isEmpty()) {
                         continue;
                         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<>();
                     List<LiveWatchLog> updateLog = new ArrayList<>();
@@ -1163,15 +1250,22 @@ public class Task {
                         for (LiveWatchLog liveWatchLog : updateLog) {
                         for (LiveWatchLog liveWatchLog : updateLog) {
                             redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
                             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) {
                 } 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) {
         } catch (Exception e) {
-            log.error("实时扫描用户直播数据任务异常: error={}", e.getMessage(), e);
+            logTaskError("scanLiveWatchUserStatus", e);
         }
         }
     }
     }
 
 
@@ -1231,8 +1325,8 @@ public class Task {
                 }
                 }
             }
             }
         } catch (Exception e) {
         } 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 * * * ?")
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
     @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
     public void updateLiveWatchUserStatus() {
     public void updateLiveWatchUserStatus() {
+        int updatedLogCount = 0;
         try {
         try {
             Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
             Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
             LocalDateTime now = LocalDateTime.now();
             LocalDateTime now = LocalDateTime.now();
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             List<LiveWatchLog> updateLog = new ArrayList<>();
             List<LiveWatchLog> updateLog = new ArrayList<>();
-            if (keys != null && !keys.isEmpty()) {
+            if (keys == null || keys.isEmpty()) {
+                logTaskFinish("updateLiveWatchUserStatus", "无用户活跃缓存,跳过");
+                return;
+            }
                 for (String key : keys) {
                 for (String key : keys) {
                     String[] split = key.split(":");
                     String[] split = key.split(":");
                     String cacheTime = redisCache.getCacheObject(key);
                     String cacheTime = redisCache.getCacheObject(key);
@@ -1277,7 +1375,8 @@ public class Task {
                                 }
                                 }
                             }
                             }
                         } catch (Exception e) {
                         } 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) {
                     for (LiveWatchLog liveWatchLog : updateLog) {
                         redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
                         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) {
         } catch (Exception ex) {
-            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+            logTaskError("updateLiveWatchUserStatus", ex);
         }
         }
     }
     }
 
 
@@ -1313,7 +1414,7 @@ public class Task {
 //            List<Live> activeLives = liveService.selectNoEndLiveList();
 //            List<Live> activeLives = liveService.selectNoEndLiveList();
 //
 //
 //            if (activeLives == null || activeLives.isEmpty()) {
 //            if (activeLives == null || activeLives.isEmpty()) {
-//                log.debug("当前没有活跃的直播间");
+//                log.info("当前没有活跃的直播间");
 //                return;
 //                return;
 //            }
 //            }
 //
 //
@@ -1405,4 +1506,18 @@ public class Task {
         }
         }
         return title + ",发放" + userCount + "人";
         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);
         msg.setStatus(status);
         Long couponIssueId = jsonObject.getLong("couponIssueId");
         Long couponIssueId = jsonObject.getLong("couponIssueId");
         // ①  检查  缓存是否存在  ② 如果是发布 放入缓存 ③ 删除缓存
         // ①  检查  缓存是否存在  ② 如果是发布 放入缓存 ③ 删除缓存
-        if (status == 1) {
+        if (status != null && status == 1) {
             Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
             Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
             if (cacheObject == null) {
             if (cacheObject == null) {
                 LiveCouponIssue liveCoupon = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
                 LiveCouponIssue liveCoupon = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
@@ -671,7 +671,8 @@ public class WebSocketServer {
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
         }
         }
         LiveConsoleOpLog opLog = saveConsoleCouponOpLog(liveId, couponIssueId, status);
         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);
         enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
     }
@@ -698,13 +699,14 @@ public class WebSocketServer {
     private void processRed(Long liveId, SendMsgVo msg) {
     private void processRed(Long liveId, SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
         Integer status = jsonObject.getInteger("status");
-        msg.setStatus( status);
+        msg.setStatus(status);
         LiveRedConf liveRedConf = liveRedConfService.selectLiveRedConfByRedId(jsonObject.getLong("redId"));
         LiveRedConf liveRedConf = liveRedConfService.selectLiveRedConfByRedId(jsonObject.getLong("redId"));
         if (Objects.nonNull(liveRedConf)) {
         if (Objects.nonNull(liveRedConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveRedConf));
             msg.setData(JSONObject.toJSONString(liveRedConf));
             LiveConsoleOpLog opLog = saveConsoleRedOpLog(liveId, liveRedConf, status);
             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);
             enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
         }
@@ -716,90 +718,59 @@ public class WebSocketServer {
     private void processLottery(Long liveId, SendMsgVo msg) {
     private void processLottery(Long liveId, SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
         Integer status = jsonObject.getInteger("status");
-        msg.setStatus( status);
+        msg.setStatus(status);
         LiveLotteryConf liveLotteryConf = liveLotteryConfService.selectLiveLotteryConfByLotteryId(jsonObject.getLong("lotteryId"));
         LiveLotteryConf liveLotteryConf = liveLotteryConfService.selectLiveLotteryConfByLotteryId(jsonObject.getLong("lotteryId"));
         if (Objects.nonNull(liveLotteryConf)) {
         if (Objects.nonNull(liveLotteryConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveLotteryConf));
             msg.setData(JSONObject.toJSONString(liveLotteryConf));
             LiveConsoleOpLog opLog = saveConsoleLotteryOpLog(liveId, liveLotteryConf, status);
             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);
             enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
         }
     }
     }
 
 
     /**
     /**
-     * 中控台红包:仅结算时挂载 REST/定时任务已写入的留存,不在 WebSocket 侧新建记录
+     * 中控台红包:仅开始(含暂停后再开始)写入发放留存
      */
      */
     private LiveConsoleOpLog saveConsoleRedOpLog(Long liveId, LiveRedConf liveRedConf, Integer status) {
     private LiveConsoleOpLog saveConsoleRedOpLog(Long liveId, LiveRedConf liveRedConf, Integer status) {
-        if (liveRedConf == null || status == null) {
+        if (liveRedConf == null || status == null || status != 1) {
             return null;
             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) {
     private LiveConsoleOpLog saveConsoleLotteryOpLog(Long liveId, LiveLotteryConf liveLotteryConf, Integer status) {
-        if (liveLotteryConf == null || status == null) {
+        if (liveLotteryConf == null || status == null || status != 1) {
             return null;
             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 {
             try {
                 SendMsgVo msg = JSONObject.parseObject(text, SendMsgVo.class);
                 SendMsgVo msg = JSONObject.parseObject(text, SendMsgVo.class);
                 if (msg != null) {
                 if (msg != null) {
+                    normalizeNestedMessageData(text, msg);
                     fillMessageDefaults(msg, liveId, userId, userType);
                     fillMessageDefaults(msg, liveId, userId, userType);
                     if (StringUtils.isEmpty(msg.getCmd()) && StringUtils.isNotEmpty(msg.getMsg())) {
                     if (StringUtils.isEmpty(msg.getCmd()) && StringUtils.isNotEmpty(msg.getMsg())) {
                         msg.setCmd("sendMsg");
                         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) {
     public void sendIntegralMessage(Long liveId, Long userId, Long scoreAmount) {
         sendIntegralMessage(liveId, userId, scoreAmount, null);
         sendIntegralMessage(liveId, userId, scoreAmount, null);
     }
     }
@@ -1444,6 +1475,14 @@ public class WebSocketServer {
                 msg.setData(JSON.toJSONString(liveRedConf));
                 msg.setData(JSON.toJSONString(liveRedConf));
                 liveRedConfService.updateLiveRedConf(liveRedConf);
                 liveRedConfService.updateLiveRedConf(liveRedConf);
                 liveService.asyncToCacheLiveConfig(task.getLiveId());
                 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) {
             }else if (task.getTaskType() == 4L) {
                 msg.setCmd("lottery");
                 msg.setCmd("lottery");
                 LiveLotteryConf liveLotteryConf = JSON.parseObject(task.getContent(), LiveLotteryConf.class);
                 LiveLotteryConf liveLotteryConf = JSON.parseObject(task.getContent(), LiveLotteryConf.class);
@@ -1456,6 +1495,14 @@ public class WebSocketServer {
                 msg.setData(JSON.toJSONString(liveLotteryConf));
                 msg.setData(JSON.toJSONString(liveLotteryConf));
                 liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
                 liveLotteryConfService.updateLiveLotteryConf(liveLotteryConf);
                 liveService.asyncToCacheLiveConfig(task.getLiveId());
                 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) {
             }else if (task.getTaskType() == 3L) {
                 msg.setCmd("sendMsg");
                 msg.setCmd("sendMsg");
                 msg.setMsg(task.getContent());
                 msg.setMsg(task.getContent());
@@ -1729,14 +1776,7 @@ public class WebSocketServer {
                                                    Long companyUserId, Long onlineSeconds) {
                                                    Long companyUserId, Long onlineSeconds) {
         try {
         try {
             // 获取直播视频总时长(videoType = 1 的视频,使用带缓存的查询方法)
             // 获取直播视频总时长(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
             LiveWatchLog queryLog = new 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 appActivitlLink = "/pages_course/activity.html?link=";
     private static final String registeredRealLink = "/pages_course/register.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 h5LiveShortLink = "/pages_course/livingInvite.html?s=";
+    private static final String appLiveShortLink = "/pages_live/livingList?link=";
 
 
 
 
 //    private static final String miniappRealLink = "/pages/index/index?course=";
 //    private static final String miniappRealLink = "/pages/index/index?course=";
@@ -1219,6 +1220,19 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     setting.setMiniprogramPage(recordShortH5Link);
                     setting.setMiniprogramPage(recordShortH5Link);
                     break;
                     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:
                 default:
                     break;
                     break;
             }
             }
@@ -1477,6 +1491,19 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     setting.setMiniprogramPage(liveShortH5Link);
                     setting.setMiniprogramPage(liveShortH5Link);
                     break;
                     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:
                 default:
                     break;
                     break;
             }
             }
@@ -1837,6 +1864,18 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
                     setting.setMiniprogramPage(shortH5Link);
                     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;
                     break;
                 default:
                 default:
                     break;
                     break;
@@ -1883,6 +1922,31 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         return link.getRealLink();
         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,
     private String getAppIdFromMiniMap(Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                        String companyId,
                                        String companyId,
                                        int sendMsgType,
                                        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
      * @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);
     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;
 //    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 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);
     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);
     R accountCheck(String userId, String type);
     void checkAndImportFriend(Long companyUserId,String fsUserId);
     void checkAndImportFriend(Long companyUserId,String fsUserId);
     OpenImResponseDTO sendCourse(Long userId,Long companyUserId,String url,String title,String linkImageUrl,String cropId) throws JsonProcessingException;
     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);
     void checkAndImportFriendByDianBo(Long companyUserId,String fsUserId,String cropId, boolean isUpdate);
 
 
     OpenImResponseDTO updateUserInfo(CompanyUser companyUser);
     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;
         content = null;
         return openImResponseDTO;
         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
     @Override
     public OpenImResponseDTO sendPackageUtil(String sendID, String recvID, Integer contentType, String payloadData,String packageName,String packageId){
     public OpenImResponseDTO sendPackageUtil(String sendID, String recvID, Integer contentType, String payloadData,String packageName,String packageId){
         try {
         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);
     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);
     List<LiveConsoleOpLog> selectLiveConsoleOpLogList(LiveConsoleOpLog liveConsoleOpLog);
 
 
+    LiveConsoleOpLog selectLiveConsoleOpLogById(Long id);
+
     LiveConsoleOpLog saveLog(Long liveId, Integer opType, Integer handleType, Long bizId, String bizName);
     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 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);
     LiveConsoleOpLog saveCouponShowLog(Long liveId, Long couponIssueId, LiveCoupon coupon, Integer handleType);
 
 
     void bindOpLogUsers(Long opLogId, Long liveId, List<LiveConsoleOpLogUser> relations);
     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);
     void clearLiveFlagCache(Long liveId);
 
 
     List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser);
     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.Calendar;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.List;
 import java.util.List;
 import java.util.Objects;
 import java.util.Objects;
 import java.util.Set;
 import java.util.Set;
@@ -68,6 +70,14 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         return liveConsoleOpLogMapper.selectLiveConsoleOpLogList(liveConsoleOpLog);
         return liveConsoleOpLogMapper.selectLiveConsoleOpLogList(liveConsoleOpLog);
     }
     }
 
 
+    @Override
+    public LiveConsoleOpLog selectLiveConsoleOpLogById(Long id) {
+        if (id == null) {
+            return null;
+        }
+        return liveConsoleOpLogMapper.selectLiveConsoleOpLogById(id);
+    }
+
     @Override
     @Override
     public LiveConsoleOpLog saveLog(Long liveId, Integer opType, Integer handleType, Long bizId, String bizName) {
     public LiveConsoleOpLog saveLog(Long liveId, Integer opType, Integer handleType, Long bizId, String bizName) {
         LiveConsoleOpLog log = new LiveConsoleOpLog();
         LiveConsoleOpLog log = new LiveConsoleOpLog();
@@ -128,6 +138,38 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         return saveLog(liveId, LiveConsoleOpLog.OP_RED_SETTLE, handleType, redId, bizName);
         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
     @Override
     public LiveConsoleOpLog saveCouponShowLog(Long liveId, Long couponIssueId, LiveCoupon coupon, Integer handleType) {
     public LiveConsoleOpLog saveCouponShowLog(Long liveId, Long couponIssueId, LiveCoupon coupon, Integer handleType) {
         int opType = isVerifyCouponType(coupon)
         int opType = isVerifyCouponType(coupon)
@@ -201,15 +243,53 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         }
         }
 
 
         Date now = DateUtils.getNowDate();
         Date now = DateUtils.getNowDate();
+        Map<Long, Long> couponTypeMap = resolveCouponTypeMap(opLogs);
         List<LiveConsoleOpLogRecordVo> result = new ArrayList<>(opLogs.size());
         List<LiveConsoleOpLogRecordVo> result = new ArrayList<>(opLogs.size());
         for (LiveConsoleOpLog opLog : opLogs) {
         for (LiveConsoleOpLog opLog : opLogs) {
             boolean claimed = opLog.getId() != null && claimedOpLogIds.contains(opLog.getId());
             boolean claimed = opLog.getId() != null && claimedOpLogIds.contains(opLog.getId());
             int status = resolveOpLogStatus(opLog, claimed, now);
             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;
         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
     @Override
     public int insertLiveCouponEntity(Map<String, Object> payload) {
     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);
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if(live == null) return -1;
         if(live == null) return -1;
         String s = String.valueOf(payload.get("couponIds"));
         String s = String.valueOf(payload.get("couponIds"));
@@ -221,7 +221,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
 
 
     @Override
     @Override
     public R handleIsShowChange(Map<String, Object> payload) {
     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);
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if(live == null) return R.error("直播间不存在");
         if(live == null) return R.error("直播间不存在");
         boolean isShow = Boolean.parseBoolean(payload.get("isShow").toString());
         boolean isShow = Boolean.parseBoolean(payload.get("isShow").toString());
@@ -245,8 +245,9 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         if (isShow) {
         if (isShow) {
             // updateChangeShow 会自动收起其它券,仅保留当前展示的一张
             // updateChangeShow 会自动收起其它券,仅保留当前展示的一张
             liveCouponMapper.updateChangeShow(liveId, couponIssueId);
             liveCouponMapper.updateChangeShow(liveId, couponIssueId);
-            liveConsoleOpLogService.saveCouponShowLog(
+            LiveConsoleOpLog opLog = liveConsoleOpLogService.saveCouponShowLog(
                     liveId, couponIssueId, liveCoupon, LiveConsoleOpLog.HANDLE_CONSOLE);
                     liveId, couponIssueId, liveCoupon, LiveConsoleOpLog.HANDLE_CONSOLE);
+            return R.ok("操作成功").put("opLogId", opLog != null ? opLog.getId() : null);
         } else {
         } else {
             liveCouponMapper.updateShow(liveId, couponIssueId, 0);
             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.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
-import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryRegistration;
 import com.fs.live.domain.LiveLotteryRegistration;
 import com.fs.live.mapper.LiveLotteryConfMapper;
 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.LiveLotteryProductSaveParam;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.service.ILiveAutoTaskService;
 import com.fs.live.service.ILiveAutoTaskService;
-import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.vo.LiveLotteryConfVo;
 import com.fs.live.vo.LiveLotteryConfVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
@@ -56,8 +54,6 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
     @Autowired
     @Autowired
     private ILiveAutoTaskService liveAutoTaskService;
     private ILiveAutoTaskService liveAutoTaskService;
-    @Autowired
-    private ILiveConsoleOpLogService liveConsoleOpLogService;
     /**
     /**
      * 查询直播抽奖配置
      * 查询直播抽奖配置
      *
      *
@@ -120,15 +116,6 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
             redisCache.deleteObject(cacheKey);
             redisCache.deleteObject(cacheKey);
         }
         }
         int rows = baseMapper.updateLiveLotteryConf(liveLotteryConf);
         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);
         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()));
             redStatusUpdate(CollUtil.newHashSet(liveRedConf.getRedId()));
         }
         }
         int rows = baseMapper.updateLiveRedConf(liveRedConf);
         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;
         return rows;
     }
     }
 
 
@@ -426,27 +417,15 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
 
 
     @Override
     @Override
     public List<LiveConsoleOpLog> finishRedStatusBySetIds(Set<String> range) {
     public List<LiveConsoleOpLog> finishRedStatusBySetIds(Set<String> range) {
-        List<LiveConsoleOpLog> opLogs = new ArrayList<>();
         try {
         try {
             log.info("开始结束红包状态:{}",range);
             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);
             baseMapper.finishRedStatusBySetIds(range);
             redStatusUpdate(range.stream().map(Long::valueOf).collect(Collectors.toSet()));
             redStatusUpdate(range.stream().map(Long::valueOf).collect(Collectors.toSet()));
             log.info("结束红包状态完成");
             log.info("结束红包状态完成");
         }catch (Exception e){
         }catch (Exception e){
             log.info("红包状态结束异常",e);
             log.info("红包状态结束异常",e);
         }
         }
-        return opLogs;
+        return Collections.emptyList();
     }
     }
 
 
     @Override
     @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缓存键
         // Redis缓存键
         String cacheKey = "live:user:first:entry:" + liveId + ":" + userId;
         String cacheKey = "live:user:first:entry:" + liveId + ":" + userId;
         
         
-        // 先从缓存中获取
-        LiveUserFirstEntry cachedEntry = redisCache.getCacheObject(cacheKey);
+        LiveUserFirstEntry cachedEntry = redisCache.getCacheObject(cacheKey, LiveUserFirstEntry.class);
         if (cachedEntry != null) {
         if (cachedEntry != null) {
             return cachedEntry;
             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
     @Override
     public List<LiveVideo> listByLiveIdWithCache(Long liveId, Integer type) {
     public List<LiveVideo> listByLiveIdWithCache(Long liveId, Integer type) {
-        // Redis缓存键
         String cacheKey = "live:video:list:" + liveId + ":" + type;
         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()) {
         if (cachedVideos != null && !cachedVideos.isEmpty()) {
             return cachedVideos;
             return cachedVideos;
         }
         }
+        if (redisCache.hasKey(cacheKey)) {
+            redisCache.deleteObject(cacheKey);
+        }
 
 
-        // 缓存未命中,从数据库查询
         List<LiveVideo> videos = liveVideoMapper.selectByIdAndType(liveId, type);
         List<LiveVideo> videos = liveVideoMapper.selectByIdAndType(liveId, type);
-
-        // 将查询结果存入缓存,缓存时间1小时
-        if (videos != null) {
+        if (videos != null && !videos.isEmpty()) {
             redisCache.setCacheObject(cacheKey, videos, 1, TimeUnit.HOURS);
             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;
             return null;
         }
         }
         
         
-        // 先从缓存中获取
         String cacheKey = String.format(LIVE_WATCH_LOG_CACHE_KEY, logId);
         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) {
         if (cachedLog != null) {
             return cachedLog;
             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);
         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
     @Override
     public Long getTotalWatchDuration(Long liveId, Long userId) {
     public Long getTotalWatchDuration(Long liveId, Long userId) {
+        if (liveId == null || userId == null) {
+            return 0L;
+        }
         try {
         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={}",
             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;
             return totalDuration;
         } catch (Exception e) {
         } 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
     @Override
     public Long getUserWatchDuration(Long liveId, Long userId) {
     public Long getUserWatchDuration(Long liveId, Long userId) {
         Long total = getTotalWatchDuration(liveId, 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;
     private String statusName;
 
 
+    /** 优惠券类型(opType=8完课优惠券、10观看奖励优惠券时返回,对应 live_coupon.type:0普通 1套餐 2制单 3无门槛) */
+    private Long couponType;
+
     public static LiveConsoleOpLogRecordVo from(LiveConsoleOpLog log, int status) {
     public static LiveConsoleOpLogRecordVo from(LiveConsoleOpLog log, int status) {
         LiveConsoleOpLogRecordVo vo = new LiveConsoleOpLogRecordVo();
         LiveConsoleOpLogRecordVo vo = new LiveConsoleOpLogRecordVo();
         if (log == null) {
         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;
         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版
      * 异步录入 发送有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);
                                     log.error("APP直播模板解析失败:" + e);
                                 }
                                 }
                                 break;
                                 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:
                             default:
                                 break;
                                 break;
                         }
                         }
@@ -1423,6 +1439,29 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             }
                             }
 
 
                             break;
                             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:
                         default:
                             break;
                             break;
 
 
@@ -2088,6 +2127,29 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         log.error("浏览器看课模板解析失败:" + e);
                         log.error("浏览器看课模板解析失败:" + e);
                     }
                     }
                     break;
                     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:
                 default:
                     break;
                     break;
 
 
@@ -2357,6 +2419,65 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return updateTime;
         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,
     public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                            String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
                                            String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
         // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
         // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties

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

@@ -40,6 +40,24 @@
         </where>
         </where>
     </select>
     </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">
     <select id="selectLiveWatchUserById" parameterType="Long" resultMap="LiveWatchUserResult">
         <include refid="selectLiveWatchUserVo"/>
         <include refid="selectLiveWatchUserVo"/>
         where id = #{id}
         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.setPhone(param.getPhone());
             user.setNickName("app用户" + param.getPhone().substring(param.getPhone().length() - 4));
             user.setNickName("app用户" + param.getPhone().substring(param.getPhone().length() - 4));
             user.setStatus(1);
             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.setPassword(Md5Utils.hash(param.getPassword()));
             user.setCreateTime(new Date());
             user.setCreateTime(new Date());
 
 
@@ -197,7 +197,7 @@ public class AppLoginController extends AppBaseController{
         user.setSource(map.get("source"));
         user.setSource(map.get("source"));
         user.setNickName("app用户" + phone.substring(phone.length() - 4));
         user.setNickName("app用户" + phone.substring(phone.length() - 4));
         user.setStatus(1);
         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.setPassword(Md5Utils.hash(password));
         user.setCreateTime(new Date());
         user.setCreateTime(new Date());
         if (userService.insertFsUser(user) > 0) {
         if (userService.insertFsUser(user) > 0) {
@@ -372,7 +372,7 @@ public class AppLoginController extends AppBaseController{
         newUser.setNickName("苹果用户" + param.getPhone().substring(param.getPhone().length() - 4));
         newUser.setNickName("苹果用户" + param.getPhone().substring(param.getPhone().length() - 4));
         newUser.setCreateTime(new Date());
         newUser.setCreateTime(new Date());
         newUser.setStatus(1);
         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())) {
         if (StringUtils.isNotEmpty(param.getJpushId())) {
             newUser.setJpushId(param.getJpushId());
             newUser.setJpushId(param.getJpushId());
         }
         }
@@ -883,7 +883,7 @@ public class AppLoginController extends AppBaseController{
         newUser.setPhone(param.getPhone());
         newUser.setPhone(param.getPhone());
         newUser.setCreateTime(new Date());
         newUser.setCreateTime(new Date());
         newUser.setStatus(1);
         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())) {
         if (StringUtils.isNotEmpty(param.getJpushId())) {
             newUser.setJpushId(param.getJpushId());
             newUser.setJpushId(param.getJpushId());
         }
         }