3 Commits 5abe326188 ... 24b8f9e130

Auteur SHA1 Message Date
  yys 24b8f9e130 1、处理完课优惠卷定时展示 il y a 3 jours
  yys f3730e9f71 1、调整看课时长 il y a 3 jours
  yys e7074871b0 1、调整直播奖励记录 il y a 3 jours

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

@@ -192,6 +192,7 @@ public class LiveCompletionPointsTask {
                 queryUser.setLiveId(liveId);
                 List<LiveWatchUser> watchUsers = liveWatchUserService.selectAllWatchUser(queryUser);
                 if (watchUsers == null || watchUsers.isEmpty()) {
+                    log.debug("[完课定时] 直播间无观看用户, liveId={}", liveId);
                     continue;
                 }
 
@@ -202,10 +203,15 @@ public class LiveCompletionPointsTask {
                     }
                 }
 
+                log.info("[完课定时] 直播liveId={} 共有{}个观看用户, 去重后{}个",
+                        liveId, watchUsers.size(), userIds.size());
+
                 for (Long userId : userIds) {
                     try {
                         long duration = resolveEffectiveWatchDuration(liveId, userId);
                         if (duration <= 0) {
+                            log.info("[完课定时] 用户观看时长<=0, liveId={}, userId={}, duration={}s",
+                                    liveId, userId, duration);
                             continue;
                         }
                         handler.handle(liveId, userId, duration);
@@ -228,7 +234,10 @@ public class LiveCompletionPointsTask {
         String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
         Long entryTime = redisCache.getCacheObject(entryTimeKey);
         if (entryTime != null) {
-            duration += (System.currentTimeMillis() - entryTime) / 1000;
+            long sessionSeconds = (System.currentTimeMillis() - entryTime) / 1000;
+            duration += sessionSeconds;
+            log.debug("[完课定时] 累加Redis在线时长, liveId={}, userId={}, dbDuration={}s, redisSession={}s, total={}s",
+                    liveId, userId, total, sessionSeconds, duration);
         }
         return duration;
     }

+ 29 - 15
fs-service/src/main/java/com/fs/his/service/impl/FsUserSignServiceImpl.java

@@ -121,29 +121,35 @@ public class FsUserSignServiceImpl implements IFsUserSignService
     @Override
     @Transactional
     public Long sign(FsUser user) {
+        if (user == null || user.getUserId() == null) {
+            throw new CustomException("用户不存在");
+        }
         String json=configService.selectConfigByKey("his.sign");
         if(StringUtils.isEmpty(json)) {
             throw new CustomException("请先配置签到天数");
         }
         JSONArray jsonArray= JSONUtil.parseArray(json);
         List<StoreSignConfig> signs=jsonArray.toList(StoreSignConfig.class);
+        if (signs == null || signs.isEmpty()) {
+            throw new CustomException("请先配置签到天数");
+        }
         boolean isDaySign = this.getToDayIsSign(user.getUserId());
         if(isDaySign) {
             throw new CustomException("已签到");
         }
-        Long signNumber = 0l; //积分
-        Long userSignNum = user.getSignNum(); //签到次数
+        Long signNumber = 0L; //积分
+        Long userSignNum = user.getSignNum() != null ? user.getSignNum() : 0L; //签到次数
         if(getYesterDayIsSign(user.getUserId())){
-            if(user.getSignNum() > (signs.size() - 1)){
-                userSignNum = 0l;
+            if(userSignNum > (signs.size() - 1)){
+                userSignNum = 0L;
             }
         }else{
-            userSignNum = 0l;
+            userSignNum = 0L;
         }
         int index = 0;
         for (StoreSignConfig config : signs) {
             if(index == userSignNum){
-                signNumber = config.getSignNum();
+                signNumber = config.getSignNum() != null ? config.getSignNum() : 0L;
                 break;
             }
             index++;
@@ -160,7 +166,7 @@ public class FsUserSignServiceImpl implements IFsUserSignService
         userSign.setTitle(title);
         userSign.setNumber(signNumber);
         userSign.setCreateTime(new Date());
-        userSign.setBalance(user.getIntegral().longValue());
+        userSign.setBalance(user.getIntegral() != null ? user.getIntegral() : 0L);
         fsUserSignMapper.insertFsUserSign(userSign);
 
 //        FsUserIntegralLogs logs = new FsUserIntegralLogs();
@@ -208,29 +214,35 @@ public class FsUserSignServiceImpl implements IFsUserSignService
 
     @Override
     public Long getSign(FsUser user) {
+        if (user == null || user.getUserId() == null) {
+            return 0L;
+        }
         String json=configService.selectConfigByKey("his.sign");
         if(StringUtils.isEmpty(json)) {
             throw new CustomException("请先配置签到天数");
         }
         JSONArray jsonArray= JSONUtil.parseArray(json);
         List<StoreSignConfig> signs=jsonArray.toList(StoreSignConfig.class);
+        if (signs == null || signs.isEmpty()) {
+            return 0L;
+        }
         boolean isDaySign = this.getToDayIsSign(user.getUserId());
-        Long userSignNum = user.getSignNum(); //签到次数
+        Long userSignNum = user.getSignNum() != null ? user.getSignNum() : 0L; //签到次数
         if(getYesterDayIsSign(user.getUserId())){
-            if(user.getSignNum() > (signs.size() - 1)){
+            if(userSignNum > (signs.size() - 1)){
                 if(isDaySign) {
-                    return user.getSignNum();
+                    return userSignNum;
                 }
                 else{
-                    userSignNum = 0l;
+                    userSignNum = 0L;
                 }
             }
         }else{
             if(isDaySign) {
-                return user.getSignNum();
+                return userSignNum;
             }
             else{
-                userSignNum = 0l;
+                userSignNum = 0L;
             }
 
         }
@@ -239,8 +251,10 @@ public class FsUserSignServiceImpl implements IFsUserSignService
 
     @Override
     public Boolean isDaySign(FsUser user) {
-        boolean isDaySign = this.getToDayIsSign(user.getUserId());
-        return isDaySign;
+        if (user == null || user.getUserId() == null) {
+            return false;
+        }
+        return this.getToDayIsSign(user.getUserId());
     }
 
     /**

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

@@ -137,7 +137,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') = 3 " +
+            "and JSON_EXTRACT(config_json, '$.participateCondition') = '3' " +
             "and JSON_EXTRACT(config_json, '$.finishCouponId') is not null " +
             "and JSON_EXTRACT(config_json, '$.finishCouponId') != ''")
     List<Live> selectLiveListWithCompletionCouponEnabled();

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

@@ -107,33 +107,39 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         try {
             CompletionCouponConfig config = resolveConfig(liveId);
             if (!config.isEnabled()) {
+                log.info("[完课优惠券] 配置未启用, liveId={}, userId={}", liveId, userId);
                 return result;
             }
 
             result.setCoupon(loadCouponInfo(config.getCouponId()));
 
             if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
+                log.info("[完课优惠券] 观看比例未达标(已在上层方法记录详情), liveId={}, userId={}", liveId, userId);
                 return result;
             }
             result.setEligible(true);
 
             if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+                log.info("[完课优惠券] 今日已发券, 跳过, liveId={}, userId={}", liveId, userId);
                 return result;
             }
 
             List<LiveCompletionQuestionVO> questions = loadQuestions(config.getFinishQuestionIds());
             if (questions.isEmpty()) {
-                log.debug("完课优惠券未配置直播课题, 跳过弹窗, liveId={}", liveId);
+                log.info("[完课优惠券] 未配置直播课题, 跳过弹窗, liveId={}, userId={}", liveId, userId);
                 return result;
             }
 
             if (!forcePush && hasNotifiedToday(liveId, userId)) {
+                log.info("[完课优惠券] 今日已推送过弹窗, 跳过, liveId={}, userId={}", liveId, userId);
                 result.setQuestions(questions);
                 return result;
             }
 
             result.setShouldNotify(true);
             result.setQuestions(questions);
+            log.info("[完课优惠券] 满足推送条件, liveId={}, userId={}, watchDuration={}, completionRate={}%",
+                    liveId, userId, watchDuration, config.getCompletionRate());
         } catch (Exception e) {
             log.error("预检查完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
         }
@@ -424,16 +430,21 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
         }
         if (actualWatchDuration == null || actualWatchDuration <= 0) {
+            log.info("[完课优惠券] 用户观看时长为0或null, liveId={}, userId={}, watchDuration={}",
+                    liveId, userId, actualWatchDuration);
             return false;
         }
 
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
+            log.info("[完课优惠券] 直播间不存在, liveId={}, userId={}", liveId, userId);
             return false;
         }
 
         Long videoDuration = live.getDuration();
         if (videoDuration == null || videoDuration <= 0) {
+            log.info("[完课优惠券] 视频总时长为0或null, liveId={}, userId={}, videoDuration={}",
+                    liveId, userId, videoDuration);
             return false;
         }
 
@@ -443,7 +454,18 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
             watchRate = BigDecimal.valueOf(100);
         }
-        return watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) >= 0;
+
+        boolean eligible = watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) >= 0;
+        if (!eligible) {
+            log.info("[完课优惠券] 观看比例未达到完课率要求, liveId={}, userId={}, " +
+                            "watchDuration={}s, videoDuration={}s, watchRate={}%, requiredRate={}%",
+                    liveId, userId, actualWatchDuration, videoDuration, watchRate, config.getCompletionRate());
+        } else {
+            log.info("[完课优惠券] 观看比例达标, liveId={}, userId={}, " +
+                            "watchDuration={}s, videoDuration={}s, watchRate={}%, requiredRate={}%",
+                    liveId, userId, actualWatchDuration, videoDuration, watchRate, config.getCompletionRate());
+        }
+        return eligible;
     }
 
     private CompletionCouponConfig resolveConfig(Long liveId) {

+ 75 - 7
fs-service/src/main/java/com/fs/live/service/impl/LiveConsoleOpLogServiceImpl.java

@@ -5,6 +5,7 @@ import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DictUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveConsoleOpLog;
 import com.fs.live.domain.LiveConsoleOpLogUser;
 import com.fs.live.domain.LiveCoupon;
@@ -15,6 +16,7 @@ import com.fs.live.domain.LiveRedConf;
 import com.fs.live.mapper.LiveConsoleOpLogMapper;
 import com.fs.live.mapper.LiveConsoleOpLogUserMapper;
 import com.fs.live.mapper.LiveCouponMapper;
+import com.fs.live.mapper.LiveMapper;
 import com.fs.live.service.ILiveConsoleOpLogService;
 import com.fs.live.service.ILiveCouponIssueService;
 import com.fs.live.service.ILiveLotteryConfService;
@@ -24,6 +26,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
@@ -46,6 +50,8 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
     private static final String AUTO_OPERATOR_NAME = "运营自动化";
     /** 中控台 WebSocket 无登录上下文时的默认操作人 */
     private static final String CONSOLE_OPERATOR_NAME = "中控台";
+    /** 超过领取截止时间或直播结束后的宽限期(毫秒) */
+    private static final long EXPIRE_GRACE_MILLIS = 60_000L;
 
     @Autowired
     private LiveConsoleOpLogMapper liveConsoleOpLogMapper;
@@ -65,6 +71,9 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
     @Autowired
     private LiveCouponMapper liveCouponMapper;
 
+    @Autowired
+    private LiveMapper liveMapper;
+
     @Override
     public List<LiveConsoleOpLog> selectLiveConsoleOpLogList(LiveConsoleOpLog liveConsoleOpLog) {
         return liveConsoleOpLogMapper.selectLiveConsoleOpLogList(liveConsoleOpLog);
@@ -243,18 +252,47 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         }
 
         Date now = DateUtils.getNowDate();
+        Live live = liveMapper.selectLiveByLiveId(liveId);
         Map<Long, Long> couponTypeMap = resolveCouponTypeMap(opLogs);
         List<LiveConsoleOpLogRecordVo> result = new ArrayList<>(opLogs.size());
         for (LiveConsoleOpLog opLog : opLogs) {
+            if (isInternalSettleOpLog(opLog)) {
+                continue;
+            }
             boolean claimed = opLog.getId() != null && claimedOpLogIds.contains(opLog.getId());
-            int status = resolveOpLogStatus(opLog, claimed, now);
+            int status = resolveOpLogStatus(opLog, claimed, now, live);
             LiveConsoleOpLogRecordVo recordVo = LiveConsoleOpLogRecordVo.from(opLog, status);
             fillCouponType(recordVo, opLog, couponTypeMap);
             result.add(recordVo);
         }
+        result.sort(this::compareOpLogRecordByTime);
         return result;
     }
 
+    /** 按创建时间倒序(最新在前) */
+    private int compareOpLogRecordByTime(LiveConsoleOpLogRecordVo a, LiveConsoleOpLogRecordVo b) {
+        Date timeA = a.getCreateTime();
+        Date timeB = b.getCreateTime();
+        if (timeA == null && timeB == null) {
+            return 0;
+        }
+        if (timeA == null) {
+            return 1;
+        }
+        if (timeB == null) {
+            return -1;
+        }
+        return timeB.compareTo(timeA);
+    }
+
+    private boolean isInternalSettleOpLog(LiveConsoleOpLog opLog) {
+        if (opLog == null || opLog.getOpType() == null) {
+            return false;
+        }
+        int opType = opLog.getOpType();
+        return opType == LiveConsoleOpLog.OP_RED_SETTLE || opType == LiveConsoleOpLog.OP_LOTTERY_SETTLE;
+    }
+
     private Map<Long, Long> resolveCouponTypeMap(List<LiveConsoleOpLog> opLogs) {
         List<Long> couponIds = opLogs.stream()
                 .filter(log -> log.getOpType() != null && log.getBizId() != null
@@ -292,23 +330,53 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
 
     /**
      * 状态优先级:已领取 > 已结束 > 待领取
+     * <p>已领取:用户已成功领取/参与;已结束:活动已截止或用户未领到(如红包抢完),不再展示待领取</p>
      */
-    private int resolveOpLogStatus(LiveConsoleOpLog opLog, boolean claimed, Date now) {
+    private int resolveOpLogStatus(LiveConsoleOpLog opLog, boolean claimed, Date now, Live live) {
         if (claimed) {
             return LiveConsoleOpLogRecordVo.STATUS_CLAIMED;
         }
-        if (isOpLogEnded(opLog, now)) {
+        if (isLiveBroadcastEndedOverGrace(live, now) || isOpLogItemEnded(opLog, now)) {
             return LiveConsoleOpLogRecordVo.STATUS_ENDED;
         }
         return LiveConsoleOpLogRecordVo.STATUS_PENDING;
     }
 
-    private boolean isOpLogEnded(LiveConsoleOpLog opLog, Date now) {
-        Date expireTime = resolveExpireTime(opLog);
-        if (expireTime != null && !now.before(expireTime)) {
+    private boolean isLiveBroadcastEndedOverGrace(Live live, Date now) {
+        Date liveEnd = resolveLiveEndTime(live);
+        if (liveEnd == null || now == null) {
+            return false;
+        }
+        return now.getTime() >= liveEnd.getTime() + EXPIRE_GRACE_MILLIS;
+    }
+
+    private Date resolveLiveEndTime(Live live) {
+        if (live == null) {
+            return null;
+        }
+        if (live.getFinishTime() != null) {
+            return Date.from(live.getFinishTime().atZone(ZoneId.systemDefault()).toInstant());
+        }
+        if (live.getStartTime() != null && live.getDuration() != null && live.getDuration() > 0) {
+            LocalDateTime endTime = live.getStartTime().plusSeconds(live.getDuration());
+            return Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant());
+        }
+        if (live.getStartTime() != null && live.getVideoDuration() != null && live.getVideoDuration() > 0) {
+            LocalDateTime endTime = live.getStartTime().plusSeconds(live.getVideoDuration());
+            return Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant());
+        }
+        return null;
+    }
+
+    private boolean isOpLogItemEnded(LiveConsoleOpLog opLog, Date now) {
+        if (isBizEnded(opLog)) {
             return true;
         }
-        return isBizEnded(opLog);
+        Date expireTime = resolveExpireTime(opLog);
+        if (expireTime != null && now != null) {
+            return now.getTime() >= expireTime.getTime() + EXPIRE_GRACE_MILLIS;
+        }
+        return false;
     }
 
     private Date resolveExpireTime(LiveConsoleOpLog opLog) {

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

@@ -178,11 +178,17 @@ public class LiveServiceImpl implements ILiveService
         if(!videos.isEmpty()){
             LiveVideo liveVideo = videos.get(0);
             byId.setVideoUrl(liveVideo.getVideoUrl());
-            byId.setDuration(liveVideo.getDuration());
             byId.setVideoId(liveVideo.getVideoId());
             byId.setVideoType(liveVideo.getVideoType());
             byId.setVideoFileSize(liveVideo.getFileSize());
-            byId.setVideoDuration(liveVideo.getDuration());
+
+            // 汇总所有视频时长(totalDuration)作为完课判断基准,而非只取第一个
+            Long totalDuration = videos.stream()
+                    .filter(v -> v.getDuration() != null)
+                    .mapToLong(LiveVideo::getDuration)
+                    .sum();
+            byId.setDuration(totalDuration > 0 ? totalDuration : liveVideo.getDuration());
+            byId.setVideoDuration(totalDuration > 0 ? totalDuration : liveVideo.getDuration());
         }
         List<LiveTagItemVO> list = liveTagConfigMapper.getLiveTagListByliveId(liveId);
         if(null != list && !list.isEmpty()){
@@ -274,6 +280,8 @@ public class LiveServiceImpl implements ILiveService
         String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId);
         Live cachedLive = redisCache.getCacheObject(cacheKey, Live.class);
         if (cachedLive != null) {
+            log.debug("[完课优惠券] selectLiveByLiveId命中Redis缓存, liveId={}, duration={}s",
+                    liveId, cachedLive.getDuration());
             return cachedLive;
         }
 
@@ -287,11 +295,25 @@ public class LiveServiceImpl implements ILiveService
         if(!videos.isEmpty()){
             LiveVideo liveVideo = videos.get(0);
             byId.setVideoUrl(liveVideo.getVideoUrl());
-            byId.setDuration(liveVideo.getDuration());
             byId.setVideoId(liveVideo.getVideoId());
             byId.setVideoType(liveVideo.getVideoType());
             byId.setVideoFileSize(liveVideo.getFileSize());
-            byId.setVideoDuration(liveVideo.getDuration());
+
+            // 汇总所有视频时长(totalDuration)作为完课判断基准,而非只取第一个
+            Long totalDuration = videos.stream()
+                    .filter(v -> v.getDuration() != null)
+                    .mapToLong(LiveVideo::getDuration)
+                    .sum();
+            byId.setDuration(totalDuration > 0 ? totalDuration : liveVideo.getDuration());
+            byId.setVideoDuration(totalDuration > 0 ? totalDuration : liveVideo.getDuration());
+
+            log.info("[完课优惠券] DB重新查询视频时长, liveId={}, 视频数={}, 各视频时长={}, 汇总totalDuration={}s, 最终duration={}s",
+                    liveId, videos.size(),
+                    videos.stream().map(v -> v.getDuration() == null ? "null" : String.valueOf(v.getDuration())).collect(java.util.stream.Collectors.joining(",")),
+                    totalDuration, byId.getDuration());
+        } else {
+            log.info("[完课优惠券] DB重新查询, 未找到视频记录, liveId={}, duration={}s",
+                    liveId, byId.getDuration());
         }
 
         // 将结果存入缓存
@@ -503,7 +525,13 @@ public class LiveServiceImpl implements ILiveService
         if(!videos.isEmpty()){
             LiveVideo liveVideo = videos.get(0);
             byId.setVideoUrl(liveVideo.getVideoUrl());
-            byId.setDuration(liveVideo.getDuration());
+
+            // 汇总所有视频时长,而非只取第一个
+            long totalDuration = videos.stream()
+                    .filter(v -> v.getDuration() != null)
+                    .mapToLong(LiveVideo::getDuration)
+                    .sum();
+            byId.setDuration(totalDuration > 0 ? totalDuration : liveVideo.getDuration());
         }
         return byId;
     }

+ 29 - 11
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -1301,13 +1301,16 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             return 0L;
         }
         try {
-            long liveDuration = safeOnlineSeconds(baseMapper.selectByUniqueIndex(liveId, userId, 1, 0));
-            long replayDuration = safeOnlineSeconds(baseMapper.selectByUniqueIndex(liveId, userId, 0, 1));
-            long totalDuration = liveDuration + replayDuration;
-
-            log.debug("查询总观看时长: liveId={}, userId={}, liveDuration={}, replayDuration={}, total={}",
-                    liveId, userId, liveDuration, replayDuration, totalDuration);
-
+            Map<String, Object> params = new HashMap<>();
+            params.put("liveId", liveId);
+            params.put("userId", userId);
+            List<LiveWatchUser> records = baseMapper.selectUserByLiveIdAndUserId(params);
+            if (records == null || records.isEmpty()) {
+                return 0L;
+            }
+            long totalDuration = records.stream().mapToLong(LiveWatchUserServiceImpl::safeOnlineSeconds).sum();
+            log.debug("查询总观看时长: liveId={}, userId={}, recordCount={}, total={}",
+                    liveId, userId, records.size(), totalDuration);
             return totalDuration;
         } catch (Exception e) {
             log.error("查询总观看时长失败: liveId={}, userId={}", liveId, userId, e);
@@ -1324,19 +1327,34 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
     @Override
     public Long getUserWatchDuration(Long liveId, Long userId) {
-        Long total = getTotalWatchDuration(liveId, userId);
-        long dbDuration = total != null ? total : 0L;
+        long duration = safeLong(getTotalWatchDuration(liveId, userId));
+        try {
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            if (entryTime != null) {
+                long sessionSeconds = (System.currentTimeMillis() - entryTime) / 1000;
+                if (sessionSeconds > 0) {
+                    duration += sessionSeconds;
+                }
+            }
+        } catch (Exception e) {
+            log.debug("读取 Redis 进入时间失败: liveId={}, userId={}", liveId, userId, e);
+        }
         try {
             String hashKey = "live:watch:duration:hash:" + liveId;
             Object redisValue = redisCache.hashGet(hashKey, String.valueOf(userId));
             if (redisValue != null) {
                 long redisDuration = Long.parseLong(redisValue.toString());
-                return Math.max(dbDuration, redisDuration);
+                duration = Math.max(duration, redisDuration);
             }
         } catch (Exception e) {
             log.debug("读取 Redis 观看时长失败: liveId={}, userId={}", liveId, userId, e);
         }
-        return dbDuration;
+        return duration;
+    }
+
+    private static long safeLong(Long value) {
+        return value != null ? value : 0L;
     }
 
 }

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

@@ -16,6 +16,6 @@ public class LiveUserRewardRecordsVo {
     /** 开播总时长(秒) */
     private Long liveDuration;
 
-    /** 用户观看时长(秒,直播+回放,取 DB 与 Redis 较大值) */
+    /** 用户有效观看时长(秒,直播+回放累计后与开播时长取较小值,用于进度展示) */
     private Long watchDuration;
 }

+ 8 - 1
fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

@@ -212,18 +212,25 @@ public class IntegralController extends  AppBaseController {
     @GetMapping("/getUserSign")
     public R getUserSign(HttpServletRequest request){
         FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+        if (user == null) {
+            return R.error("用户不存在");
+        }
         //获取签到配置
         String json=configService.selectConfigByKey("his.sign");
         //判断用户昨天是否签到过
         Long signNum=userSignService.getSign(user);
         Boolean isDaySign=userSignService.isDaySign(user);
-        return R.ok().put("isDaySign", isDaySign).put("signNum", signNum).put("integral",user.getIntegral().intValue()).put("sign", json);
+        int integral = user.getIntegral() != null ? user.getIntegral().intValue() : 0;
+        return R.ok().put("isDaySign", isDaySign).put("signNum", signNum).put("integral", integral).put("sign", json);
     }
     @Login
     @ApiOperation("签到")
     @PostMapping("/sign")
     public R sign(HttpServletRequest request){
         FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+        if (user == null) {
+            return R.error("用户不存在");
+        }
         Long integral=userSignService.sign(user);
         return R.ok("签到获得" + integral + "积分").put("data", fsUserCourseVideoService.getSignGrandGiftRules(user.getUserId()));
     }

+ 5 - 2
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionCouponController.java

@@ -3,6 +3,7 @@ package com.fs.app.controller.live;
 import com.fs.app.controller.AppBaseController;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.base.BaseException;
 import com.fs.live.param.LiveCompletionCouponAnswerParam;
 import com.fs.live.param.LiveCompletionCouponClaimParam;
 import com.fs.live.service.ILiveCompletionCouponService;
@@ -41,8 +42,10 @@ public class LiveCompletionCouponController extends AppBaseController {
     public R answer(@RequestBody LiveCompletionCouponAnswerParam param) {
         Long userId = Long.parseLong(getUserId());
         LiveCompletionCouponAnswerResult result = completionCouponService.submitAnswers(param, userId);
-        String msg = result.isAllCorrect() ? "答题正确,请点击领取福利券" : "回答错误,请重新作答";
-        return R.ok(msg).put("data", result);
+        if (!result.isAllCorrect()) {
+            throw new BaseException("回答错误,请重新作答");
+        }
+        return R.ok("答题正确,请点击领取福利券").put("data", result);
     }
 
     /**

+ 49 - 18
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -54,7 +54,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
-    
+
     @Autowired
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
@@ -137,32 +137,32 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         if(liveVo.getIsShow() == 2) {
             return R.error("直播未开放");
         }
-        
+
         // 查询用户今天是否已领取完课奖励
         if (userId != null) {
             try {
-                List<LiveCompletionPointsRecord> unreceivedRecords = 
+                List<LiveCompletionPointsRecord> unreceivedRecords =
                     completionPointsRecordService.getUserUnreceivedRecords(id, userId);
-                
+
                 // 判断是否有未领取的奖励,如果有则说明今天还未领取
                 // 如果没有未领取的,再查询是否有已领取的记录
                 if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
                     liveVo.setTodayRewardReceived(false);
                 } else {
                     // 查询所有记录(包括已领取和未领取)
-                    List<LiveCompletionPointsRecord> allRecords = 
+                    List<LiveCompletionPointsRecord> allRecords =
                         completionPointsRecordService.getUserRecords(id, userId);
-                    
+
                     if (allRecords != null && !allRecords.isEmpty()) {
                         // 检查最近一条记录是否是今天的且已领取
                         LiveCompletionPointsRecord latestRecord = allRecords.get(0);
                         Date today = new Date();
                         Date recordDate = latestRecord.getCurrentCompletionDate();
-                        
+
                         // 判断是否为同一天
-                        boolean isSameDay = recordDate != null && 
+                        boolean isSameDay = recordDate != null &&
                             isSameDay(recordDate, today);
-                        
+
                         if (isSameDay && latestRecord.getReceiveStatus() == 1) {
                             liveVo.setTodayRewardReceived(true);
                         } else {
@@ -179,10 +179,10 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         } else {
             liveVo.setTodayRewardReceived(false);
         }
-        
+
         return R.ok().put("data", liveVo);
     }
-    
+
     /**
      * 判断两个日期是否为同一天
      */
@@ -270,8 +270,14 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
     public LiveUserRewardRecordsVo getUserRewardRecords(Long liveId, Long userId) {
         LiveUserRewardRecordsVo vo = new LiveUserRewardRecordsVo();
         vo.setRecords(liveConsoleOpLogService.listUserOpLogRecords(liveId, userId));
-        vo.setLiveDuration(resolveLiveDuration(liveId));
-        vo.setWatchDuration(liveWatchUserService.getUserWatchDuration(liveId, userId));
+        Long liveDuration = resolveLiveDuration(liveId);
+        vo.setLiveDuration(liveDuration);
+        Long totalWatch = liveWatchUserService.getUserWatchDuration(liveId, userId);
+        long watchDuration = totalWatch != null ? totalWatch : 0L;
+        if (liveDuration != null && liveDuration > 0) {
+            watchDuration = Math.min(watchDuration, liveDuration);
+        }
+        vo.setWatchDuration(watchDuration);
         return vo;
     }
 
@@ -283,17 +289,42 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         if (live == null) {
             return 0L;
         }
+        if (live.getStartTime() != null) {
+            LocalDateTime endTime = resolveLiveDurationEndTime(live);
+            long seconds = ChronoUnit.SECONDS.between(live.getStartTime(), endTime);
+            return Math.max(seconds, 0L);
+        }
         if (live.getDuration() != null && live.getDuration() > 0) {
             return live.getDuration();
         }
         if (live.getVideoDuration() != null && live.getVideoDuration() > 0) {
             return live.getVideoDuration();
         }
-        if (live.getStartTime() != null) {
-            LocalDateTime endTime = live.getFinishTime() != null ? live.getFinishTime() : LocalDateTime.now();
-            long seconds = ChronoUnit.SECONDS.between(live.getStartTime(), endTime);
-            return Math.max(seconds, 0L);
-        }
         return 0L;
     }
+
+    /**
+     * 计算开播总时长的结束时间点。
+     * finishTime 在创建直播间时会被预填为 startTime + 视频时长,不能作为进行中的实际结束时间。
+     */
+    private LocalDateTime resolveLiveDurationEndTime(Live live) {
+        LocalDateTime now = LocalDateTime.now();
+        if (live.getStartTime() == null) {
+            return now;
+        }
+        Integer status = live.getStatus();
+        if (status != null && status == 1 && live.getStartTime().isAfter(now)) {
+            return live.getStartTime();
+        }
+        if (status != null && (status == 2 || status == 4)) {
+            return now;
+        }
+        if (status != null && status == 3 && live.getFinishTime() != null) {
+            return live.getFinishTime();
+        }
+        if (live.getFinishTime() != null && !live.getFinishTime().isAfter(now)) {
+            return live.getFinishTime();
+        }
+        return now;
+    }
 }