瀏覽代碼

1、积分明细记录问题
2、im新增接口查询信息
3、奖励记录展示不确定
4、定时奖励发放不时实

yys 1 天之前
父節點
當前提交
b32683f056
共有 19 個文件被更改,包括 384 次插入63 次删除
  1. 5 17
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  2. 46 13
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  3. 8 3
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  4. 1 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  5. 3 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  6. 2 0
      fs-service/src/main/java/com/fs/im/service/OpenIMService.java
  7. 24 1
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  8. 5 1
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  9. 5 0
      fs-service/src/main/java/com/fs/live/service/ILiveConsoleOpLogService.java
  10. 23 9
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java
  11. 6 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  12. 103 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveConsoleOpLogServiceImpl.java
  13. 8 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java
  14. 2 7
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  15. 34 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  16. 60 0
      fs-service/src/main/java/com/fs/live/utils/LiveCompletionConfigUtils.java
  17. 3 3
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionAnswerDetailVO.java
  18. 40 0
      fs-user-app/src/main/java/com/fs/app/controller/app/ImController.java
  19. 6 2
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

+ 5 - 17
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -681,17 +681,10 @@ public class IpadSendServer {
         );
         Integer courseType = setting.getCourseType();
         String logId = qwSopLogs.getId();
-        if(null != liveWatchLog){
-                    if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
-                        // 作废消息
-                        log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
-                        qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
-                        return false;
-                    }
-        }
-        else{
-            log.warn("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
-            qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "无观看记录,不发送");
+        if (liveWatchLog != null
+                && !QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
+            log.warn("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
             return false;
         }
 //        if (qwSopLogs.getSendType() != 6 && noSop) {
@@ -1351,13 +1344,8 @@ public class IpadSendServer {
             }
             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
+            if (liveWatchLog != null && courseType != null && courseType != 0
                     && !QwSopLogsServiceImpl.isCourseTypeValid(courseType, liveWatchLog.getLogType())) {
                 qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "看课状态未满足,不发送");
                 return false;

+ 46 - 13
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -10,6 +10,9 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.framework.aspectj.lock.DistributeLock;
 import com.fs.erp.service.FsJstAftersalePushService;
+import com.fs.his.enums.FsUserIntegralLogTypeEnum;
+import com.fs.his.param.FsUserAddIntegralTemplateParam;
+import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserService;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveLotteryRegistrationMapper;
@@ -69,6 +72,8 @@ public class Task {
     @Autowired
     private IFsUserService fsUserService;
     @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+    @Autowired
     private ILiveRewardRecordService liveRewardRecordService;
     @Autowired
     private WebSocketServer webSocketServer;
@@ -530,11 +535,7 @@ public class Task {
 
             switch (action.intValue()) {
                 case 2: // 积分红包
-                    // 4.保存用户领取记录
                     saveUserRewardRecord(openRewardLive, userIds, BigDecimal.valueOf(config.getScoreAmount()), 2);
-                    // 5.更新用户积分
-                    fsUserService.increaseIntegral(userIds, config.getScoreAmount());
-                    // 6.发送websocket事件消息 通知用户自动领取成功
                     LiveConsoleOpLog watchPointsOpLog = liveConsoleOpLogService.saveLog(
                             openRewardLive.getLiveId(),
                             LiveConsoleOpLog.OP_WATCH_REWARD_POINTS,
@@ -542,15 +543,25 @@ public class Task {
                             openRewardLive.getLiveId(),
                             resolveWatchRewardPointsBizName(config, userIds.size())
                     );
-                    userIds.forEach(userId -> {
-                        liveConsoleOpLogService.bindOpLogUser(watchPointsOpLog.getId(), openRewardLive.getLiveId(), userId);
-                        webSocketServer.sendIntegralMessage(
-                                openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog);
-                    });
-                    rewardLiveCount++;
-                    rewardedUserCount += userIds.size();
-                    log.info("{} autoUpdateWatchReward 积分发放完成: liveId={}, 用户数={}",
-                            LOG_PREFIX, openRewardLive.getLiveId(), userIds.size());
+                    int pointsSuccessCount = 0;
+                    for (Long userId : userIds) {
+                        if (grantWatchRewardIntegral(openRewardLive.getLiveId(), userId, config.getScoreAmount())) {
+                            liveConsoleOpLogService.bindOpLogUser(
+                                    watchPointsOpLog.getId(), openRewardLive.getLiveId(), userId);
+                            webSocketServer.sendIntegralMessage(
+                                    openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog);
+                            pointsSuccessCount++;
+                        }
+                    }
+                    if (pointsSuccessCount > 0) {
+                        rewardLiveCount++;
+                        rewardedUserCount += pointsSuccessCount;
+                        log.info("{} autoUpdateWatchReward 积分发放完成: liveId={}, 成功用户数={}/{}",
+                                LOG_PREFIX, openRewardLive.getLiveId(), pointsSuccessCount, userIds.size());
+                    } else {
+                        log.warn("{} autoUpdateWatchReward 积分发放全部失败: liveId={}, 用户数={}",
+                                LOG_PREFIX, openRewardLive.getLiveId(), userIds.size());
+                    }
                     break;
 
                 case 3: // 优惠券
@@ -801,6 +812,28 @@ public class Task {
     private void saveUserRewardRecord(Live live, List<Long> userIds, Long scoreAmount) {
         saveUserRewardRecord(live, userIds, BigDecimal.valueOf(scoreAmount), 2);
     }
+
+    /**
+     * 发放观看奖励积分:更新用户积分并写入 fs_user_integral_logs
+     */
+    private boolean grantWatchRewardIntegral(Long liveId, Long userId, Long scoreAmount) {
+        if (liveId == null || userId == null || scoreAmount == null || scoreAmount <= 0) {
+            return false;
+        }
+        FsUserAddIntegralTemplateParam integralParam = new FsUserAddIntegralTemplateParam();
+        integralParam.setUserId(userId);
+        integralParam.setLogType(FsUserIntegralLogTypeEnum.TYPE_35);
+        integralParam.setBusinessId("live_watch_reward_" + liveId + "_" + userId);
+        integralParam.setPoints(scoreAmount);
+        R result = fsUserIntegralLogsService.addIntegralTemplate(integralParam);
+        if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
+            log.warn("{} 观看奖励积分写入失败: liveId={}, userId={}, msg={}",
+                    LOG_PREFIX, liveId, userId, result != null ? result.get("msg") : "null");
+            return false;
+        }
+        return true;
+    }
+
     /**
      * 从Redis获取对象并转换为Long类型
      * @param redisCache Redis缓存操作对象

+ 8 - 3
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -1841,7 +1841,10 @@ public class WebSocketServer {
             if (duration == null || duration <= 0) {
                 return;
             }
-            checkAndSendCompletionPointsInRealTime(liveId, userId, duration);
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live != null && com.fs.live.utils.LiveCompletionConfigUtils.isCompletionPointsMode(live.getConfigJson())) {
+                checkAndSendCompletionPointsInRealTime(liveId, userId, duration);
+            }
             SpringUtils.getBean(LiveCompletionPointsTask.class)
                     .tryDispatchCompletionCouponNotify(liveId, userId, duration);
         } catch (Exception e) {
@@ -1937,8 +1940,10 @@ public class WebSocketServer {
             }
 
             JSONObject jsonConfig = JSON.parseObject(configJson);
-            boolean enabled = jsonConfig.getBooleanValue("enabled");
-            if (!enabled) {
+            if (com.fs.live.utils.LiveCompletionConfigUtils.isCompletionCouponMode(configJson)) {
+                return;
+            }
+            if (!com.fs.live.utils.LiveCompletionConfigUtils.isCompletionPointsMode(configJson)) {
                 return;
             }
 

+ 1 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java

@@ -44,6 +44,7 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_32(32,"积分兑换佣金"),
     TYPE_33(33,"福袋积分"),
     TYPE_34(34,"随访获取积分"),
+    TYPE_35(35, "直播观看奖励积分"),
     ;
 
 

+ 3 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java

@@ -501,6 +501,9 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                 case 27: //直播完课积分
                     integralNum = param.getPoints();
                     break;
+                case 35: //直播观看奖励积分
+                    integralNum = param.getPoints();
+                    break;
                 case 29: //游戏积分
                     integralNum = param.getPoints();
                     break;

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

@@ -91,4 +91,6 @@ public interface OpenIMService {
     OpenImResponseDTO doctorSendMsgToUser(Long userId,Long doctorId,Integer type,Integer status);
 
     OpenImResponseDTO getFriendList(String userID, int pageNumber, int showNumber,Integer applyType);
+
+    OpenImResponseDTO getUserInfo(String id);
 }

+ 24 - 1
fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java

@@ -103,9 +103,12 @@ public class OpenIMServiceImpl implements OpenIMService {
     private FsImMsgSendDetailServiceImpl fsImMsgSendDetailServiceImpl;
     @Autowired
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    /** HTTP请求超时时间(毫秒) */
+    private static final int HTTP_TIMEOUT_MS = 60000;
 
 
-//    @Value("${openIM.prefix}")
+
+    //    @Value("${openIM.prefix}")
 //    private String openImPrefix;
     /*@Autowired
     private IFsUserService fsUserService;*/
@@ -1734,4 +1737,24 @@ public class OpenIMServiceImpl implements OpenIMService {
         }
         return null;
     }
+    @Override
+    public OpenImResponseDTO getUserInfo(String id) {
+        // 是数字的话默认用户
+        if(id.matches("\\d+")){
+            id = "U"+id;
+        }
+        String adminToken = getAdminToken();
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("userIDs", Collections.singletonList(id));
+        String body = HttpRequest.post(IMConfig.URL+"/user/get_users_info")
+                .header("operationID", String.valueOf(System.currentTimeMillis()))
+                .header("token", adminToken)
+                .timeout(HTTP_TIMEOUT_MS)
+                .body(jsonObject.toString())
+                .execute()
+                .body();
+        OpenImResponseDTO responseDTO= JSONUtil.toBean(body,OpenImResponseDTO.class);
+        return responseDTO;
+    }
+
 }

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

@@ -127,7 +127,11 @@ 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, '$.enabled') = true " +
+            "and (JSON_EXTRACT(config_json, '$.participateCondition') is null " +
+            "     or CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.participateCondition')) AS UNSIGNED) != 3) " +
+            "and JSON_EXTRACT(config_json, '$.pointsConfig') is not null " +
+            "and JSON_LENGTH(JSON_EXTRACT(config_json, '$.pointsConfig')) > 0")
     List<Live> selectLiveListWithCompletionPointsEnabled();
 
     /**

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

@@ -52,4 +52,9 @@ public interface ILiveConsoleOpLogService {
      * 查询用户在指定直播间的留存记录(含领取/结束状态)
      */
     List<LiveConsoleOpLogRecordVo> listUserOpLogRecords(Long liveId, Long userId);
+
+    /**
+     * 查询指定直播间某次优惠券发放对应的最新展示留存 ID
+     */
+    Long resolveLatestCouponShowOpLogId(Long liveId, Long couponIssueId);
 }

+ 23 - 9
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -425,19 +426,20 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     }
 
     private boolean isWatchRateEligible(Long liveId, Long userId, Long watchDuration, CompletionCouponConfig config) {
-        Long actualWatchDuration = watchDuration;
-        if (actualWatchDuration == null) {
-            actualWatchDuration = liveWatchUserService.getUserWatchDuration(liveId, userId);
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            log.info("[完课优惠券] 直播间不存在, liveId={}, userId={}", liveId, userId);
+            return false;
         }
-        if (actualWatchDuration == null || actualWatchDuration <= 0) {
-            log.info("[完课优惠券] 用户观看时长为0或null, liveId={}, userId={}, watchDuration={}",
-                    liveId, userId, actualWatchDuration);
+        if (!isLiveWatchDurationCountable(live)) {
+            log.info("[完课优惠券] 直播未开始,不计看课时长, liveId={}, userId={}", liveId, userId);
             return false;
         }
 
-        Live live = liveService.selectLiveByLiveId(liveId);
-        if (live == null) {
-            log.info("[完课优惠券] 直播间不存在, liveId={}, userId={}", liveId, userId);
+        Long actualWatchDuration = liveWatchUserService.getUserWatchDuration(liveId, userId);
+        if (actualWatchDuration == null || actualWatchDuration <= 0) {
+            log.info("[完课优惠券] 用户观看时长为0或null, liveId={}, userId={}, watchDuration={}",
+                    liveId, userId, actualWatchDuration);
             return false;
         }
 
@@ -468,6 +470,18 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         return eligible;
     }
 
+    private boolean isLiveWatchDurationCountable(Live live) {
+        if (live == null) {
+            return false;
+        }
+        Integer status = live.getStatus();
+        if (status != null && (status == 2 || status == 3 || status == 4)) {
+            return true;
+        }
+        LocalDateTime startTime = live.getStartTime();
+        return startTime != null && !startTime.isAfter(LocalDateTime.now());
+    }
+
     private CompletionCouponConfig resolveConfig(Long liveId) {
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {

+ 6 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -84,9 +84,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             // 3. 获取观看时长(如果为null,则从数据库累计直播+回放时长)
             Long actualWatchDuration = watchDuration;
             if (actualWatchDuration == null) {
-                // 自动累加直播和回放的观看时长
-                actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
-
+                actualWatchDuration = liveWatchUserService.getUserWatchDuration(liveId, userId);
             }
 
             if (actualWatchDuration == null || actualWatchDuration <= 0) {
@@ -385,6 +383,11 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         try {
             JSONObject jsonConfig = JSON.parseObject(configJson);
 
+            Long participateCondition = jsonConfig.getLong("participateCondition");
+            if (participateCondition != null && participateCondition == 3L) {
+                return config;
+            }
+
             config.setEnabled(jsonConfig.getBooleanValue("enabled"));
 
             Integer rate = jsonConfig.getInteger("completionRate");

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

@@ -11,16 +11,21 @@ import com.fs.live.domain.LiveConsoleOpLogUser;
 import com.fs.live.domain.LiveCoupon;
 import com.fs.live.domain.LiveCouponIssue;
 import com.fs.live.domain.LiveCouponIssueRelation;
+import com.fs.live.domain.LiveCouponIssueUser;
+import com.fs.live.domain.LiveCouponUser;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveRedConf;
 import com.fs.live.mapper.LiveConsoleOpLogMapper;
 import com.fs.live.mapper.LiveConsoleOpLogUserMapper;
+import com.fs.live.mapper.LiveCouponIssueUserMapper;
 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.ILiveCouponUserService;
 import com.fs.live.service.ILiveLotteryConfService;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.utils.LiveCompletionConfigUtils;
 import com.fs.live.vo.LiveConsoleOpLogRecordVo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -28,6 +33,7 @@ import org.springframework.util.CollectionUtils;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.Comparator;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
@@ -71,6 +77,15 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
     @Autowired
     private LiveCouponMapper liveCouponMapper;
 
+    /** 完课优惠券 live_coupon_user.type 前缀 */
+    private static final String COMPLETION_COUPON_USER_TYPE_PREFIX = "4-";
+
+    @Autowired
+    private LiveCouponIssueUserMapper liveCouponIssueUserMapper;
+
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+
     @Autowired
     private LiveMapper liveMapper;
 
@@ -250,16 +265,23 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
                     .filter(Objects::nonNull)
                     .collect(Collectors.toSet());
         }
+        Set<Long> claimedCouponIssueIds = loadUserClaimedCouponIssueIds(userId);
+        Set<Long> claimedCompletionCouponIds = loadUserClaimedCompletionCouponIds(liveId, userId);
 
         Date now = DateUtils.getNowDate();
         Live live = liveMapper.selectLiveByLiveId(liveId);
+        boolean completionCouponMode = live != null && LiveCompletionConfigUtils.isCompletionCouponMode(live.getConfigJson());
         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());
+            if (completionCouponMode && opLog.getOpType() != null
+                    && opLog.getOpType() == LiveConsoleOpLog.OP_COMPLETION_POINTS) {
+                continue;
+            }
+            boolean claimed = isUserClaimedOpLog(opLog, claimedOpLogIds, claimedCouponIssueIds, claimedCompletionCouponIds);
             int status = resolveOpLogStatus(opLog, claimed, now, live);
             LiveConsoleOpLogRecordVo recordVo = LiveConsoleOpLogRecordVo.from(opLog, status);
             fillCouponType(recordVo, opLog, couponTypeMap);
@@ -328,6 +350,86 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         recordVo.setCouponType(couponTypeMap.get(opLog.getBizId()));
     }
 
+    private Set<Long> loadUserClaimedCouponIssueIds(Long userId) {
+        if (userId == null) {
+            return Collections.emptySet();
+        }
+        LiveCouponIssueUser query = new LiveCouponIssueUser();
+        query.setUserId(userId);
+        query.setIsDel(0);
+        List<LiveCouponIssueUser> issueUsers = liveCouponIssueUserMapper.selectLiveCouponIssueUserList(query);
+        if (issueUsers == null || issueUsers.isEmpty()) {
+            return Collections.emptySet();
+        }
+        return issueUsers.stream()
+                .map(LiveCouponIssueUser::getIssueId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+    }
+
+    private Set<Long> loadUserClaimedCompletionCouponIds(Long liveId, Long userId) {
+        if (liveId == null || userId == null) {
+            return Collections.emptySet();
+        }
+        LiveCouponUser query = new LiveCouponUser();
+        query.setUserId(userId.intValue());
+        query.setType(COMPLETION_COUPON_USER_TYPE_PREFIX + liveId);
+        List<LiveCouponUser> couponUsers = liveCouponUserService.selectLiveCouponUserList(query);
+        if (couponUsers == null || couponUsers.isEmpty()) {
+            return Collections.emptySet();
+        }
+        return couponUsers.stream()
+                .map(LiveCouponUser::getCouponId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * 判断用户是否已领取/参与该条留存。
+     * 普通优惠券 additionally 以 live_coupon_issue_user 为准,避免领券时未绑定 opLog 被误判为已结束。
+     */
+    private boolean isUserClaimedOpLog(LiveConsoleOpLog opLog, Set<Long> claimedOpLogIds,
+                                       Set<Long> claimedCouponIssueIds, Set<Long> claimedCompletionCouponIds) {
+        if (opLog == null) {
+            return false;
+        }
+        if (opLog.getId() != null && claimedOpLogIds.contains(opLog.getId())) {
+            return true;
+        }
+        if (opLog.getOpType() == null || opLog.getBizId() == null) {
+            return false;
+        }
+        int opType = opLog.getOpType();
+        if (opType == LiveConsoleOpLog.OP_COUPON_SHOW || opType == LiveConsoleOpLog.OP_VERIFY_COUPON_SHOW) {
+            return claimedCouponIssueIds.contains(opLog.getBizId());
+        }
+        if (opType == LiveConsoleOpLog.OP_COMPLETION_COUPON) {
+            return claimedCompletionCouponIds.contains(opLog.getBizId());
+        }
+        return false;
+    }
+
+    @Override
+    public Long resolveLatestCouponShowOpLogId(Long liveId, Long couponIssueId) {
+        if (liveId == null || couponIssueId == null) {
+            return null;
+        }
+        LiveConsoleOpLog query = new LiveConsoleOpLog();
+        query.setLiveId(liveId);
+        query.setBizId(couponIssueId);
+        List<LiveConsoleOpLog> logs = liveConsoleOpLogMapper.selectLiveConsoleOpLogList(query);
+        if (logs == null || logs.isEmpty()) {
+            return null;
+        }
+        return logs.stream()
+                .filter(log -> log.getOpType() != null
+                        && (log.getOpType() == LiveConsoleOpLog.OP_COUPON_SHOW
+                        || log.getOpType() == LiveConsoleOpLog.OP_VERIFY_COUPON_SHOW))
+                .max(Comparator.comparing(LiveConsoleOpLog::getCreateTime, Comparator.nullsLast(Date::compareTo)))
+                .map(LiveConsoleOpLog::getId)
+                .orElse(null);
+    }
+
     /**
      * 状态优先级:已领取 > 已结束 > 待领取
      * <p>已领取:用户已成功领取/参与;已结束:活动已截止或用户未领到(如红包抢完),不再展示待领取</p>

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

@@ -358,8 +358,12 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         liveCouponUserService.insertLiveCouponUser(userRecord);
         liveCouponIssueUserService.insertLiveCouponIssueUser(record);
 
+        Long opLogId = coupon.getOpLogId();
+        if (opLogId == null) {
+            opLogId = liveConsoleOpLogService.resolveLatestCouponShowOpLogId(coupon.getLiveId(), issue.getId());
+        }
         liveConsoleOpLogService.bindOpLogUser(
-                coupon.getOpLogId(), coupon.getLiveId(), coupon.getUserId(), userRecord.getId());
+                opLogId, coupon.getLiveId(), coupon.getUserId(), userRecord.getId());
 
         // 更新优惠卷数量
         if (issue.getRemainCount() > 0) {
@@ -370,7 +374,9 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         }
 
 
-        return R.ok("恭喜您抢到优惠券");
+        return R.ok("恭喜您抢到优惠券")
+                .put("opLogId", opLogId)
+                .put("couponUserId", userRecord.getId());
     }
 
     @Override

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

@@ -399,13 +399,8 @@ public class LiveServiceImpl implements ILiveService
         String configJson = live.getConfigJson();
         if (StringUtils.isNotEmpty(configJson)) {
             try {
-                JSONObject jsonConfig = JSON.parseObject(configJson);
-                completionPointsEnabled = jsonConfig.getBooleanValue("enabled");
-                Long participateCondition = jsonConfig.getLong("participateCondition");
-                if (jsonConfig.getBooleanValue("enabled")
-                        && participateCondition != null && participateCondition == 3L) {
-                    completionCouponEnabled = true;
-                }
+                completionPointsEnabled = com.fs.live.utils.LiveCompletionConfigUtils.isCompletionPointsMode(configJson);
+                completionCouponEnabled = com.fs.live.utils.LiveCompletionConfigUtils.isCompletionCouponMode(configJson);
             } catch (Exception e) {
                 log.warn("解析直播完课奖励配置失败, liveId={}", id, e);
             }

+ 34 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -50,6 +50,8 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
@@ -1327,12 +1329,21 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
     @Override
     public Long getUserWatchDuration(Long liveId, Long userId) {
+        if (liveId == null || userId == null) {
+            return 0L;
+        }
+        Live live = liveMapper.selectLiveByLiveId(liveId);
+        if (!isLiveWatchDurationCountable(live)) {
+            return 0L;
+        }
+        long liveStartMillis = resolveLiveStartMillis(live);
         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;
+                long countFrom = liveStartMillis > 0 ? Math.max(entryTime, liveStartMillis) : entryTime;
+                long sessionSeconds = (System.currentTimeMillis() - countFrom) / 1000;
                 if (sessionSeconds > 0) {
                     duration += sessionSeconds;
                 }
@@ -1353,6 +1364,28 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return duration;
     }
 
+    /**
+     * 直播未开始不计看课时长;开播后(含直播中、已结束、回放中)再累计。
+     */
+    private boolean isLiveWatchDurationCountable(Live live) {
+        if (live == null) {
+            return false;
+        }
+        Integer status = live.getStatus();
+        if (status != null && (status == 2 || status == 3 || status == 4)) {
+            return true;
+        }
+        LocalDateTime startTime = live.getStartTime();
+        return startTime != null && !startTime.isAfter(LocalDateTime.now());
+    }
+
+    private long resolveLiveStartMillis(Live live) {
+        if (live == null || live.getStartTime() == null) {
+            return 0L;
+        }
+        return live.getStartTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
     private static long safeLong(Long value) {
         return value != null ? value : 0L;
     }

+ 60 - 0
fs-service/src/main/java/com/fs/live/utils/LiveCompletionConfigUtils.java

@@ -0,0 +1,60 @@
+package com.fs.live.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.utils.StringUtils;
+
+import java.util.List;
+
+/**
+ * 直播完课奖励 config_json 解析(积分 / 优惠券互斥)
+ */
+public final class LiveCompletionConfigUtils {
+
+    /** 参与条件:完课领取优惠券 */
+    public static final long PARTICIPATE_CONDITION_COMPLETION_COUPON = 3L;
+
+    private LiveCompletionConfigUtils() {
+    }
+
+    public static JSONObject parseConfig(String configJson) {
+        if (StringUtils.isEmpty(configJson)) {
+            return null;
+        }
+        try {
+            return JSONObject.parseObject(configJson);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 完课优惠券模式:enabled + participateCondition=3 + finishCouponId
+     */
+    public static boolean isCompletionCouponMode(String configJson) {
+        JSONObject json = parseConfig(configJson);
+        if (json == null || !json.getBooleanValue("enabled")) {
+            return false;
+        }
+        Long participateCondition = json.getLong("participateCondition");
+        if (participateCondition == null || participateCondition != PARTICIPATE_CONDITION_COMPLETION_COUPON) {
+            return false;
+        }
+        return StringUtils.isNotEmpty(json.getString("finishCouponId"));
+    }
+
+    /**
+     * 完课积分模式:enabled + 非优惠券模式 + 配置了 pointsConfig
+     */
+    public static boolean isCompletionPointsMode(String configJson) {
+        JSONObject json = parseConfig(configJson);
+        if (json == null || !json.getBooleanValue("enabled")) {
+            return false;
+        }
+        Long participateCondition = json.getLong("participateCondition");
+        if (participateCondition != null && participateCondition == PARTICIPATE_CONDITION_COMPLETION_COUPON) {
+            return false;
+        }
+        List<?> pointsConfig = json.getObject("pointsConfig", List.class);
+        return pointsConfig != null && !pointsConfig.isEmpty();
+    }
+}

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

@@ -3,7 +3,7 @@ package com.fs.live.vo;
 import lombok.Data;
 
 /**
- * 俇諺湘枙等枙隴牉ㄗ邈踱辦桽ㄘ
+ * 摰諹紋蝑娪��閖��𡒊�嚗�氜摨枏翰�改�
  */
 @Data
 public class LiveCompletionAnswerDetailVO {
@@ -12,13 +12,13 @@ public class LiveCompletionAnswerDetailVO {
 
     private String title;
 
-    /** 枙倰 1等恁 2嗣恁 */
+    /** 憸睃� 1�閖�� 2憭𡁻�� */
     private Long type;
 
     private String userAnswer;
 
     private String correctAnswer;
 
-    /** 掛枙岆瘁湘勤 0瘁 1岆 */
+    /** �祇��臬炏蝑𥪜笆 0�� 1�� */
     private Integer isRight;
 }

+ 40 - 0
fs-user-app/src/main/java/com/fs/app/controller/app/ImController.java

@@ -0,0 +1,40 @@
+package com.fs.app.controller.app;
+
+import com.fs.common.core.domain.R;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.service.OpenIMService;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @description: im 相关接口
+ * @author: Xgb
+ * @createDate: 2026/4/3
+ * @version: 1.0
+ */
+@Api("im 相关接口")
+@RestController
+@RequestMapping(value = "/app/im")
+@Slf4j
+public class ImController {
+
+    @Autowired
+    private OpenIMService openIMService;
+    /**
+     * @Description: 查询用户信息
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2026/4/3 15:45
+     */
+    @GetMapping("/getUserInfo")
+    public R getUserInfo(String id) {
+        OpenImResponseDTO responseDTO = openIMService.getUserInfo(id);
+        return R.ok().put("data",responseDTO);
+    }
+
+}

+ 6 - 2
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -270,7 +270,8 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
     public LiveUserRewardRecordsVo getUserRewardRecords(Long liveId, Long userId) {
         LiveUserRewardRecordsVo vo = new LiveUserRewardRecordsVo();
         vo.setRecords(liveConsoleOpLogService.listUserOpLogRecords(liveId, userId));
-        Long liveDuration = resolveLiveDuration(liveId);
+        Live live = liveId != null ? liveService.selectLiveByLiveId(liveId) : null;
+        Long liveDuration = resolveLiveDuration(live);
         vo.setLiveDuration(liveDuration);
         Long totalWatch = liveWatchUserService.getUserWatchDuration(liveId, userId);
         long watchDuration = totalWatch != null ? totalWatch : 0L;
@@ -285,7 +286,10 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         if (liveId == null) {
             return 0L;
         }
-        Live live = liveService.selectLiveByLiveId(liveId);
+        return resolveLiveDuration(liveService.selectLiveByLiveId(liveId));
+    }
+
+    private Long resolveLiveDuration(Live live) {
         if (live == null) {
             return 0L;
         }