Преглед на файлове

1、调整多优惠卷发放处理

yys преди 1 седмица
родител
ревизия
54129d3b41

+ 202 - 48
fs-service/src/main/java/com/fs/live/service/impl/LiveConsoleOpLogServiceImpl.java

@@ -205,15 +205,38 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         if (opLogId == null || CollectionUtils.isEmpty(relations)) {
             return;
         }
+        // uk_op_log_user(op_log_id, user_id):同一留存每人仅一条;多人发券/每人多张时需去重
+        Set<Long> existingUserIds = new HashSet<>();
+        List<LiveConsoleOpLogUser> existing = liveConsoleOpLogUserMapper.selectByOpLogId(opLogId);
+        if (existing != null) {
+            for (LiveConsoleOpLogUser row : existing) {
+                if (row.getUserId() != null) {
+                    existingUserIds.add(row.getUserId());
+                }
+            }
+        }
         Date now = DateUtils.getNowDate();
+        List<LiveConsoleOpLogUser> toInsert = new ArrayList<>(relations.size());
+        Set<Long> seenUserIds = new HashSet<>();
         for (LiveConsoleOpLogUser relation : relations) {
+            if (relation == null || relation.getUserId() == null) {
+                continue;
+            }
+            Long userId = relation.getUserId();
+            if (existingUserIds.contains(userId) || !seenUserIds.add(userId)) {
+                continue;
+            }
             relation.setOpLogId(opLogId);
             if (relation.getLiveId() == null) {
                 relation.setLiveId(liveId);
             }
             relation.setCreateTime(now);
+            toInsert.add(relation);
+        }
+        if (toInsert.isEmpty()) {
+            return;
         }
-        liveConsoleOpLogUserMapper.batchInsertLiveConsoleOpLogUser(relations);
+        liveConsoleOpLogUserMapper.batchInsertLiveConsoleOpLogUser(toInsert);
     }
 
     @Override
@@ -235,8 +258,20 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
             return;
         }
         List<LiveConsoleOpLogUser> existing = liveConsoleOpLogUserMapper.selectByOpLogId(opLogId);
-        if (existing != null && existing.stream().anyMatch(r -> userId.equals(r.getUserId()))) {
-            return;
+        if (existing != null) {
+            boolean duplicated = existing.stream().anyMatch(r -> {
+                if (!userId.equals(r.getUserId())) {
+                    return false;
+                }
+                // 同一留存可绑定多张券:有 couponUserId 时按券记录去重;无券记录时按用户去重
+                if (couponUserId != null) {
+                    return couponUserId.equals(r.getCouponUserId());
+                }
+                return r.getCouponUserId() == null;
+            });
+            if (duplicated) {
+                return;
+            }
         }
         bindOpLogUsers(opLogId, liveId,
                 Collections.singletonList(new LiveConsoleOpLogUser(opLogId, userId, liveId, couponUserId)));
@@ -254,18 +289,29 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
             return Collections.emptyList();
         }
 
-        Set<Long> claimedOpLogIds = new HashSet<>();
         List<LiveConsoleOpLogUser> relations = liveConsoleOpLogUserMapper.selectByLiveIdAndUserId(liveId, userId);
-        if (relations != null) {
-            claimedOpLogIds = relations.stream()
-                    .map(LiveConsoleOpLogUser::getOpLogId)
-                    .filter(Objects::nonNull)
-                    .collect(Collectors.toSet());
+        if (relations == null) {
+            relations = Collections.emptyList();
         }
-        // 中控台发券以 live_coupon_user(type=live-{liveId}) 为准,避免跨直播间共用 issue 误判已领取
+        Set<Long> claimedOpLogIds = relations.stream()
+                .map(LiveConsoleOpLogUser::getOpLogId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        // 一条留存可对应多张券:按留存统计当前用户领取张数
+        Map<Long, Integer> couponCountByOpLogId = buildCouponCountByOpLogId(relations);
+
+        // 中控台发券:按具体留存绑定;无绑定时 live_coupon_user 仅兜底同券最新一条
         Set<Long> claimedLiveCouponIds = loadUserClaimedLiveCouponIds(liveId, userId);
-        Set<Long> claimedCompletionCouponIds = loadUserClaimedCompletionCouponIds(liveId, userId);
-        Map<Long, Long> issueCouponIdMap = resolveIssueCouponIdMap(opLogs);
+        enrichClaimedCouponShowOpLogIds(opLogs, claimedOpLogIds, claimedLiveCouponIds);
+
+        // 完课优惠券:同上,避免同券多次留存 / 一次多张券时状态误判
+        Map<Long, Integer> completionCouponCountMap = loadUserClaimedCouponCountMap(
+                userId, COMPLETION_COUPON_USER_TYPE_PREFIX + liveId);
+        enrichClaimedCouponOpLogIdsByBizId(opLogs, claimedOpLogIds, couponCountByOpLogId,
+                completionCouponCountMap, LiveConsoleOpLog.OP_COMPLETION_COUPON);
+
+        // 观看奖励优惠券:发放时会写入留存绑定;一条留存多张券时张数由 couponCountByOpLogId 统计
+        // type=3 无直播间维度,不做跨券兜底,避免跨场误判已领取
 
         Date now = DateUtils.getNowDate();
         Live live = liveMapper.selectLiveByLiveId(liveId);
@@ -280,11 +326,11 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
                     && opLog.getOpType() == LiveConsoleOpLog.OP_COMPLETION_POINTS) {
                 continue;
             }
-            boolean claimed = isUserClaimedOpLog(opLog, claimedOpLogIds, claimedLiveCouponIds,
-                    claimedCompletionCouponIds, issueCouponIdMap);
+            boolean claimed = isUserClaimedOpLog(opLog, claimedOpLogIds);
             int status = resolveOpLogStatus(opLog, claimed, now, live);
             LiveConsoleOpLogRecordVo recordVo = LiveConsoleOpLogRecordVo.from(opLog, status);
             fillCouponType(recordVo, opLog, couponTypeMap);
+            fillCouponCount(recordVo, opLog, claimed, couponCountByOpLogId);
             result.add(recordVo);
         }
         result.sort(this::compareOpLogRecordByTime);
@@ -350,21 +396,46 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         recordVo.setCouponType(couponTypeMap.get(opLog.getBizId()));
     }
 
+    private void fillCouponCount(LiveConsoleOpLogRecordVo recordVo, LiveConsoleOpLog opLog,
+                                 boolean claimed, Map<Long, Integer> couponCountByOpLogId) {
+        if (recordVo == null || opLog == null || opLog.getId() == null) {
+            return;
+        }
+        Integer count = couponCountByOpLogId != null ? couponCountByOpLogId.get(opLog.getId()) : null;
+        if (count != null && count > 0) {
+            recordVo.setCouponCount(count);
+            return;
+        }
+        recordVo.setCouponCount(claimed ? 1 : 0);
+    }
+
+    /** 按留存统计当前用户领取张数(一条留存可对应多条 live_console_op_log_user / 多张券) */
+    private Map<Long, Integer> buildCouponCountByOpLogId(List<LiveConsoleOpLogUser> relations) {
+        Map<Long, Integer> countMap = new HashMap<>();
+        if (relations == null || relations.isEmpty()) {
+            return countMap;
+        }
+        for (LiveConsoleOpLogUser relation : relations) {
+            if (relation == null || relation.getOpLogId() == null) {
+                continue;
+            }
+            countMap.merge(relation.getOpLogId(), 1, Integer::sum);
+        }
+        return countMap;
+    }
+
     /**
      * 当前直播间中控台发券已领取的优惠券 ID(live_coupon_user.type = live-{liveId})。
      * 按直播间隔离,避免同一 issue 绑定到多个直播间时跨场误判。
      */
     private Set<Long> loadUserClaimedLiveCouponIds(Long liveId, Long userId) {
-        return loadUserClaimedCouponIdsByType(userId, LIVE_COUPON_USER_TYPE_PREFIX + liveId);
+        return loadUserClaimedCouponCountMap(userId, LIVE_COUPON_USER_TYPE_PREFIX + liveId).keySet();
     }
 
-    private Set<Long> loadUserClaimedCompletionCouponIds(Long liveId, Long userId) {
-        return loadUserClaimedCouponIdsByType(userId, COMPLETION_COUPON_USER_TYPE_PREFIX + liveId);
-    }
-
-    private Set<Long> loadUserClaimedCouponIdsByType(Long userId, String type) {
+    private Map<Long, Integer> loadUserClaimedCouponCountMap(Long userId, String type) {
+        Map<Long, Integer> countMap = new HashMap<>();
         if (userId == null || StringUtils.isEmpty(type)) {
-            return Collections.emptySet();
+            return countMap;
         }
         LiveCouponUser query = new LiveCouponUser();
         query.setUserId(userId.intValue());
@@ -372,12 +443,111 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
         query.setIsDel(0);
         List<LiveCouponUser> couponUsers = liveCouponUserService.selectLiveCouponUserList(query);
         if (couponUsers == null || couponUsers.isEmpty()) {
-            return Collections.emptySet();
+            return countMap;
+        }
+        for (LiveCouponUser couponUser : couponUsers) {
+            if (couponUser == null || couponUser.getCouponId() == null) {
+                continue;
+            }
+            countMap.merge(couponUser.getCouponId(), 1, Integer::sum);
+        }
+        return countMap;
+    }
+
+    /**
+     * 中控台发券:已领取以「具体留存绑定」为准;若领券未绑定任何展示留存,
+     * 再按 live_coupon_user 兜底,仅将同券最新一条展示留存记为已领取,避免同券多次操作整批已领取。
+     */
+    private void enrichClaimedCouponShowOpLogIds(List<LiveConsoleOpLog> opLogs, Set<Long> claimedOpLogIds,
+                                                 Set<Long> claimedLiveCouponIds) {
+        if (opLogs == null || opLogs.isEmpty() || claimedOpLogIds == null
+                || claimedLiveCouponIds == null || claimedLiveCouponIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> issueCouponIdMap = resolveIssueCouponIdMap(opLogs);
+        if (issueCouponIdMap.isEmpty()) {
+            return;
+        }
+
+        Map<Long, List<LiveConsoleOpLog>> showLogsByCouponId = new HashMap<>();
+        for (LiveConsoleOpLog opLog : opLogs) {
+            if (opLog == null || opLog.getId() == null || opLog.getOpType() == null || opLog.getBizId() == null) {
+                continue;
+            }
+            int opType = opLog.getOpType();
+            if (opType != LiveConsoleOpLog.OP_COUPON_SHOW && opType != LiveConsoleOpLog.OP_VERIFY_COUPON_SHOW) {
+                continue;
+            }
+            Long couponId = issueCouponIdMap.get(opLog.getBizId());
+            if (couponId == null || !claimedLiveCouponIds.contains(couponId)) {
+                continue;
+            }
+            showLogsByCouponId.computeIfAbsent(couponId, key -> new ArrayList<>()).add(opLog);
+        }
+        markLatestUnboundOpLogsAsClaimed(showLogsByCouponId, claimedOpLogIds, null, null);
+    }
+
+    /**
+     * 完课/观看奖励:bizId=couponId。
+     * 已有留存绑定则严格按绑定;无绑定但有领取记录时,仅把同券最新一条留存记为已领取,并回填领取张数。
+     */
+    private void enrichClaimedCouponOpLogIdsByBizId(List<LiveConsoleOpLog> opLogs, Set<Long> claimedOpLogIds,
+                                                    Map<Long, Integer> couponCountByOpLogId,
+                                                    Map<Long, Integer> claimedCouponCountMap,
+                                                    int targetOpType) {
+        if (opLogs == null || opLogs.isEmpty() || claimedOpLogIds == null
+                || claimedCouponCountMap == null || claimedCouponCountMap.isEmpty()) {
+            return;
+        }
+        Map<Long, List<LiveConsoleOpLog>> logsByCouponId = new HashMap<>();
+        for (LiveConsoleOpLog opLog : opLogs) {
+            if (opLog == null || opLog.getId() == null || opLog.getOpType() == null || opLog.getBizId() == null) {
+                continue;
+            }
+            if (opLog.getOpType() != targetOpType) {
+                continue;
+            }
+            Long couponId = opLog.getBizId();
+            if (!claimedCouponCountMap.containsKey(couponId)) {
+                continue;
+            }
+            logsByCouponId.computeIfAbsent(couponId, key -> new ArrayList<>()).add(opLog);
+        }
+        markLatestUnboundOpLogsAsClaimed(logsByCouponId, claimedOpLogIds, couponCountByOpLogId, claimedCouponCountMap);
+    }
+
+    private void markLatestUnboundOpLogsAsClaimed(Map<Long, List<LiveConsoleOpLog>> logsByCouponId,
+                                                  Set<Long> claimedOpLogIds,
+                                                  Map<Long, Integer> couponCountByOpLogId,
+                                                  Map<Long, Integer> claimedCouponCountMap) {
+        if (logsByCouponId == null || logsByCouponId.isEmpty() || claimedOpLogIds == null) {
+            return;
+        }
+        for (Map.Entry<Long, List<LiveConsoleOpLog>> entry : logsByCouponId.entrySet()) {
+            Long couponId = entry.getKey();
+            List<LiveConsoleOpLog> logs = entry.getValue();
+            if (logs == null || logs.isEmpty()) {
+                continue;
+            }
+            boolean alreadyBound = logs.stream().anyMatch(log -> claimedOpLogIds.contains(log.getId()));
+            if (alreadyBound) {
+                continue;
+            }
+            LiveConsoleOpLog latest = logs.stream()
+                    .sorted(Comparator.comparing(LiveConsoleOpLog::getCreateTime, Comparator.nullsLast(Date::compareTo)).reversed())
+                    .findFirst()
+                    .orElse(null);
+            if (latest == null || latest.getId() == null) {
+                continue;
+            }
+            claimedOpLogIds.add(latest.getId());
+            if (couponCountByOpLogId != null && claimedCouponCountMap != null) {
+                Integer count = claimedCouponCountMap.get(couponId);
+                if (count != null && count > 0) {
+                    couponCountByOpLogId.put(latest.getId(), count);
+                }
+            }
         }
-        return couponUsers.stream()
-                .map(LiveCouponUser::getCouponId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
     }
 
     /** 优惠券展示留存 bizId=issueId → couponId,用于对齐 live_coupon_user.coupon_id */
@@ -404,29 +574,13 @@ public class LiveConsoleOpLogServiceImpl implements ILiveConsoleOpLogService {
 
     /**
      * 判断用户是否已领取/参与该条留存。
-     * 普通优惠券以当前直播间 live_coupon_user 为准,避免领券未绑定 opLog 或跨直播间共用 issue 误判
+     * 统一按具体留存绑定(含兜底补齐后的结果)判定,避免同券多次留存或一次多张券时状态扩散
      */
-    private boolean isUserClaimedOpLog(LiveConsoleOpLog opLog, Set<Long> claimedOpLogIds,
-                                       Set<Long> claimedLiveCouponIds, Set<Long> claimedCompletionCouponIds,
-                                       Map<Long, Long> issueCouponIdMap) {
-        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) {
-            Long couponId = issueCouponIdMap != null ? issueCouponIdMap.get(opLog.getBizId()) : null;
-            return couponId != null && claimedLiveCouponIds != null && claimedLiveCouponIds.contains(couponId);
-        }
-        if (opType == LiveConsoleOpLog.OP_COMPLETION_COUPON) {
-            return claimedCompletionCouponIds != null && claimedCompletionCouponIds.contains(opLog.getBizId());
-        }
-        return false;
+    private boolean isUserClaimedOpLog(LiveConsoleOpLog opLog, Set<Long> claimedOpLogIds) {
+        return opLog != null
+                && opLog.getId() != null
+                && claimedOpLogIds != null
+                && claimedOpLogIds.contains(opLog.getId());
     }
 
     @Override

+ 7 - 0
fs-service/src/main/java/com/fs/live/vo/LiveConsoleOpLogRecordVo.java

@@ -38,6 +38,12 @@ public class LiveConsoleOpLogRecordVo {
     /** 优惠券类型(opType=8完课优惠券、10观看奖励优惠券时返回,对应 live_coupon.type:0普通 1套餐 2制单 3无门槛) */
     private Long couponType;
 
+    /**
+     * 当前用户在该留存下领取的优惠券张数。
+     * <p>完课/观看奖励等「一条留存发放多张券」时返回实际张数;未领取为 0。</p>
+     */
+    private Integer couponCount;
+
     public static LiveConsoleOpLogRecordVo from(LiveConsoleOpLog log, int status) {
         LiveConsoleOpLogRecordVo vo = new LiveConsoleOpLogRecordVo();
         if (log == null) {
@@ -54,6 +60,7 @@ public class LiveConsoleOpLogRecordVo {
         vo.setCreateTime(log.getCreateTime());
         vo.setStatus(status);
         vo.setStatusName(resolveStatusName(status));
+        vo.setCouponCount(0);
         return vo;
     }