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

1、调整签到任务问题处理

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

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

@@ -42,6 +42,9 @@ public class LiveKeysConstant {
 
 
     public static final String LIVE_ROOM_PASSWORD_CACHE = "live:room:password:%s";
     public static final String LIVE_ROOM_PASSWORD_CACHE = "live:room:password:%s";
 
 
+    /** 签到任务触发后缓存奖励留存 liveId:signNo,含 opLogId,供 App 领取绑定 */
+    public static final String LIVE_SIGN_OP_LOGS = "live:sign:oplogs:%s:%s";
+
     /** 直播 WebSocket 跨服务广播频道(admin 等服务发布,live-app 订阅后推送给 App) */
     /** 直播 WebSocket 跨服务广播频道(admin 等服务发布,live-app 订阅后推送给 App) */
     public static final String LIVE_WS_BROADCAST_CHANNEL = "live:ws:broadcast";
     public static final String LIVE_WS_BROADCAST_CHANNEL = "live:ws:broadcast";
 
 

+ 175 - 2
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -1578,9 +1578,51 @@ public class WebSocketServer {
 //                    msg.setStatus(status);
 //                    msg.setStatus(status);
 //                }
 //                }
             }else if (task.getTaskType() == 7L) {
             }else if (task.getTaskType() == 7L) {
-                // 签到
+                // 签到任务:激活 2积分红包/5优惠券/6金额红包,写自动化留存,推 cmd=sign
                 msg.setCmd("sign");
                 msg.setCmd("sign");
-                msg.setData(JSON.toJSONString(task.getContent()));
+                JSONObject content = JSON.parseObject(task.getContent());
+                if (content == null) {
+                    log.error("签到任务content为空, taskId={}", task.getId());
+                    return;
+                }
+                String signNo = content.getString("signNo");
+                List<JSONObject> rewardItems = parseSignRewardItems(content);
+                if (rewardItems.isEmpty()) {
+                    log.error("签到任务未配置奖励, taskId={}", task.getId());
+                    return;
+                }
+                List<JSONObject> pushRewards = new ArrayList<>();
+                String signPrefix = "第" + signNo + "次签到-";
+                for (JSONObject item : rewardItems) {
+                    Long rewardType = item.getLong("rewardType");
+                    if (rewardType == null) {
+                        continue;
+                    }
+                    JSONObject pushItem = activateSignRewardAndSaveOpLog(
+                            task.getLiveId(), signPrefix, rewardType, item, now);
+                    if (pushItem != null) {
+                        pushRewards.add(pushItem);
+                    }
+                }
+                if (pushRewards.isEmpty()) {
+                    log.error("签到任务奖励激活失败, taskId={}", task.getId());
+                    return;
+                }
+                JSONObject data = new JSONObject();
+                data.put("signNo", signNo);
+                data.put("taskId", task.getId());
+                data.put("liveId", task.getLiveId());
+                data.put("rewards", pushRewards);
+                msg.setData(data.toJSONString());
+                // 缓存本次签到奖励留存(含 opLogId),供 App /sign 领取时绑定已领状态
+                try {
+                    redisCache.setCacheObject(
+                            String.format(LiveKeysConstant.LIVE_SIGN_OP_LOGS, task.getLiveId(), signNo),
+                            data.toJSONString(),
+                            1, TimeUnit.DAYS);
+                } catch (Exception e) {
+                    log.warn("缓存签到留存失败, liveId={}, signNo={}", task.getLiveId(), signNo, e);
+                }
             }
             }
             msg.setStatus(1);
             msg.setStatus(1);
             // 定时任务消息作为管理员消息插队
             // 定时任务消息作为管理员消息插队
@@ -2313,5 +2355,136 @@ public class WebSocketServer {
         return total;
         return total;
     }
     }
 
 
+    /**
+     * 解析签到任务 content.rewards(兼容旧单奖励 rewardType/rewardId 结构)。
+     * 当前类型:2积分红包、5优惠券(+couponCount)、6金额红包(+amount)。
+     */
+    private List<JSONObject> parseSignRewardItems(JSONObject content) {
+        List<JSONObject> list = new ArrayList<>();
+        if (content == null) {
+            return list;
+        }
+        com.alibaba.fastjson.JSONArray arr = content.getJSONArray("rewards");
+        if (arr != null && !arr.isEmpty()) {
+            for (int i = 0; i < arr.size(); i++) {
+                JSONObject item = arr.getJSONObject(i);
+                if (item != null) {
+                    list.add(item);
+                }
+            }
+            return list;
+        }
+        if (content.getLong("rewardType") != null) {
+            JSONObject single = new JSONObject();
+            single.put("rewardType", content.getLong("rewardType"));
+            single.put("rewardId", content.getLong("rewardId"));
+            single.put("reward", content.get("reward"));
+            list.add(single);
+        }
+        return list;
+    }
+
+    /**
+     * 激活签到奖励并写入自动化留存。
+     * <ul>
+     *   <li>2 积分红包:激活红包配置,opType=红包发放</li>
+     *   <li>5 优惠券:展示优惠券,bizName 带张数,写入 couponCount</li>
+     *   <li>6 金额红包:opType=OP_SIGN_CASH_RED(11),amount 精确到 0.1 元</li>
+     * </ul>
+     */
+    private JSONObject activateSignRewardAndSaveOpLog(Long liveId, String signPrefix,
+                                                     Long rewardType, JSONObject item, Date now) {
+        try {
+            JSONObject pushItem = new JSONObject();
+            pushItem.put("rewardType", rewardType);
+            if (rewardType == 2L) {
+                Long redId = item.getLong("rewardId");
+                if (redId == null && item.getJSONObject("reward") != null) {
+                    redId = item.getJSONObject("reward").getLong("redId");
+                }
+                LiveRedConf liveRedConf = liveRedConfService.selectLiveRedConfByRedId(redId);
+                if (liveRedConf == null) {
+                    log.error("签到积分红包不存在, redId={}", redId);
+                    return null;
+                }
+                if (liveRedConf.getRedStatus() != null && liveRedConf.getRedStatus() == 0L) {
+                    liveRedConf.setRedStatus(1L);
+                    liveRedConf.setUpdateTime(now);
+                    liveRedConfService.updateLiveRedConf(liveRedConf);
+                    liveService.asyncToCacheLiveConfig(liveId);
+                }
+                String bizName = signPrefix + (StringUtils.isNotEmpty(liveRedConf.getDesc())
+                        ? liveRedConf.getDesc() : "积分红包 #" + liveRedConf.getRedId());
+                LiveConsoleOpLog opLog = liveConsoleOpLogService.saveRedSendLog(
+                        liveId, LiveConsoleOpLog.HANDLE_AUTO, liveRedConf.getRedId(), bizName);
+                pushItem.put("rewardId", liveRedConf.getRedId());
+                pushItem.put("opLogId", opLog != null ? opLog.getId() : null);
+                pushItem.put("reward", liveRedConf);
+                return pushItem;
+            } else if (rewardType == 5L) {
+                Long couponId = item.getLong("rewardId");
+                JSONObject rewardJson = item.getJSONObject("reward");
+                if (couponId == null && rewardJson != null) {
+                    couponId = rewardJson.getLong("couponId");
+                }
+                Integer couponCount = item.getInteger("couponCount");
+                if (couponCount == null || couponCount < 1) {
+                    couponCount = 1;
+                }
+                LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(couponId);
+                if (liveCoupon == null) {
+                    log.error("签到优惠券不存在, couponId={}", couponId);
+                    return null;
+                }
+                LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectIssueByLiveIdAndCouponId(liveId, liveCoupon.getCouponId());
+                if (liveCouponIssue == null) {
+                    log.error("签到优惠券未关联直播间, liveId={}, couponId={}", liveId, liveCoupon.getCouponId());
+                    return null;
+                }
+                LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(liveId, liveCouponIssue.getId());
+                redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, liveCouponIssue.getId()),
+                        liveCouponIssue.getRemainCount() != null ? liveCouponIssue.getRemainCount().intValue() : 0,
+                        30, TimeUnit.MINUTES);
+                liveCouponMapper.updateChangeShow(liveId, liveCouponIssue.getId());
+                String couponBizName = signPrefix + (StringUtils.isNotEmpty(liveCoupon.getTitle())
+                        ? liveCoupon.getTitle() : "优惠券 #" + liveCoupon.getCouponId()) + "×" + couponCount;
+                int couponOpType = (liveCoupon.getType() != null && liveCoupon.getType() == 3L)
+                        ? LiveConsoleOpLog.OP_VERIFY_COUPON_SHOW
+                        : LiveConsoleOpLog.OP_COUPON_SHOW;
+                LiveConsoleOpLog opLog = liveConsoleOpLogService.saveLog(
+                        liveId, couponOpType, LiveConsoleOpLog.HANDLE_AUTO,
+                        liveCouponIssue.getId(), couponBizName);
+                pushItem.put("rewardId", liveCoupon.getCouponId());
+                pushItem.put("couponIssueId", liveCouponIssue.getId());
+                pushItem.put("couponCount", couponCount);
+                pushItem.put("goodsId", relation != null ? relation.getGoodsId() : null);
+                pushItem.put("opLogId", opLog != null ? opLog.getId() : null);
+                pushItem.put("reward", liveCoupon);
+                return pushItem;
+            } else if (rewardType == 6L) {
+                java.math.BigDecimal amount = item.getBigDecimal("amount");
+                if (amount == null || amount.compareTo(java.math.BigDecimal.ZERO) <= 0) {
+                    log.error("签到金额红包金额无效, liveId={}", liveId);
+                    return null;
+                }
+                amount = amount.setScale(1, java.math.RoundingMode.HALF_UP);
+                String bizName = signPrefix + "金额红包¥" + amount.toPlainString();
+                // bizId 存金额分(整数)便于关联;领取时按 amount 发放
+                long amountFen = amount.multiply(new java.math.BigDecimal("100")).longValue();
+                LiveConsoleOpLog opLog = liveConsoleOpLogService.saveLog(
+                        liveId, LiveConsoleOpLog.OP_SIGN_CASH_RED, LiveConsoleOpLog.HANDLE_AUTO,
+                        amountFen, bizName);
+                pushItem.put("amount", amount);
+                pushItem.put("opLogId", opLog != null ? opLog.getId() : null);
+                return pushItem;
+            }
+            log.warn("不支持的签到奖励类型: {}", rewardType);
+            return null;
+        } catch (Exception e) {
+            log.error("激活签到奖励失败, liveId={}, rewardType={}", liveId, rewardType, e);
+            return null;
+        }
+    }
+
 }
 }
 
 

+ 3 - 3
fs-service/src/main/java/com/fs/live/domain/LiveAutoTask.java

@@ -29,8 +29,8 @@ public class LiveAutoTask extends BaseEntity{
     @Excel(name = "任务名称")
     @Excel(name = "任务名称")
     private String taskName;
     private String taskName;
 
 
-    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动  4-抽奖 5-优惠券 6-自动上下架*/
-    @Excel(name = "任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 4-抽奖")
+    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 4-抽奖 5-优惠券 6-自动上下架 7-签到任务 */
+    @Excel(name = "任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 4-抽奖 5-优惠券 6-自动上下架 7-签到任务")
     private Long taskType;
     private Long taskType;
 
 
     /** 触发类型:1-绝对时间 2-相对直播开始时间 */
     /** 触发类型:1-绝对时间 2-相对直播开始时间 */
@@ -45,7 +45,7 @@ public class LiveAutoTask extends BaseEntity{
     @Excel(name = "触发值:绝对时间用yyyy-MM-dd HH:mm:ss,相对时间用分钟数")
     @Excel(name = "触发值:绝对时间用yyyy-MM-dd HH:mm:ss,相对时间用分钟数")
     private Date absValue;
     private Date absValue;
 
 
-    /** 任务内容:如消息文本、红包配置等JSON格式 */
+    /** 任务内容 JSON。签到任务(7):{"signNo":1,"rewards":[{"rewardType":2|5|6,...}]},积分红包与金额红包各最多1个 */
     @Excel(name = "任务内容:如消息文本、红包配置等JSON格式")
     @Excel(name = "任务内容:如消息文本、红包配置等JSON格式")
     private String content;
     private String content;
 
 

+ 3 - 1
fs-service/src/main/java/com/fs/live/domain/LiveConsoleOpLog.java

@@ -35,6 +35,8 @@ public class LiveConsoleOpLog extends BaseEntity {
     public static final int OP_WATCH_REWARD_POINTS = 9;
     public static final int OP_WATCH_REWARD_POINTS = 9;
     /** 操作类型:观看奖励优惠券 */
     /** 操作类型:观看奖励优惠券 */
     public static final int OP_WATCH_REWARD_COUPON = 10;
     public static final int OP_WATCH_REWARD_COUPON = 10;
+    /** 操作类型:签到金额红包(微信商家转账,App 确认收款) */
+    public static final int OP_SIGN_CASH_RED = 11;
 
 
     /** 触发类型:中控台人工操作 */
     /** 触发类型:中控台人工操作 */
     public static final int HANDLE_CONSOLE = 1;
     public static final int HANDLE_CONSOLE = 1;
@@ -52,7 +54,7 @@ public class LiveConsoleOpLog extends BaseEntity {
      * 操作类型
      * 操作类型
      * @see #OP_COUPON_SHOW
      * @see #OP_COUPON_SHOW
      */
      */
-    @Excel(name = "操作类型", readConverterExp = "1=优惠券展示,2=核销券展示,3=红包结算,4=抽奖结算,5=红包发放,6=抽奖发放,7=完课积分,8=完课优惠券,9=观看奖励积分,10=观看奖励优惠券")
+    @Excel(name = "操作类型", readConverterExp = "1=优惠券展示,2=核销券展示,3=红包结算,4=抽奖结算,5=红包发放,6=抽奖发放,7=完课积分,8=完课优惠券,9=观看奖励积分,10=观看奖励优惠券,11=签到金额红包")
     private Integer opType;
     private Integer opType;
 
 
     /**
     /**

+ 1 - 1
fs-service/src/main/java/com/fs/live/domain/LiveSignRecord.java

@@ -11,7 +11,7 @@ import java.io.Serializable;
 import java.util.Date;
 import java.util.Date;
 
 
 /**
 /**
- * 直播签到记录对象 live_sign_record
+ * 直播签到记录对象 live_sign_record(防重/统计;奖励领取状态走 live_console_op_log)
  *
  *
  * @author ylrz
  * @author ylrz
  * @date 2026-04-07
  * @date 2026-04-07

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

@@ -0,0 +1,29 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+/**
+ * 直播签到领取参数。
+ * <p>
+ * 签到成功后按该次任务配置的奖励一并领取:
+ * 2=积分红包、5=优惠券(按 couponCount 发张数)、6=金额红包(微信转账,需 App 确认收款)。
+ * 金额红包领取时需传 appId、source。
+ */
+@Data
+public class SignPO {
+
+    /** 直播间ID */
+    private Long liveId;
+
+    /** 签到序号(第几次签到:1/2/3) */
+    private String signNo;
+
+    /** 自动化任务ID(可选,用于精确定位任务) */
+    private Long taskId;
+
+    /** 小程序/公众号 appId(领取金额红包微信转账时必传) */
+    private String appId;
+
+    /** 来源:1=H5 2=小程序(金额红包微信转账时使用) */
+    private Integer source;
+}

+ 213 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java

@@ -188,6 +188,23 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
             baseMapper.insertLiveAutoTask(liveAutoTask);
             baseMapper.insertLiveAutoTask(liveAutoTask);
 
 
         }else if(liveAutoTask.getTaskType() == 7L){
         }else if(liveAutoTask.getTaskType() == 7L){
+            // 签到任务:rewards 支持 2积分红包(最多1个)/5优惠券(+张数)/6金额红包(最多1个,精确到0.1元)
+            JSONObject reqContent;
+            try {
+                reqContent = JSON.parseObject(liveAutoTask.getContent());
+            } catch (Exception e) {
+                return R.error("签到任务内容格式错误");
+            }
+            if (reqContent == null) {
+                return R.error("请配置签到奖励");
+            }
+            R rewardsResult = buildSignRewards(liveAutoTask.getLiveId(), reqContent);
+            if (!Integer.valueOf(200).equals(rewardsResult.get("code"))) {
+                return rewardsResult;
+            }
+            @SuppressWarnings("unchecked")
+            List<JSONObject> rewardList = (List<JSONObject>) rewardsResult.get("data");
+
             // 查询最新序号
             // 查询最新序号
             LiveAutoTask task = baseMapper.selectLastSignTaskByLiveId(liveAutoTask.getLiveId());
             LiveAutoTask task = baseMapper.selectLastSignTaskByLiveId(liveAutoTask.getLiveId());
             int nextSignNo = 1; // 默认第一次签到
             int nextSignNo = 1; // 默认第一次签到
@@ -213,12 +230,12 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
                 }
                 }
             }
             }
 
 
-            // 设置新的签到序号
+            // content:signNo + rewards(积分红包/优惠券/金额红包可组合,红包与积分各最多1个)
             JSONObject content = new JSONObject();
             JSONObject content = new JSONObject();
             content.put("signNo", nextSignNo);
             content.put("signNo", nextSignNo);
+            content.put("rewards", rewardList);
             liveAutoTask.setContent(content.toJSONString());
             liveAutoTask.setContent(content.toJSONString());
 
 
-
             baseMapper.insertLiveAutoTask(liveAutoTask);
             baseMapper.insertLiveAutoTask(liveAutoTask);
 
 
         } else {
         } else {
@@ -363,6 +380,39 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
                 return R.error("上架/下架商品自动化任务,更新异常!");
                 return R.error("上架/下架商品自动化任务,更新异常!");
             }
             }
         } else if(liveAutoTask.getTaskType() == 7L){
         } else if(liveAutoTask.getTaskType() == 7L){
+            // 签到任务更新:保留原 signNo,按最新规则重建 rewards
+            JSONObject reqContent;
+            try {
+                reqContent = JSON.parseObject(liveAutoTask.getContent());
+            } catch (Exception e) {
+                return R.error("签到任务内容格式错误");
+            }
+            if (reqContent == null) {
+                return R.error("请配置签到奖励");
+            }
+            R rewardsResult = buildSignRewards(liveAutoTask.getLiveId(), reqContent);
+            if (!Integer.valueOf(200).equals(rewardsResult.get("code"))) {
+                return rewardsResult;
+            }
+            @SuppressWarnings("unchecked")
+            List<JSONObject> rewardList = (List<JSONObject>) rewardsResult.get("data");
+
+            int signNo = 1;
+            if (existTask != null && StringUtils.isNotEmpty(existTask.getContent())) {
+                try {
+                    JSONObject oldContent = JSON.parseObject(existTask.getContent());
+                    if (oldContent != null && oldContent.get("signNo") != null) {
+                        signNo = oldContent.getIntValue("signNo");
+                    }
+                } catch (Exception e) {
+                    log.error("解析原签到任务content失败", e);
+                }
+            }
+
+            JSONObject content = new JSONObject();
+            content.put("signNo", signNo);
+            content.put("rewards", rewardList);
+            liveAutoTask.setContent(content.toJSONString());
             baseMapper.updateLiveAutoTask(liveAutoTask);
             baseMapper.updateLiveAutoTask(liveAutoTask);
         } else {
         } else {
             return R.error("任务类型错误");
             return R.error("任务类型错误");
@@ -754,4 +804,165 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
         return StringUtils.isNotEmpty(typeLabel)
         return StringUtils.isNotEmpty(typeLabel)
                 && (typeLabel.contains("核销") || typeLabel.contains("代金券"));
                 && (typeLabel.contains("核销") || typeLabel.contains("代金券"));
     }
     }
+
+    /**
+     * 构建签到任务多奖励列表(兼容旧单奖励字段 rewardType/rewardId)。
+     * <ul>
+     *   <li>2=积分红包:选 LiveRedConf,整场任务最多配置 1 个</li>
+     *   <li>5=优惠券:选优惠券 + couponCount(领取张数)</li>
+     *   <li>6=金额红包:输入 amount(元,精确到 0.1,最低 0.1),最多配置 1 个;领取走微信转账</li>
+     * </ul>
+     * 积分红包与金额红包可同时存在,各自不能重复;已不支持抽奖(4)。
+     */
+    private R buildSignRewards(Long liveId, JSONObject reqContent) {
+        List<JSONObject> inputList = new ArrayList<>();
+        com.alibaba.fastjson.JSONArray rewardsArr = reqContent.getJSONArray("rewards");
+        if (rewardsArr != null && !rewardsArr.isEmpty()) {
+            for (int i = 0; i < rewardsArr.size(); i++) {
+                JSONObject item = rewardsArr.getJSONObject(i);
+                if (item != null) {
+                    inputList.add(item);
+                }
+            }
+        } else if (reqContent.getLong("rewardType") != null) {
+            JSONObject single = new JSONObject();
+            single.put("rewardType", reqContent.getLong("rewardType"));
+            single.put("rewardId", reqContent.getLong("rewardId"));
+            if (reqContent.get("couponCount") != null) {
+                single.put("couponCount", reqContent.getInteger("couponCount"));
+            }
+            if (reqContent.get("amount") != null) {
+                single.put("amount", reqContent.getBigDecimal("amount"));
+            }
+            inputList.add(single);
+        }
+        if (inputList.isEmpty()) {
+            return R.error("请至少配置一个签到奖励");
+        }
+
+        List<JSONObject> resultList = new ArrayList<>();
+        Set<String> dedupe = new HashSet<>();
+        boolean hasPointRed = false;
+        boolean hasCashRed = false;
+        for (JSONObject item : inputList) {
+            Long rewardType = item.getLong("rewardType");
+            if (rewardType == null) {
+                return R.error("签到奖励类型不能为空");
+            }
+            if (rewardType == 4L) {
+                return R.error("签到任务已不支持抽奖奖励");
+            }
+            if (rewardType == 2L) {
+                if (hasPointRed) {
+                    return R.error("积分红包只能配置一个");
+                }
+                hasPointRed = true;
+            } else if (rewardType == 6L) {
+                if (hasCashRed) {
+                    return R.error("金额红包只能配置一个");
+                }
+                hasCashRed = true;
+            }
+            R rewardResult = buildSignRewardItem(liveId, item);
+            if (!Integer.valueOf(200).equals(rewardResult.get("code"))) {
+                return rewardResult;
+            }
+            JSONObject stored = (JSONObject) rewardResult.get("data");
+            String key = rewardType + "_" + stored.getString("rewardId") + "_"
+                    + stored.getString("amount") + "_" + stored.getString("couponCount");
+            if (!dedupe.add(key)) {
+                continue;
+            }
+            resultList.add(stored);
+        }
+        if (resultList.isEmpty()) {
+            return R.error("请至少配置一个签到奖励");
+        }
+        return R.ok().put("data", resultList);
+    }
+
+    /**
+     * 构建签到任务单条奖励内容并校验。
+     * <ul>
+     *   <li>2:校验积分红包存在且未发放,写入 rewardId + reward</li>
+     *   <li>5:校验优惠券已关联直播间及库存,写入 rewardId + couponCount + reward</li>
+     *   <li>6:校验金额精确到 0.1 元且 ≥0.1,写入 amount</li>
+     * </ul>
+     */
+    private R buildSignRewardItem(Long liveId, JSONObject item) {
+        Long rewardType = item.getLong("rewardType");
+        if (rewardType == 2L) {
+            Long rewardId = item.getLong("rewardId");
+            if (rewardId == null) {
+                return R.error("请选择积分红包");
+            }
+            LiveRedConf liveRedConf = liveRedConfMapper.selectLiveRedConfByRedId(rewardId);
+            if (liveRedConf == null) {
+                return R.error("积分红包配置不存在");
+            }
+            if (liveRedConf.getRedStatus() != 0L) {
+                return R.error("积分红包状态应该为:未发放");
+            }
+            JSONObject stored = new JSONObject();
+            stored.put("rewardType", rewardType);
+            stored.put("rewardId", rewardId);
+            stored.put("reward", liveRedConf);
+            return R.ok().put("data", stored);
+        } else if (rewardType == 5L) {
+            Long rewardId = item.getLong("rewardId");
+            if (rewardId == null) {
+                return R.error("请选择优惠券");
+            }
+            Integer couponCount = item.getInteger("couponCount");
+            if (couponCount == null || couponCount < 1) {
+                couponCount = 1;
+            }
+            LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(rewardId);
+            if (liveCoupon == null) {
+                return R.error("优惠券不存在");
+            }
+            LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectIssueByLiveIdAndCouponId(liveId, liveCoupon.getCouponId());
+            if (liveCouponIssue == null) {
+                return R.error("优惠券未发布或未关联到直播间");
+            }
+            LiveCouponIssueRelation liveCouponIssueRelation = liveCouponMapper.selectCouponRelation(liveId, liveCouponIssue.getId());
+            if (liveCouponIssueRelation == null) {
+                return R.error("优惠券尚未添加在直播间");
+            }
+            if (!isVerifyCouponType(liveCoupon) && ObjectUtil.isEmpty(liveCouponIssueRelation.getGoodsId())) {
+                return R.error("未绑定商品,无法制定自动化任务!");
+            }
+            if (ObjectUtil.isNotEmpty(liveCouponIssueRelation.getGoodsId())) {
+                liveCoupon.setGoodsId(liveCouponIssueRelation.getGoodsId());
+            }
+            if (liveCouponIssue.getRemainCount() != null && liveCouponIssue.getRemainCount() < couponCount) {
+                return R.error("优惠券剩余库存不足,无法配置领取数量:" + couponCount);
+            }
+            JSONObject stored = new JSONObject();
+            stored.put("rewardType", rewardType);
+            stored.put("rewardId", rewardId);
+            stored.put("couponCount", couponCount);
+            stored.put("reward", liveCoupon);
+            return R.ok().put("data", stored);
+        } else if (rewardType == 6L) {
+            java.math.BigDecimal amount = item.getBigDecimal("amount");
+            if (amount == null || amount.compareTo(java.math.BigDecimal.ZERO) <= 0) {
+                return R.error("请输入正确的金额红包金额");
+            }
+            // 金额红包精确到 0.1 元(不允许分位)
+            java.math.BigDecimal scaled = amount.setScale(1, java.math.RoundingMode.DOWN);
+            if (amount.compareTo(scaled) != 0) {
+                return R.error("金额红包需精确到0.1元");
+            }
+            amount = scaled;
+            if (amount.compareTo(new java.math.BigDecimal("0.1")) < 0) {
+                return R.error("金额红包不能低于0.1元");
+            }
+            JSONObject stored = new JSONObject();
+            stored.put("rewardType", rewardType);
+            stored.put("amount", amount);
+            return R.ok().put("data", stored);
+        }
+        return R.error("不支持的签到奖励类型");
+    }
 }
 }

+ 17 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveController.java

@@ -425,6 +425,23 @@ public class LiveController extends AppBaseController {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * 直播签到并领取该次配置奖励(积分红包/优惠券/金额红包)。
+	 * 金额红包成功时响应含 needWxConfirm、package 等,App 需跳转微信确认收款。
+	 */
+	@Login
+	@ApiOperation("直播签到领取奖励")
+	@PostMapping("/sign")
+	public R sign(@RequestBody com.fs.live.param.SignPO sign) {
+		try {
+			Long userId = Long.valueOf(getUserId());
+			return liveFacadeService.signClaim(sign, userId);
+		} catch (Exception e) {
+			log.error("直播签到失败, liveId={}, userId={}", sign != null ? sign.getLiveId() : null, getUserId(), e);
+			return R.error("签到失败: " + e.getMessage());
+		}
+	}
+
 	/**
 	/**
 	 * 查询直播间观看奖励配置的观看时长(后台 liveConfig / 观看奖励 中配置,单位:分钟)
 	 * 查询直播间观看奖励配置的观看时长(后台 liveConfig / 观看奖励 中配置,单位:分钟)
 	 *
 	 *

+ 8 - 0
fs-user-app/src/main/java/com/fs/app/facade/LiveFacadeService.java

@@ -7,6 +7,7 @@ import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.param.CouponPO;
 import com.fs.live.param.CouponPO;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.param.RedPO;
 import com.fs.live.param.RedPO;
+import com.fs.live.param.SignPO;
 import com.fs.live.vo.LiveUserRewardRecordsVo;
 import com.fs.live.vo.LiveUserRewardRecordsVo;
 
 
 public interface LiveFacadeService {
 public interface LiveFacadeService {
@@ -29,4 +30,11 @@ public interface LiveFacadeService {
      */
      */
     LiveUserRewardRecordsVo getUserRewardRecords(Long liveId, Long userId);
     LiveUserRewardRecordsVo getUserRewardRecords(Long liveId, Long userId);
 
 
+    /**
+     * 直播签到并领取该次配置的全部奖励。
+     * 奖励类型:2积分红包 / 5优惠券(couponCount) / 6金额红包(微信确认收款)。
+     * 领取成功会绑定自动化留存(myRewardRecords 显示已领取)。
+     */
+    R signClaim(SignPO sign, Long userId);
+
 }
 }

+ 272 - 0
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -11,11 +11,13 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.PageRequest;
 import com.fs.common.core.page.PageRequest;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.*;
 import com.fs.live.domain.*;
 import com.fs.framework.aspectj.lock.DistributeLock;
 import com.fs.framework.aspectj.lock.DistributeLock;
 import com.fs.live.param.CouponPO;
 import com.fs.live.param.CouponPO;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.param.LotteryPO;
 import com.fs.live.param.RedPO;
 import com.fs.live.param.RedPO;
+import com.fs.live.param.SignPO;
 import com.fs.live.service.*;
 import com.fs.live.service.*;
 import com.fs.live.vo.*;
 import com.fs.live.vo.*;
 import com.github.pagehelper.PageInfo;
 import com.github.pagehelper.PageInfo;
@@ -62,6 +64,24 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
     @Autowired
     @Autowired
     private ILiveConsoleOpLogService liveConsoleOpLogService;
     private ILiveConsoleOpLogService liveConsoleOpLogService;
 
 
+    @Autowired
+    private ILiveSignRecordService liveSignRecordService;
+
+    @Autowired
+    private ILiveAutoTaskService liveAutoTaskService;
+
+    @Autowired
+    private ILiveCouponIssueService liveCouponIssueService;
+
+    @Autowired
+    private com.fs.his.service.IFsUserService fsUserService;
+
+    @Autowired
+    private com.fs.his.service.IFsStorePaymentService paymentService;
+
+    @Autowired
+    private ILiveRewardRecordService liveRewardRecordService;
+
     @Override
     @Override
     public R liveList(PageRequest pageRequest) {
     public R liveList(PageRequest pageRequest) {
         int start = (pageRequest.getCurrentPage() - 1) * pageRequest.getPageSize();
         int start = (pageRequest.getCurrentPage() - 1) * pageRequest.getPageSize();
@@ -286,6 +306,258 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         return vo;
         return vo;
     }
     }
 
 
+    /**
+     * 直播签到领取:写 live_sign_record 防重,再按 rewards 发奖并绑定留存。
+     * 金额红包成功时返回 package 等字段,供 App 跳转微信确认收款。
+     */
+    @Override
+    @DistributeLock(keyExpression = "#sign.liveId +'_'+#userId +'_'+#sign.signNo", scene = "live_sign_claim", waitTime = 1000, errorMsg = "签到处理中,请稍后")
+    public R signClaim(SignPO sign, Long userId) {
+        if (sign == null || sign.getLiveId() == null || StringUtils.isEmpty(sign.getSignNo())) {
+            return R.error("签到参数不完整");
+        }
+        Long liveId = sign.getLiveId();
+        String signNo = String.valueOf(sign.getSignNo());
+
+        // 防重复签到
+        LiveSignRecord existQuery = new LiveSignRecord();
+        existQuery.setLiveId(liveId);
+        existQuery.setUserId(userId);
+        existQuery.setSignNo(signNo);
+        List<LiveSignRecord> exists = liveSignRecordService.selectLiveSignRecordList(existQuery);
+        if (CollUtil.isNotEmpty(exists)) {
+            return R.error("您已完成该次签到");
+        }
+
+        // 从触发缓存取奖励留存;没有则从已执行签到任务解析
+        List<com.alibaba.fastjson.JSONObject> rewards = resolveSignRewards(liveId, signNo, sign.getTaskId());
+        if (CollUtil.isEmpty(rewards)) {
+            return R.error("签到任务尚未开启或奖励配置不存在");
+        }
+
+        // 写入签到记录
+        LiveSignRecord record = new LiveSignRecord();
+        record.setLiveId(liveId);
+        record.setUserId(userId);
+        record.setSignNo(signNo);
+        record.setCreateTime(new Date());
+        liveSignRecordService.save(record);
+
+        List<Map<String, Object>> claimResults = new ArrayList<>();
+        for (com.alibaba.fastjson.JSONObject item : rewards) {
+            Long rewardType = item.getLong("rewardType");
+            Long opLogId = item.getLong("opLogId");
+            Map<String, Object> one = new HashMap<>();
+            one.put("rewardType", rewardType);
+            one.put("opLogId", opLogId);
+            try {
+                if (rewardType != null && rewardType == 2L) {
+                    // 积分红包:走直播积分红包领取
+                    RedPO red = new RedPO();
+                    red.setLiveId(liveId);
+                    red.setUserId(userId);
+                    red.setRedId(item.getLong("rewardId"));
+                    red.setOpLogId(opLogId);
+                    R r = iLiveRedConfService.claimRedPacket(red);
+                    one.put("success", Integer.valueOf(200).equals(r.get("code")));
+                    one.put("msg", r.get("msg"));
+                } else if (rewardType != null && rewardType == 5L) {
+                    // 优惠券:按配置张数发放
+                    CouponPO coupon = new CouponPO();
+                    coupon.setLiveId(liveId);
+                    coupon.setUserId(userId);
+                    Long couponIssueId = item.getLong("couponIssueId");
+                    if (couponIssueId == null) {
+                        Long couponId = item.getLong("rewardId");
+                        LiveCouponIssue issue = liveCouponIssueService.selectIssueByLiveIdAndCouponId(liveId, couponId);
+                        couponIssueId = issue != null ? issue.getId() : null;
+                    }
+                    coupon.setCouponIssueId(couponIssueId);
+                    coupon.setGoodsId(item.getLong("goodsId"));
+                    coupon.setOpLogId(opLogId);
+                    Integer couponCount = item.getInteger("couponCount");
+                    coupon.setCouponCount(couponCount != null && couponCount > 0 ? couponCount : 1);
+                    R r = iLiveCouponService.claimCoupon(coupon);
+                    one.put("success", Integer.valueOf(200).equals(r.get("code")));
+                    one.put("msg", r.get("msg"));
+                    one.put("couponCount", coupon.getCouponCount());
+                    if (r.get("opLogId") != null) {
+                        one.put("opLogId", r.get("opLogId"));
+                    }
+                } else if (rewardType != null && rewardType == 6L) {
+                    // 金额红包:微信商家转账,App 用返回参数确认收款
+                    R r = claimSignCashRed(liveId, userId, item, sign);
+                    one.put("success", Integer.valueOf(200).equals(r.get("code")));
+                    one.put("msg", r.get("msg"));
+                    // App 调起微信确认收款所需字段
+                    if (Integer.valueOf(200).equals(r.get("code"))) {
+                        one.put("needWxConfirm", true);
+                        one.put("package", r.get("package"));
+                        one.put("mchId", r.get("mchId"));
+                        one.put("appId", r.get("appId"));
+                        one.put("orderCode", r.get("orderCode"));
+                        one.put("amount", item.getBigDecimal("amount"));
+                        if (opLogId != null) {
+                            liveConsoleOpLogService.bindOpLogUser(opLogId, liveId, userId);
+                        }
+                    }
+                } else {
+                    one.put("success", false);
+                    one.put("msg", "不支持的奖励类型");
+                }
+            } catch (Exception e) {
+                log.error("签到领取奖励失败, liveId={}, userId={}, rewardType={}", liveId, userId, rewardType, e);
+                one.put("success", false);
+                one.put("msg", "领取失败");
+            }
+            claimResults.add(one);
+        }
+
+        boolean anySuccess = claimResults.stream().anyMatch(m -> Boolean.TRUE.equals(m.get("success")));
+        return R.ok(anySuccess ? "签到成功" : "签到成功,但奖励领取失败")
+                .put("signNo", signNo)
+                .put("rewards", claimResults);
+    }
+
+    /**
+     * 签到金额红包:发起微信商家转账(金额精确到 0.1 元)。
+     * 成功时返回 package/mchId/appId/orderCode,供 App 跳转微信确认收款。
+     */
+    private R claimSignCashRed(Long liveId, Long userId, com.alibaba.fastjson.JSONObject item, SignPO sign) {
+        java.math.BigDecimal amount = item.getBigDecimal("amount");
+        if (amount == null || amount.compareTo(java.math.BigDecimal.ZERO) <= 0) {
+            return R.error("金额红包配置无效");
+        }
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            return R.error("直播间不存在");
+        }
+        com.fs.his.domain.FsUser user = fsUserService.selectFsUserById(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+        if (live.getCompanyId() == null) {
+            return R.error("直播间未绑定公司,无法发放金额红包");
+        }
+        String openId = null;
+        if (sign != null && sign.getSource() != null && sign.getSource() == 2
+                && StringUtils.isNotEmpty(user.getCourseMaOpenId())) {
+            openId = user.getCourseMaOpenId();
+        } else if (StringUtils.isNotEmpty(user.getMpOpenId())) {
+            openId = user.getMpOpenId();
+        } else if (StringUtils.isNotEmpty(user.getCourseMaOpenId())) {
+            openId = user.getCourseMaOpenId();
+        }
+        if (StringUtils.isEmpty(openId)) {
+            return R.error("请使用微信登录后再领取金额红包");
+        }
+        com.fs.his.param.WxSendRedPacketParam packetParam = new com.fs.his.param.WxSendRedPacketParam();
+        packetParam.setOpenId(openId);
+        packetParam.setAmount(amount.setScale(1, java.math.RoundingMode.HALF_UP));
+        packetParam.setCompanyId(live.getCompanyId());
+        packetParam.setSource(sign != null && sign.getSource() != null ? sign.getSource() : 1);
+        packetParam.setAppId(sign != null ? sign.getAppId() : null);
+        packetParam.setRedPacketMode(1);
+        packetParam.setUser(user);
+        try {
+            R sendResult = paymentService.sendRedPacket(packetParam);
+            if (!Integer.valueOf(200).equals(sendResult.get("code"))) {
+                return sendResult;
+            }
+            // 写入直播奖励流水(现金,来源类型 7=签到)
+            try {
+                LiveRewardRecord rewardRecord = new LiveRewardRecord();
+                rewardRecord.setLiveId(liveId);
+                rewardRecord.setUserId(userId);
+                rewardRecord.setRewardType(1L);
+                rewardRecord.setNum(amount);
+                rewardRecord.setIncomeType(1L);
+                rewardRecord.setSourceType(7L);
+                rewardRecord.setSourceId(live.getCompanyId() != null ? live.getCompanyId() : 0L);
+                rewardRecord.setCreateBy(String.valueOf(userId));
+                liveRewardRecordService.insertLiveRewardRecord(rewardRecord);
+            } catch (Exception e) {
+                log.warn("签到金额红包流水写入失败, liveId={}, userId={}", liveId, userId, e);
+            }
+            return sendResult;
+        } catch (Exception e) {
+            log.error("签到金额红包发放失败, liveId={}, userId={}, amount={}", liveId, userId, amount, e);
+            return R.error("金额红包发放失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 解析本次签到可领取奖励:优先读触发后 Redis 缓存(含 opLogId),否则回落已执行签到任务 content.rewards。
+     */
+    private List<com.alibaba.fastjson.JSONObject> resolveSignRewards(Long liveId, String signNo, Long taskId) {
+        List<com.alibaba.fastjson.JSONObject> rewards = new ArrayList<>();
+        try {
+            Object cache = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_SIGN_OP_LOGS, liveId, signNo));
+            if (cache != null) {
+                com.alibaba.fastjson.JSONObject data = JSON.parseObject(String.valueOf(cache));
+                if (data != null && data.getJSONArray("rewards") != null) {
+                    com.alibaba.fastjson.JSONArray arr = data.getJSONArray("rewards");
+                    for (int i = 0; i < arr.size(); i++) {
+                        rewards.add(arr.getJSONObject(i));
+                    }
+                    if (!rewards.isEmpty()) {
+                        return rewards;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("读取签到奖励缓存失败, liveId={}, signNo={}", liveId, signNo, e);
+        }
+
+        LiveAutoTask task = null;
+        if (taskId != null) {
+            task = liveAutoTaskService.selectLiveAutoTaskById(taskId);
+        }
+        if (task == null) {
+            LiveAutoTask query = new LiveAutoTask();
+            query.setLiveId(liveId);
+            query.setTaskType(7L);
+            query.setFinishStatus(1L);
+            List<LiveAutoTask> tasks = liveAutoTaskService.selectLiveAutoTaskList(query);
+            if (CollUtil.isNotEmpty(tasks)) {
+                for (LiveAutoTask t : tasks) {
+                    if (t.getContent() == null) {
+                        continue;
+                    }
+                    try {
+                        com.alibaba.fastjson.JSONObject c = JSON.parseObject(t.getContent());
+                        if (c != null && signNo.equals(String.valueOf(c.get("signNo")))) {
+                            task = t;
+                            break;
+                        }
+                    } catch (Exception ignored) {
+                    }
+                }
+            }
+        }
+        if (task == null || task.getContent() == null) {
+            return rewards;
+        }
+        try {
+            com.alibaba.fastjson.JSONObject content = JSON.parseObject(task.getContent());
+            com.alibaba.fastjson.JSONArray arr = content.getJSONArray("rewards");
+            if (arr != null) {
+                for (int i = 0; i < arr.size(); i++) {
+                    rewards.add(arr.getJSONObject(i));
+                }
+            } else if (content.getLong("rewardType") != null) {
+                com.alibaba.fastjson.JSONObject single = new com.alibaba.fastjson.JSONObject();
+                single.put("rewardType", content.getLong("rewardType"));
+                single.put("rewardId", content.getLong("rewardId"));
+                single.put("reward", content.get("reward"));
+                rewards.add(single);
+            }
+        } catch (Exception e) {
+            log.error("解析签到任务奖励失败, taskId={}", task.getId(), e);
+        }
+        return rewards;
+    }
+
     private Long resolveLiveDuration(Long liveId) {
     private Long resolveLiveDuration(Long liveId) {
         if (liveId == null) {
         if (liveId == null) {
             return 0L;
             return 0L;