Quellcode durchsuchen

运营自动化

xw vor 3 Tagen
Ursprung
Commit
c539fa7aa0

+ 10 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveLotteryConfController.java

@@ -140,4 +140,14 @@ public class LiveLotteryConfController extends BaseController
         liveLotteryConfService.saveProducts(liveLotteryProducts);
     }
 
+    /**
+     * 设置自动化抽奖
+     */
+    @PostMapping("/setAutoLottery")
+    public R setAutoLottery(@RequestParam Long lotteryId,
+                            @RequestParam String autoStartTime,
+                            @RequestParam String autoSettlementTime) {
+        return liveLotteryConfService.setAutoLottery(lotteryId, autoStartTime, autoSettlementTime);
+    }
+
 }

+ 9 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveRedConfController.java

@@ -120,6 +120,15 @@ public class LiveRedConfController extends BaseController
         return liveRedConfService.start(redId, SecurityUtils.getLoginUser().getUser().getUserId());
     }
 
+    /**
+     * 设置自动化红包
+     */
+    @PostMapping("/setAutoRed")
+    public R setAutoRed(@RequestParam Long redId,
+                        @RequestParam String autoStartTime,
+                        @RequestParam String autoSettlementTime) {
 
+        return liveRedConfService.setAutoRed(redId, autoStartTime, autoSettlementTime);
+    }
 
 }

+ 1 - 1
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -22,7 +22,7 @@ public class LiveKeysConstant {
 
     public static final String LIVE_HOME_PAGE_CONFIG = "live:config:%s:%s"; //直播间配置信息
     public static final Integer LIVE_HOME_PAGE_CONFIG_EXPIRE = 300; //直播间配置信息过期时间
-    public static final String LIVE_HOME_PAGE_CONFIG_RED = "live:config:%s:red:%s"; //红包记录
+    public static final String LIVE_HOME_PAGE_CONFIG_RED = "live:config:%s:red:%s"; //积分记录
     public static final String LIVE_HOME_PAGE_CONFIG_COUPON = "live:config:%s:coupon:%s"; //优惠券记录
     public static final String LIVE_HOME_PAGE_CONFIG_DRAW = "live:config:%s:draw:%s"; //抽奖记录
     public static final String TOP_MSG = "topMsg"; //抽奖记录

+ 338 - 13
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -30,6 +30,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.live.domain.*;
+import com.fs.live.param.RedPO;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import lombok.extern.slf4j.Slf4j;
@@ -526,6 +527,20 @@ public class WebSocketServer {
                         } catch (Exception e) {
                             log.error("[埋点] 记录发送消息行为失败, liveId={}, userId={}", msg.getLiveId(), msg.getUserId(), e);
                         }
+
+                        // 检查是否有进行中的抽奖,如果评论匹配关键词,写入Redis
+                        try {
+                            handleCommentLottery(msg.getLiveId(), msg.getUserId(), msg.getMsg());
+                        } catch (Exception e) {
+                            log.error("[评论抽奖] 处理失败, liveId={}, userId={}, msg={}", msg.getLiveId(), msg.getUserId(), msg.getMsg(), e);
+                        }
+
+                        // 检查是否有进行中的红包,如果评论匹配关键词,写入Redis
+                        try {
+                            handleCommentRed(msg.getLiveId(), msg.getUserId(), msg.getMsg());
+                        } catch (Exception e) {
+                            log.error("[评论抽红包] 处理失败, liveId={}, userId={}, msg={}", msg.getLiveId(), msg.getUserId(), msg.getMsg(), e);
+                        }
                     }
 
                     msg.setOn(true);
@@ -816,7 +831,11 @@ public class WebSocketServer {
         if (session == null || !session.isOpen()) {
             return;
         }
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error("发送完课积分消息失败: liveId={}, userId={}, error={}", liveId, userId, e.getMessage(), e);
+        }
     }
 
     private void sendBlockMessage(Long liveId, Long userId) {
@@ -862,7 +881,7 @@ public class WebSocketServer {
         for (Map.Entry<Long, Session> entry : room.entrySet()) {
             Session session = entry.getValue();
             if (session != null && session.isOpen()) {
-                sendWithRetry(session, message, 1);
+                sendWithRetry(session, message, 3);
             }
         }
     }
@@ -1074,7 +1093,7 @@ public class WebSocketServer {
         for (Map.Entry<Long, Session> entry : room.entrySet()) {
             Session session = entry.getValue();
             if (session != null && session.isOpen()) {
-                sendWithRetry(session, message, 1);
+                sendWithRetry(session, message, 3);
             }
         }
     }
@@ -1194,17 +1213,24 @@ public class WebSocketServer {
                     log.error("商品ID或状态为空");
                     return;
                 }
-                // 更新商品上下架状态
                 liveGoodsService.updateLiveGoodsStatus(goodsId, status);
-                return ;
-                // 更新直播间配置缓存
-//                liveService.asyncToCacheLiveConfig(task.getLiveId());
-                // 查询商品信息并广播
-//                LiveGoodsVo liveGoodsVo = liveGoodsService.selectLiveGoodsVoByGoodsId(goodsId);
-//                if (liveGoodsVo != null) {
-//                    msg.setData(JSON.toJSONString(liveGoodsVo));
-//                    msg.setStatus(status);
-//                }
+                return;
+            } else if (task.getTaskType() == 7L) {
+                // 自动化抽奖:开始
+                handleAutoLotteryStart(task);
+                return;
+            } else if (task.getTaskType() == 8L) {
+                // 自动化抽奖:结算
+                handleAutoLotterySettle(task);
+                return;
+            } else if (task.getTaskType() == 9L) {
+                // 自动化红包:开始
+                handleAutoRedStart(task);
+                return;
+            } else if (task.getTaskType() == 10L) {
+                // 自动化红包:结算
+                handleAutoRedSettle(task);
+                return;
             }
             msg.setStatus(1);
             // 定时任务消息作为管理员消息插队
@@ -1883,5 +1909,304 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 处理自动化抽奖开始
+     */
+    private void handleAutoLotteryStart(LiveAutoTask task) {
+        try {
+            LiveLotteryConf conf = JSON.parseObject(task.getContent(), LiveLotteryConf.class);
+            if (conf == null || conf.getLotteryId() == null) {
+                log.error("[自动抽奖-开始] 配置解析失败");
+                return;
+            }
+
+            // 调用Service层处理
+            liveLotteryConfService.autoStartLottery(conf.getLotteryId());
+
+            // 查询最新状态
+            LiveLotteryConf latest = liveLotteryConfService.selectLiveLotteryConfByLotteryId(conf.getLotteryId());
+            if (latest == null) {
+                log.error("[自动抽奖-开始] 抽奖不存在,lotteryId={}", conf.getLotteryId());
+                return;
+            }
+
+            // 更新缓存
+            liveService.asyncToCacheLiveConfig(task.getLiveId());
+
+            // 广播消息
+            SendMsgVo msg = new SendMsgVo();
+            msg.setLiveId(task.getLiveId());
+            msg.setCmd("lottery");
+            msg.setStatus(1);
+            msg.setData(JSON.toJSONString(latest));
+
+            enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)), true);
+
+            log.info("[自动抽奖-开始] 推送消息成功,liveId={}, lotteryId={}", task.getLiveId(), conf.getLotteryId());
+
+        } catch (Exception e) {
+            log.error("[自动抽奖-开始] 处理失败,taskId={}", task.getId(), e);
+        }
+    }
+
+    /**
+     * 处理自动化抽奖结算
+     */
+    private void handleAutoLotterySettle(LiveAutoTask task) {
+        try {
+            LiveLotteryConf conf = JSON.parseObject(task.getContent(), LiveLotteryConf.class);
+            if (conf == null || conf.getLotteryId() == null) {
+                log.error("[自加抽奖-结算] 配置解析失败");
+                return;
+            }
+
+            // 调用Service层处理并获取中奖用户
+            List<Map<String, Object>> winners = liveLotteryConfService.autoSettleLottery(conf.getLotteryId());
+
+            // 查询最新状态
+            LiveLotteryConf latest = liveLotteryConfService.selectLiveLotteryConfByLotteryId(conf.getLotteryId());
+            if (latest == null) {
+                log.error("[自加抽奖-结算] 抽奖不存在,lotteryId={}", conf.getLotteryId());
+                return;
+            }
+
+            // 更新缓存
+            liveService.asyncToCacheLiveConfig(task.getLiveId());
+
+            // 广播结算消息
+            SendMsgVo msg = new SendMsgVo();
+            msg.setLiveId(task.getLiveId());
+            msg.setCmd("lottery");
+            msg.setStatus(2);
+            msg.setData(JSON.toJSONString(latest));
+
+            enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)), true);
+
+            // 广播中奖名单
+            if (winners != null && !winners.isEmpty()) {
+                SendMsgVo winnerMsg = new SendMsgVo();
+                winnerMsg.setLiveId(task.getLiveId());
+                winnerMsg.setCmd("LotteryDetail");
+                winnerMsg.setData(JSON.toJSONString(winners));
+                enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", winnerMsg)), true);
+            }
+
+            log.info("[自加抽奖-结算] 推送消息成功,liveId={}, lotteryId={}, 中奖人数={}",
+                    task.getLiveId(), conf.getLotteryId(), winners != null ? winners.size() : 0);
+
+        } catch (Exception e) {
+            log.error("[自加抽奖-结算] 处理失败,taskId={}", task.getId(), e);
+        }
+    }
+
+    /**
+     * 处理评论抽奖:检查用户评论是否匹配进行中的抽奖关键词,如果匹配则写入Redis
+     */
+    private void handleCommentLottery(Long liveId, Long userId, String comment) {
+        try {
+            // 查询进行中的评论抽奖(require=4)
+            LiveLotteryConf queryConf = new LiveLotteryConf();
+            queryConf.setLiveId(liveId);
+            queryConf.setLotteryStatus("1");
+            queryConf.setRequire(4L);
+            List<LiveLotteryConf> lotteries = liveLotteryConfService.selectLiveLotteryConfList(queryConf);
+
+            if (lotteries == null || lotteries.isEmpty()) {
+                return;
+            }
+
+            Date now = new Date();
+            for (LiveLotteryConf lottery : lotteries) {
+                log.info("[评论抽奖] 检查抽奖,lotteryId={}, 配置关键词={}, 用户评论={}",
+                        lottery.getLotteryId(), lottery.getRequireConf(), comment);
+
+                if (lottery.getRequireConf() != null && lottery.getRequireConf().equals(comment)) {
+                    String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveId, lottery.getLotteryId());
+
+                    Object existing = redisCache.redisTemplate.opsForHash().get(redisKey, String.valueOf(userId));
+                    if (existing != null) {
+                        log.info("[评论抽奖] 用户已参与,liveId={}, lotteryId={}, userId={}",
+                                liveId, lottery.getLotteryId(), userId);
+                        continue;
+                    }
+
+                    LiveLotteryRegistration registration = new LiveLotteryRegistration();
+                    registration.setLotteryId(lottery.getLotteryId());
+                    registration.setLiveId(liveId);
+                    registration.setUserId(userId);
+                    registration.setIsWin(0L);
+                    registration.setRizeLevel(-1L);
+                    registration.setCreateTime(now);
+
+                    redisCache.redisTemplate.opsForHash().put(
+                        redisKey,
+                        String.valueOf(userId),
+                        JSON.toJSONString(registration)
+                    );
+
+                    redisCache.expire(redisKey, 30, TimeUnit.MINUTES);
+
+                    log.info("[评论抽奖] 用户参与成功,liveId={}, lotteryId={}, userId={}, keyword={}",
+                            liveId, lottery.getLotteryId(), userId, comment);
+                } else {
+                    log.info("[评论抽奖] 关键词不匹配,lotteryId={}, 配置={}, 用户评论={}",
+                            lottery.getLotteryId(), lottery.getRequireConf(), comment);
+                }
+            }
+        } catch (Exception e) {
+            log.error("[评论抽奖] 处理失败,liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
+    /**
+     * 处理评论领积分:仅处理 red_type=4 的进行中活动,用户评论匹配 requireConf 配置的指定评论则可领取
+     * requireConf 为指定评论内容,多个用逗号分隔,用户评论包含任一词即算匹配;为空则不限制评论内容
+     */
+    private void handleCommentRed(Long liveId, Long userId, String comment) {
+
+        try {
+            List<LiveRedConf> reds = liveRedConfService.selectActivedCommentRed(liveId);
+            if (reds == null || reds.isEmpty()) {
+                return;
+            }
+            log.info("[评论领积分] 收到评论,liveId={}, userId={}, comment={}", liveId, userId, comment);
+
+            String commentTrim = comment != null ? comment.trim() : "";
+            for (LiveRedConf red : reds) {
+                if (!matchCommentRequire(red.getRequireConf(), commentTrim)) {
+                    log.debug("[评论领积分] 评论未匹配关键词,redId={}, requireConf={}", red.getRedId(), red.getRequireConf());
+                    continue;
+                }
+
+                String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveId, red.getRedId());
+                String userIdStr = String.valueOf(userId);
+
+                Object existing = redisCache.redisTemplate.opsForHash().get(redisKey, userIdStr);
+                if (existing != null) {
+                    log.info("[评论领积分] 用户已领取,liveId={}, redId={}, userId={}", liveId, red.getRedId(), userId);
+                    continue;
+                }
+
+                RedPO redPO = new RedPO();
+                redPO.setLiveId(liveId);
+                redPO.setRedId(red.getRedId());
+                redPO.setUserId(userId);
+
+                R result = liveRedConfService.claimRedPacket(redPO);
+                if (result.get("code").equals(200)) {
+                    log.info("[评论领积分] 用户领取成功,liveId={}, redId={}, userId={}, msg={}",
+                            liveId, red.getRedId(), userId, result.get("msg"));
+                } else {
+                    log.warn("[评论领积分] 用户领取失败,liveId={}, redId={}, userId={}, msg={}",
+                            liveId, red.getRedId(), userId, result.get("msg"));
+                }
+            }
+        } catch (Exception e) {
+            log.error("[评论领积分] 处理失败,liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
+    /**
+     * 判断用户评论是否匹配 requireConf:为空或空串视为不限制(匹配);
+     * 否则按逗号分隔为多个关键词,评论包含任一词即匹配
+     */
+    private boolean matchCommentRequire(String requireConf, String comment) {
+        if (requireConf == null || requireConf.trim().isEmpty()) {
+            return true;
+        }
+        if (comment.isEmpty()) {
+            return false;
+        }
+        String[] keywords = requireConf.split("[,,]");
+        for (String kw : keywords) {
+            if (kw != null && !kw.trim().isEmpty() && comment.contains(kw.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 处理自动化积分开始
+     */
+    private void handleAutoRedStart(LiveAutoTask task) {
+        try {
+            LiveRedConf conf = JSON.parseObject(task.getContent(), LiveRedConf.class);
+            if (conf == null || conf.getRedId() == null) {
+                log.error("[自动积分-开始] 配置解析失败");
+                return;
+            }
+
+            // 调用Service层处理
+            liveRedConfService.autoStartRed(conf.getRedId());
+
+            // 查询最新状态
+            LiveRedConf latest = liveRedConfService.selectLiveRedConfByRedId(conf.getRedId());
+            if (latest == null) {
+                log.error("[自动积分呢-开始] 积分不存在,redId={}", conf.getRedId());
+                return;
+            }
+
+            // 更新缓存
+            liveService.asyncToCacheLiveConfig(task.getLiveId());
+
+            // 广播开始消息
+            SendMsgVo msg = new SendMsgVo();
+            msg.setLiveId(task.getLiveId());
+            msg.setCmd("red");
+            msg.setStatus(1);
+            msg.setData(JSON.toJSONString(latest));
+
+            enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)), true);
+
+            log.info("[自动积分-开始] 推送消息成功,liveId={}, redId={}",
+                    task.getLiveId(), conf.getRedId());
+
+        } catch (Exception e) {
+            log.error("[自动积分-开始] 处理失败,taskId={}", task.getId(), e);
+        }
+    }
+
+    /**
+     * 处理自动化红包结算
+     */
+    private void handleAutoRedSettle(LiveAutoTask task) {
+        try {
+            LiveRedConf conf = JSON.parseObject(task.getContent(), LiveRedConf.class);
+            if (conf == null || conf.getRedId() == null) {
+                log.error("[自动红包-结算] 配置解析失败");
+                return;
+            }
+
+            // 调用Service层处理并获取领取用户
+            List<Map<String, Object>> result = liveRedConfService.autoSettleRed(conf.getRedId());
+
+            // 查询最新状态
+            LiveRedConf latest = liveRedConfService.selectLiveRedConfByRedId(conf.getRedId());
+            if (latest == null) {
+                log.error("[自动红包-结算] 红包不存在,redId={}", conf.getRedId());
+                return;
+            }
+
+            // 更新缓存
+            liveService.asyncToCacheLiveConfig(task.getLiveId());
+
+            // 广播结算消息
+            SendMsgVo msg = new SendMsgVo();
+            msg.setLiveId(task.getLiveId());
+            msg.setCmd("red");
+            msg.setStatus(2);
+            msg.setData(JSON.toJSONString(latest));
+
+            enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)), true);
+
+            log.info("[自动红包-结算] 推送消息成功,liveId={}, redId={}, 领取人数={}",
+                    task.getLiveId(), conf.getRedId(), result != null ? result.size() : 0);
+
+        } catch (Exception e) {
+            log.error("[自动红包-结算] 处理失败,taskId={}", task.getId(), e);
+        }
+    }
+
 }
 

+ 6 - 2
fs-service/src/main/java/com/fs/live/domain/LiveRedConf.java

@@ -27,10 +27,14 @@ public class LiveRedConf extends BaseEntity{
     @Excel(name = "有效时间 单位:分")
     private Long duration;
 
-    /** 红包类型 1:主播发起 2:事件红包 */
-    @Excel(name = "红包类型 1:主播发起 2:事件红包")
+    /** 红包类型 1:主播发起 2:事件红包 4:评论领积分 */
+    @Excel(name = "红包类型 1:主播发起 2:事件红包 4:评论领积分")
     private Long redType;
 
+    /** 评论领积分(red_type=4)时的配置:指定评论内容,多个用逗号分隔,用户评论包含任一词即可领 */
+    @Excel(name = "评论领积分配置,指定评论关键词多个用逗号分隔")
+    private String requireConf;
+
     /** 直播间ID */
     @Excel(name = "直播间ID")
     private Long liveId;

+ 7 - 3
fs-service/src/main/java/com/fs/live/mapper/LiveRedConfMapper.java

@@ -69,9 +69,9 @@ public interface LiveRedConfMapper {
     List<LiveRedConf> selectByLiveId(Long liveId);
 
     @Insert({
-            "INSERT INTO live_red_conf (red_status, duration, red_type, live_id, red_num,"+
+            "INSERT INTO live_red_conf (red_status, duration, red_type, require_conf, live_id, red_num,"+
             "total_lots, total_send, `desc`, create_time, update_time, create_by, update_by)"+
-            "VALUES (#{redStatus}, #{duration}, #{redType}, #{liveId}, #{redNum},"+
+            "VALUES (#{redStatus}, #{duration}, #{redType}, #{requireConf}, #{liveId}, #{redNum},"+
             "#{totalLots}, #{totalSend}, #{desc}, NOW(), null, null, null)"
     })
     @Options(useGeneratedKeys = true, keyProperty = "redId")
@@ -79,7 +79,7 @@ public interface LiveRedConfMapper {
 
     @Update({
             "UPDATE live_red_conf SET red_status = #{redStatus}, duration = #{duration},"+
-            "red_type = #{redType}, live_id = #{liveId}, red_num = #{redNum},"+
+            "red_type = #{redType}, require_conf = #{requireConf}, live_id = #{liveId}, red_num = #{redNum},"+
             "total_lots = #{totalLots}, total_send = #{totalSend}, `desc` = #{desc},"+
             "update_time = NOW()"+
             "WHERE red_id = #{redId}"
@@ -92,6 +92,10 @@ public interface LiveRedConfMapper {
     @Select("SELECT * FROM live_red_conf WHERE red_status = 1 AND live_id = #{liveId} ")
     List<LiveRedConf> selectActivedRed(@Param("liveId") Long liveId);
 
+    /** 查询进行中的评论领积分活动(red_type=4,red_status=1) */
+    @Select("SELECT * FROM live_red_conf WHERE red_status = 1 AND live_id = #{liveId} AND red_type = 4")
+    List<LiveRedConf> selectActivedCommentRed(@Param("liveId") Long liveId);
+
     void finishRedStatusBySetIds(@Param("ids") Set<String> ids);
 
     @Select("SELECT * FROM live_red_conf WHERE red_status = 0 and live_id=#{liveId}")

+ 23 - 0
fs-service/src/main/java/com/fs/live/service/ILiveLotteryConfService.java

@@ -10,6 +10,7 @@ import com.fs.live.vo.LiveLotteryProductVO;
 import com.fs.live.vo.LiveUserLotteryRecordVo;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -100,4 +101,26 @@ public interface ILiveLotteryConfService {
     List<LiveLotteryConf> selectLiveLotteryConfListOn(LiveLotteryConf liveLotteryConf);
 
     List<LiveUserLotteryRecordVo> myLottery(long l);
+
+    /**
+     * 自动化开始抽奖
+     * @param lotteryId 抽奖ID
+     */
+    void autoStartLottery(Long lotteryId);
+
+    /**
+     * 自动化结算抽奖
+     * @param lotteryId 抽奖ID
+     * @return 中奖用户列表
+     */
+    List<Map<String, Object>> autoSettleLottery(Long lotteryId);
+
+    /**
+     * 设置自动化抽奖
+     * @param lotteryId 抽奖ID
+     * @param autoStartTime 自动开始时间
+     * @param autoSettlementTime 自动结算时间
+     * @return 结果
+     */
+    R setAutoLottery(Long lotteryId, String autoStartTime, String autoSettlementTime);
 }

+ 29 - 0
fs-service/src/main/java/com/fs/live/service/ILiveRedConfService.java

@@ -6,6 +6,7 @@ import com.fs.live.domain.LiveRedConf;
 import com.fs.live.param.RedPO;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -81,6 +82,9 @@ public interface ILiveRedConfService {
 
     List<LiveRedConf> selectActivedRed(Long liveId);
 
+    /** 查询进行中的评论领积分活动(red_type=4) */
+    List<LiveRedConf> selectActivedCommentRed(Long liveId);
+
     // 结算掉红包
     void finishRedStatusBySetIds(Set<String> range);
 
@@ -90,4 +94,29 @@ public interface ILiveRedConfService {
     void updateRedQuantityNum();
 
     List<LiveRedConf> selectLiveRedConfListOn(LiveRedConf liveRedConf);
+
+    /**
+     * 自动化开始红包
+     *
+     * @param redId 红包ID
+     */
+    void autoStartRed(Long redId);
+
+    /**
+     * 自动化结算红包
+     *
+     * @param redId 红包ID
+     * @return 中奖用户列表
+     */
+    List<Map<String, Object>> autoSettleRed(Long redId);
+
+    /**
+     * 设置自动化红包
+     *
+     * @param redId              红包ID
+     * @param autoStartTime      自动开始时间
+     * @param autoSettlementTime 自动结算时间
+     * @return 结果
+     */
+    R setAutoRed(Long redId, String autoStartTime, String autoSettlementTime);
 }

+ 412 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java

@@ -1,14 +1,22 @@
 package com.fs.live.service.impl;
 
 
+import com.alibaba.fastjson.JSON;
+import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveAutoTask;
 import com.fs.live.domain.LiveLotteryConf;
 import com.fs.live.domain.LiveLotteryRegistration;
+import com.fs.live.domain.LiveUserLotteryRecord;
+import com.fs.live.mapper.LiveAutoTaskMapper;
 import com.fs.live.mapper.LiveLotteryConfMapper;
 import com.fs.live.mapper.LiveLotteryProductConfMapper;
 import com.fs.live.mapper.LiveLotteryRegistrationMapper;
+import com.fs.live.mapper.LiveMapper;
+import com.fs.live.mapper.LiveUserLotteryRecordMapper;
 import com.fs.live.param.LiveLotteryProduct;
 import com.fs.live.param.LiveLotteryProductSaveParam;
 import com.fs.live.param.LotteryPO;
@@ -18,12 +26,14 @@ import com.fs.live.vo.LiveLotteryConfVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
 import com.fs.live.vo.LiveLotteryProductVO;
 import com.fs.live.vo.LiveUserLotteryRecordVo;
+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.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -33,6 +43,7 @@ import java.util.stream.Collectors;
  * @date 2025-07-17
  */
 @Service
+@Slf4j
 public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
 
     @Autowired
@@ -44,11 +55,15 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
     private RedisCache redisCache;
     @Autowired
     private LiveLotteryRegistrationMapper lotteryRegistrationMapper;
+    @Autowired
+    private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
 
     @Autowired
     private LiveLotteryConfMapper baseMapper;
     @Autowired
     private ILiveAutoTaskService liveAutoTaskService;
+    @Autowired
+    private LiveMapper liveMapper;
     /**
      * 查询直播抽奖配置
      *
@@ -313,5 +328,402 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
         return baseMapper.selectLiveUserLotteryRecordByUserId(userId);
     }
 
+    /**
+     * 自动化开始抽奖
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void autoStartLottery(Long lotteryId) {
+        String lockKey = "lottery:auto:start:" + lotteryId;
+        try {
+            Boolean lockAcquired = redisCache.redisTemplate.opsForValue()
+                    .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
+
+            if (!Boolean.TRUE.equals(lockAcquired)) {
+                log.warn("[自动抽奖-开始] 任务正在执行,lotteryId={}", lotteryId);
+                return;
+            }
+
+            LiveLotteryConf conf = baseMapper.selectLiveLotteryConfByLotteryId(lotteryId);
+            if (conf == null) {
+                log.error("[自动抽奖-开始] 抽奖不存在,lotteryId={}", lotteryId);
+                return;
+            }
+
+            if (!"0".equals(conf.getLotteryStatus())) {
+                log.warn("[自动抽奖-开始] 抽奖状态不是未开始,lotteryId={}, status={}", lotteryId, conf.getLotteryStatus());
+                return;
+            }
+
+            log.info("[自动抽奖-开始] 开始执行,liveId={}, lotteryId={}", conf.getLiveId(), lotteryId);
+
+            // 更新抽奖状态为进行中
+            conf.setLotteryStatus("1");
+            conf.setUpdateTime(new Date());
+            baseMapper.updateLiveLotteryConf(conf);
+
+            log.info("[自动抽奖-开始] 执行成功,liveId={}, lotteryId={}", conf.getLiveId(), lotteryId);
+
+        } catch (Exception e) {
+            log.error("[自动抽奖-开始] 执行失败,lotteryId={}", lotteryId, e);
+            throw e;
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+    }
+
+    /**
+     * 自动化结算抽奖
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public List<Map<String, Object>> autoSettleLottery(Long lotteryId) {
+        String lockKey = "lottery:auto:settle:" + lotteryId;
+        List<Map<String, Object>> winners = new ArrayList<>();
+
+        try {
+            Boolean lockAcquired = redisCache.redisTemplate.opsForValue()
+                    .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+
+            if (!Boolean.TRUE.equals(lockAcquired)) {
+                log.warn("[自动抽奖-结算] 任务正在执行,lotteryId={}", lotteryId);
+                return winners;
+            }
+
+            LiveLotteryConf conf = baseMapper.selectLiveLotteryConfByLotteryId(lotteryId);
+            if (conf == null) {
+                log.error("[自动抽奖-结算] 抽奖不存在,lotteryId={}", lotteryId);
+                return winners;
+            }
+
+            if ("2".equals(conf.getLotteryStatus())) {
+                log.warn("[自加抽奖-结算] 抽奖已结束,跳过重复执行,lotteryId={}", lotteryId);
+                return winners;
+            }
+
+            log.info("[自动抽奖-结算] 开始执行,liveId={}, lotteryId={}", conf.getLiveId(), lotteryId);
+
+            // 从Redis读取参与者
+            String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, conf.getLiveId(), lotteryId);
+            Map<Object, Object> redisData = redisCache.redisTemplate.opsForHash().entries(redisKey);
+
+            if (redisData == null || redisData.isEmpty()) {
+                log.info("[自动抽奖-结算] Redis无参与者,liveId={}, lotteryId={}", conf.getLiveId(), lotteryId);
+                // 直接更新状态为已结束
+                conf.setLotteryStatus("2");
+                conf.setUpdateTime(new Date());
+                baseMapper.updateLiveLotteryConf(conf);
+                return winners;
+            }
+
+            // 解析参与者
+            List<LiveLotteryRegistration> participants = new ArrayList<>();
+            for (Map.Entry<Object, Object> entry : redisData.entrySet()) {
+                try {
+                    LiveLotteryRegistration registration = JSON.parseObject(
+                        entry.getValue().toString(),
+                        LiveLotteryRegistration.class
+                    );
+                    if (registration != null) {
+                        participants.add(registration);
+                    }
+                } catch (Exception e) {
+                    log.error("[自动抽奖-结算] 解析参与者失败", e);
+                }
+            }
+
+            if (participants.isEmpty()) {
+                log.info("[自加抽奖-结算] 无有效参与者,liveId={}, lotteryId={}", conf.getLiveId(), lotteryId);
+                conf.setLotteryStatus("2");
+                conf.setUpdateTime(new Date());
+                baseMapper.updateLiveLotteryConf(conf);
+                return winners;
+            }
+
+            // 查询奖品配置
+            List<LiveLotteryProduct> prizes = productMapper.selectLiveLotteryProductConfByLotteryId(lotteryId);
+            if (prizes == null || prizes.isEmpty()) {
+                log.error("[自动抽奖-结算] 无奖品配置,lotteryId={}", lotteryId);
+                return winners;
+            }
+
+            // 执行抽奖逻辑
+            winners = performLottery(participants, prizes, conf.getLiveId(), lotteryId);
+
+            // 更新抽奖状态为已结束
+            conf.setLotteryStatus("2");
+            conf.setUpdateTime(new Date());
+            baseMapper.updateLiveLotteryConf(conf);
+
+            // 清理Redis
+            redisCache.deleteObject(redisKey);
+
+            log.info("[自加抽奖-结算] 执行成功,liveId={}, lotteryId={}, 参与人数={}, 中奖人数={}",
+                    conf.getLiveId(), lotteryId, participants.size(), winners.size());
+
+        } catch (Exception e) {
+            log.error("[自加抽奖-结算] 执行失败,lotteryId={}", lotteryId, e);
+            throw e;
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+
+        return winners;
+    }
+
+    /**
+     * 执行抽奖逻辑
+     */
+    private List<Map<String, Object>> performLottery(List<LiveLotteryRegistration> participants,
+                                                      List<LiveLotteryProduct> prizes,
+                                                      Long liveId, Long lotteryId) {
+        List<Map<String, Object>> winners = new ArrayList<>();
+
+        // 打乱参与者
+        Collections.shuffle(participants);
+
+        // 按奖品等级排序
+        prizes.sort(Comparator.comparing(LiveLotteryProduct::getPrizeLevel));
+
+        // 分配奖品
+        int winnerIndex = 0;
+        List<LiveLotteryRegistration> winnerRegistrations = new ArrayList<>();
+        List<LiveUserLotteryRecord> lotteryRecords = new ArrayList<>();
+        Date now = new Date();
+
+        for (LiveLotteryProduct prize : prizes) {
+            int totalLots = prize.getTotalLots() != null ? prize.getTotalLots().intValue() : 0;
+
+            for (int i = 0; i < totalLots && winnerIndex < participants.size(); i++) {
+                LiveLotteryRegistration winner = participants.get(winnerIndex);
+                winner.setIsWin(1L);
+                winner.setRizeLevel(prize.getPrizeLevel());
+                winner.setUpdateTime(now);
+                winnerRegistrations.add(winner);
+
+                // 创建中奖记录
+                LiveUserLotteryRecord record = new LiveUserLotteryRecord();
+                record.setLotteryId(lotteryId);
+                record.setLiveId(liveId);
+                record.setUserId(winner.getUserId());
+                record.setProductId(prize.getProductId());
+                record.setOrderStatus(-9);
+                record.setCreateTime(now);
+                lotteryRecords.add(record);
+
+                // 构造返回结果
+                Map<String, Object> winnerInfo = new HashMap<>();
+                winnerInfo.put("userId", winner.getUserId());
+                winnerInfo.put("prizeLevel", prize.getPrizeLevel());
+                winnerInfo.put("productId", prize.getProductId());
+                winners.add(winnerInfo);
+
+                winnerIndex++;
+            }
+        }
+
+        // 批量写入数据库:先插入参与者,再更新中奖者,最后插入中奖记录
+        if (!participants.isEmpty()) {
+            batchInsertOrUpdateParticipants(participants, liveId, lotteryId);
+        }
+
+        if (!lotteryRecords.isEmpty()) {
+            batchInsertLotteryRecords(lotteryRecords, lotteryId);
+        }
+
+        return winners;
+    }
+
+    /**
+     * 批量插入或更新参与者(包括中奖者)
+     */
+    private void batchInsertOrUpdateParticipants(List<LiveLotteryRegistration> participants, Long liveId, Long lotteryId) {
+        // 查询已存在的记录
+        LiveLotteryRegistration query = new LiveLotteryRegistration();
+        query.setLotteryId(lotteryId);
+        List<LiveLotteryRegistration> existing = lotteryRegistrationMapper.selectLiveLotteryRegistrationList(query);
+
+        Map<Long, LiveLotteryRegistration> existingMap = new HashMap<>();
+        if (existing != null) {
+            for (LiveLotteryRegistration reg : existing) {
+                existingMap.put(reg.getUserId(), reg);
+            }
+        }
+
+        List<LiveLotteryRegistration> toInsert = new ArrayList<>();
+        List<LiveLotteryRegistration> toUpdate = new ArrayList<>();
+        Date now = new Date();
+
+        for (LiveLotteryRegistration registration : participants) {
+            registration.setLiveId(liveId);
+            registration.setLotteryId(lotteryId);
+            registration.setUpdateTime(now);
+
+            if (existingMap.containsKey(registration.getUserId())) {
+                // 已存在,需要更新
+                toUpdate.add(registration);
+            } else {
+                // 不存在,需要插入
+                if (registration.getIsWin() == null) {
+                    registration.setIsWin(0L);
+                }
+                if (registration.getRizeLevel() == null) {
+                    registration.setRizeLevel(-1L);
+                }
+                registration.setCreateTime(now);
+                toInsert.add(registration);
+            }
+        }
+
+        // 批量插入
+        if (!toInsert.isEmpty()) {
+            int batchSize = 500;
+            for (int i = 0; i < toInsert.size(); i += batchSize) {
+                int end = Math.min(i + batchSize, toInsert.size());
+                List<LiveLotteryRegistration> batch = toInsert.subList(i, end);
+                lotteryRegistrationMapper.insertLiveLotteryRegistrationBatch(batch);
+            }
+            log.info("[自动抽奖] 批量插入参与者,lotteryId={}, count={}", lotteryId, toInsert.size());
+        }
+
+        // 批量更新
+        if (!toUpdate.isEmpty()) {
+            for (LiveLotteryRegistration registration : toUpdate) {
+                lotteryRegistrationMapper.updateLiveLotteryRegistrationNoId(registration);
+            }
+            log.info("[自动抽奖] 批量更新参与者,lotteryId={}, count={}", lotteryId, toUpdate.size());
+        }
+    }
+
+    /**
+     * 批量插入中奖记录
+     */
+    private void batchInsertLotteryRecords(List<LiveUserLotteryRecord> records, Long lotteryId) {
+        if (records.isEmpty()) {
+            return;
+        }
+
+        Set<Long> existingUserIds = new HashSet<>();
+        for (LiveUserLotteryRecord record : records) {
+            LiveUserLotteryRecord query = new LiveUserLotteryRecord();
+            query.setLotteryId(record.getLotteryId());
+            query.setUserId(record.getUserId());
+            List<LiveUserLotteryRecord> existing = liveUserLotteryRecordMapper.selectLiveUserLotteryRecordList(query);
+
+            if (existing != null && !existing.isEmpty()) {
+                existingUserIds.add(record.getUserId());
+            }
+        }
+
+        List<LiveUserLotteryRecord> toInsert = new ArrayList<>();
+        for (LiveUserLotteryRecord record : records) {
+            if (!existingUserIds.contains(record.getUserId())) {
+                toInsert.add(record);
+            }
+        }
+
+        if (toInsert.isEmpty()) {
+            return;
+        }
+
+        // 单条插入
+        for (LiveUserLotteryRecord record : toInsert) {
+            liveUserLotteryRecordMapper.insertLiveUserLotteryRecord(record);
+        }
+
+        log.info("[自加抽奖] 批量插入中奖记录,lotteryId={}, count={}", lotteryId, toInsert.size());
+    }
+
+    /**
+     * 设置自动化抽奖
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R setAutoLottery(Long lotteryId, String autoStartTime, String autoSettlementTime) {
+        try {
+            LiveLotteryConf conf = baseMapper.selectLiveLotteryConfByLotteryId(lotteryId);
+            if (conf == null) {
+                return R.error("抽奖配置不存在");
+            }
+
+            if (!"0".equals(conf.getLotteryStatus())) {
+                return R.error("抽奖已开始,无法设置自动化");
+            }
+
+            List<LiveLotteryProduct> prizes = productMapper.selectLiveLotteryProductConfByLotteryId(lotteryId);
+            if (prizes == null || prizes.isEmpty()) {
+                return R.error("请先添加奖品");
+            }
+
+            Live live = liveMapper.selectLiveByLiveId(conf.getLiveId());
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            Date startTime = com.fs.common.utils.DateUtils.parseDate(autoStartTime);
+            Date settlementTime = com.fs.common.utils.DateUtils.parseDate(autoSettlementTime);
+            Date now = new Date();
+
+            if (startTime.before(now)) {
+                return R.error("开始时间不能早于当前时间");
+            }
+
+            if (settlementTime.before(startTime)) {
+                return R.error("结算时间不能早于开始时间");
+            }
+
+            LiveAutoTask startTask = new LiveAutoTask();
+            startTask.setLiveId(conf.getLiveId());
+            startTask.setTaskName("自动开始抽奖-" + conf.getDesc());
+            startTask.setTaskType(7L);
+            startTask.setTriggerType(1L);
+            startTask.setTriggerValue(startTime);
+            startTask.setAbsValue(startTime);
+            startTask.setContent(JSON.toJSONString(conf));
+            startTask.setStatus(1L);
+            startTask.setFinishStatus(0L);
+            startTask.setCreateTime(now);
+            startTask.setUpdateTime(now);
+            liveAutoTaskService.directInsertLiveAutoTask(startTask);
+
+            LiveAutoTask settlementTask = new LiveAutoTask();
+            settlementTask.setLiveId(conf.getLiveId());
+            settlementTask.setTaskName("自动结算抽奖-" + conf.getDesc());
+            settlementTask.setTaskType(8L);
+            settlementTask.setTriggerType(1L);
+            settlementTask.setTriggerValue(settlementTime);
+            settlementTask.setAbsValue(settlementTime);
+            settlementTask.setContent(JSON.toJSONString(conf));
+            settlementTask.setStatus(1L);
+            settlementTask.setFinishStatus(0L);
+            settlementTask.setCreateTime(now);
+            settlementTask.setUpdateTime(now);
+            liveAutoTaskService.directInsertLiveAutoTask(settlementTask);
+
+            if (live.getStatus() == 2) {
+                redisCache.redisTemplate.opsForZSet().add(
+                    "live:auto_task:" + live.getLiveId(),
+                    JSON.toJSONString(startTask),
+                    startTime.getTime()
+                );
+                redisCache.redisTemplate.opsForZSet().add(
+                    "live:auto_task:" + live.getLiveId(),
+                    JSON.toJSONString(settlementTask),
+                    settlementTime.getTime()
+                );
+                redisCache.redisTemplate.expire(
+                    "live:auto_task:" + live.getLiveId(),
+                    30,
+                    TimeUnit.MINUTES
+                );
+            }
+
+            return R.ok("自动化抽奖设置成功");
+
+        } catch (Exception e) {
+            log.error("设置自动化抽奖失败,lotteryId: {}", lotteryId, e);
+            return R.error("设置失败:" + e.getMessage());
+        }
+    }
 
 }

+ 256 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java

@@ -404,6 +404,11 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         return baseMapper.selectActivedRed(liveId);
     }
 
+    @Override
+    public List<LiveRedConf> selectActivedCommentRed(Long liveId) {
+        return baseMapper.selectActivedCommentRed(liveId);
+    }
+
     @Override
     public void finishRedStatusBySetIds(Set<String> range) {
         try {
@@ -590,4 +595,255 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         }
         return conf.getRedNum() / conf.getTotalLots();
     }
+
+    /**
+     * 自动化开始红包
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void autoStartRed(Long redId) {
+        String lockKey = "red:auto:start:" + redId;
+        try {
+            Boolean lockAcquired = redisCache.redisTemplate.opsForValue()
+                    .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
+
+            if (!Boolean.TRUE.equals(lockAcquired)) {
+                log.warn("[自动红包-开始] 任务正在执行,redId={}", redId);
+                return;
+            }
+
+            LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(redId);
+            if (conf == null) {
+                log.error("[自动红包-开始] 红包不存在,redId={}", redId);
+                return;
+            }
+
+            if (conf.getRedStatus() != 0) {
+                log.warn("[自动红包-开始] 红包状态不是未发放,redId={}, status={}", redId, conf.getRedStatus());
+                return;
+            }
+
+            log.info("[自动红包-开始] 开始执行,liveId={}, redId={}", conf.getLiveId(), redId);
+
+            // 更新红包状态为发放中
+            conf.setRedStatus(1L);
+            conf.setUpdateTime(new Date());
+            baseMapper.updateLiveRedConf(conf);
+
+            // 初始化Redis缓存
+            setRemaining(redId, conf.getTotalLots());
+            String redConfCacheKey = REDPACKET_CONF_CACHE_KEY + redId;
+            redisCache.setCacheObject(redConfCacheKey, JSONUtil.toJsonStr(conf), conf.getDuration().intValue() + 5, TimeUnit.MINUTES);
+
+            log.info("[自动红包-开始] 执行成功,liveId={}, redId={}", conf.getLiveId(), redId);
+
+        } catch (Exception e) {
+            log.error("[自动红包-开始] 执行失败,redId={}", redId, e);
+            throw e;
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+    }
+
+    /**
+     * 自动化结算红包
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public List<Map<String, Object>> autoSettleRed(Long redId) {
+        String lockKey = "red:auto:settle:" + redId;
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        try {
+            Boolean lockAcquired = redisCache.redisTemplate.opsForValue()
+                    .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+
+            if (!Boolean.TRUE.equals(lockAcquired)) {
+                log.warn("[自动红包-结算] 任务正在执行,redId={}", redId);
+                return result;
+            }
+
+            LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(redId);
+            if (conf == null) {
+                log.error("[自动红包-结算] 红包不存在,redId={}", redId);
+                return result;
+            }
+
+            if (conf.getRedStatus() == 2) {
+                log.warn("[自动红包-结算] 红包已结束,跳过重复执行,redId={}", redId);
+                return result;
+            }
+
+            log.info("[自动红包-结算] 开始执行,liveId={}, redId={}", conf.getLiveId(), redId);
+
+            // 从Redis读取参与者
+            String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, conf.getLiveId(), redId);
+            Map<Object, Object> hashEntries = redisCache.hashEntries(redisKey);
+
+            if (hashEntries == null || hashEntries.isEmpty()) {
+                log.info("[自动红包-结算] Redis无参与者,liveId={}, redId={}", conf.getLiveId(), redId);
+                // 直接更新状态为已结束
+                conf.setRedStatus(2L);
+                conf.setUpdateTime(new Date());
+                baseMapper.updateLiveRedConf(conf);
+                return result;
+            }
+
+            // 解析参与者
+            List<LiveUserRedRecord> records = new ArrayList<>();
+            for (Map.Entry<Object, Object> entry : hashEntries.entrySet()) {
+                try {
+                    LiveUserRedRecord record = JSONUtil.toBean(entry.getValue().toString(), LiveUserRedRecord.class);
+                    if (record != null) {
+                        records.add(record);
+                        
+                        Map<String, Object> winner = new HashMap<>();
+                        winner.put("userId", record.getUserId());
+                        winner.put("integral", record.getIntegral());
+                        result.add(winner);
+                    }
+                } catch (Exception e) {
+                    log.error("[自动红包-结算] 解析参与者失败", e);
+                }
+            }
+
+            if (records.isEmpty()) {
+                log.info("[自动红包-结算] 无有效参与者,liveId={}, redId={}", conf.getLiveId(), redId);
+                conf.setRedStatus(2L);
+                conf.setUpdateTime(new Date());
+                baseMapper.updateLiveRedConf(conf);
+                return result;
+            }
+
+            // 批量写入数据库
+            List<LiveUserRedRecord> newRecords = new ArrayList<>();
+            for (LiveUserRedRecord record : records) {
+                LiveUserRedRecord queryRecord = new LiveUserRedRecord();
+                queryRecord.setUserId(record.getUserId());
+                queryRecord.setRedId(record.getRedId());
+                List<LiveUserRedRecord> existingRecords = userRedRecordMapper.selectLiveUserRedRecordList(queryRecord);
+
+                if (existingRecords == null || existingRecords.isEmpty()) {
+                    newRecords.add(record);
+                }
+            }
+
+            if (!newRecords.isEmpty()) {
+                userRedRecordMapper.insertLiveUserRedRecordBatch(newRecords);
+                log.info("[自动红包-结算] 批量插入红包记录,redId={}, count={}", redId, newRecords.size());
+            }
+
+            // 更新红包状态为已结束
+            conf.setRedStatus(2L);
+            conf.setTotalSend((long) records.size());
+            conf.setUpdateTime(new Date());
+            baseMapper.updateLiveRedConf(conf);
+
+            // 清理Redis
+            redisCache.deleteObject(redisKey);
+            redisCache.deleteObject(REDPACKET_CONF_CACHE_KEY + redId);
+            redisCache.deleteObject(REDPACKET_REMAININGLOTS_KEY + redId);
+
+            log.info("[自动红包-结算] 执行成功,liveId={}, redId={}, 参与人数={}",
+                    conf.getLiveId(), redId, records.size());
+
+        } catch (Exception e) {
+            log.error("[自动红包-结算] 执行失败,redId={}", redId, e);
+            throw e;
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+
+        return result;
+    }
+
+    /**
+     * 设置自动化红包
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R setAutoRed(Long redId, String autoStartTime, String autoSettlementTime) {
+        try {
+            LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(redId);
+            if (conf == null) {
+                return R.error("红包配置不存在");
+            }
+
+            if (conf.getRedStatus() != 0) {
+                return R.error("红包已开始,无法设置自动化");
+            }
+
+            Live live = liveMapper.selectLiveByLiveId(conf.getLiveId());
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            Date startTime = com.fs.common.utils.DateUtils.parseDate(autoStartTime);
+            Date settlementTime = com.fs.common.utils.DateUtils.parseDate(autoSettlementTime);
+            Date now = new Date();
+
+            if (startTime.before(now)) {
+                return R.error("开始时间不能早于当前时间");
+            }
+
+            if (settlementTime.before(startTime)) {
+                return R.error("结算时间不能早于开始时间");
+            }
+
+            // 创建自动开始任务 taskType=9
+            LiveAutoTask startTask = new LiveAutoTask();
+            startTask.setLiveId(conf.getLiveId());
+            startTask.setTaskName("自动开始红包-" + conf.getDesc());
+            startTask.setTaskType(9L);
+            startTask.setTriggerType(1L);
+            startTask.setTriggerValue(startTime);
+            startTask.setAbsValue(startTime);
+            startTask.setContent(com.alibaba.fastjson.JSON.toJSONString(conf));
+            startTask.setStatus(1L);
+            startTask.setFinishStatus(0L);
+            startTask.setCreateTime(now);
+            startTask.setUpdateTime(now);
+            liveAutoTaskService.directInsertLiveAutoTask(startTask);
+
+            // 创建自动结算任务 taskType=10
+            LiveAutoTask settlementTask = new LiveAutoTask();
+            settlementTask.setLiveId(conf.getLiveId());
+            settlementTask.setTaskName("自动结算红包-" + conf.getDesc());
+            settlementTask.setTaskType(10L);
+            settlementTask.setTriggerType(1L);
+            settlementTask.setTriggerValue(settlementTime);
+            settlementTask.setAbsValue(settlementTime);
+            settlementTask.setContent(com.alibaba.fastjson.JSON.toJSONString(conf));
+            settlementTask.setStatus(1L);
+            settlementTask.setFinishStatus(0L);
+            settlementTask.setCreateTime(now);
+            settlementTask.setUpdateTime(now);
+            liveAutoTaskService.directInsertLiveAutoTask(settlementTask);
+
+            // 如果直播中,加入Redis
+            if (live.getStatus() == 2) {
+                redisCache.redisTemplate.opsForZSet().add(
+                    "live:auto_task:" + live.getLiveId(),
+                    com.alibaba.fastjson.JSON.toJSONString(startTask),
+                    startTime.getTime()
+                );
+                redisCache.redisTemplate.opsForZSet().add(
+                    "live:auto_task:" + live.getLiveId(),
+                    com.alibaba.fastjson.JSON.toJSONString(settlementTask),
+                    settlementTime.getTime()
+                );
+                redisCache.redisTemplate.expire(
+                    "live:auto_task:" + live.getLiveId(),
+                    30,
+                    TimeUnit.MINUTES
+                );
+            }
+
+            return R.ok("自动化红包设置成功");
+
+        } catch (Exception e) {
+            log.error("设置自动化红包失败,redId: {}", redId, e);
+            return R.error("设置失败:" + e.getMessage());
+        }
+    }
 }

+ 11 - 6
fs-service/src/main/resources/mapper/live/LiveLotteryRegistrationMapper.xml

@@ -24,6 +24,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectLiveLotteryRegistrationList" parameterType="LiveLotteryRegistration" resultMap="LiveLotteryRegistrationResult">
         <include refid="selectLiveLotteryRegistrationVo"/>
         <where>
+            <if test="liveId != null">and live_id = #{liveId}</if>
+            <if test="userId != null">and user_id = #{userId}</if>
+            <if test="lotteryId != null">and lottery_id = #{lotteryId}</if>
+            <if test="isWin != null">and is_win = #{isWin}</if>
+            <if test="rizeLevel != null">and rize_level = #{rizeLevel}</if>
         </where>
     </select>
 
@@ -68,9 +73,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         is_win,
         rize_level,
         create_time,
-        update_time,
-        create_by,
-        update_by
+        update_time
+        <if test="list[0].createBy != null">,create_by</if>
+        <if test="list[0].updateBy != null">,update_by</if>
         ) VALUES
         <foreach collection="list" item="item" separator=",">
             (
@@ -80,9 +85,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{item.isWin},
             #{item.rizeLevel},
             #{item.createTime},
-            #{item.updateTime},
-            #{item.createBy},
-            #{item.updateBy}
+            #{item.updateTime}
+            <if test="item.createBy != null">,#{item.createBy}</if>
+            <if test="item.updateBy != null">,#{item.updateBy}</if>
             )
         </foreach>
     </insert>

+ 5 - 1
fs-service/src/main/resources/mapper/live/LiveRedConfMapper.xml

@@ -9,6 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="redStatus"    column="red_status"    />
         <result property="duration"    column="duration"    />
         <result property="redType"    column="red_type"    />
+        <result property="requireConf"    column="require_conf"    />
         <result property="liveId"    column="live_id"    />
         <result property="redNum"    column="red_num"    />
         <result property="totalLots"    column="total_lots"    />
@@ -21,7 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectLiveRedConfVo">
-        select red_id, red_status, duration, red_type, live_id, red_num, total_lots, total_send, `desc`, create_time, update_time, create_by, update_by from live_red_conf
+        select red_id, red_status, duration, red_type, require_conf, live_id, red_num, total_lots, total_send, `desc`, create_time, update_time, create_by, update_by from live_red_conf
     </sql>
 
     <select id="selectLiveRedConfList" parameterType="LiveRedConf" resultMap="LiveRedConfResult">
@@ -48,6 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="redStatus != null">red_status,</if>
             <if test="duration != null">duration,</if>
             <if test="redType != null">red_type,</if>
+            <if test="requireConf != null">require_conf,</if>
             <if test="liveId != null">live_id,</if>
             <if test="redNum != null">red_num,</if>
             <if test="totalLots != null">total_lots,</if>
@@ -62,6 +64,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="redStatus != null">#{redStatus},</if>
             <if test="duration != null">#{duration},</if>
             <if test="redType != null">#{redType},</if>
+            <if test="requireConf != null">#{requireConf},</if>
             <if test="liveId != null">#{liveId},</if>
             <if test="redNum != null">#{redNum},</if>
             <if test="totalLots != null">#{totalLots},</if>
@@ -80,6 +83,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="redStatus != null">red_status = #{redStatus},</if>
             <if test="duration != null">duration = #{duration},</if>
             <if test="redType != null">red_type = #{redType},</if>
+            <if test="requireConf != null">require_conf = #{requireConf},</if>
             <if test="liveId != null">live_id = #{liveId},</if>
             <if test="redNum != null">red_num = #{redNum},</if>
             <if test="totalLots != null">total_lots = #{totalLots},</if>

+ 1 - 0
fs-service/src/main/resources/mapper/live/LiveUserLotteryRecordMapper.xml

@@ -25,6 +25,7 @@
     <select id="selectLiveUserLotteryRecordList" parameterType="com.fs.live.domain.LiveUserLotteryRecord" resultMap="LiveUserLotteryRecordResult">
         <include refid="selectLiveUserLotteryRecordVo"/>
         <where>
+            <if test="lotteryId != null "> and lottery_id = #{lotteryId}</if>
             <if test="liveId != null "> and live_id = #{liveId}</if>
             <if test="userId != null "> and user_id = #{userId}</if>
             <if test="productId != null "> and product_id = #{productId}</if>