Просмотр исходного кода

1、观看奖励可多选和设置数量、直播题库 可多选关联
签到、观看奖励、答题奖励:都需要关联奖励(选择奖励类型(可多选):红包、积分、核销卷(可多选、数量可设置)、优惠劵(可多选、数量可设置))(待测试)

yys 1 неделя назад
Родитель
Сommit
c3a227364d

+ 48 - 19
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -690,23 +690,32 @@ public class Task {
     }
     }
 
 
     /**
     /**
-     * 发放观看奖励优惠券
+     * 发放观看奖励优惠券(支持多选,每张券独立数量、独立留存)
      *
      *
      * @return 成功发放的用户ID集合
      * @return 成功发放的用户ID集合
      */
      */
     private Set<Long> grantWatchRewardCoupons(Live live, LiveWatchConfig config, List<Long> userIds) {
     private Set<Long> grantWatchRewardCoupons(Live live, LiveWatchConfig config, List<Long> userIds) {
         Set<Long> notifiedUserIds = new HashSet<>();
         Set<Long> notifiedUserIds = new HashSet<>();
         Long liveId = live.getLiveId();
         Long liveId = live.getLiveId();
-        String actionCouponIdStr = config.getActionCouponId();
-        if (StringUtils.isBlank(actionCouponIdStr)) {
-            log.warn("直播间观看奖励配置为优惠券,但未配置优惠券ID,liveId={}", liveId);
+        List<com.fs.live.vo.LiveRewardCouponItem> couponItems = config.resolveActionCoupons();
+        if (couponItems == null || couponItems.isEmpty()) {
+            log.warn("直播间观看奖励配置为优惠券,但未配置优惠券,liveId={}", liveId);
             return notifiedUserIds;
             return notifiedUserIds;
         }
         }
-        Long actionCouponId = Long.parseLong(actionCouponIdStr);
-        int actionCouponCount = resolveActionCouponCount(config);
-        LiveCoupon watchRewardCoupon = liveCouponService.selectLiveCouponById(actionCouponId);
-        List<LiveConsoleOpLogUser> couponRelations = bindCouponToUsers(live, userIds, actionCouponId, false, actionCouponCount);
-        if (!couponRelations.isEmpty()) {
+        for (com.fs.live.vo.LiveRewardCouponItem item : couponItems) {
+            if (item == null || item.getCouponId() == null) {
+                continue;
+            }
+            Long actionCouponId = item.getCouponId();
+            int actionCouponCount = item.resolveCount();
+            LiveCoupon watchRewardCoupon = liveCouponService.selectLiveCouponById(actionCouponId);
+            List<LiveConsoleOpLogUser> couponRelations =
+                    bindCouponToUsers(live, userIds, actionCouponId, false, actionCouponCount);
+            if (couponRelations.isEmpty()) {
+                log.warn("{} 观看奖励优惠券发放无成功用户: liveId={}, couponId={}",
+                        LOG_PREFIX, liveId, actionCouponId);
+                continue;
+            }
             LiveConsoleOpLog watchCouponOpLog = liveConsoleOpLogService.saveLog(
             LiveConsoleOpLog watchCouponOpLog = liveConsoleOpLogService.saveLog(
                     liveId,
                     liveId,
                     LiveConsoleOpLog.OP_WATCH_REWARD_COUPON,
                     LiveConsoleOpLog.OP_WATCH_REWARD_COUPON,
@@ -715,17 +724,16 @@ public class Task {
                     resolveWatchRewardCouponBizName(watchRewardCoupon, actionCouponId, couponRelations.size())
                     resolveWatchRewardCouponBizName(watchRewardCoupon, actionCouponId, couponRelations.size())
             );
             );
             liveConsoleOpLogService.bindOpLogUsers(watchCouponOpLog.getId(), liveId, couponRelations);
             liveConsoleOpLogService.bindOpLogUsers(watchCouponOpLog.getId(), liveId, couponRelations);
+            Set<Long> notifiedForCoupon = new HashSet<>();
             couponRelations.forEach(relation -> {
             couponRelations.forEach(relation -> {
-                if (relation.getUserId() != null && notifiedUserIds.add(relation.getUserId())) {
+                if (relation.getUserId() != null && notifiedForCoupon.add(relation.getUserId())) {
+                    notifiedUserIds.add(relation.getUserId());
                     sendCouponRewardMessage(liveId, relation.getUserId(), watchRewardCoupon, watchCouponOpLog);
                     sendCouponRewardMessage(liveId, relation.getUserId(), watchRewardCoupon, watchCouponOpLog);
                 }
                 }
             });
             });
-            log.info("{} 观看奖励优惠券发放完成: liveId={}, 用户数={}, 每人张数={}",
-                    LOG_PREFIX, liveId, notifiedUserIds.size(), actionCouponCount);
-            return notifiedUserIds;
+            log.info("{} 观看奖励优惠券发放完成: liveId={}, couponId={}, 用户数={}, 每人张数={}",
+                    LOG_PREFIX, liveId, actionCouponId, notifiedForCoupon.size(), actionCouponCount);
         }
         }
-        log.warn("{} 观看奖励优惠券发放无成功用户: liveId={}, couponId={}",
-                LOG_PREFIX, liveId, actionCouponId);
         return notifiedUserIds;
         return notifiedUserIds;
     }
     }
 
 
@@ -863,13 +871,34 @@ public class Task {
                 if (!config.hasAction(WATCH_REWARD_ACTION_COUPON)) {
                 if (!config.hasAction(WATCH_REWARD_ACTION_COUPON)) {
                     return R.error("观看奖励类型不是优惠券,请传入 couponId");
                     return R.error("观看奖励类型不是优惠券,请传入 couponId");
                 }
                 }
-                String actionCouponIdStr = config.getActionCouponId();
-                if (StringUtils.isBlank(actionCouponIdStr)) {
+                List<com.fs.live.vo.LiveRewardCouponItem> items = config.resolveActionCoupons();
+                if (items == null || items.isEmpty()) {
                     return R.error("观看奖励未配置优惠券ID");
                     return R.error("观看奖励未配置优惠券ID");
                 }
                 }
-                targetCouponId = Long.parseLong(actionCouponIdStr);
+                // 未指定 couponId 时发放全部配置券
+                Set<Long> allGranted = grantWatchRewardCoupons(live, config, Collections.singletonList(userId));
+                if (allGranted.isEmpty()) {
+                    return R.error("优惠券发放失败,请检查优惠券配置及库存");
+                }
+                return R.ok("观看奖励优惠券发放完成")
+                        .put("couponCount", items.size())
+                        .put("grantedUser", true);
+            }
+            // 指定券:取该券配置数量
+            actionCouponCount = 1;
+            boolean matched = false;
+            if (config != null) {
+                for (com.fs.live.vo.LiveRewardCouponItem item : config.resolveActionCoupons()) {
+                    if (item != null && targetCouponId.equals(item.getCouponId())) {
+                        actionCouponCount = item.resolveCount();
+                        matched = true;
+                        break;
+                    }
+                }
+                if (!matched) {
+                    actionCouponCount = resolveActionCouponCount(config);
+                }
             }
             }
-            actionCouponCount = resolveActionCouponCount(config);
 
 
             List<LiveConsoleOpLogUser> couponRelations = bindCouponToUsers(live, Collections.singletonList(userId), targetCouponId, false, actionCouponCount);
             List<LiveConsoleOpLogUser> couponRelations = bindCouponToUsers(live, Collections.singletonList(userId), targetCouponId, false, actionCouponCount);
             if (couponRelations.isEmpty()) {
             if (couponRelations.isEmpty()) {

+ 59 - 4
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -2,6 +2,7 @@ package com.fs.live.domain;
 
 
 import com.fs.common.annotation.Excel;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.utils.LiveCompletionConfigUtils;
 import com.fs.live.utils.LiveCompletionConfigUtils;
 import lombok.Data;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.EqualsAndHashCode;
@@ -112,26 +113,36 @@ public class LiveWatchConfig extends BaseEntity{
     @Excel(name = "引导语")
     @Excel(name = "引导语")
     private String scoreGuideText;
     private String scoreGuideText;
 
 
-    /** 实施动作优惠券ID(action=3时使用) */
+    /** 实施动作优惠券ID(旧单选,兼容;多选见 actionCoupons) */
     @Excel(name = "实施动作优惠券ID")
     @Excel(name = "实施动作优惠券ID")
     private String actionCouponId;
     private String actionCouponId;
 
 
-    /** 实施动作优惠券领取数量(action=3时使用,每人领取张数) */
+    /** 实施动作优惠券领取数量(旧单选,兼容) */
     @Excel(name = "实施动作优惠券领取数量")
     @Excel(name = "实施动作优惠券领取数量")
     private Long actionCouponCount;
     private Long actionCouponCount;
 
 
+    /**
+     * 观看时长优惠券多选:[{couponId, count}, ...]
+     */
+    private Object actionCoupons;
+
     /** 优惠券引导语 */
     /** 优惠券引导语 */
     @Excel(name = "优惠券引导语")
     @Excel(name = "优惠券引导语")
     private String couponGuideText;
     private String couponGuideText;
 
 
-    /** 完课优惠券ID(完课类型含优惠券时使用) */
+    /** 完课优惠券ID(旧单选,兼容;多选见 finishCoupons) */
     @Excel(name = "完课优惠券ID")
     @Excel(name = "完课优惠券ID")
     private String finishCouponId;
     private String finishCouponId;
 
 
-    /** 完课优惠券领取数量(完课类型含优惠券时使用,每人领取张数) */
+    /** 完课优惠券领取数量(旧单选,兼容) */
     @Excel(name = "完课优惠券领取数量")
     @Excel(name = "完课优惠券领取数量")
     private Long finishCouponCount;
     private Long finishCouponCount;
 
 
+    /**
+     * 完课优惠券多选:[{couponId, count}, ...]
+     */
+    private Object finishCoupons;
+
     /** 配置json */
     /** 配置json */
     @Excel(name = "配置json")
     @Excel(name = "配置json")
     private String configJson;
     private String configJson;
@@ -157,6 +168,50 @@ public class LiveWatchConfig extends BaseEntity{
         return resolveActions().contains(actionType);
         return resolveActions().contains(actionType);
     }
     }
 
 
+    /**
+     * 解析观看时长优惠券多选(兼容旧单选字段)
+     */
+    public List<com.fs.live.vo.LiveRewardCouponItem> resolveActionCoupons() {
+        List<com.fs.live.vo.LiveRewardCouponItem> items =
+                LiveCompletionConfigUtils.parseCouponItems(this.actionCoupons);
+        if (!items.isEmpty()) {
+            return items;
+        }
+        if (StringUtils.isNotEmpty(this.actionCouponId)) {
+            try {
+                Long id = Long.parseLong(this.actionCouponId.trim());
+                int count = this.actionCouponCount != null && this.actionCouponCount > 0
+                        ? this.actionCouponCount.intValue() : 1;
+                return java.util.Collections.singletonList(new com.fs.live.vo.LiveRewardCouponItem(id, count));
+            } catch (NumberFormatException ignored) {
+                return java.util.Collections.emptyList();
+            }
+        }
+        return java.util.Collections.emptyList();
+    }
+
+    /**
+     * 解析完课优惠券多选(兼容旧单选字段)
+     */
+    public List<com.fs.live.vo.LiveRewardCouponItem> resolveFinishCoupons() {
+        List<com.fs.live.vo.LiveRewardCouponItem> items =
+                LiveCompletionConfigUtils.parseCouponItems(this.finishCoupons);
+        if (!items.isEmpty()) {
+            return items;
+        }
+        if (StringUtils.isNotEmpty(this.finishCouponId)) {
+            try {
+                Long id = Long.parseLong(this.finishCouponId.trim());
+                int count = this.finishCouponCount != null && this.finishCouponCount > 0
+                        ? this.finishCouponCount.intValue() : 1;
+                return java.util.Collections.singletonList(new com.fs.live.vo.LiveRewardCouponItem(id, count));
+            } catch (NumberFormatException ignored) {
+                return java.util.Collections.emptyList();
+            }
+        }
+        return java.util.Collections.emptyList();
+    }
+
     /**
     /**
      * 解析完课奖励类型列表(兼容旧 participateCondition=2/3)
      * 解析完课奖励类型列表(兼容旧 participateCondition=2/3)
      */
      */

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

@@ -149,8 +149,12 @@ public interface LiveMapper
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
             "and config_json is not null " +
             "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, '$.finishCouponId') is not null " +
-            "and JSON_EXTRACT(config_json, '$.finishCouponId') != '' " +
+            "and (" +
+            "  (JSON_EXTRACT(config_json, '$.finishCouponId') is not null " +
+            "   and JSON_EXTRACT(config_json, '$.finishCouponId') != '') " +
+            "  or (JSON_EXTRACT(config_json, '$.finishCoupons') is not null " +
+            "      and JSON_LENGTH(JSON_EXTRACT(config_json, '$.finishCoupons')) > 0)" +
+            ") " +
             "and (" +
             "and (" +
             "  CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.participateCondition')) AS UNSIGNED) = 3 " +
             "  CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.participateCondition')) AS UNSIGNED) = 3 " +
             "  or (" +
             "  or (" +

+ 68 - 19
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -19,6 +19,7 @@ import com.fs.live.vo.LiveCompletionCouponInfoVO;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.vo.LiveCompletionCouponNotifyResult;
 import com.fs.live.vo.LiveCompletionCouponStatusVO;
 import com.fs.live.vo.LiveCompletionCouponStatusVO;
 import com.fs.live.vo.LiveCompletionQuestionVO;
 import com.fs.live.vo.LiveCompletionQuestionVO;
+import com.fs.live.vo.LiveRewardCouponItem;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -86,6 +87,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         vo.setCompletionRate(config.getCompletionRate());
         vo.setCompletionRate(config.getCompletionRate());
         vo.setCouponId(config.getCouponId());
         vo.setCouponId(config.getCouponId());
         vo.setCouponCount(config.getCouponCount());
         vo.setCouponCount(config.getCouponCount());
+        vo.setCouponItems(config.resolveCouponItems());
         vo.setFinishQuestionIds(config.getFinishQuestionIds());
         vo.setFinishQuestionIds(config.getFinishQuestionIds());
         if (config.isEnabled() && live != null && live.getDuration() != null && live.getDuration() > 0) {
         if (config.isEnabled() && live != null && live.getDuration() != null && live.getDuration() > 0) {
             vo.setRequiredDurationSeconds(calculateRequiredDuration(live.getDuration(), config.getCompletionRate()));
             vo.setRequiredDurationSeconds(calculateRequiredDuration(live.getDuration(), config.getCompletionRate()));
@@ -114,7 +116,10 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                 return result;
                 return result;
             }
             }
 
 
-            result.setCoupon(loadCouponInfo(config.getCouponId()));
+            List<LiveRewardCouponItem> couponItems = config.resolveCouponItems();
+            List<LiveCompletionCouponInfoVO> couponInfos = loadCouponInfos(couponItems);
+            result.setCoupons(couponInfos);
+            result.setCoupon(couponInfos.isEmpty() ? null : couponInfos.get(0));
 
 
             if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
             if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
                 log.info("[完课优惠券] 观看比例未达标(已在上层方法记录详情), liveId={}, userId={}", liveId, userId);
                 log.info("[完课优惠券] 观看比例未达标(已在上层方法记录详情), liveId={}, userId={}", liveId, userId);
@@ -122,7 +127,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             }
             }
             result.setEligible(true);
             result.setEligible(true);
 
 
-            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+            if (hasIssuedToday(liveId, userId)) {
                 log.info("[完课优惠券] 今日已发券, 跳过, liveId={}, userId={}", liveId, userId);
                 log.info("[完课优惠券] 今日已发券, 跳过, liveId={}, userId={}", liveId, userId);
                 return result;
                 return result;
             }
             }
@@ -182,7 +187,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         status.setHasQuestions(!questions.isEmpty());
         status.setHasQuestions(!questions.isEmpty());
         status.setQuestions(questions);
         status.setQuestions(questions);
         status.setEligible(isWatchRateEligible(liveId, userId, watchDuration, config));
         status.setEligible(isWatchRateEligible(liveId, userId, watchDuration, config));
-        status.setReceivedToday(hasIssuedToday(liveId, userId, config.getCouponId()));
+        status.setReceivedToday(hasIssuedToday(liveId, userId));
 
 
         LiveCompletionAnswerRecord dbAnswer = liveCompletionAnswerRecordService.selectTodayLatest(liveId, userId);
         LiveCompletionAnswerRecord dbAnswer = liveCompletionAnswerRecordService.selectTodayLatest(liveId, userId);
         if (dbAnswer != null) {
         if (dbAnswer != null) {
@@ -217,13 +222,13 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
 
 
         Long liveId = param.getLiveId();
         Long liveId = param.getLiveId();
         CompletionCouponConfig config = resolveConfig(liveId);
         CompletionCouponConfig config = resolveConfig(liveId);
-        if (!config.isEnabled() || config.getCouponId() == null) {
+        if (!config.isEnabled() || config.resolveCouponItems().isEmpty()) {
             throw new BaseException("完课优惠券未开启");
             throw new BaseException("完课优惠券未开启");
         }
         }
         if (!isWatchRateEligible(liveId, userId, null, config)) {
         if (!isWatchRateEligible(liveId, userId, null, config)) {
             throw new BaseException("未达到完课要求");
             throw new BaseException("未达到完课要求");
         }
         }
-        if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+        if (hasIssuedToday(liveId, userId)) {
             throw new BaseException("今日福利券已领取");
             throw new BaseException("今日福利券已领取");
         }
         }
 
 
@@ -252,13 +257,13 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         }
         }
         Long liveId = param.getLiveId();
         Long liveId = param.getLiveId();
         CompletionCouponConfig config = resolveConfig(liveId);
         CompletionCouponConfig config = resolveConfig(liveId);
-        if (!config.isEnabled() || config.getCouponId() == null) {
+        if (!config.isEnabled() || config.resolveCouponItems().isEmpty()) {
             throw new BaseException("完课优惠券未开启");
             throw new BaseException("完课优惠券未开启");
         }
         }
         if (!isWatchRateEligible(liveId, userId, null, config)) {
         if (!isWatchRateEligible(liveId, userId, null, config)) {
             throw new BaseException("未达到完课要求");
             throw new BaseException("未达到完课要求");
         }
         }
-        if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+        if (hasIssuedToday(liveId, userId)) {
             throw new BaseException("今日福利券已领取");
             throw new BaseException("今日福利券已领取");
         }
         }
 
 
@@ -272,16 +277,23 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         if (live == null) {
         if (live == null) {
             throw new BaseException("直播不存在");
             throw new BaseException("直播不存在");
         }
         }
-        List<LiveCouponUser> couponUsers = issueCoupon(live, userId, config.getCouponId(), config.getCouponCount());
-        if (couponUsers.isEmpty()) {
+        List<LiveCouponUser> allCouponUsers = new ArrayList<>();
+        for (LiveRewardCouponItem item : config.resolveCouponItems()) {
+            if (item == null || item.getCouponId() == null) {
+                continue;
+            }
+            List<LiveCouponUser> issued = issueCoupon(live, userId, item.getCouponId(), item.resolveCount());
+            allCouponUsers.addAll(issued);
+        }
+        if (allCouponUsers.isEmpty()) {
             throw new BaseException("优惠券发放失败");
             throw new BaseException("优惠券发放失败");
         }
         }
         List<LiveConsoleOpLogUser> relations = new ArrayList<>();
         List<LiveConsoleOpLogUser> relations = new ArrayList<>();
-        for (LiveCouponUser couponUser : couponUsers) {
+        for (LiveCouponUser couponUser : allCouponUsers) {
             relations.add(new LiveConsoleOpLogUser(null, userId, liveId, couponUser.getId()));
             relations.add(new LiveConsoleOpLogUser(null, userId, liveId, couponUser.getId()));
         }
         }
         liveConsoleOpLogService.bindOpLogUsers(opLog.getId(), liveId, relations);
         liveConsoleOpLogService.bindOpLogUsers(opLog.getId(), liveId, relations);
-        return couponUsers.get(0);
+        return allCouponUsers.get(0);
     }
     }
 
 
     /**
     /**
@@ -631,9 +643,8 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         return couponUsers;
         return couponUsers;
     }
     }
 
 
-    private boolean hasIssuedToday(Long liveId, Long userId, Long couponId) {
+    private boolean hasIssuedToday(Long liveId, Long userId) {
         LiveCouponUser query = new LiveCouponUser();
         LiveCouponUser query = new LiveCouponUser();
-        query.setCouponId(couponId);
         query.setUserId(userId.intValue());
         query.setUserId(userId.intValue());
         query.setType(COMPLETION_COUPON_TYPE + "-" + liveId);
         query.setType(COMPLETION_COUPON_TYPE + "-" + liveId);
         List<LiveCouponUser> existingList = liveCouponUserService.selectLiveCouponUserList(query);
         List<LiveCouponUser> existingList = liveCouponUserService.selectLiveCouponUserList(query);
@@ -646,6 +657,24 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                         && item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(today));
                         && item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(today));
     }
     }
 
 
+    private List<LiveCompletionCouponInfoVO> loadCouponInfos(List<LiveRewardCouponItem> items) {
+        if (items == null || items.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<LiveCompletionCouponInfoVO> list = new ArrayList<>();
+        for (LiveRewardCouponItem item : items) {
+            if (item == null || item.getCouponId() == null) {
+                continue;
+            }
+            LiveCompletionCouponInfoVO vo = loadCouponInfo(item.getCouponId());
+            if (vo != null) {
+                vo.setCount(item.resolveCount());
+                list.add(vo);
+            }
+        }
+        return list;
+    }
+
     private LiveCompletionCouponInfoVO loadCouponInfo(Long couponId) {
     private LiveCompletionCouponInfoVO loadCouponInfo(Long couponId) {
         if (couponId == null) {
         if (couponId == null) {
             return null;
             return null;
@@ -727,20 +756,20 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                 return config;
                 return config;
             }
             }
 
 
-            String finishCouponId = jsonConfig.getString("finishCouponId");
-            if (StringUtils.isEmpty(finishCouponId)) {
+            List<LiveRewardCouponItem> couponItems = LiveCompletionConfigUtils.resolveFinishCoupons(jsonConfig);
+            if (couponItems == null || couponItems.isEmpty()) {
                 return config;
                 return config;
             }
             }
 
 
             config.setEnabled(true);
             config.setEnabled(true);
-            config.setCouponId(Long.parseLong(finishCouponId));
+            config.setCouponItems(couponItems);
+            // 兼容旧字段:首张券
+            config.setCouponId(couponItems.get(0).getCouponId());
+            config.setCouponCount(couponItems.get(0).resolveCount());
             config.setFinishQuestionIds(jsonConfig.getString("finishQuestionIds"));
             config.setFinishQuestionIds(jsonConfig.getString("finishQuestionIds"));
 
 
             Integer completionRate = jsonConfig.getInteger("completionRate");
             Integer completionRate = jsonConfig.getInteger("completionRate");
             config.setCompletionRate(completionRate != null && completionRate > 0 && completionRate <= 100 ? completionRate : 90);
             config.setCompletionRate(completionRate != null && completionRate > 0 && completionRate <= 100 ? completionRate : 90);
-
-            Integer finishCouponCount = jsonConfig.getInteger("finishCouponCount");
-            config.setCouponCount(finishCouponCount != null && finishCouponCount > 0 ? finishCouponCount : 1);
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("解析完课优惠券配置失败, liveId={}", live.getLiveId(), e);
             log.warn("解析完课优惠券配置失败, liveId={}", live.getLiveId(), e);
             return disabledConfig();
             return disabledConfig();
@@ -753,6 +782,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         config.setEnabled(false);
         config.setEnabled(false);
         config.setCompletionRate(90);
         config.setCompletionRate(90);
         config.setCouponCount(1);
         config.setCouponCount(1);
+        config.setCouponItems(Collections.emptyList());
         return config;
         return config;
     }
     }
 
 
@@ -801,6 +831,7 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         private Integer completionRate;
         private Integer completionRate;
         private Long couponId;
         private Long couponId;
         private Integer couponCount;
         private Integer couponCount;
+        private List<LiveRewardCouponItem> couponItems;
         private String finishQuestionIds;
         private String finishQuestionIds;
 
 
         public boolean isEnabled() {
         public boolean isEnabled() {
@@ -835,6 +866,14 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             this.couponCount = couponCount;
             this.couponCount = couponCount;
         }
         }
 
 
+        public List<LiveRewardCouponItem> getCouponItems() {
+            return couponItems;
+        }
+
+        public void setCouponItems(List<LiveRewardCouponItem> couponItems) {
+            this.couponItems = couponItems;
+        }
+
         public String getFinishQuestionIds() {
         public String getFinishQuestionIds() {
             return finishQuestionIds;
             return finishQuestionIds;
         }
         }
@@ -842,6 +881,16 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         public void setFinishQuestionIds(String finishQuestionIds) {
         public void setFinishQuestionIds(String finishQuestionIds) {
             this.finishQuestionIds = finishQuestionIds;
             this.finishQuestionIds = finishQuestionIds;
         }
         }
+
+        public List<LiveRewardCouponItem> resolveCouponItems() {
+            if (couponItems != null && !couponItems.isEmpty()) {
+                return couponItems;
+            }
+            if (couponId != null) {
+                return Collections.singletonList(new LiveRewardCouponItem(couponId, couponCount));
+            }
+            return Collections.emptyList();
+        }
     }
     }
 
 
     private static class AnswerRecord {
     private static class AnswerRecord {

+ 134 - 26
fs-service/src/main/java/com/fs/live/utils/LiveCompletionConfigUtils.java

@@ -1,13 +1,17 @@
 package com.fs.live.utils;
 package com.fs.live.utils;
 
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.live.vo.LiveRewardCouponItem;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 
 
 /**
 /**
@@ -15,6 +19,7 @@ import java.util.Set;
  * <p>
  * <p>
  * 参与条件:1=达到指定观看时长,2=完课奖励(积分/优惠券合并,由 completionTypes 区分)
  * 参与条件:1=达到指定观看时长,2=完课奖励(积分/优惠券合并,由 completionTypes 区分)
  * 兼容旧值:participateCondition=3 视为完课优惠券
  * 兼容旧值:participateCondition=3 视为完课优惠券
+ * 优惠券支持多选:actionCoupons / finishCoupons,每项含 couponId + count
  * </p>
  * </p>
  */
  */
 public final class LiveCompletionConfigUtils {
 public final class LiveCompletionConfigUtils {
@@ -45,9 +50,6 @@ public final class LiveCompletionConfigUtils {
         }
         }
     }
     }
 
 
-    /**
-     * 是否为完课参与条件(含旧值 3)
-     */
     public static boolean isCompletionParticipate(JSONObject json) {
     public static boolean isCompletionParticipate(JSONObject json) {
         if (json == null) {
         if (json == null) {
             return false;
             return false;
@@ -58,10 +60,6 @@ public final class LiveCompletionConfigUtils {
                 || participateCondition == PARTICIPATE_CONDITION_COMPLETION_COUPON);
                 || participateCondition == PARTICIPATE_CONDITION_COMPLETION_COUPON);
     }
     }
 
 
-    /**
-     * 解析完课奖励类型多选:2=完课积分,3=完课优惠券
-     * <p>兼容:旧 condition=3 → [3];旧 condition=2 且无 completionTypes → [2]</p>
-     */
     public static List<Long> resolveCompletionTypes(JSONObject json) {
     public static List<Long> resolveCompletionTypes(JSONObject json) {
         if (json == null) {
         if (json == null) {
             return Collections.emptyList();
             return Collections.emptyList();
@@ -80,7 +78,6 @@ public final class LiveCompletionConfigUtils {
         if (!types.isEmpty()) {
         if (!types.isEmpty()) {
             return types;
             return types;
         }
         }
-        // 旧数据:condition=2 仅表示完课积分
         return Collections.singletonList(TYPE_POINTS);
         return Collections.singletonList(TYPE_POINTS);
     }
     }
 
 
@@ -88,10 +85,6 @@ public final class LiveCompletionConfigUtils {
         return resolveCompletionTypes(json).contains(type);
         return resolveCompletionTypes(json).contains(type);
     }
     }
 
 
-    /**
-     * 解析观看时长实施动作多选:2=积分,3=优惠券
-     * <p>兼容旧字段 action(单值)</p>
-     */
     public static List<Long> resolveWatchActions(JSONObject json) {
     public static List<Long> resolveWatchActions(JSONObject json) {
         if (json == null) {
         if (json == null) {
             return Collections.emptyList();
             return Collections.emptyList();
@@ -111,9 +104,38 @@ public final class LiveCompletionConfigUtils {
         return resolveWatchActions(json).contains(action);
         return resolveWatchActions(json).contains(action);
     }
     }
 
 
-    /**
-     * 完课优惠券模式:enabled + 完课条件 + 类型含优惠券 + finishCouponId
-     */
+    /** 观看时长优惠券多选:优先 actionCoupons,兼容旧单选字段 */
+    public static List<LiveRewardCouponItem> resolveActionCoupons(JSONObject json) {
+        if (json == null) {
+            return Collections.emptyList();
+        }
+        List<LiveRewardCouponItem> items = parseCouponItems(json.get("actionCoupons"));
+        if (!items.isEmpty()) {
+            return items;
+        }
+        return fallbackSingleCoupon(json.getString("actionCouponId"), json.getInteger("actionCouponCount"));
+    }
+
+    /** 完课优惠券多选:优先 finishCoupons,兼容旧单选字段 */
+    public static List<LiveRewardCouponItem> resolveFinishCoupons(JSONObject json) {
+        if (json == null) {
+            return Collections.emptyList();
+        }
+        List<LiveRewardCouponItem> items = parseCouponItems(json.get("finishCoupons"));
+        if (!items.isEmpty()) {
+            return items;
+        }
+        return fallbackSingleCoupon(json.getString("finishCouponId"), json.getInteger("finishCouponCount"));
+    }
+
+    public static List<LiveRewardCouponItem> resolveActionCoupons(String configJson) {
+        return resolveActionCoupons(parseConfig(configJson));
+    }
+
+    public static List<LiveRewardCouponItem> resolveFinishCoupons(String configJson) {
+        return resolveFinishCoupons(parseConfig(configJson));
+    }
+
     public static boolean isCompletionCouponMode(String configJson) {
     public static boolean isCompletionCouponMode(String configJson) {
         JSONObject json = parseConfig(configJson);
         JSONObject json = parseConfig(configJson);
         if (json == null || !json.getBooleanValue("enabled")) {
         if (json == null || !json.getBooleanValue("enabled")) {
@@ -122,12 +144,9 @@ public final class LiveCompletionConfigUtils {
         if (!isCompletionParticipate(json) || !hasCompletionType(json, TYPE_COUPON)) {
         if (!isCompletionParticipate(json) || !hasCompletionType(json, TYPE_COUPON)) {
             return false;
             return false;
         }
         }
-        return StringUtils.isNotEmpty(json.getString("finishCouponId"));
+        return !resolveFinishCoupons(json).isEmpty();
     }
     }
 
 
-    /**
-     * 完课积分模式:enabled + 完课条件 + 类型含积分 + 配置了 pointsConfig
-     */
     public static boolean isCompletionPointsMode(String configJson) {
     public static boolean isCompletionPointsMode(String configJson) {
         JSONObject json = parseConfig(configJson);
         JSONObject json = parseConfig(configJson);
         if (json == null || !json.getBooleanValue("enabled")) {
         if (json == null || !json.getBooleanValue("enabled")) {
@@ -140,9 +159,6 @@ public final class LiveCompletionConfigUtils {
         return pointsConfig != null && !pointsConfig.isEmpty();
         return pointsConfig != null && !pointsConfig.isEmpty();
     }
     }
 
 
-    /**
-     * 观看时长奖励是否开启(enabled + condition=1 + 至少一项实施动作)
-     */
     public static boolean isWatchDurationRewardMode(String configJson) {
     public static boolean isWatchDurationRewardMode(String configJson) {
         JSONObject json = parseConfig(configJson);
         JSONObject json = parseConfig(configJson);
         if (json == null || !json.getBooleanValue("enabled")) {
         if (json == null || !json.getBooleanValue("enabled")) {
@@ -155,9 +171,102 @@ public final class LiveCompletionConfigUtils {
         return !resolveWatchActions(json).isEmpty();
         return !resolveWatchActions(json).isEmpty();
     }
     }
 
 
-    /**
-     * 将 actions / completionTypes 等字段统一解析为 Long 列表(支持数组、逗号串、单值)
-     */
+    public static List<LiveRewardCouponItem> parseCouponItems(Object raw) {
+        if (raw == null) {
+            return Collections.emptyList();
+        }
+        JSONArray arr;
+        if (raw instanceof JSONArray) {
+            arr = (JSONArray) raw;
+        } else if (raw instanceof List) {
+            arr = JSONArray.parseArray(JSON.toJSONString(raw));
+        } else {
+            String text = String.valueOf(raw).trim();
+            if (StringUtils.isEmpty(text) || "null".equalsIgnoreCase(text) || !text.startsWith("[")) {
+                return Collections.emptyList();
+            }
+            try {
+                arr = JSONArray.parseArray(text);
+            } catch (Exception e) {
+                return Collections.emptyList();
+            }
+        }
+        if (arr == null || arr.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, LiveRewardCouponItem> dedup = new LinkedHashMap<>();
+        for (int i = 0; i < arr.size(); i++) {
+            LiveRewardCouponItem item = toCouponItem(arr.get(i));
+            if (item != null && item.getCouponId() != null) {
+                dedup.put(item.getCouponId(), item);
+            }
+        }
+        return new ArrayList<>(dedup.values());
+    }
+
+    @SuppressWarnings("unchecked")
+    private static LiveRewardCouponItem toCouponItem(Object el) {
+        if (el == null) {
+            return null;
+        }
+        if (el instanceof LiveRewardCouponItem) {
+            LiveRewardCouponItem item = (LiveRewardCouponItem) el;
+            if (item.getCount() == null || item.getCount() < 1) {
+                item.setCount(1);
+            }
+            return item;
+        }
+        if (el instanceof Number) {
+            return new LiveRewardCouponItem(((Number) el).longValue(), 1);
+        }
+        try {
+            JSONObject obj;
+            if (el instanceof JSONObject) {
+                obj = (JSONObject) el;
+            } else if (el instanceof Map) {
+                obj = new JSONObject((Map<String, Object>) el);
+            } else {
+                String text = String.valueOf(el).trim();
+                if (StringUtils.isEmpty(text)) {
+                    return null;
+                }
+                if (text.matches("^\\d+$")) {
+                    return new LiveRewardCouponItem(Long.parseLong(text), 1);
+                }
+                obj = JSONObject.parseObject(text);
+            }
+            if (obj == null) {
+                return null;
+            }
+            Long couponId = obj.getLong("couponId");
+            if (couponId == null) {
+                couponId = obj.getLong("id");
+            }
+            if (couponId == null) {
+                return null;
+            }
+            Integer count = obj.getInteger("count");
+            if (count == null) {
+                count = obj.getInteger("couponCount");
+            }
+            return new LiveRewardCouponItem(couponId, count != null && count > 0 ? count : 1);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static List<LiveRewardCouponItem> fallbackSingleCoupon(String couponIdStr, Integer count) {
+        if (StringUtils.isEmpty(couponIdStr)) {
+            return Collections.emptyList();
+        }
+        try {
+            Long couponId = Long.parseLong(couponIdStr.trim());
+            return Collections.singletonList(new LiveRewardCouponItem(couponId, count != null && count > 0 ? count : 1));
+        } catch (NumberFormatException e) {
+            return Collections.emptyList();
+        }
+    }
+
     public static List<Long> parseLongList(Object raw) {
     public static List<Long> parseLongList(Object raw) {
         if (raw == null) {
         if (raw == null) {
             return Collections.emptyList();
             return Collections.emptyList();
@@ -179,7 +288,6 @@ public final class LiveCompletionConfigUtils {
             if (StringUtils.isEmpty(text) || "null".equalsIgnoreCase(text)) {
             if (StringUtils.isEmpty(text) || "null".equalsIgnoreCase(text)) {
                 return Collections.emptyList();
                 return Collections.emptyList();
             }
             }
-            // 兼容 JSON 数组字符串
             if (text.startsWith("[") && text.endsWith("]")) {
             if (text.startsWith("[") && text.endsWith("]")) {
                 try {
                 try {
                     return parseLongList(JSONArray.parseArray(text));
                     return parseLongList(JSONArray.parseArray(text));

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

@@ -2,6 +2,8 @@ package com.fs.live.vo;
 
 
 import lombok.Data;
 import lombok.Data;
 
 
+import java.util.List;
+
 /**
 /**
  * 完课优惠券配置(定时任务缓存用)
  * 完课优惠券配置(定时任务缓存用)
  */
  */
@@ -14,9 +16,12 @@ public class LiveCompletionCouponConfigVO {
 
 
     private Long couponId;
     private Long couponId;
 
 
-    /** 每人领取张数 */
+    /** 每人领取张数(旧单选兼容,取首张) */
     private Integer couponCount;
     private Integer couponCount;
 
 
+    /** 完课优惠券多选 */
+    private List<LiveRewardCouponItem> couponItems;
+
     private String finishQuestionIds;
     private String finishQuestionIds;
 
 
     /** 完课所需观看时长(秒) */
     /** 完课所需观看时长(秒) */

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

@@ -19,4 +19,7 @@ public class LiveCompletionCouponInfoVO {
     private BigDecimal useMinPrice;
     private BigDecimal useMinPrice;
 
 
     private Long couponTime;
     private Long couponTime;
+
+    /** 每人领取张数 */
+    private Integer count;
 }
 }

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

@@ -19,6 +19,9 @@ public class LiveCompletionCouponNotifyResult {
     /** 今日问题(不含答案) */
     /** 今日问题(不含答案) */
     private List<LiveCompletionQuestionVO> questions;
     private List<LiveCompletionQuestionVO> questions;
 
 
-    /** 完课奖励优惠券信息 */
+    /** 完课奖励优惠券信息(首张,兼容旧客户端) */
     private LiveCompletionCouponInfoVO coupon;
     private LiveCompletionCouponInfoVO coupon;
+
+    /** 完课奖励优惠券列表(多选) */
+    private List<LiveCompletionCouponInfoVO> coupons;
 }
 }

+ 32 - 0
fs-service/src/main/java/com/fs/live/vo/LiveRewardCouponItem.java

@@ -0,0 +1,32 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 观看/完课奖励优惠券发放项(支持多选,每券独立领取数量)
+ */
+@Data
+public class LiveRewardCouponItem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 优惠券ID */
+    private Long couponId;
+
+    /** 每人领取张数,默认 1 */
+    private Integer count;
+
+    public LiveRewardCouponItem() {
+    }
+
+    public LiveRewardCouponItem(Long couponId, Integer count) {
+        this.couponId = couponId;
+        this.count = count;
+    }
+
+    public int resolveCount() {
+        return count != null && count > 0 ? count : 1;
+    }
+}