Prechádzať zdrojové kódy

1、达到指定观看时长增加优惠卷
2、启用完课优惠卷新增

yys 2 týždňov pred
rodič
commit
330eda1dc1

+ 60 - 42
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -1,26 +1,20 @@
 package com.fs.live.task;
 
-import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.live.domain.Live;
-import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionCouponService;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveService;
-import com.fs.live.websocket.bean.SendMsgVo;
-import com.fs.live.websocket.service.WebSocketServer;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
 
 /**
- * 直播完课积分定时任务
+ * 直播完课奖励定时任务(积分 / 优惠券)
  */
 @Slf4j
 @Component
@@ -33,59 +27,83 @@ public class LiveCompletionPointsTask {
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
     @Autowired
-    private WebSocketServer webSocketServer;
+    private ILiveCompletionCouponService completionCouponService;
 
     @Autowired
     private ILiveService liveService;
 
     /**
-     * 定时检查观看时长并创建完课记录(兜底机制)
+     * 定时检查观看时长并创建完课积分记录(兜底机制)
      * 每分钟执行一次
-     * 优化:防重复推送 + 只查询开启了完课积分的直播间
      */
     @Scheduled(cron = "0 */1 * * * ?")
     public void checkCompletionStatus() {
         try {
-            // 只查询开启了完课积分配置的直播间
             List<Live> activeLives = liveService.selectLiveListWithCompletionPointsEnabled();
-            
+
             if (activeLives == null || activeLives.isEmpty()) {
                 log.debug("当前没有开启完课积分的直播间");
                 return;
             }
 
-            for (Live live : activeLives) {
-                try {
-                    Long liveId = live.getLiveId();
-                    
-                    // 使用Hash结构获取该直播间所有用户的观看时长
-                    String hashKey = "live:watch:duration:hash:" + liveId;
-                    Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
-                    
-                    if (userDurations == null || userDurations.isEmpty()) {
-
-                        continue;
-                    }
-                    // 3. 逐个用户处理
-                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
-                        try {
-                            Long userId = Long.parseLong(entry.getKey().toString());
-                            Long duration = Long.parseLong(entry.getValue().toString());  // 从 Redis 直接获取观看时长
-                            
-                            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
-
-                        } catch (Exception e) {
-                            log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
-                        }
-                    }
-                    
-                } catch (Exception e) {
-                    log.error("处理直播间完课状态失败, liveId={}", live.getLiveId(), e);
-                }
+            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) ->
+                    completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration));
+
+        } catch (Exception e) {
+            log.error("检查完课积分定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 定时检查观看时长并发放完课优惠券(兜底机制)
+     * 每分钟执行一次
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionCouponStatus() {
+        try {
+            List<Live> activeLives = liveService.selectLiveListWithCompletionCouponEnabled();
+
+            if (activeLives == null || activeLives.isEmpty()) {
+                log.debug("当前没有开启完课优惠券的直播间");
+                return;
             }
 
+            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) ->
+                    completionCouponService.checkAndIssueCompletionCoupon(liveId, userId, duration));
+
         } catch (Exception e) {
-            log.error("检查完课状态定时任务执行失败", e);
+            log.error("检查完课优惠券定时任务执行失败", e);
+        }
+    }
+
+    private void processCompletionByWatchDuration(List<Live> activeLives, CompletionHandler handler) {
+        for (Live live : activeLives) {
+            try {
+                Long liveId = live.getLiveId();
+                String hashKey = "live:watch:duration:hash:" + liveId;
+                Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
+
+                if (userDurations == null || userDurations.isEmpty()) {
+                    continue;
+                }
+
+                for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                    try {
+                        Long userId = Long.parseLong(entry.getKey().toString());
+                        Long duration = Long.parseLong(entry.getValue().toString());
+                        handler.handle(liveId, userId, duration);
+                    } catch (Exception e) {
+                        log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("处理直播间完课状态失败, liveId={}", live.getLiveId(), e);
+            }
         }
     }
+
+    @FunctionalInterface
+    private interface CompletionHandler {
+        void handle(Long liveId, Long userId, Long duration);
+    }
 }

+ 181 - 23
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -78,6 +78,10 @@ public class Task {
     @Autowired
     private ILiveWatchLogService liveWatchLogService;
     @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+    @Autowired
+    private ILiveCouponService liveCouponService;
+    @Autowired
     private ILiveUserFirstEntryService liveUserFirstEntryService;
 
     @Autowired
@@ -428,44 +432,198 @@ public class Task {
         for (Live openRewardLive : openRewardLives) {
             String configJson = openRewardLive.getConfigJson();
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-            if (config.getEnabled() && 1 == config.getParticipateCondition()) {
-                List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
-                if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
-                    continue;
-                }
-                // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
-                List<LiveWatchUser> onlineUser = liveWatchUsers
-                        .stream().filter(user -> (now.getTime() - user.getUpdateTime().getTime() + ( user.getOnlineSeconds() == null ? 0L : user.getOnlineSeconds())) > config.getWatchDuration() * 60 * 1000)
-                        .collect(Collectors.toList());
-                if(onlineUser.isEmpty()) continue;
+            if (!config.getEnabled() || config.getParticipateCondition() == null || config.getAction() == null) {
+                continue;
+            }
+            // 只处理 "达到指定观看时长" 的参与条件
+            if (1 != config.getParticipateCondition()) {
+                continue;
+            }
+
+            List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
+            if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
+                continue;
+            }
+            // 3.检查当前直播间的在线用户(可以传入一个时间,然后查出来当天没领取奖励的用户)
+            List<LiveWatchUser> onlineUser = liveWatchUsers
+                    .stream().filter(user -> (now.getTime() - user.getUpdateTime().getTime() + (user.getOnlineSeconds() == null ? 0L : user.getOnlineSeconds())) > config.getWatchDuration() * 60 * 1000)
+                    .collect(Collectors.toList());
+            if (onlineUser.isEmpty()) continue;
 
-                List<Long> userIds = onlineUser.stream().map(LiveWatchUser::getUserId).collect(Collectors.toList());
-                // 4.保存用户领取记录
-                saveUserRewardRecord(openRewardLive, userIds,config.getScoreAmount());
-                // 5.更新用户积分(积分
-                fsUserService.increaseIntegral(userIds,config.getScoreAmount());
-                // 6.发送websocket事件消息 通知用户自动领取成功
-                userIds.forEach(userId -> webSocketServer.sendIntegralMessage(openRewardLive.getLiveId(),userId,config.getScoreAmount()));
+            List<Long> userIds = onlineUser.stream().map(LiveWatchUser::getUserId).collect(Collectors.toList());
 
+            // 根据 action 类型处理不同的奖励
+            Long action = config.getAction();
+            if (action == null) {
+                continue;
+            }
+
+            switch (action.intValue()) {
+                case 2: // 积分红包
+                    // 4.保存用户领取记录
+                    saveUserRewardRecord(openRewardLive, userIds, BigDecimal.valueOf(config.getScoreAmount()), 2);
+                    // 5.更新用户积分
+                    fsUserService.increaseIntegral(userIds, config.getScoreAmount());
+                    // 6.发送websocket事件消息 通知用户自动领取成功
+                    userIds.forEach(userId -> webSocketServer.sendIntegralMessage(openRewardLive.getLiveId(), userId, config.getScoreAmount()));
+                    break;
+
+                case 3: // 优惠券
+                    // 获取配置的优惠券ID
+                    String actionCouponIdStr = config.getActionCouponId();
+                    if (StringUtils.isBlank(actionCouponIdStr)) {
+                        log.warn("直播间观看奖励配置为优惠券,但未配置优惠券ID,liveId={}", openRewardLive.getLiveId());
+                        continue;
+                    }
+                    Long actionCouponId = Long.parseLong(actionCouponIdStr);
+                    bindCouponToUsers(openRewardLive, userIds, actionCouponId);
+                    break;
+
+                case 1: // 现金红包 - 暂不处理(现有逻辑)
+                default:
+                    log.info("观看奖励类型 {} 暂不处理,liveId={}", action, openRewardLive.getLiveId());
+                    break;
             }
         }
     }
-    private void saveUserRewardRecord(Live live, List<Long> userIds,Long scoreAmount) {
+
+    /**
+     * 将优惠券绑定到用户
+     * @param live 直播间
+     * @param userIds 用户ID列表
+     * @param couponId 优惠券ID
+     */
+    private void bindCouponToUsers(Live live, List<Long> userIds, Long couponId) {
+        try {
+            // 查询优惠券信息
+            LiveCoupon coupon = liveCouponService.selectLiveCouponById(couponId);
+            if (coupon == null) {
+                log.error("优惠券不存在,couponId={}", couponId);
+                return;
+            }
+
+            // 查询优惠券领取信息
+            LiveCouponIssue couponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(couponId);
+            if (couponIssue == null) {
+                log.error("优惠券领取信息不存在,couponId={}", couponId);
+                return;
+            }
+
+            // 检查优惠券状态
+            if (couponIssue.getStatus() == null || couponIssue.getStatus() != 1) {
+                log.error("优惠券状态不正常,couponId={}, status={}", couponId, couponIssue.getStatus());
+                return;
+            }
+
+            Date now = new Date();
+            int successCount = 0;
+
+            for (Long userId : userIds) {
+                try {
+                    // 检查用户是否已领取过该优惠券
+                    LiveCouponUser query = new LiveCouponUser();
+                    query.setCouponId(couponId);
+                    query.setUserId(userId.intValue());
+                    List<LiveCouponUser> existingList = liveCouponUserService.selectLiveCouponUserList(query);
+                    if (existingList != null && !existingList.isEmpty()) {
+                        log.info("用户已领取过该优惠券,跳过,userId={}, couponId={}", userId, couponId);
+                        continue;
+                    }
+
+                    // 创建用户优惠券记录
+                    LiveCouponUser couponUser = new LiveCouponUser();
+                    couponUser.setCouponId(couponId);
+                    couponUser.setUserId(userId.intValue());
+                    couponUser.setCouponTitle(coupon.getTitle());
+                    couponUser.setCouponPrice(coupon.getCouponPrice());
+                    couponUser.setUseMinPrice(coupon.getUseMinPrice());
+
+                    // 计算优惠券过期时间
+                    if (couponIssue.getLimitTime() != null) {
+                        couponUser.setLimitTime(couponIssue.getLimitTime());
+                    } else if (coupon.getCouponTime() != null) {
+                        // 如果没有设置领取结束时间,使用优惠券有效期限计算
+                        java.util.Calendar cal = java.util.Calendar.getInstance();
+                        cal.setTime(now);
+                        cal.add(java.util.Calendar.DATE, coupon.getCouponTime().intValue());
+                        couponUser.setLimitTime(cal.getTime());
+                    }
+
+                    couponUser.setType("3"); // 获取方式:3-观看奖励
+                    couponUser.setStatus(0); // 状态:0-未核销
+                    couponUser.setIsFail(0); // 是否有效:0-有效
+                    couponUser.setIsDel(0);
+                    couponUser.setCreateTime(now);
+
+                    liveCouponUserService.insertLiveCouponUser(couponUser);
+
+                    // 保存奖励记录
+                    saveUserRewardRecord(live, Collections.singletonList(userId), coupon.getCouponPrice(), 3);
+
+                    successCount++;
+
+                    // 发送WebSocket消息通知用户
+                    sendCouponRewardMessage(live.getLiveId(), userId, coupon);
+
+                } catch (Exception e) {
+                    log.error("绑定优惠券到用户失败,userId={}, couponId={}", userId, couponId, e);
+                }
+            }
+
+            log.info("直播间观看奖励-优惠券发放完成,liveId={}, couponId={}, 成功发放 {} 个用户", 
+                    live.getLiveId(), couponId, successCount);
+
+        } catch (Exception e) {
+            log.error("绑定优惠券到用户异常,liveId={}, couponId={}", live.getLiveId(), couponId, e);
+        }
+    }
+
+    /**
+     * 发送优惠券奖励消息给前端
+     */
+    private void sendCouponRewardMessage(Long liveId, Long userId, LiveCoupon coupon) {
+        try {
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setUserType(0L);
+            sendMsgVo.setCmd("watchRewardCoupon");
+            sendMsgVo.setMsg("恭喜你获得观看奖励优惠券:" + coupon.getTitle());
+
+            // 构建优惠券信息
+            JSONObject couponData = new JSONObject();
+            couponData.put("couponId", coupon.getCouponId());
+            couponData.put("title", coupon.getTitle());
+            couponData.put("couponPrice", coupon.getCouponPrice());
+            couponData.put("useMinPrice", coupon.getUseMinPrice());
+            couponData.put("couponTime", coupon.getCouponTime());
+            sendMsgVo.setData(couponData.toJSONString());
+
+            webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+        } catch (Exception e) {
+            log.error("发送优惠券奖励消息失败,liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+    private void saveUserRewardRecord(Live live, List<Long> userIds, BigDecimal amount, int rewardType) {
         for (Long userId : userIds) {
             LiveRewardRecord record = new LiveRewardRecord();
             record.setLiveId(live.getLiveId());
             record.setUserId(userId);
-            record.setIncomeType(1L);
-            record.setSourceType(3L);
+            record.setIncomeType(1L); // 收入
+            record.setSourceType(3L); // 观看奖励
             record.setSourceId(live.getCompanyId() == null ? 0L : live.getCompanyId());
-            record.setRewardType(2L);
-            record.setNum(BigDecimal.valueOf(scoreAmount));
-            record.setRewardType(2L);
+            record.setRewardType((long) rewardType); // 1-现金 2-积分 3-优惠券
+            record.setNum(amount);
             record.setCreateTime(new Date());
             record.setCreateBy(String.valueOf(userId));
             liveRewardRecordService.insertLiveRewardRecord(record);
         }
     }
+
+    // 保留原有方法签名以兼容其他调用
+    private void saveUserRewardRecord(Live live, List<Long> userIds, Long scoreAmount) {
+        saveUserRewardRecord(live, userIds, BigDecimal.valueOf(scoreAmount), 2);
+    }
     /**
      * 从Redis获取对象并转换为Long类型
      * @param redisCache Redis缓存操作对象

+ 13 - 0
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -97,6 +97,19 @@ public class LiveWatchConfig extends BaseEntity{
     /** 引导语 */
     @Excel(name = "引导语")
     private String scoreGuideText;
+
+    /** 实施动作优惠券ID(action=3时使用) */
+    @Excel(name = "实施动作优惠券ID")
+    private String actionCouponId;
+
+    /** 优惠券引导语 */
+    @Excel(name = "优惠券引导语")
+    private String couponGuideText;
+
+    /** 完课优惠券ID(participateCondition=3时使用) */
+    @Excel(name = "完课优惠券ID")
+    private String finishCouponId;
+
     /** 配置json */
     @Excel(name = "配置json")
     private String configJson;

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

@@ -130,6 +130,19 @@ public interface LiveMapper
             "and JSON_EXTRACT(config_json, '$.enabled') = true")
     List<Live> selectLiveListWithCompletionPointsEnabled();
 
+    /**
+     * 查询开启了完课优惠券配置的直播间(用于完课优惠券定时任务)
+     * @return 直播列表
+     */
+    @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
+            "and config_json is not null " +
+            "and JSON_EXTRACT(config_json, '$.enabled') = true " +
+            "and JSON_EXTRACT(config_json, '$.participateCondition') = 3 " +
+            "and JSON_EXTRACT(config_json, '$.action') = 3 " +
+            "and JSON_EXTRACT(config_json, '$.finishCouponId') is not null " +
+            "and JSON_EXTRACT(config_json, '$.finishCouponId') != ''")
+    List<Live> selectLiveListWithCompletionCouponEnabled();
+
     void updateStatusAndTimeBatchById(@Param("liveList") List<Live> list);
 
     @Select("select * from live where (company_id = #{companyId} or company_id is null)  and is_audit = 1 and is_del = 0 and is_show = 1 and status != 3\n")

+ 16 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java

@@ -0,0 +1,16 @@
+package com.fs.live.service;
+
+/**
+ * 直播完课优惠券Service接口
+ */
+public interface ILiveCompletionCouponService {
+
+    /**
+     * 检查完课状态并发放优惠券(定时任务调用)
+     *
+     * @param liveId        直播ID
+     * @param userId        用户ID
+     * @param watchDuration 观看时长(秒)
+     */
+    void checkAndIssueCompletionCoupon(Long liveId, Long userId, Long watchDuration);
+}

+ 6 - 0
fs-service/src/main/java/com/fs/live/service/ILiveService.java

@@ -171,6 +171,12 @@ public interface ILiveService
      */
     List<Live> selectLiveListWithCompletionPointsEnabled();
 
+    /**
+     * 查询开启了完课优惠券配置的直播间
+     * @return 直播列表
+     */
+    List<Live> selectLiveListWithCompletionCouponEnabled();
+
     void updateBatchById(List<Live> list);
 
     void updateStatusAndTimeBatchById(List<Live> list);

+ 233 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -0,0 +1,233 @@
+package com.fs.live.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.*;
+import com.fs.live.service.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课优惠券Service业务层处理
+ */
+@Slf4j
+@Service
+public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponService {
+
+    /** 获取方式:4-完课奖励 */
+    private static final String COMPLETION_COUPON_TYPE = "4";
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @Autowired
+    private ILiveCouponService liveCouponService;
+
+    @Autowired
+    private ILiveCouponIssueService liveCouponIssueService;
+
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+
+    @Autowired
+    private ILiveRewardRecordService liveRewardRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void checkAndIssueCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+        try {
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return;
+            }
+
+            CompletionCouponConfig config = getCompletionCouponConfig(live);
+            if (!config.isEnabled() || config.getCouponId() == null || config.getCompletionRate() == null) {
+                return;
+            }
+
+            Long actualWatchDuration = watchDuration;
+            if (actualWatchDuration == null) {
+                actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
+            }
+            if (actualWatchDuration == null || actualWatchDuration <= 0) {
+                return;
+            }
+
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                return;
+            }
+
+            BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+            if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                watchRate = BigDecimal.valueOf(100);
+            }
+
+            if (watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) < 0) {
+                return;
+            }
+
+            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+                return;
+            }
+
+            issueCoupon(live, userId, config.getCouponId());
+        } catch (Exception e) {
+            log.error("检查并发放完课优惠券失败, liveId={}, userId={}", liveId, userId, e);
+            throw e;
+        }
+    }
+
+    private void issueCoupon(Live live, Long userId, Long couponId) {
+        LiveCoupon coupon = liveCouponService.selectLiveCouponById(couponId);
+        if (coupon == null) {
+            log.warn("完课优惠券不存在, couponId={}", couponId);
+            return;
+        }
+
+        LiveCouponIssue couponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(couponId);
+        if (couponIssue == null || couponIssue.getStatus() == null || couponIssue.getStatus() != 1) {
+            log.warn("完课优惠券领取配置不可用, couponId={}", couponId);
+            return;
+        }
+
+        Date now = new Date();
+        LiveCouponUser couponUser = new LiveCouponUser();
+        couponUser.setCouponId(couponId);
+        couponUser.setUserId(userId.intValue());
+        couponUser.setCouponTitle(coupon.getTitle());
+        couponUser.setCouponPrice(coupon.getCouponPrice());
+        couponUser.setUseMinPrice(coupon.getUseMinPrice());
+
+        if (couponIssue.getLimitTime() != null) {
+            couponUser.setLimitTime(couponIssue.getLimitTime());
+        } else if (coupon.getCouponTime() != null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(now);
+            cal.add(Calendar.DATE, coupon.getCouponTime().intValue());
+            couponUser.setLimitTime(cal.getTime());
+        }
+
+        couponUser.setType(COMPLETION_COUPON_TYPE + "-" + live.getLiveId());
+        couponUser.setStatus(0);
+        couponUser.setIsFail(0);
+        couponUser.setIsDel(0);
+        couponUser.setCreateTime(now);
+        liveCouponUserService.insertLiveCouponUser(couponUser);
+
+        LiveRewardRecord record = new LiveRewardRecord();
+        record.setLiveId(live.getLiveId());
+        record.setUserId(userId);
+        record.setIncomeType(1L);
+        record.setSourceType(4L);
+        record.setSourceId(live.getCompanyId() == null ? 0L : live.getCompanyId());
+        record.setRewardType(3L);
+        record.setNum(coupon.getCouponPrice() == null ? BigDecimal.ZERO : coupon.getCouponPrice());
+        record.setCreateTime(now);
+        record.setCreateBy(String.valueOf(userId));
+        liveRewardRecordService.insertLiveRewardRecord(record);
+
+        log.info("完课优惠券发放成功, liveId={}, userId={}, couponId={}", live.getLiveId(), userId, couponId);
+    }
+
+    private boolean hasIssuedToday(Long liveId, Long userId, Long couponId) {
+        LiveCouponUser query = new LiveCouponUser();
+        query.setCouponId(couponId);
+        query.setUserId(userId.intValue());
+        query.setType(COMPLETION_COUPON_TYPE + "-" + liveId);
+        List<LiveCouponUser> existingList = liveCouponUserService.selectLiveCouponUserList(query);
+        if (existingList == null || existingList.isEmpty()) {
+            return false;
+        }
+        LocalDate today = LocalDate.now();
+        return existingList.stream()
+                .anyMatch(item -> item.getCreateTime() != null
+                        && item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(today));
+    }
+
+    private CompletionCouponConfig getCompletionCouponConfig(Live live) {
+        CompletionCouponConfig config = new CompletionCouponConfig();
+        config.setEnabled(false);
+
+        String configJson = live.getConfigJson();
+        if (StringUtils.isEmpty(configJson)) {
+            return config;
+        }
+
+        try {
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+
+            Long participateCondition = jsonConfig.getLong("participateCondition");
+            Long action = jsonConfig.getLong("action");
+            if (participateCondition == null || participateCondition != 3L
+                    || action == null || action != 3L) {
+                config.setEnabled(false);
+                return config;
+            }
+
+            String finishCouponId = jsonConfig.getString("finishCouponId");
+            if (StringUtils.isEmpty(finishCouponId)) {
+                config.setEnabled(false);
+                return config;
+            }
+            config.setCouponId(Long.parseLong(finishCouponId));
+
+            Integer completionRate = jsonConfig.getInteger("completionRate");
+            if (completionRate != null && completionRate > 0 && completionRate <= 100) {
+                config.setCompletionRate(completionRate);
+            }
+        } catch (Exception e) {
+            log.warn("解析完课优惠券配置失败, liveId={}", live.getLiveId(), e);
+            config.setEnabled(false);
+        }
+        return config;
+    }
+
+    private static class CompletionCouponConfig {
+        private boolean enabled;
+        private Integer completionRate;
+        private Long couponId;
+
+        public boolean isEnabled() {
+            return enabled;
+        }
+
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+
+        public Integer getCompletionRate() {
+            return completionRate;
+        }
+
+        public void setCompletionRate(Integer completionRate) {
+            this.completionRate = completionRate;
+        }
+
+        public Long getCouponId() {
+            return couponId;
+        }
+
+        public void setCouponId(Long couponId) {
+            this.couponId = couponId;
+        }
+    }
+}

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

@@ -325,6 +325,11 @@ public class LiveServiceImpl implements ILiveService
         return baseMapper.selectLiveListWithCompletionPointsEnabled();
     }
 
+    @Override
+    public List<Live> selectLiveListWithCompletionCouponEnabled() {
+        return baseMapper.selectLiveListWithCompletionCouponEnabled();
+    }
+
     @Override
     public void updateBatchById(List<Live> list) {
         baseMapper.updateLiveList(list);

+ 1 - 1
fs-service/src/main/resources/application-druid-tyt-test.yml

@@ -245,6 +245,6 @@ wechat:
         appid: wxd7c1e221622a0ccf
         secret: 70d3ed4f8eb68cca0cf525b8ce07405d
         redirectUri: https://admin.tyt.com/prod-api/callback
-        isNeedScan: true
+        isNeedScan: false