Pārlūkot izejas kodu

1、定时完课发送优惠卷

yys 1 nedēļu atpakaļ
vecāks
revīzija
be854c0344

+ 17 - 10
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -1,7 +1,6 @@
 package com.fs.live.task;
 
 import com.alibaba.fastjson.JSONObject;
-import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveCompletionCouponService;
@@ -25,6 +24,8 @@ import java.util.Map;
 @Component
 public class LiveCompletionPointsTask {
 
+    private static final String WATCH_DURATION_HASH_PREFIX = "live:watch:duration:hash:";
+
     @Autowired
     private RedisCache redisCache;
 
@@ -63,8 +64,8 @@ public class LiveCompletionPointsTask {
     }
 
     /**
-     * 定时检查观看时长并推送完课优惠券今日问题弹窗(兜底机制
-     * 每分钟执行一次
+     * 定时检查观看时长并推送完课优惠券「今日问题」弹窗(兜底,仅完课优惠券业务
+     * 每分钟执行一次;未配置课题时静默跳过,不影响其他奖励逻辑
      */
     @Scheduled(cron = "0 */1 * * * ?")
     public void checkCompletionCouponStatus() {
@@ -78,9 +79,12 @@ public class LiveCompletionPointsTask {
 
             processCompletionByWatchDuration(activeLives, (liveId, userId, duration) -> {
                 LiveCompletionCouponNotifyResult notifyResult =
-                        completionCouponService.checkAndNotifyCompletionCoupon(liveId, userId, duration);
-                if (notifyResult != null && notifyResult.isShouldNotify()) {
-                    pushCompletionCouponQuestion(liveId, userId, notifyResult);
+                        completionCouponService.prepareCompletionCouponNotify(liveId, userId, duration);
+                if (notifyResult == null || !notifyResult.isShouldNotify()) {
+                    return;
+                }
+                if (pushCompletionCouponQuestion(liveId, userId, notifyResult)) {
+                    completionCouponService.markCompletionCouponNotified(liveId, userId);
                 }
             });
 
@@ -89,22 +93,25 @@ public class LiveCompletionPointsTask {
         }
     }
 
-    private void pushCompletionCouponQuestion(Long liveId, Long userId, LiveCompletionCouponNotifyResult notifyResult) {
+    private boolean pushCompletionCouponQuestion(Long liveId, Long userId, LiveCompletionCouponNotifyResult notifyResult) {
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);
         sendMsgVo.setUserId(userId);
         sendMsgVo.setCmd("completionCouponQuestion");
         sendMsgVo.setMsg("今日问题");
         sendMsgVo.setData(JSONObject.toJSONString(notifyResult.getQuestions()));
-        webSocketServer.sendCompletionCouponQuestionMessage(liveId, userId, sendMsgVo);
-        log.info("[完课优惠券] 推送今日问题弹窗, liveId={}, userId={}", liveId, userId);
+        boolean pushed = webSocketServer.sendCompletionCouponQuestionMessage(liveId, userId, sendMsgVo);
+        if (pushed) {
+            log.info("[完课优惠券] 推送今日问题弹窗, liveId={}, userId={}", liveId, userId);
+        }
+        return pushed;
     }
 
     private void processCompletionByWatchDuration(List<Live> activeLives, CompletionHandler handler) {
         for (Live live : activeLives) {
             try {
                 Long liveId = live.getLiveId();
-                String hashKey = "live:watch:duration:hash:" + liveId;
+                String hashKey = WATCH_DURATION_HASH_PREFIX + liveId;
                 Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
 
                 if (userDurations == null || userDurations.isEmpty()) {

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

@@ -316,7 +316,6 @@ public class WebSocketServer {
             // 推送完课积分倒计时配置信息给前端
 //            sendCompletionPointsConfigToUser(session, liveId, userId, live);
 
-
         } else {
             adminRoom.add(session);
             // 为admin房间创建单线程执行器,保证串行化发送
@@ -455,62 +454,6 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
-
-                    // 心跳时同步更新观看时长到Redis Hash
-                    long watchUserId = (long) userProperties.get("userId");
-
-
-
-//                    if (msg.getData() != null && !msg.getData().isEmpty()) {
-//                        try {
-//                            Long currentDuration = Long.parseLong(msg.getData());
-//
-//                            Live currentLive = liveService.selectLiveByLiveId(liveId);
-//                            if (currentLive == null) {
-//                                break;
-//                            }
-//
-//
-//                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
-//                            boolean isLiveStarted = false;
-//                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
-//                                // status=2 表示直播中
-//                                isLiveStarted = true;
-//                            } else if (currentLive.getStartTime() != null) {
-//                                // 判断当前时间是否已超过开播时间
-//                                LocalDateTime now = LocalDateTime.now();
-//                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
-//                            }
-//
-//                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
-//                            String hashKey = "live:watch:duration:hash:" + liveId;
-//                            String userIdField = String.valueOf(watchUserId);
-//
-//                            if (!isLiveStarted) {
-//                                redisCache.hashDelete(hashKey, userIdField);
-//                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
-//                                break;
-//                            }
-//
-//                            // 直播已开始,记录观看时长
-//                            // 获取现有时长
-//                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
-//                            // 只有当新的时长更大时才更新
-//                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
-//                                // 更新Hash中的用户时长
-//                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
-//                                // 设置过期时间(2小时)
-//                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
-//
-//                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
-//
-//                            }
-//                        } catch (Exception e) {
-//                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
-//                                    liveId, watchUserId, msg.getData(), e);
-//                        }
-//                    }
-
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -999,17 +942,20 @@ public class WebSocketServer {
 
     /**
      * 发送完课优惠券今日问题弹窗通知给特定用户
+     * @return 是否推送成功(用户在线且发送成功)
      */
-    public void sendCompletionCouponQuestionMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+    public boolean sendCompletionCouponQuestionMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         Session session = room.get(userId);
         if (session == null || !session.isOpen()) {
-            return;
+            return false;
         }
         try {
             sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            return true;
         } catch (Exception e) {
             log.error("发送完课优惠券今日问题消息失败: liveId={}, userId={}", liveId, userId, e);
+            return false;
         }
     }
 

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

@@ -127,9 +127,7 @@ public interface LiveMapper
      */
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
             "and config_json is not null " +
-            "and JSON_EXTRACT(config_json, '$.enabled') = true " +
-            "and (JSON_EXTRACT(config_json, '$.participateCondition') = 2 " +
-            "or JSON_EXTRACT(config_json, '$.participateCondition') is null)")
+            "and JSON_EXTRACT(config_json, '$.enabled') = true")
     List<Live> selectLiveListWithCompletionPointsEnabled();
 
     /**

+ 18 - 1
fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java

@@ -1,6 +1,8 @@
 package com.fs.live.service;
 
+import com.fs.live.domain.Live;
 import com.fs.live.param.LiveCompletionCouponAnswerParam;
+import com.fs.live.vo.LiveCompletionCouponConfigVO;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.vo.LiveCompletionCouponStatusVO;
 import com.fs.live.vo.LiveCompletionQuestionVO;
@@ -13,7 +15,22 @@ import java.util.List;
 public interface ILiveCompletionCouponService {
 
     /**
-     * 检查完课状态,达到条件时标记可弹窗(不直接发券)
+     * 解析完课优惠券配置
+     */
+    LiveCompletionCouponConfigVO parseCompletionCouponConfig(Live live);
+
+    /**
+     * 预检查完课弹窗(不标记已通知,供定时任务/WebSocket在推送成功后标记)
+     */
+    LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 标记今日已推送完课优惠券弹窗
+     */
+    void markCompletionCouponNotified(Long liveId, Long userId);
+
+    /**
+     * @deprecated 请使用 {@link #prepareCompletionCouponNotify} + {@link #markCompletionCouponNotified}
      */
     LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration);
 

+ 43 - 4
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -9,6 +9,7 @@ import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveQuestionBankMapper;
 import com.fs.live.param.LiveCompletionCouponAnswerParam;
 import com.fs.live.service.*;
+import com.fs.live.vo.LiveCompletionCouponConfigVO;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.vo.LiveCompletionCouponStatusVO;
 import com.fs.live.vo.LiveCompletionQuestionVO;
@@ -64,9 +65,27 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     private RedisCache redisCache;
 
     @Override
-    public LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+    public LiveCompletionCouponConfigVO parseCompletionCouponConfig(Live live) {
+        LiveCompletionCouponConfigVO vo = new LiveCompletionCouponConfigVO();
+        CompletionCouponConfig config = live == null ? disabledConfig() : getCompletionCouponConfig(live);
+        vo.setEnabled(config.isEnabled());
+        vo.setCompletionRate(config.getCompletionRate());
+        vo.setCouponId(config.getCouponId());
+        vo.setFinishQuestionIds(config.getFinishQuestionIds());
+        if (config.isEnabled() && live != null && live.getDuration() != null && live.getDuration() > 0) {
+            vo.setRequiredDurationSeconds(calculateRequiredDuration(live.getDuration(), config.getCompletionRate()));
+        } else {
+            vo.setRequiredDurationSeconds(-1L);
+        }
+        return vo;
+    }
+
+    @Override
+    public LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration) {
         LiveCompletionCouponNotifyResult result = new LiveCompletionCouponNotifyResult();
         result.setShouldNotify(false);
+        result.setEligible(false);
+        result.setQuestions(Collections.emptyList());
 
         try {
             CompletionCouponConfig config = resolveConfig(liveId);
@@ -77,6 +96,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
                 return result;
             }
+            result.setEligible(true);
 
             if (hasIssuedToday(liveId, userId, config.getCouponId())) {
                 return result;
@@ -84,19 +104,33 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
 
             List<LiveCompletionQuestionVO> questions = loadQuestions(config.getFinishQuestionIds());
             if (questions.isEmpty()) {
-                log.warn("完课优惠券已开启但未配置今日问题, liveId={}", liveId);
+                log.debug("完课优惠券未配置直播课题, 跳过弹窗, liveId={}", liveId);
                 return result;
             }
 
             if (hasNotifiedToday(liveId, userId)) {
+                result.setQuestions(questions);
                 return result;
             }
 
-            markNotifiedToday(liveId, userId);
             result.setShouldNotify(true);
             result.setQuestions(questions);
         } catch (Exception e) {
-            log.error("检查完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
+            log.error("预检查完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
+        }
+        return result;
+    }
+
+    @Override
+    public void markCompletionCouponNotified(Long liveId, Long userId) {
+        markNotifiedToday(liveId, userId);
+    }
+
+    @Override
+    public LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+        LiveCompletionCouponNotifyResult result = prepareCompletionCouponNotify(liveId, userId, watchDuration);
+        if (result.isShouldNotify()) {
+            markCompletionCouponNotified(liveId, userId);
         }
         return result;
     }
@@ -424,6 +458,11 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         return NOTIFY_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + today;
     }
 
+    private long calculateRequiredDuration(long videoDurationSeconds, Integer completionRate) {
+        int rate = completionRate == null || completionRate <= 0 ? 90 : completionRate;
+        return (long) Math.ceil(videoDurationSeconds * rate / 100.0);
+    }
+
     private static class CompletionCouponConfig {
         private boolean enabled;
         private Integer completionRate;

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

@@ -400,16 +400,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         try {
             JSONObject jsonConfig = JSON.parseObject(configJson);
 
-            if (!jsonConfig.getBooleanValue("enabled")) {
-                return config;
-            }
-
-            Long participateCondition = jsonConfig.getLong("participateCondition");
-            if (participateCondition != null && participateCondition != 2L) {
-                return config;
-            }
-
-            config.setEnabled(true);
+            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
 
             Integer rate = jsonConfig.getInteger("completionRate");
             if (rate != null && rate > 0 && rate <= 100) {

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

@@ -378,12 +378,10 @@ public class LiveServiceImpl implements ILiveService
         if (StringUtils.isNotEmpty(configJson)) {
             try {
                 JSONObject jsonConfig = JSON.parseObject(configJson);
-                boolean enabled = jsonConfig.getBooleanValue("enabled");
+                completionPointsEnabled = jsonConfig.getBooleanValue("enabled");
                 Long participateCondition = jsonConfig.getLong("participateCondition");
-                if (enabled && (participateCondition == null || participateCondition == 2L)) {
-                    completionPointsEnabled = true;
-                }
-                if (enabled && participateCondition != null && participateCondition == 3L) {
+                if (jsonConfig.getBooleanValue("enabled")
+                        && participateCondition != null && participateCondition == 3L) {
                     completionCouponEnabled = true;
                 }
             } catch (Exception e) {

+ 21 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponConfigVO.java

@@ -0,0 +1,21 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * 完课优惠券配置(定时任务缓存用)
+ */
+@Data
+public class LiveCompletionCouponConfigVO {
+
+    private boolean enabled;
+
+    private Integer completionRate;
+
+    private Long couponId;
+
+    private String finishQuestionIds;
+
+    /** 完课所需观看时长(秒) */
+    private long requiredDurationSeconds;
+}

+ 21 - 16
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponNotifyResult.java

@@ -1,16 +1,21 @@
-package com.fs.live.vo;

-

-import lombok.Data;

-

-import java.util.List;

-

-/**

- * 完课优惠券弹窗通知结果

- */

-@Data

-public class LiveCompletionCouponNotifyResult {

-

-    private boolean shouldNotify;

-

-    private List<LiveCompletionQuestionVO> questions;

-}

+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 完课优惠券弹窗通知结果
+ */
+@Data
+public class LiveCompletionCouponNotifyResult {
+
+    /** 是否应推送弹窗 */
+    private boolean shouldNotify;
+
+    /** 是否已达到完课条件 */
+    private boolean eligible;
+
+    /** 今日问题(不含答案) */
+    private List<LiveCompletionQuestionVO> questions;
+}