Forráskód Böngészése

1、同场已领观看/完课奖励结束前不能再领
2、结束后重开 时长从 0 重新计,可再领
3、结束后仍挂在直播间 不再累计旧时长

yys 5 napja
szülő
commit
3fe196b5fb

+ 7 - 0
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -252,6 +252,13 @@ public class Task {
                 redisCache.deleteObject(cacheKey);
                 webSocketServer.removeLikeCountCache(live.getLiveId());
 
+                // 结束直播:清理看课时长,避免结束后继续累计;重开后重新计算
+                try {
+                    liveWatchUserService.resetWatchDurationOnLiveEnd(live.getLiveId());
+                } catch (Exception e) {
+                    log.error("直播结束清理看课时长失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+                }
+
                 // 删除打标签缓存
                 try {
                     String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());

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

@@ -106,6 +106,14 @@ public interface LiveWatchUserMapper {
 
     List<LiveWatchUser> checkOnlineNoRewardUser(@Param("params") Map<String, Object> params);
 
+    /** 直播结束后重置本场看课时长 */
+    @Update("UPDATE live_watch_user SET online_seconds = 0 WHERE live_id = #{liveId}")
+    int resetOnlineSecondsByLiveId(@Param("liveId") Long liveId);
+
+    /** 查询直播间出现过的用户ID(用于按 key 精确删除 Redis,避免 KEYS) */
+    @Select("SELECT DISTINCT user_id FROM live_watch_user WHERE live_id = #{liveId} AND user_id IS NOT NULL")
+    List<Long> selectUserIdsByLiveId(@Param("liveId") Long liveId);
+
     List<LiveWatchUser> selectLiveWatchAndRegisterUser(@Param("params") Map<String, Object> params);
 
     List<LiveWatchUser> selectUserByLiveIdAndUserId(@Param("params") Map<String, Object> params);

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

@@ -188,6 +188,11 @@ public interface ILiveWatchUserService {
      */
     void clearLiveFlagCache(Long liveId);
 
+    /**
+     * 直播结束时清理本场看课时长(DB + Redis),重开后重新累计
+     */
+    void resetWatchDurationOnLiveEnd(Long liveId);
+
     List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser);
 
     /**

+ 39 - 7
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -112,7 +112,8 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         result.setQuestions(Collections.emptyList());
 
         try {
-            CompletionCouponConfig config = resolveConfig(liveId);
+            Live live = liveService.selectLiveByLiveId(liveId);
+            CompletionCouponConfig config = live == null ? disabledConfig() : getCompletionCouponConfig(live);
             if (!config.isEnabled()) {
                 log.info("[完课优惠券] 配置未启用, liveId={}, userId={}", liveId, userId);
                 return result;
@@ -134,7 +135,8 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             }
             result.setEligible(true);
 
-            if (hasIssuedToday(liveId, userId)) {
+            String sessionKey = resolveLiveSessionKey(live);
+            if (hasIssuedInSession(liveId, userId, live)) {
                 log.info("[完课优惠券] 今日已发券, 跳过, liveId={}, userId={}", liveId, userId);
                 return result;
             }
@@ -145,7 +147,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                 return result;
             }
 
-            if (!forcePush && hasNotifiedToday(liveId, userId)) {
+            if (!forcePush && hasNotifiedInSession(liveId, userId, sessionKey)) {
                 log.info("[完课优惠券] 今日已推送过弹窗, 跳过, liveId={}, userId={}", liveId, userId);
                 result.setQuestions(questions);
                 return result;
@@ -735,6 +737,10 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     }
 
     private boolean hasIssuedToday(Long liveId, Long userId) {
+        return hasIssuedInSession(liveId, userId, liveService.selectLiveByLiveId(liveId));
+    }
+
+    private boolean hasIssuedInSession(Long liveId, Long userId, Live live) {
         LiveCouponUser query = new LiveCouponUser();
         query.setUserId(userId.intValue());
         query.setType(COMPLETION_COUPON_TYPE + "-" + liveId);
@@ -742,6 +748,14 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         if (existingList == null || existingList.isEmpty()) {
             return false;
         }
+        LocalDateTime startTime = live != null ? live.getStartTime() : null;
+        if (startTime != null) {
+            // 本场已发券(开播后);结束后重开可再领
+            return existingList.stream()
+                    .anyMatch(item -> item.getCreateTime() != null
+                            && !item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()
+                            .isBefore(startTime));
+        }
         LocalDate today = LocalDate.now();
         return existingList.stream()
                 .anyMatch(item -> item.getCreateTime() != null
@@ -881,6 +895,10 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         return Boolean.TRUE.equals(redisCache.getCacheObject(buildNotifyRedisKey(liveId, userId)));
     }
 
+    private boolean hasNotifiedInSession(Long liveId, Long userId, String sessionKey) {
+        return Boolean.TRUE.equals(redisCache.getCacheObject(buildNotifyRedisKey(liveId, userId, sessionKey)));
+    }
+
     private void markNotifiedToday(Long liveId, Long userId) {
         redisCache.setCacheObject(buildNotifyRedisKey(liveId, userId), Boolean.TRUE, 1, TimeUnit.DAYS);
     }
@@ -903,13 +921,27 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     }
 
     private String buildAnswerRecordRedisKey(Long liveId, Long userId) {
-        String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
-        return ANSWER_RECORD_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + today;
+        return ANSWER_RECORD_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + resolveLiveSessionKey(liveId);
     }
 
     private String buildNotifyRedisKey(Long liveId, Long userId) {
-        String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
-        return NOTIFY_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + today;
+        return buildNotifyRedisKey(liveId, userId, resolveLiveSessionKey(liveId));
+    }
+
+    private String buildNotifyRedisKey(Long liveId, Long userId, String sessionKey) {
+        return NOTIFY_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + sessionKey;
+    }
+
+    private String resolveLiveSessionKey(Long liveId) {
+        return resolveLiveSessionKey(liveService.selectLiveByLiveId(liveId));
+    }
+
+    /** 按本场开播时间区分会话,结束后重开可再推送/领取 */
+    private String resolveLiveSessionKey(Live live) {
+        if (live != null && live.getStartTime() != null) {
+            return live.getStartTime().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        }
+        return LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
     }
 
     private long calculateRequiredDuration(long videoDurationSeconds, Integer completionRate) {

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

@@ -116,17 +116,31 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
                 return null;
             }
 
-            // 7. 检查今天是否已有完课记录
+            // 7. 本场直播(按 startTime)是否已创建过完课记录;结束后重开可再领
+            LiveCompletionPointsRecord sessionRecord = recordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            if (sessionRecord != null && live.getStartTime() != null) {
+                Date recordTime = sessionRecord.getCreateTime() != null
+                        ? sessionRecord.getCreateTime()
+                        : sessionRecord.getCurrentCompletionDate();
+                if (recordTime != null) {
+                    LocalDateTime recordLdt = recordTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+                    if (!recordLdt.isBefore(live.getStartTime())) {
+                        return null;
+                    }
+                }
+            } else if (sessionRecord != null && live.getStartTime() == null) {
+                LocalDate today = LocalDate.now();
+                Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+                LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+                if (todayRecord != null) {
+                    return null;
+                }
+            }
+
             LocalDate today = LocalDate.now();
             Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
 
-            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
-            if (todayRecord != null) {
-
-                return null;
-            }
-
-            // 7. 查询最近一次完课记录(不限直播间),计算连续天数
+            // 8. 查询最近一次完课记录(不限直播间),计算连续天数
             LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
             int continuousDays = 1;
 
@@ -148,10 +162,10 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
                 }
             }
 
-            // 8. 计算积分
+            // 9. 计算积分
             int points = calculatePoints(continuousDays, pointsConfig);
 
-            // 9. 创建完课记录
+            // 10. 创建完课记录
             LiveCompletionPointsRecord record = new LiveCompletionPointsRecord();
             record.setLiveId(liveId);
             record.setUserId(userId);

+ 4 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -1101,6 +1101,8 @@ public class LiveServiceImpl implements ILiveService
         baseMapper.updateLive(exist);
         // 清除缓存
         clearLiveCache(live.getLiveId());
+        // 结束直播清理看课时长,避免结束后继续累计
+        liveWatchUserService.resetWatchDurationOnLiveEnd(live.getLiveId());
 
         return R.ok();
     }
@@ -1146,6 +1148,8 @@ public class LiveServiceImpl implements ILiveService
         baseMapper.updateLive(exist);
         // 清除缓存
         clearLiveCache(live.getLiveId());
+        // 重新开播:清理旧场看课时长,本场重新累计
+        liveWatchUserService.resetWatchDurationOnLiveEnd(live.getLiveId());
 //        List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectNoActivedByLiveId(exist.getLiveId(), new Date());
 //        liveAutoTasks.forEach(liveAutoTask -> {
 //            liveAutoTask.setCreateTime(null);

+ 45 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -610,6 +610,11 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         params.put("now", now);
         params.put("liveFlag", flagMap.get("liveFlag"));
         params.put("replayFlag", flagMap.get("replayFlag"));
+        // 本场开播时间一次查出传入,避免 SQL 内重复子查询
+        Live live = liveMapper.selectLiveByLiveId(liveId);
+        if (live != null && live.getStartTime() != null) {
+            params.put("liveStartTime", Date.from(live.getStartTime().atZone(ZoneId.systemDefault()).toInstant()));
+        }
         return baseMapper.checkOnlineNoRewardUser(params);
     }
 
@@ -1327,6 +1332,46 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return record.getOnlineSeconds();
     }
 
+    @Override
+    public void resetWatchDurationOnLiveEnd(Long liveId) {
+        if (liveId == null) {
+            return;
+        }
+        try {
+            baseMapper.resetOnlineSecondsByLiveId(liveId);
+        } catch (Exception e) {
+            log.error("重置直播看课时长(DB)失败: liveId={}", liveId, e);
+        }
+        try {
+            redisCache.deleteObject("live:watch:duration:hash:" + liveId);
+        } catch (Exception e) {
+            log.error("清理直播看课时长Redis hash失败: liveId={}", liveId, e);
+        }
+        // 按用户ID精确删 key,避免 redis KEYS 全库扫描
+        try {
+            List<Long> userIds = baseMapper.selectUserIdsByLiveId(liveId);
+            if (userIds != null && !userIds.isEmpty()) {
+                List<String> entryKeys = new ArrayList<>(Math.min(userIds.size(), 500));
+                for (Long userId : userIds) {
+                    if (userId == null) {
+                        continue;
+                    }
+                    entryKeys.add(String.format(USER_ENTRY_TIME_KEY, liveId, userId));
+                    if (entryKeys.size() >= 500) {
+                        redisCache.deleteObject(entryKeys);
+                        entryKeys.clear();
+                    }
+                }
+                if (!entryKeys.isEmpty()) {
+                    redisCache.deleteObject(entryKeys);
+                }
+            }
+        } catch (Exception e) {
+            log.error("清理直播进入时间Redis失败: liveId={}", liveId, e);
+        }
+        log.info("直播结束已清理看课时长: liveId={}", liveId);
+    }
+
     @Override
     public Long getUserWatchDuration(Long liveId, Long userId) {
         if (liveId == null || userId == null) {

+ 8 - 1
fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml

@@ -225,7 +225,14 @@
             WHERE lrr.user_id = lwu.user_id
               AND lrr.live_id = lwu.live_id
               and lrr.source_type = 3
-              AND DATE(lrr.create_time) = DATE(#{params.now})
+              <choose>
+                  <when test="params.liveStartTime != null">
+                      AND lrr.create_time &gt;= #{params.liveStartTime}
+                  </when>
+                  <otherwise>
+                      AND DATE(lrr.create_time) = DATE(#{params.now})
+                  </otherwise>
+              </choose>
         )
     </select>
 

+ 145 - 6
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -1,15 +1,25 @@
 package com.fs.app.controller.live;
 
 import com.fs.app.controller.AppBaseController;
+import com.fs.app.vo.UpdateWatchDurationVO;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserService;
+import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -27,6 +37,18 @@ public class LiveCompletionPointsController extends AppBaseController {
     @Autowired
     private IFsUserService fsUserService;
 
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+
+    @Autowired
+    private LiveCompletionPointsRecordMapper completionPointsRecordMapper;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
     /**
      * 领取完课积分
      */
@@ -64,20 +86,20 @@ public class LiveCompletionPointsController extends AppBaseController {
     @GetMapping("/info")
     public R getInfo(@RequestParam Long liveId) {
         Long userId = Long.parseLong(getUserId());
-        
+
         // 1. 获取用户积分余额
         FsUser user = fsUserService.selectFsUserByUserId(userId);
         Long integral = user != null && user.getIntegral() != null ? user.getIntegral() : 0L;
-        
+
         // 2. 获取完课记录列表(包含已领取和未领取)
         List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
-        
+
         // 3. 统计信息
         long totalPoints = records.stream()
                 .filter(r -> r.getReceiveStatus() == 1)
                 .mapToLong(LiveCompletionPointsRecord::getPointsAwarded)
                 .sum();
-        
+
         long unreceivedCount = records.stream()
                 .filter(r -> r.getReceiveStatus() == 0)
                 .count();
@@ -88,7 +110,7 @@ public class LiveCompletionPointsController extends AppBaseController {
         result.put("totalDays", records.size());  // 累计看直播天数
         result.put("unreceivedCount", unreceivedCount);  // 未领取记录数
         result.put("records", records);  // 完课记录列表
-        
+
         return R.ok().put("data", result);
     }
 
@@ -98,7 +120,7 @@ public class LiveCompletionPointsController extends AppBaseController {
     @PostMapping("/test/create")
     public R testCreateRecord(@RequestParam Long liveId, @RequestParam(required = false) Long watchDuration) {
         Long userId = Long.parseLong(getUserId());
-        
+
         try {
             // 调用完课记录创建方法(watchDuration为null时会自动从数据库累计)
             completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, watchDuration);
@@ -107,4 +129,121 @@ public class LiveCompletionPointsController extends AppBaseController {
             return R.error("创建失败: " + e.getMessage());
         }
     }
+
+
+
+    /**
+     * 第二个接口:更新用户的看课时长
+     * POST请求,传入直播间id和看课时长
+     * 更新用户看课completionPointsRecordService看课记录里面的时长
+     */
+    @PostMapping("/update-watch-duration")
+    @Transactional
+    public R updateWatchDuration(@RequestParam Long liveId, @RequestParam Long watchDuration) {
+        Long userId = Long.parseLong(getUserId());
+
+        try {
+            // 1. 获取直播间信息
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            // 2. 判断当前时间是否在直播期间(状态为2,直播中)
+            boolean isLiveInProgress = false;
+            LocalDateTime now = LocalDateTime.now();
+
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                // status=2 表示直播中
+                isLiveInProgress = true;
+            } else if (live.getStartTime() != null && live.getFinishTime() != null) {
+                // 判断当前时间是否在开播时间和结束时间之间
+                isLiveInProgress = (now.isAfter(live.getStartTime()) || now.isEqual(live.getStartTime()))
+                        && (now.isBefore(live.getFinishTime()) || now.isEqual(live.getFinishTime()));
+            }
+
+            if (!isLiveInProgress) {
+                return R.error("当前不在直播期间,无法更新看课时长");
+            }
+
+            // 3. 查询当前直播间的完课记录(不限制日期)
+            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+
+            // 4. 计算看课时长
+            Date updateTime = null;
+            if (record != null && record.getUpdateTime() != null) {
+                updateTime = record.getUpdateTime();
+            }
+
+            // 判断更新时间与直播间开始时间的关系
+            Date startTime = live.getStartTime() != null
+                    ? java.sql.Timestamp.valueOf(live.getStartTime()) : null;
+
+            Date currentTime = new Date();
+            long timeDiff = 0L;
+
+            if (updateTime != null && startTime != null) {
+                if (updateTime.before(startTime)) {
+                    // 更新时间小于直播间开始时间,使用直播间开始时间进行计算
+                    timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
+                } else {
+                    // 更新时间大于等于开播时间,按照更新时间进行计算
+                    timeDiff = (currentTime.getTime() - updateTime.getTime()) / 1000; // 转换为秒
+                }
+            } else if (startTime != null) {
+                // 没有更新记录,使用直播间开始时间计算
+                timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
+            }
+
+            // 5. 如果请求传入的时间大于这个时间差,就使用计算出的看课时长,否则使用请求传入的时长
+            Long finalWatchDuration;
+            if (watchDuration > timeDiff) {
+                // 请求传入的时间大于时间差,使用计算出的看课时长
+                finalWatchDuration = timeDiff;
+            } else {
+                // 否则使用请求传入的时长
+                finalWatchDuration = watchDuration;
+            }
+
+            // 6. 更新完课记录中的看课时长
+            if (record == null) {
+                // 如果没有记录,先创建记录
+                record = completionPointsRecordService.createCompletionRecord(liveId, userId);
+                record.setWatchDuration(finalWatchDuration);
+                completionPointsRecordMapper.updateRecord(record);
+            } else {
+                // 更新现有记录的看课时长
+                Long currentWatchDuration = record.getWatchDuration() != null
+                        ? record.getWatchDuration() : 0L;
+                record.setWatchDuration(currentWatchDuration + finalWatchDuration);
+
+                // 重新计算完课比例
+                Long videoDuration = live.getDuration();
+                if (videoDuration != null && videoDuration > 0) {
+                    BigDecimal completionRate = BigDecimal.valueOf(record.getWatchDuration())
+                            .multiply(BigDecimal.valueOf(100))
+                            .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
+                    if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                        completionRate = BigDecimal.valueOf(100);
+                    }
+                    record.setCompletionRate(completionRate);
+                }
+
+                int updateResult = completionPointsRecordMapper.updateRecord(record);
+                if (updateResult <= 0) {
+                    return R.error("更新看课时间失败");
+                }
+            }
+
+            UpdateWatchDurationVO vo = new UpdateWatchDurationVO();
+            vo.setWatchDuration(finalWatchDuration);
+            vo.setTotalWatchDuration(record != null && record.getWatchDuration() != null
+                    ? record.getWatchDuration() : finalWatchDuration);
+
+            return R.ok().put("data", vo);
+        } catch (Exception e) {
+            return R.error("更新失败: " + e.getMessage());
+        }
+    }
+
 }

+ 21 - 0
fs-user-app/src/main/java/com/fs/app/vo/UpdateWatchDurationVO.java

@@ -0,0 +1,21 @@
+package com.fs.app.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 更新看课时长返回VO
+ */
+@Data
+public class UpdateWatchDurationVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 本次更新的看课时长(秒) */
+    private Long watchDuration;
+    
+    /** 总看课时长(秒) */
+    private Long totalWatchDuration;
+}
+