Jelajahi Sumber

app-直播发红包

三七 1 Minggu lalu
induk
melakukan
ddd129e8a3
33 mengubah file dengan 1574 tambahan dan 43 penghapusan
  1. 14 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  2. 8 5
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  3. 7 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  4. 15 1
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  5. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  6. 1 0
      fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java
  7. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  8. 8 0
      fs-service/src/main/java/com/fs/his/config/AppRedPacketConfig.java
  9. 4 1
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java
  10. 2 1
      fs-service/src/main/java/com/fs/his/enums/FsUserOperationEnum.java
  11. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  12. 89 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  13. 4 3
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  14. 9 1
      fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java
  15. 77 0
      fs-service/src/main/java/com/fs/live/domain/LiveRedPacketLog.java
  16. 11 2
      fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
  17. 11 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java
  18. 71 0
      fs-service/src/main/java/com/fs/live/mapper/LiveRedPacketLogMapper.java
  19. 14 1
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  20. 33 0
      fs-service/src/main/java/com/fs/live/param/LiveRedPacketParam.java
  21. 68 0
      fs-service/src/main/java/com/fs/live/service/ILiveRedPacketLogService.java
  22. 1 1
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  23. 4 0
      fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  24. 3 5
      fs-service/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java
  25. 687 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveRedPacketLogServiceImpl.java
  26. 18 4
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  27. 143 10
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  28. 28 0
      fs-service/src/main/java/com/fs/live/vo/LiveWatchUserStatusVO.java
  29. 128 0
      fs-service/src/main/resources/mapper/live/LiveRedPacketLogMapper.xml
  30. 15 5
      fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
  31. 6 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseTransferController.java
  32. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedController.java
  33. 72 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedPacketLogController.java

+ 14 - 2
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -14,6 +14,8 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyConfigService;
+import com.fs.company.vo.CompanyMiniAppVO;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.his.domain.FsPayConfig;
@@ -62,6 +64,9 @@ public class LiveController extends BaseController
     @Autowired
     private ILiveCompanyCodeService liveCompanyCodeService;
 
+    @Autowired
+    private ICompanyConfigService companyConfigService;
+
     /**
      * 查询未结束直播间
      */
@@ -418,9 +423,16 @@ public class LiveController extends BaseController
     @ApiOperation("创建App跳转通用链接")
     @GetMapping("/createAppLink")
     @PreAuthorize("@ss.hasPermi('live:live:createAppLink')")
-    public R createAppLink(@RequestParam("liveId") Long liveId,@RequestParam("corpId")String corpId) {
+    public R createAppLink(@RequestParam("liveId") Long liveId,
+                           @RequestParam("corpId")String corpId,
+                           @RequestParam("appName")String appName) {
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
-        return liveService.createAppLink(user,liveId,corpId);
+        return liveService.createAppLink(user,liveId,corpId,appName);
+    }
+
+    @GetMapping("/getAppAllList")
+    public R getAppAllList(){
+        return companyConfigService.getCompanyMiniAppAllList();
     }
 
 }

+ 8 - 5
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -654,7 +654,7 @@ public class Task {
             }
 
             long currentTimeMillis = System.currentTimeMillis();
-            LocalDateTime now = LocalDateTime.now();
+
             List<Long> processedLiveIds = new ArrayList<>();
             Date nowDate = new Date();
             for (String key : keys) {
@@ -672,7 +672,7 @@ public class Task {
                     Long videoDuration = tagMarkInfo.getLong("videoDuration");
 
                     if (liveId == null || startTimeMillis == null || videoDuration == null || videoDuration <= 0) {
-                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}",
+                        log.info("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}",
                                 key, liveId, startTimeMillis, videoDuration);
                         continue;
                     }
@@ -680,12 +680,13 @@ public class Task {
                     // 查询直播间信息
                     Live live = liveService.selectLiveDbByLiveId(liveId);
                     if (live == null || live.getStartTime() == null) {
+                        log.info("没查到直播间-{}",liveId);
                         continue;
                     }
                     // 计算结束时间:开始时间 + 视频时长(秒转毫秒)
                     long endTimeMillis = startTimeMillis + (videoDuration * 1000);
 
-                    // 如果当前时间已经超过了结束时间,执行打标签操作
+                    // 如果当前时间已经超过了结束时间,执行打标签操作(约等于直播完成)
                     if (currentTimeMillis >= endTimeMillis) {
                         // 查询当前直播间的在线用户(liveFlag = 1, replayFlag = 0)
                         LiveWatchUser queryUser = new LiveWatchUser();
@@ -736,12 +737,14 @@ public class Task {
                                 }
                                 long totalOnlineSeconds = historyOnlineSeconds + currentWatchDuration;
 
-                                // 更新直播用户的在线时长
+                                log.info("更新直播用户的在线时长 用户直播离线 录播在线");
+                                // 更新直播用户的在线时长 用户直播离线 录播在线
                                 liveUser.setOnlineSeconds(totalOnlineSeconds);
                                 liveUser.setUpdateTime(nowDate);
                                 liveUser.setOnline(1);
                                 updateLiveUsers.add(liveUser);
 
+                                log.info(" 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始");
                                 // 2. 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始
                                 LiveWatchUser replayUser = new LiveWatchUser();
                                 replayUser.setLiveId(liveUser.getLiveId());
@@ -757,7 +760,7 @@ public class Task {
                                 replayUser.setCreateTime(nowDate);
                                 replayUser.setUpdateTime(nowDate);
                                 replayUsers.add(replayUser);
-                                redisCache.setCacheObject(entryTimeKey,now);
+                                redisCache.setCacheObject(entryTimeKey,currentTimeMillis);
                             }
 
                             // 批量更新直播用户的在线时长

+ 7 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -136,6 +136,8 @@ public class WebSocketServer {
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
             log.warn("WebSocket连接拒绝: 直播间不存在, liveId={}, userId={}", liveId, userId);
+            heartbeatCache.remove(session.getId());
+            sessionLocks.remove(session.getId());
             closeSession(session, 4004, "未找到直播间");
             return;
         }
@@ -171,6 +173,8 @@ public class WebSocketServer {
             }
             if (Objects.isNull(fsUser)) {
                 log.warn("WebSocket连接拒绝: 用户信息错误, liveId={}, userId={}", liveId, userId);
+                heartbeatCache.remove(session.getId());
+                sessionLocks.remove(session.getId());
                 closeSession(session, 4003, "用户信息错误");
                 return;
             }
@@ -364,6 +368,9 @@ public class WebSocketServer {
         if (userType == 0) {
             if (room == null || !room.containsKey(userId)) {
                 log.info("连接未成功建立,跳过清理: liveId={}, userId={}", liveId, userId);
+                heartbeatCache.remove(session.getId());
+                sessionLocks.remove(session.getId());
+                cleanupEmptyRoom(liveId);
                 return;
             }
             String userCacheKey = "fs:user:" + userId;

+ 15 - 1
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -11,6 +11,8 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.param.VcCompanyUser;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
@@ -62,6 +64,7 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 import com.fs.app.task.qwTask;
 
+import java.math.BigDecimal;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -202,11 +205,22 @@ public class CommonController {
     @Autowired
     private TtsServiceImpl ttsServiceImpl;
 
+    @Autowired
+    private CompanyMapper companyMapper;
+
+
+    @GetMapping("/test")
+    public void test(){
+        Company company = companyMapper.selectCompanyById(321L);
+        BigDecimal money = company.getMoney();
+        if (money.compareTo(BigDecimal.ZERO) <= 0) {
+        }
+    }
 
-    @GetMapping("/resetQwContactWayUserLimit")
     /**
      * 定时 将当天 生成的语音文本 转为 声音
      */
+    @GetMapping("/resetQwContactWayUserLimit")
     public void convertSopVoiceEveryDay() {
 
         try {

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java

@@ -91,4 +91,18 @@ public interface CompanyConfigMapper
     List<CompanyMiniAppVO> getCompanyMiniAppList(@Param("companyId") Long companyId);
 
     List<CompanyConfig> selectListByKey(String configKey);
+
+
+    @Select("select \n" +
+            "id,\n" +
+            "name,\n" +
+            "appid\n" +
+            "from\n" +
+            "fs_course_play_source_config\n" +
+            "where \n" +
+            "is_del = 0 \n" +
+            "and status = 0" +
+            "and name like '%app%' ")
+    List<CompanyMiniAppVO> getCompanyMiniAppAllList();
+
 }

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java

@@ -80,6 +80,7 @@ public interface ICompanyConfigService
      * @return
      */
     List<CompanyMiniAppVO> getCompanyMiniAppList(Long companyId);
+    R getCompanyMiniAppAllList();
 
     SaveCompanyMiniAppParam getCurrentCompanyMiniApp(Long companyId);
     /**

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java

@@ -216,6 +216,11 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
         return companyConfigMapper.getCompanyMiniAppList(companyId);
     }
 
+    @Override
+    public R getCompanyMiniAppAllList() {
+        return  R.ok().put("data",companyConfigMapper.getCompanyMiniAppAllList());
+    }
+
     //主要小程序
     Integer type_main = 0;
     //备用小程序

+ 8 - 0
fs-service/src/main/java/com/fs/his/config/AppRedPacketConfig.java

@@ -45,5 +45,13 @@ public class AppRedPacketConfig {
      */
     private String publicKeyPath;
 
+    /**
+    * 普通的回调地址
+    */
     private String notifyUrl;
+
+    /**
+    * 直播的回调地址
+    */
+    private String LiveNotifyUrl;
 }

+ 4 - 1
fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java

@@ -72,7 +72,8 @@ public class FsIntegralOrder
     private Integer isPay;
 
     /** 支付时间 */
-    @Excel(name = "支付时间")
+    @Excel(name = "支付时间",width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime payTime;
 
     /** 支付类型 1积分 2现金 3积分+现金 */
@@ -124,7 +125,9 @@ public class FsIntegralOrder
 
     private String remark;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
+
     @TableField(exist = false)
     private Date updateTime;
     @TableField(exist = false)

+ 2 - 1
fs-service/src/main/java/com/fs/his/enums/FsUserOperationEnum.java

@@ -10,7 +10,8 @@ public enum FsUserOperationEnum {
     STUDY("学习课程",5),
     ANSWER("答题",6),
     SENDREWARD("发送奖励",7),
-    USERIP("获取用户IP",7);
+    USERIP("获取用户IP",8),
+    SENDLIVEREWARD("发送直播红包奖励",9);
 
     private final String label;
     private final Integer value;

+ 3 - 0
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -16,6 +16,7 @@ import com.fs.his.vo.FsStorePaymentExcelVO;
 import com.fs.his.vo.FsStorePaymentVO;
 import com.fs.hisStore.param.FsStorePaymentGetWxaCodeParam;
 import com.fs.hisStore.param.FsStorePaymentPayParam;
+import com.fs.live.domain.LiveWatchConfig;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -119,6 +120,7 @@ public interface IFsStorePaymentService
     String v3TransferNotify(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
+    String v3TransferNotifyAppLive(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
     String v3TransferNotifyWithCompanyIdApp(Long companyId,String notifyData, HttpServletRequest request);
@@ -147,4 +149,5 @@ public interface IFsStorePaymentService
     List<FsStorePayment> selectAllPayment();
 
     R sendAppRedPacket(WxSendRedPacketParam packetParam,CourseConfig config);
+    R sendAppLiveRedPacket(WxSendRedPacketParam packetParam);
 }

+ 89 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -79,6 +79,9 @@ import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayRefundRequest;
 import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
 import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.domain.LiveWatchConfig;
+import com.fs.live.service.ILiveRedPacketLogService;
+import com.fs.live.service.impl.LiveRedPacketLogServiceImpl;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.oss.CloudStorageService;
@@ -177,6 +180,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     FsExportTaskMapper fsExportTaskMapper;
     @Autowired
     private IFsCourseRedPacketLogService redPacketLogService;
+
+    @Autowired
+    private ILiveRedPacketLogService liveRedPacketLogService;
+
     @Autowired
     private CompanyConfigMapper companyConfigMapper;
     @Autowired
@@ -1097,6 +1104,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
     }
 
+
     /**
      * 分公司配置回调地址
      * @param companyId
@@ -1304,6 +1312,13 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 //        }
     }
 
+    @Override
+    public String v3TransferNotifyAppLive(String notifyData, HttpServletRequest request) {
+        logger.info("【app直播-收到转账回调V3】:{}",notifyData);
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        return handleTransferV3NotifyLive(json,notifyData,request);
+    }
+
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);
@@ -1400,6 +1415,51 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
     }
 
+
+    private String handleTransferV3NotifyLive(String json, String notifyData, HttpServletRequest request) {
+        logger.info("【收到直播转账回调V3】:{}", notifyData);
+        try {
+
+            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+
+            //创建微信订单
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config, payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+
+            SignatureHeader signatureHeader = new SignatureHeader();
+            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData, signatureHeader);
+            logger.info("直播到零钱回调1:{}", result.getResult());
+
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = liveRedPacketLogService.syncLiveRedPacket(
+                        result.getResult().getOutBillNo(),
+                        result.getResult().getTransferBillNo()
+                );
+                logger.info("直播红包result:{}", r);
+
+                if (r.get("code").equals(200)) {
+                    return WxPayNotifyResponse.success("处理成功");
+                } else {
+                    return WxPayNotifyResponse.fail("");
+                }
+            } else {
+                return WxPayNotifyResponse.fail("");
+            }
+        } catch (WxPayException e) {
+            e.printStackTrace();
+            logger.error("【直播红包转账回调异常】:{}", e.getReturnMsg());
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
+
     @Override
     @Transactional
     public R sendRedPacketTest(WxSendRedPacketParam param) {
@@ -2170,6 +2230,35 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return result;
     }
 
+    /**
+    * 直播 发红包
+    */
+    @Override
+    @Transactional
+    public R sendAppLiveRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+
+        // 走app提现配置的 商户
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+
+        AppRedPacketConfig appRedConfig = JSONUtil.toBean(json, AppRedPacketConfig.class);
+
+        //直播 红包回调改一下 回调地址
+        appRedConfig.setNotifyUrl(appRedConfig.getLiveNotifyUrl());
+
+        if (appRedConfig.getIsNew() != null && appRedConfig.getIsNew() == 1) {
+            result = sendRedPacketV3(param, appRedConfig);
+        } else {
+            result= sendRedPacketLegacy(param, appRedConfig);
+        }
+
+        result.put("mchId", appRedConfig.getMchId());
+        result.put("isNew",appRedConfig.getIsNew());
+        logger.info("App直播提现返回:{}",result);
+        return result;
+    }
+
     // 内部方法:处理新版本的发红包逻辑
     private R sendRedPacketV3(WxSendRedPacketParam param,AppRedPacketConfig config) {
 

+ 4 - 3
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -1139,9 +1139,10 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
         String type = null;
         FsPayConfig payConfig = new FsPayConfig();
         if (PaymentMethodEnum.WX_APP == payOrderParam.getPaymentMethod()) {
-            String json = configService.selectConfigByKey("app.config");
-            AppConfig config = JSONUtil.toBean(json, AppConfig.class);
-            payOrderParam.setAppId(config.getAppId());
+//            String json = configService.selectConfigByKey("app.config");
+//            AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+//            payOrderParam.setAppId(config.getAppId());
+            //注释了 改为由前端传
             type = "wxApp";
         }
         //支付宝可以不需要appid(在没有appid的情况下)【ps:小程序的支付宝没传appid 就G】

+ 9 - 1
fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.exception.CustomException;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
@@ -113,7 +114,14 @@ public class HuiFuServiceImpl implements HuiFuService {
             request.setGoodsDesc(order.getGoodsDesc());
             extendInfoMap.put("fq_mer_discount_flag", "N");
             logger.info("汇付回调地址=================:"+config.getHfPayNotifyUrl());
-            extendInfoMap.put("notify_url", config.getHfPayNotifyUrl());
+            //临时用的汇付大额退款回调地址,互医app里 用直播支付(直播是商城)[先只管鸿森堂]
+            if ("直播订单支付".equals(order.getGoodsDesc()) && CloudHostUtils.hasCloudHostName("鸿森堂")){
+
+                extendInfoMap.put("notify_url", config.getHfOnlineRefundNotifyUrl());
+            }else {
+                extendInfoMap.put("notify_url", config.getHfPayNotifyUrl());
+            }
+
             extendInfoMap.put("remark", "string");
             request.setExtendInfo(extendInfoMap);
             logger.info("汇付传参:"+request);

+ 77 - 0
fs-service/src/main/java/com/fs/live/domain/LiveRedPacketLog.java

@@ -0,0 +1,77 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 直播红包 记录对象 live_red_packet_log
+ *
+ * @author fs
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveRedPacketLog extends BaseEntity{
+
+    /** 日志id */
+    private Long logId;
+
+    /** 批次单号 */
+    @Excel(name = "批次单号")
+    private String outBatchNo;
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 公司员工id */
+    @Excel(name = "公司员工id")
+    private Long companyUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 转载金额 */
+    @Excel(name = "转载金额")
+    private BigDecimal amount;
+
+    /** 状态 0 发送中  1  已发送  2余额不足待发送 */
+    @Excel(name = "状态 0 发送中  1  已发送  2余额不足待发送")
+    private Long status;
+
+    /** 观看记录id live_watch_log */
+    @Excel(name = "观看记录id live_watch_log")
+    private Long watchLogId;
+
+    /** 微信返回结果 */
+    @Excel(name = "微信返回结果")
+    private String result;
+
+    /** 微信批次ID */
+    @Excel(name = "微信批次ID")
+    private String batchId;
+
+    /** $column.columnComment */
+    @Excel(name = "微信批次ID")
+    private String appId;
+
+    /** 商户ID */
+    @Excel(name = "商户ID")
+    private String mchId;
+
+    /** 直播方式 1 app 2浏览器(可能有) */
+    @Excel(name = "直播方式 1 app 2浏览器(可能有)")
+    private Long wathcType;
+
+
+}

+ 11 - 2
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -26,8 +26,8 @@ public class LiveWatchConfig extends BaseEntity{
 
     private Boolean enabled;
 
-    /** 参与条件 1达到指定观看时长 */
-    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分")
+    /** 参与条件 参与条件 1达到指定观看时长 2观看比例达到指定积分 3 直播完课奖励红包录播积分 */
+    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分 3 直播完课奖励红包录播积分")
     private Long participateCondition;
 
     /** 观看时长 */
@@ -37,6 +37,10 @@ public class LiveWatchConfig extends BaseEntity{
     /** 实施动作 1现金红包 2积分红包 */
     @Excel(name = "实施动作 1现金红包 2积分红包")
     private Long action;
+    /**
+    * 完课要求百分比 观看时长占直播总时长的比例
+    */
+    private Long completionRate;
 
     /** 领取提示语 */
     @Excel(name = "领取提示语")
@@ -46,6 +50,11 @@ public class LiveWatchConfig extends BaseEntity{
     @Excel(name = "红包发放方式 1固定金额 2随机金额")
     private Long redPacketType;
 
+    /**
+     * 是否红包余额抵扣 1是 0否
+     */
+    private String isRedPackageBalanceDeduction;
+
     /** 红包金额 */
     @Excel(name = "红包金额")
     private BigDecimal redPacketAmount;

+ 11 - 0
fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java

@@ -6,6 +6,8 @@ import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.time.LocalDateTime;
+
 /**
  * 直播间观看用户对象 live_watch_user
  *
@@ -66,4 +68,13 @@ public class LiveWatchUser extends BaseEntity {
     private Integer pageNum;
     private Integer pageSize;
 
+    /**
+    * 归属发送方式 1 app 2 浏览器(暂无)
+    */
+    private Integer sendType;
+    /**
+    * 奖励类型 1红包 2积分
+    */
+    private Integer rewardType;
+
 }

+ 71 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveRedPacketLogMapper.java

@@ -0,0 +1,71 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsCourseRedPacketLog;
+import com.fs.live.domain.LiveRedPacketLog;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 直播红包 记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-08
+ */
+public interface LiveRedPacketLogMapper extends BaseMapper<LiveRedPacketLog>{
+    /**
+     * 查询直播红包 记录
+     *
+     * @param logId 直播红包 记录主键
+     * @return 直播红包 记录
+     */
+    LiveRedPacketLog selectLiveRedPacketLogByLogId(Long logId);
+
+
+    /**
+     * 查询直播红包 记录列表
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 直播红包 记录集合
+     */
+    List<LiveRedPacketLog> selectLiveRedPacketLogList(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 新增直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    int insertLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 修改直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    int updateLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 删除直播红包 记录
+     *
+     * @param logId 直播红包 记录主键
+     * @return 结果
+     */
+    int deleteLiveRedPacketLogByLogId(Long logId);
+
+    /**
+     * 批量删除直播红包 记录
+     *
+     * @param logIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteLiveRedPacketLogByLogIds(Long[] logIds);
+
+    @Select("select * from live_red_packet_log where live_id = #{liveId} and user_id = #{userId} order by log_id desc limit 1")
+    LiveRedPacketLog selectLiveRedPacketLogByTemporary(@Param("liveId") Long liveId, @Param("userId")Long userId);
+
+    @Select("select * from live_red_packet_log where out_batch_no = #{outBatchNo}")
+    LiveRedPacketLog selectLiveRedPacketLogByBatchNo(@Param("outBatchNo") String outBatchNo);
+}

+ 14 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java

@@ -147,10 +147,11 @@ public interface LiveWatchUserMapper {
     @Select("select * from live_watch_user where live_id = #{liveId}")
     List<LiveWatchUser> selectLiveWatchUserListByLiveId(@Param("liveId") Long liveId);
 
+    @DataSource(DataSourceType.SLAVE)
     @Select("select lufe.company_id,lufe.company_user_id,lwu.* from live_watch_user lwu" +
             " left join live_user_first_entry lufe on lwu.live_id = lufe.live_id and lwu.user_id = lufe.user_id" +
             " where lwu.live_id = #{liveId} and lwu.user_id = #{userId} and lwu.live_flag = #{liveFlag} and lwu.replay_flag = #{replayFlag} limit 1 ")
-    @DataSource(DataSourceType.SLAVE)
+
     LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(@Param("liveId") Long liveId,@Param("userId") Long userId,@Param("liveFlag") Integer liveFlag,@Param("replayFlag") Integer replayFlag);
 
     /**
@@ -166,4 +167,16 @@ public interface LiveWatchUserMapper {
      * @return 插入的记录数
      */
     int batchInsertLiveWatchUser(@Param("list") List<LiveWatchUser> liveWatchUsers);
+
+    /**
+    * 查直播
+    */
+    @Select("select * from live_watch_user where live_id = #{liveId} and user_id = #{userId} and live_flag = 1")
+    LiveWatchUser getWatchLiveByLiveFlag(@Param("userId") Long userId, @Param("liveId") Long liveId);
+
+    /**
+     * 查录播
+     */
+    @Select("select * from live_watch_user where live_id = #{liveId} and user_id = #{userId} and replay_flag = 1")
+    LiveWatchUser getWatchLiveByReplayFlag(@Param("userId") Long userId, @Param("liveId") Long liveId);
 }

+ 33 - 0
fs-service/src/main/java/com/fs/live/param/LiveRedPacketParam.java

@@ -0,0 +1,33 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class LiveRedPacketParam {
+    private Long userId;
+
+    @NotNull(message = "直播间Id不能为空")
+    private Long liveId;//直播间Id
+
+    @NotNull(message = "客服参数不能为空")
+    private Long companyUserId;
+
+    @NotNull(message = "经销商参数不能为空")
+    private Long companyId;
+
+    private Integer source=1;//来源 1 app 2 浏览器
+
+    private Integer sendType;
+
+    @NotBlank(message = "小程序参数不能为空")
+    private String appId; //前端传来的小程序的appid
+
+    private Integer rewardType; //奖励类型 1红包 2积分
+
+    private String code;
+
+    private Long watchLogId;
+}

+ 68 - 0
fs-service/src/main/java/com/fs/live/service/ILiveRedPacketLogService.java

@@ -0,0 +1,68 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.course.param.FsCourseSendRewardUParam;
+import com.fs.live.domain.LiveRedPacketLog;
+import com.fs.live.param.LiveRedPacketParam;
+
+/**
+ * 直播红包 记录Service接口
+ *
+ * @author fs
+ * @date 2026-06-08
+ */
+public interface ILiveRedPacketLogService extends IService<LiveRedPacketLog>{
+    /**
+     * 查询直播红包 记录
+     *
+     * @param logId 直播红包 记录主键
+     * @return 直播红包 记录
+     */
+    LiveRedPacketLog selectLiveRedPacketLogByLogId(Long logId);
+
+    /**
+     * 查询直播红包 记录列表
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 直播红包 记录集合
+     */
+    List<LiveRedPacketLog> selectLiveRedPacketLogList(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 新增直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    int insertLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 修改直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    int updateLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog);
+
+    /**
+     * 批量删除直播红包 记录
+     *
+     * @param logIds 需要删除的直播红包 记录主键集合
+     * @return 结果
+     */
+    int deleteLiveRedPacketLogByLogIds(Long[] logIds);
+
+    /**
+     * 删除直播红包 记录信息
+     *
+     * @param logId 直播红包 记录主键
+     * @return 结果
+     */
+    int deleteLiveRedPacketLogByLogId(Long logId);
+
+    R sendLiveReward(LiveRedPacketParam param);
+
+    R syncLiveRedPacket(String outBatchNo, String batchId);
+}

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

@@ -230,7 +230,7 @@ public interface ILiveService
 
     List<Live> selectLiveListNew(Live live);
 
-    R createAppLink(CompanyUser user, Long liveId, String corpId);
+    R createAppLink(CompanyUser user, Long liveId, String corpId,String appName);
 
     List<Live> listToLiveNoEndNew(Live live);
 

+ 4 - 0
fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java

@@ -84,6 +84,8 @@ public interface ILiveWatchUserService {
     LiveWatchUser joinWithoutLocation(FsUserScrm fsUser,long liveId, long userId);
     LiveWatchUser close(FsUserScrm fsUser,long liveId, long userId);
 
+    R sendLiveRewardClone(long liveId, long userId);
+
     /**
      * 查询直播间在线用户列表
      * @param params 参数
@@ -179,4 +181,6 @@ public interface ILiveWatchUserService {
     void clearLiveFlagCache(Long liveId);
 
     List<LiveWatchUser> selectAllWatchUser(LiveWatchUser queryUser);
+
+    R getLiveStatusByUserID(Long liveId,Long userId);
 }

+ 3 - 5
fs-service/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java

@@ -29,15 +29,13 @@ import java.util.concurrent.TimeUnit;
 public class LiveMsgServiceImpl implements ILiveMsgService
 {
     private static final Logger log = LoggerFactory.getLogger(LiveMsgServiceImpl.class);
-    
+
     @Autowired
     private LiveMsgMapper liveMsgMapper;
-    @Autowired
-    private LiveDataServiceImpl liveDataService;
-    
+
     @Autowired(required = false)
     private RedisTemplate<String, Object> redisTemplate;
-    
+
     /** Redis锁前缀 */
     private static final String LOCK_PREFIX = "live:msg:export:lock:";
     /** 锁过期时间(秒) */

+ 687 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveRedPacketLogServiceImpl.java

@@ -0,0 +1,687 @@
+package com.fs.live.service.impl;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.fs.common.constant.FsConstants;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyRedPacketBalanceLogs;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyRedPacketBalanceLogsMapper;
+import com.fs.company.service.ICompanyService;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.mapper.BalanceRollbackErrorMapper;
+import com.fs.course.param.FsCourseSendRewardUParam;
+import com.fs.course.service.impl.FsUserCourseVideoServiceImpl;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.domain.FsUserWx;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.param.WxSendRedPacketParam;
+import com.fs.his.service.IFsStorePaymentService;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveWatchConfig;
+import com.fs.live.domain.LiveWatchUser;
+import com.fs.live.mapper.LiveWatchUserMapper;
+import com.fs.live.param.LiveRedPacketParam;
+import com.fs.live.mapper.LiveMapper;
+import com.fs.live.service.ILiveMsgService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveWatchUserService;
+import com.fs.voice.utils.StringUtil;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import com.fs.live.mapper.LiveRedPacketLogMapper;
+import com.fs.live.domain.LiveRedPacketLog;
+import com.fs.live.service.ILiveRedPacketLogService;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+/**
+ * 直播红包 记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class LiveRedPacketLogServiceImpl extends ServiceImpl<LiveRedPacketLogMapper, LiveRedPacketLog> implements ILiveRedPacketLogService {
+
+    private static final Logger logger = LoggerFactory.getLogger(LiveRedPacketLogServiceImpl.class);
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private LiveWatchUserMapper watchUserMapper;
+
+    @Autowired
+    @Lazy
+    private ILiveService iLiveService;
+
+    @Autowired
+    private LiveRedPacketLogMapper redPacketLogMapper;
+
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    private IFsStorePaymentService paymentService;
+
+    @Autowired
+    private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
+
+    @Autowired
+    ICompanyService companyService;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
+    @Autowired
+    private CompanyRedPacketBalanceLogsMapper companyRedPacketBalanceLogsMapper;
+
+    @Autowired
+    @Lazy
+    private ILiveWatchUserService liveWatchUserService;
+
+    /**
+     * 查询直播红包 记录
+     *
+     * @param logId 直播红包 记录主键
+     * @return 直播红包 记录
+     */
+    @Override
+    public LiveRedPacketLog selectLiveRedPacketLogByLogId(Long logId)
+    {
+        return baseMapper.selectLiveRedPacketLogByLogId(logId);
+    }
+
+    /**
+     * 查询直播红包 记录列表
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 直播红包 记录
+     */
+    @Override
+    public List<LiveRedPacketLog> selectLiveRedPacketLogList(LiveRedPacketLog liveRedPacketLog)
+    {
+        return baseMapper.selectLiveRedPacketLogList(liveRedPacketLog);
+    }
+
+    /**
+     * 新增直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    @Override
+    public int insertLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog)
+    {
+        liveRedPacketLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertLiveRedPacketLog(liveRedPacketLog);
+    }
+
+    /**
+     * 修改直播红包 记录
+     *
+     * @param liveRedPacketLog 直播红包 记录
+     * @return 结果
+     */
+    @Override
+    public int updateLiveRedPacketLog(LiveRedPacketLog liveRedPacketLog)
+    {
+        liveRedPacketLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateLiveRedPacketLog(liveRedPacketLog);
+    }
+
+    /**
+     * 批量删除直播红包 记录
+     *
+     * @param logIds 需要删除的直播红包 记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveRedPacketLogByLogIds(Long[] logIds)
+    {
+        return baseMapper.deleteLiveRedPacketLogByLogIds(logIds);
+    }
+
+    /**
+     * 删除直播红包 记录信息
+     *
+     * @param logId 直播红包 记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveRedPacketLogByLogId(Long logId)
+    {
+        return baseMapper.deleteLiveRedPacketLogByLogId(logId);
+    }
+
+    @Override
+    @Transactional
+    public R sendLiveReward(LiveRedPacketParam param) {
+
+        // 生成锁的key,基于用户ID和直播ID确保同一用户同一直播的请求被锁定
+        String lockKey = "liveReward_lock:user:" + param.getUserId() + ":live:" + param.getLiveId();
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            // 尝试获取锁,等待时间5秒,锁过期时间120秒
+            boolean isLocked = lock.tryLock(5, 120, TimeUnit.SECONDS);
+            if (!isLocked) {
+                logger.warn("直播获取锁失败,用户ID:{},直播ID:{}", param.getUserId(), param.getLiveId());
+                return R.error("操作频繁,请稍后再试!");
+            }
+
+            // 从缓存里面更新一下时长
+            R cloneResult  = liveWatchUserService.sendLiveRewardClone(param.getLiveId(), param.getUserId());
+
+            if (cloneResult.get("code") == null || !Integer.valueOf(200).equals(cloneResult.get("code"))){
+                return cloneResult.get("msg") != null ? cloneResult : R.error("直播观看校验时长失败");
+            }
+
+            //保证锁的释放 在事物完成之后
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCompletion(int status) {
+                    try {
+                        if (lock.isHeldByCurrentThread()) {
+                            lock.unlock();
+                            logger.info("直播事务完成后释放锁,用户ID:{},直播ID:{}", param.getUserId(), param.getLiveId());
+                        }
+                    } catch (IllegalMonitorStateException e) {
+                        logger.warn("直播释放锁异常(可能已过期),用户ID:{},直播ID:{}", param.getUserId(), param.getLiveId());
+                    }
+                }
+            });
+
+
+            logger.info("直播成功获取锁,开始处理奖励发放,用户ID:{},直播ID:{}", param.getUserId(), param.getLiveId());
+
+            // 获取用户信息
+            FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+
+            log.info("直播查询会员信息:{}", user);
+            if (user.getStatus() == 0) {
+                return R.error("会员被停用,无权限,请联系客服!");
+            }
+
+            Live live = iLiveService.selectLiveByLiveId(param.getLiveId());
+            LiveWatchConfig config = JSON.parseObject(live.getConfigJson(), LiveWatchConfig.class);
+            log.info("直播配置:{}", config);
+            // 先查是否开启了启动 直播完课奖励
+            if (config.getEnabled() && 3 == config.getParticipateCondition()) {
+
+                //直播/录播
+                LiveWatchUser watchUser=null;
+                //录播
+                LiveWatchUser replayUser=null;
+
+                // 根据直播类型判断是否已发放奖励 先查直播
+                watchUser = watchUserMapper.getWatchLiveByLiveFlag(param.getUserId(), param.getLiveId());
+                log.info("直播看课记录:{}", watchUser);
+                if (watchUser == null) {
+                    // 直播没有记录 查录播
+                    watchUser = watchUserMapper.getWatchLiveByReplayFlag(param.getUserId(), param.getLiveId());
+
+                    if (watchUser==null){
+                        return R.error("您没有观看过该课程,无奖励发放!");
+                    }
+                }
+                //直播不空 那要存一下录播
+                else {
+                    // 直播没有记录 查录播
+                    replayUser = watchUserMapper.getWatchLiveByReplayFlag(param.getUserId(), param.getLiveId());
+
+                }
+
+
+                //是否以及领取了奖励 看直播或者录播 俩者只要有一个领取了奖励 就返回
+                if (watchUser.getRewardType() != null || (replayUser != null && replayUser.getRewardType() != null)) {
+
+                    LiveRedPacketLog liveRedPacketLog = redPacketLogMapper.selectLiveRedPacketLogByTemporary(param.getLiveId(), param.getUserId());
+
+                    log.info("直播课程红包:{}", liveRedPacketLog);
+                    if (liveRedPacketLog != null && liveRedPacketLog.getStatus() == 1) {
+                        return R.error("已领取该直播课程奖励,不可重复领取!");
+                    }
+                    if (liveRedPacketLog != null && liveRedPacketLog.getStatus() == 0) {
+                        if (StringUtils.isNotEmpty(liveRedPacketLog.getResult())) {
+                            R r = JSON.parseObject(liveRedPacketLog.getResult(), R.class);
+                            return r;
+                        } else {
+                            return R.error("操作频繁,请稍后再试!");
+                        }
+                    }
+                    if (liveRedPacketLog != null && liveRedPacketLog.getStatus() == 2) {
+                        return R.error("请联系客服补发");
+                    }
+                    return R.error("奖励已发放");
+                }
+
+                //判断直播数据 是否满足 完课/如果有录播数据 再判断录播是否满足完课
+                // 在线时长
+                long onlineSeconds = watchUser.getOnlineSeconds() != null ? watchUser.getOnlineSeconds() : 0L;
+                Long duration = live.getDuration();
+                Long completionRate = config.getCompletionRate();
+
+                if (duration == null || duration <= 0 || completionRate == null || completionRate <= 0) {
+                    return R.error("直播完课配置异常,无法发放奖励");
+                }
+
+                //优先 发直播红包
+                if (watchUser.getLiveFlag()==1){
+
+                    if (onlineSeconds * 100 < duration * completionRate) {
+                        //直播时长不满足,看录播 如果录播的时长看课时长 大于了 设定的百分比则 可以领取积分
+                        if (replayUser != null && replayUser.getOnlineSeconds() != null
+                                && replayUser.getOnlineSeconds() * 100 >= duration * completionRate) {
+                            // 如果是 回放完课 发积分
+                            return sendLiveIntegralReward(param, user, watchUser, config);
+                        }else {
+                            return R.error("观看时长未达到完课要求,请看继续观看回放,之后再领取");
+                        }
+
+                    }
+
+                    WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+
+                    if (StringUtil.strIsNullOrEmpty(user.getAppOpenId())){
+                        return R.error("请重新登录app");
+                    }
+
+                    // 用app的 appOpenId
+                    String openId = user.getAppOpenId();
+                    packetParam.setOpenId(openId);
+                    BeanUtils.copyProperties(param, packetParam);
+
+                   return sendAppLiveRedPacketAuto(packetParam, watchUser, config, param);
+                }
+                // 其次 是 录播 再发 回放积分
+                else if (watchUser.getReplayFlag()==1){
+
+                    if (onlineSeconds * 100 < duration * completionRate) {
+                        return R.error("观看时长未达到完课要求,请继续观看");
+                    }
+
+                    // 如果是 回放完课 发积分
+                    return sendLiveIntegralReward(param, user, watchUser, config);
+                }else {
+                    return R.error("未知 直播观看类型 ");
+                }
+            }else {
+                return R.error("当前直播 并未启动 直播完课奖励 ");
+            }
+
+
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.error("直播获取锁被中断,用户ID:{},直播ID:{}", param.getUserId(), param.getLiveId(), e);
+            return R.error("系统繁忙,请重试!");
+        }
+    }
+
+    @Override
+    public R syncLiveRedPacket(String outBatchNo, String batchId) {
+        LiveRedPacketLog log = redPacketLogMapper.selectLiveRedPacketLogByBatchNo(outBatchNo);
+        if (log!=null){
+            log.setStatus(1L);
+            log.setUpdateTime(new Date());
+            log.setBatchId(batchId);
+            redPacketLogMapper.updateLiveRedPacketLog(log);
+
+            // 更新扣减状态
+            CompanyRedPacketBalanceLogs redLogs = new CompanyRedPacketBalanceLogs();
+            redLogs.setRedPacketId(log.getLogId());
+            redLogs.setStatus(1L);
+            companyRedPacketBalanceLogsMapper.updateCompanyRedPacketBalanceLogsByRedPacketId(redLogs);
+
+            return R.ok();
+        }
+        return R.error("批次不存在");
+    }
+
+    /**
+     * 直播 看课的的
+     */
+    private R sendAppLiveRedPacketAuto(WxSendRedPacketParam packetParam,LiveWatchUser watchUser,LiveWatchConfig config,LiveRedPacketParam param) {
+
+
+        // 确定红包金额
+        BigDecimal amount = config.getRedPacketAmount();
+
+        packetParam.setAmount(amount);
+
+        if (amount.compareTo(BigDecimal.ZERO) > 0) {
+
+            // 预设值异常对象
+
+            BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
+            balanceRollbackError.setCompanyId(packetParam.getCompanyId());
+            balanceRollbackError.setUserId(watchUser.getUserId());
+            balanceRollbackError.setLogId(watchUser.getId());
+            balanceRollbackError.setVideoId(watchUser.getLiveId());
+            balanceRollbackError.setStatus(0);
+            balanceRollbackError.setMoney(amount);
+
+            // 打开红包扣减功能
+            if ("1".equals(config.getIsRedPackageBalanceDeduction())) {
+
+                if (packetParam.getCompanyId() == null) {
+                    logger.error("直播发送红包参数错误,公司不能为空,异常请求参数{}", packetParam);
+                    return R.error("发送红包失败,请联系管理员,公司不能为空");
+                }
+                String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();
+
+                // 第一次加锁:预扣减余额
+                RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
+                boolean lockAcquired = false;
+                BigDecimal newMoney;
+                try {
+                    if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
+                        lockAcquired = true;
+                        BigDecimal originalMoney;
+                        // 获取当前余额
+                        String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+                        if (StringUtils.isNotEmpty(moneyStr)) {
+                            originalMoney = new BigDecimal(moneyStr);
+                        } else {
+                            // 缓存没有值,重启系统恢复redis数据 保证数据正确性
+                            logger.error("直播发送红包获取redis余额缓存异常,异常请求参数{}", packetParam);
+                            return R.error("系统异常,余额缓存异常,请稍后重试");
+                        }
+
+                        if (originalMoney.compareTo(BigDecimal.ZERO) < 0) {
+                            logger.error("服务商余额不足,异常请求参数{}", packetParam);
+                            return R.error("服务商余额不足,请联系群主服务器充值!");
+                        }
+
+                        // 预扣减金额
+                        newMoney = originalMoney.subtract(amount);
+                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+                    } else {
+                        logger.error("获取redis锁失败,异常请求参数{}", packetParam);
+                        return R.error("系统繁忙,请稍后重试");
+                    }
+                } catch (Exception e) {
+                    logger.error("预扣减余额失败: 异常请求参数{},异常信息{}", packetParam, e.getMessage(), e);
+                    return R.error("系统异常,请稍后重试");
+                } finally {
+                    // 只有在成功获取锁的情况下才释放锁
+                    if (lockAcquired && lock1.isHeldByCurrentThread()) {
+                        try {
+                            lock1.unlock();
+                        } catch (IllegalMonitorStateException e) {
+                            logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
+                        }
+                    }
+                }
+
+
+                // 调用第三方接口(锁外操作)
+                R sendRedPacket;
+                try {
+                    sendRedPacket = paymentService.sendAppLiveRedPacket(packetParam);
+                } catch (Exception e) {
+                    logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
+                    // 异常时回滚余额
+
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // 红包发送成功处理
+                if (sendRedPacket.get("code").equals(200)) {
+
+                    // 更新观看记录的奖励类型
+                    watchUser.setRewardType(1);
+                    watchUser.setSendType(1);
+                    watchUserMapper.updateLiveWatchUser(watchUser);
+
+                    LiveRedPacketLog liveRedPacketLog = new LiveRedPacketLog();
+                    TransferBillsResult transferBillsResult;
+                    if (sendRedPacket.get("isNew").equals(1)) {
+                        transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                        liveRedPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                        liveRedPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                        liveRedPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                    } else {
+                        liveRedPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                        liveRedPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                    }
+
+                    liveRedPacketLog.setCompanyId(param.getCompanyId());
+                    liveRedPacketLog.setUserId(watchUser.getUserId());
+                    liveRedPacketLog.setLiveId(watchUser.getLiveId());
+                    liveRedPacketLog.setStatus(0L);
+                    liveRedPacketLog.setCompanyUserId(param.getCompanyUserId());
+                    liveRedPacketLog.setCreateTime(new Date());
+                    liveRedPacketLog.setAmount(amount);
+                    liveRedPacketLog.setWatchLogId(watchUser.getId());
+                    liveRedPacketLog.setAppId(packetParam.getAppId());
+                    liveRedPacketLog.setWathcType(1L);
+
+                    redPacketLogMapper.insertLiveRedPacketLog(liveRedPacketLog);
+
+                    // 异步登记余额扣减日志
+                    BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
+                    companyService.asyncRecordBalanceLog(packetParam.getCompanyId(), money, 15, newMoney, "发放直播红包", liveRedPacketLog.getLogId());
+
+                    return sendRedPacket;
+
+
+                } else {
+                    // 发送失败,回滚余额
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // ===================== 本次修改目的为了实时扣减公司余额=====================
+            } else {
+                Company company = companyMapper.selectCompanyById(packetParam.getCompanyId());
+                BigDecimal money = company.getMoney();
+                if (money.compareTo(BigDecimal.ZERO) <= 0) {
+                    return R.error("服务商余额不足,请联系群主服务器充值!");
+                }
+
+                try{
+                    // 发送红包
+                    R sendRedPacket = paymentService.sendAppLiveRedPacket(packetParam);
+                    if (sendRedPacket.get("code").equals(200)) {
+
+                        // 更新观看记录的奖励类型
+                        watchUser.setRewardType(1);
+                        watchUser.setSendType(1);
+                        watchUserMapper.updateLiveWatchUser(watchUser);
+
+                        LiveRedPacketLog liveRedPacketLog = new LiveRedPacketLog();
+                        TransferBillsResult transferBillsResult;
+                        if (sendRedPacket.get("isNew").equals(1)) {
+                            transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                            liveRedPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                            liveRedPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                            liveRedPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                        } else {
+                            liveRedPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                            liveRedPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                        }
+                        // 添加红包记录
+                        liveRedPacketLog.setCompanyId(param.getCompanyId());
+                        liveRedPacketLog.setUserId(watchUser.getUserId());
+                        liveRedPacketLog.setLiveId(watchUser.getLiveId());
+                        liveRedPacketLog.setStatus(0L);
+                        liveRedPacketLog.setCompanyUserId(param.getCompanyUserId());
+                        liveRedPacketLog.setCreateTime(new Date());
+                        liveRedPacketLog.setAmount(amount);
+                        liveRedPacketLog.setWatchLogId(watchUser.getId());
+                        liveRedPacketLog.setAppId( packetParam.getAppId());
+
+                        redPacketLogMapper.insertLiveRedPacketLog(liveRedPacketLog);
+
+                        return sendRedPacket;
+                    } else {
+                        return R.error("奖励发送失败,请联系客服");
+                    }
+                }catch (Exception e){
+                    return R.error(e.getMessage());
+                }
+
+            }
+        } else {
+
+            // 更新直播观看记录的奖励类型
+            watchUser.setRewardType(1);
+            watchUser.setSendType(1);
+            watchUserMapper.updateLiveWatchUser(watchUser);
+
+            LiveRedPacketLog liveRedPacketLog = new LiveRedPacketLog();
+            // 添加红包记录
+            liveRedPacketLog.setCompanyId(param.getCompanyId());
+            liveRedPacketLog.setUserId(watchUser.getUserId());
+            liveRedPacketLog.setLiveId(watchUser.getLiveId());
+            liveRedPacketLog.setStatus(1L);
+            liveRedPacketLog.setRemark("设置的直播红包金额为0");
+            liveRedPacketLog.setCompanyUserId(param.getCompanyUserId());
+            liveRedPacketLog.setCreateTime(new Date());
+            liveRedPacketLog.setAmount(BigDecimal.ZERO);
+            liveRedPacketLog.setWatchLogId(watchUser.getId());
+            liveRedPacketLog.setAppId( packetParam.getAppId());
+            redPacketLogMapper.insertLiveRedPacketLog(liveRedPacketLog);
+
+            return R.ok("答题成功!");
+        }
+    }
+
+
+    /**
+     * @Description: 回滚redis缓存中的余额 异常登记回滚异常表,定时重新回滚
+     * @Param:
+     * @Return:
+     * @Author
+     * @Date 2025/10/22 10:37
+     */
+    private void rollbackBalance(BalanceRollbackError balanceRollbackError) {
+        String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + balanceRollbackError.getCompanyId();
+        RLock lock2 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + balanceRollbackError.getCompanyId());
+        boolean lockAcquired = false;
+        boolean backError = true;
+        try {
+            if (lock2.tryLock(3, 10, TimeUnit.SECONDS)) {
+                lockAcquired = true;
+                // 获取当前余额
+                String currentMoneyStr = redisCache.getCacheObject(companyMoneyKey);
+                if (StringUtils.isEmpty(currentMoneyStr)) {
+                    throw new RuntimeException("回滚余额异常");
+                }
+
+                // 回滚金额(加回之前扣减的金额)
+                BigDecimal rollbackMoney = new BigDecimal(currentMoneyStr).add(balanceRollbackError.getMoney());
+                redisCache.setCacheObject(companyMoneyKey, rollbackMoney.toString());
+                backError = false;
+                logger.info("余额回滚成功: companyId={}, amount={}", balanceRollbackError.getCompanyId(), balanceRollbackError.getMoney());
+
+            } else {
+                logger.warn("回滚余额时获取锁失败: companyId={}", balanceRollbackError.getCompanyId());
+                // 登记回滚余额异常表
+                balanceRollbackErrorMapper.insert(balanceRollbackError);
+            }
+        } catch (Exception e) {
+            logger.error("回滚余额时发生异常: companyId={}", balanceRollbackError.getCompanyId(), e);
+            // 登记回滚余额异常表
+            if (backError) {
+                balanceRollbackErrorMapper.insert(balanceRollbackError);
+            }
+        } finally {
+            // 只有在成功获取锁的情况下才释放锁
+            if (lockAcquired && lock2.isHeldByCurrentThread()) {
+                try {
+                    lock2.unlock();
+                } catch (IllegalMonitorStateException e) {
+                    logger.warn("尝试释放非当前线程持有的锁: balanceRollbackError={}", balanceRollbackError);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 发放直播积分奖励
+     *
+     * @param user   用户信息
+     * @param watchUser    观看日志
+     * @param config 配置信息
+     * @return 处理结果
+     */
+    private R sendLiveIntegralReward(LiveRedPacketParam param, FsUser user, LiveWatchUser watchUser, LiveWatchConfig config) {
+        // 更新用户积分
+        FsUser userMap = new FsUser();
+        userMap.setUserId(user.getUserId());
+        userMap.setIntegral(user.getIntegral() + config.getScoreAmount());
+        fsUserMapper.updateFsUser(userMap);
+
+        // 记录积分日志
+        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+        integralLogs.setIntegral(config.getScoreAmount());
+        integralLogs.setUserId(user.getUserId());
+        integralLogs.setBalance(userMap.getIntegral());
+        integralLogs.setLogType(25);
+        integralLogs.setBusinessId(StringUtils.isNotEmpty(watchUser.getId().toString()) ? watchUser.getId().toString() : null);
+        integralLogs.setCreateTime(new Date());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+
+        //更新看课记录的奖励类型
+        watchUser.setRewardType(2);
+        watchUser.setSendType(1);
+        watchUserMapper.updateLiveWatchUser(watchUser);
+        logger.info("发放奖励====================》直播看课记录,{}", watchUser);
+
+        //积分转换红包
+        LiveRedPacketLog liveRedPacketLog = new LiveRedPacketLog();
+
+        liveRedPacketLog.setOutBatchNo(integralLogs.getId().toString());
+        liveRedPacketLog.setCompanyId(param.getCompanyId());
+        liveRedPacketLog.setUserId(param.getUserId());
+        liveRedPacketLog.setLiveId(param.getLiveId());
+        liveRedPacketLog.setStatus(1L);
+        liveRedPacketLog.setCompanyUserId(param.getCompanyUserId());
+        liveRedPacketLog.setCreateTime(new Date());
+        liveRedPacketLog.setAmount(BigDecimal.valueOf(config.getScoreAmount()).divide(BigDecimal.valueOf(1000)));
+        liveRedPacketLog.setRemark("直播答题领取积分转");
+        liveRedPacketLog.setWatchLogId(watchUser.getId() != null ? watchUser.getId() : null);
+
+        redPacketLogMapper.insertLiveRedPacketLog(liveRedPacketLog);
+        return R.ok("直播积分奖励发放成功").put("rewardType", 2);
+    }
+
+}

+ 18 - 4
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.page.PageRequest;
 import com.fs.common.core.redis.service.StockDeductService;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyUserUser;
@@ -256,15 +257,23 @@ public class LiveServiceImpl implements ILiveService
     public R liveDecryptLinkV2(String url,String userId) {
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+        String requestUrl="";
+        if (CloudHostUtils.hasCloudHostName("济世百康")){
+            requestUrl  = config.getRealLinkH5LiveName();
+        }else {
+            requestUrl = config.getRealLinkDomainName();
+        }
+
+
         Long fsUserId = Long.valueOf(userId);
         String decryptLink = LinkUtil.decryptLink(url);
         Map<String, Object> data = new HashMap<>();
-        data.put("decryptLink", config.getRealLinkH5LiveName()+decryptLink);
+        data.put("decryptLink", requestUrl+decryptLink);
         String[] split = decryptLink.split("=");
         if(split[1] != null){
             FsCourseLink courseLink = fsCourseLinkMapper.selectFsCourseLinkByLink(split[1]);
             String realLink = courseLink.getRealLink();
-            data.put("realLink", config.getRealLinkH5LiveName() + realLink);
+            data.put("realLink", requestUrl + realLink);
             String[] split1 = realLink.split("=");
             if(split1[1] != null){
                 try {
@@ -336,7 +345,7 @@ public class LiveServiceImpl implements ILiveService
     }
 
     @Override
-    public R createAppLink(CompanyUser user, Long liveId, String corpId) {
+    public R createAppLink(CompanyUser user, Long liveId, String corpId,String appName) {
         if(user == null || user.getCompanyId() == null){
             return R.error("销售账号请绑定销售公司");
         }
@@ -373,7 +382,12 @@ public class LiveServiceImpl implements ILiveService
         sendShortLink = sendShortLink.replace(".html","");
         String InvitationCode = LinkUtil.encryptLink(sendShortLink);
         log.info("真实链接为:{},发送的短链为:{}",realLinkFull, InvitationCode);
-        return R.ok().put("realLink","康好健康"+InvitationCode);
+        if (CloudHostUtils.hasCloudHostName("济世百康")){
+            return R.ok().put("realLink","康好健康"+InvitationCode);
+        }else {
+            return R.ok().put("realLink",appName+InvitationCode);
+        }
+
     }
 
     @Override

+ 143 - 10
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -43,6 +43,7 @@ import com.fs.sop.service.ISopUserLogsInfoService;
 import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
@@ -232,6 +233,8 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return baseMapper.selectLiveWatchUserList(queryUser);
     }
 
+
+
     /**
      * 批量删除直播间观看用户
      *
@@ -487,6 +490,78 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return null;
     }
 
+    /**
+    * 更新 用户的直播时间
+    */
+    @Override
+    public R sendLiveRewardClone(long liveId, long userId) {
+        // 查询直播间信息
+        Live live = liveMapper.selectLiveByLiveId(liveId);
+        if (live == null) {
+            return R.error("直播间不存在");
+        }
+        try {
+            // 从 Redis 获取用户进入时间
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Object entryTimeObj = redisCache.getCacheObject(entryTimeKey);
+            Long entryTime = null;
+            if (entryTimeObj != null) {
+                if (entryTimeObj instanceof Long) {
+                    entryTime = (Long) entryTimeObj;
+                } else if (entryTimeObj instanceof String) {
+                    try {
+                        entryTime = Long.parseLong((String) entryTimeObj);
+                    } catch (NumberFormatException e) {
+                        return R.error("无法解析进入时间字符串为Long:"+ entryTimeObj);
+                    }
+                } else if (entryTimeObj instanceof Number) {
+                    entryTime = ((Number) entryTimeObj).longValue();
+                }
+            }
+
+            if (entryTime == null) {
+                return R.error("未找到对应的观看时长-缓");
+            }
+
+            // 获取当前直播/回放状态
+            Map<String, Integer> flagMap = this.getLiveFlagWithCache(liveId);
+            Integer currentLiveFlag = flagMap.get("liveFlag");
+            Integer currentReplayFlag = flagMap.get("replayFlag");
+            // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
+            LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, currentLiveFlag, currentReplayFlag);
+            if (liveWatchUser == null) {
+                return R.error("未找到对应的观看记录");
+            }
+
+            long currentTimeMillis = System.currentTimeMillis();
+            Date now = new Date();
+            // 计算在线时长(秒)
+            long durationSeconds = (currentTimeMillis - entryTime) / 1000;
+            if (durationSeconds <= 0) {
+                return R.error("在线时长不足");
+            }
+
+            // 累加在线时长(如果没有初次进入或者没有时长,就会为null)
+            Long onlineSeconds = liveWatchUser.getOnlineSeconds();
+            if (onlineSeconds == null) {
+                onlineSeconds = 0L;
+            }
+            liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
+            liveWatchUser.setUpdateTime(now);
+            baseMapper.updateLiveWatchUser(liveWatchUser);
+
+            //重置进入时间
+            redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
+
+        } catch (Exception e) {
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}",
+                    liveId, userId, e.getMessage(), e);
+            return R.error("更新用户在线时长异常");
+        }
+
+        return  R.ok();
+    }
+
     /**
      * 查询直播间在线用户列表
      * @param params 参数
@@ -1102,7 +1177,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
      * 更新完课备注
      * 如果用户完课,在备注中添加或更新完课日期
      * 格式:MMdd完课-原有备注
-     * 
+     *
      * @param liveId 直播间ID
      * @param externalId 外部联系人ID
      * @param qwExternalContact 企微外部联系人信息
@@ -1121,10 +1196,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 // 使用传入的完课标志
                 completed = isCompleted;
             }
-            
+
             if (!completed) {
                 // 未完课,不改动备注
-                log.debug("用户未完课,不更新备注: liveId={}, externalId={}", 
+                log.debug("用户未完课,不更新备注: liveId={}, externalId={}",
                         liveId, externalId);
                 return;
             }
@@ -1132,10 +1207,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             // 完课,需要更新备注
             String currentRemark = qwExternalContact.getRemark();
             String newRemark = buildCompletedCourseRemark(currentRemark);
-            
+
             // 如果备注没有变化,跳过更新
             if (newRemark.equals(currentRemark)) {
-                log.debug("备注无需更新: liveId={}, externalId={}, remark={}", 
+                log.debug("备注无需更新: liveId={}, externalId={}, remark={}",
                         liveId, externalId, currentRemark);
                 return;
             }
@@ -1147,14 +1222,14 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             remarkParam.setRemark(newRemark);
 
             QwExternalContactRemarkResult remarkResult = qwApiService.externalcontactRemark(remarkParam, qwExternalContact.getCorpId());
-            
+
             if (remarkResult != null && remarkResult.getErrcode() == 0) {
                 // 更新成功,同步更新数据库
                 QwExternalContact updateContact = new QwExternalContact();
                 updateContact.setId(qwExternalContact.getId());
                 updateContact.setRemark(newRemark);
                 qwExternalContactMapper.updateQwExternalContact(updateContact);
-                
+
                 log.info("成功更新完课备注: liveId={}, externalId={}, oldRemark={}, newRemark={}",
                         liveId, externalId, currentRemark, newRemark);
             } else {
@@ -1172,7 +1247,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
      * 构建完课备注
      * 格式:MMdd完课-原有备注
      * 如果已有完课标记,则更新日期部分
-     * 
+     *
      * @param currentRemark 当前备注
      * @return 新的备注
      */
@@ -1196,12 +1271,12 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             // 已有完课标记,更新日期部分
             String existingDate = matcher.group(1);
             String remainingRemark = matcher.group(2);
-            
+
             // 如果日期相同,不需要更新
             if (todayDate.equals(existingDate)) {
                 return currentRemark;
             }
-            
+
             // 更新日期
             return completedPrefix + "-" + remainingRemark;
         } else {
@@ -1279,4 +1354,62 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         }
     }
 
+    @Override
+    public  R getLiveStatusByUserID(Long liveId, Long userId) {
+
+        //直播/录播
+        LiveWatchUser watchUser=null;
+        //录播
+        LiveWatchUser replayUser=null;
+
+        // 判断客户当前 是直播还是录播
+        watchUser = baseMapper.getWatchLiveByLiveFlag(userId, liveId);
+
+        if (watchUser == null) {
+            // 直播没有记录 查录播
+            replayUser = baseMapper.getWatchLiveByReplayFlag(userId, liveId);
+
+            if (replayUser==null){
+                return R.error("没有直播看课记录");
+            }
+
+            LiveWatchUserStatusVO replayUserVO=new LiveWatchUserStatusVO();
+            BeanUtils.copyProperties(replayUser, replayUserVO);
+
+            return R.ok().put("watchUser",watchUser)
+                    .put("rewardType",replayUserVO.getRewardType())
+                    .put("replayUser",replayUserVO);
+        }
+        //直播不空 那要找一下录播
+        else {
+            Integer rewardType=null;
+            // 直播没有记录 查录播
+            replayUser = baseMapper.getWatchLiveByReplayFlag(userId, liveId);
+
+            if (watchUser.getRewardType() != null || (replayUser != null && replayUser.getRewardType() != null)) {
+                rewardType=1;
+            }
+
+            LiveWatchUserStatusVO watchUserVO=new LiveWatchUserStatusVO();
+            BeanUtils.copyProperties(watchUser, watchUserVO);
+
+            //录播不空
+            if (replayUser!=null){
+
+                LiveWatchUserStatusVO replayUserVO=new LiveWatchUserStatusVO();
+                BeanUtils.copyProperties(replayUser, replayUserVO);
+
+                return R.ok().put("watchUser",watchUserVO)
+                        .put("rewardType",rewardType)
+                        .put("replayUser",replayUserVO);
+            }
+
+            return R.ok().put("watchUser",watchUserVO)
+                    .put("rewardType",rewardType)
+                    .put("replayUser",replayUser);
+        }
+
+
+    }
+
 }

+ 28 - 0
fs-service/src/main/java/com/fs/live/vo/LiveWatchUserStatusVO.java

@@ -0,0 +1,28 @@
+package com.fs.live.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+@Data
+public class LiveWatchUserStatusVO {
+
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    private Long onlineSeconds;
+
+    /** 直播进入标记:0-否 1-是 */
+    @Excel(name = "直播进入标记")
+    private Integer liveFlag = 0;
+
+    /** 回放进入标记:0-否 1-是 */
+    @Excel(name = "回放进入标记")
+    private Integer replayFlag = 0;
+
+    /**
+     * 奖励类型 1红包 2积分
+     */
+    private Integer rewardType;
+}

+ 128 - 0
fs-service/src/main/resources/mapper/live/LiveRedPacketLogMapper.xml

@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveRedPacketLogMapper">
+
+    <resultMap type="LiveRedPacketLog" id="LiveRedPacketLogResult">
+        <result property="logId"    column="log_id"    />
+        <result property="outBatchNo"    column="out_batch_no"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="amount"    column="amount"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="status"    column="status"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="watchLogId"    column="watch_log_id"    />
+        <result property="remark"    column="remark"    />
+        <result property="result"    column="result"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="appId"    column="app_id"    />
+        <result property="mchId"    column="mch_id"    />
+        <result property="wathcType"    column="wathc_type"    />
+    </resultMap>
+
+    <sql id="selectLiveRedPacketLogVo">
+        select log_id, out_batch_no, live_id, user_id, company_user_id, company_id, amount, create_time, status, update_time, watch_log_id, remark, result, batch_id, app_id, mch_id, wathc_type from live_red_packet_log
+    </sql>
+
+    <select id="selectLiveRedPacketLogList" parameterType="LiveRedPacketLog" resultMap="LiveRedPacketLogResult">
+        <include refid="selectLiveRedPacketLogVo"/>
+        <where>
+            <if test="outBatchNo != null  and outBatchNo != ''"> and out_batch_no = #{outBatchNo}</if>
+            <if test="liveId != null "> and live_id = #{liveId}</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="amount != null "> and amount = #{amount}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="watchLogId != null "> and watch_log_id = #{watchLogId}</if>
+            <if test="result != null  and result != ''"> and result = #{result}</if>
+            <if test="batchId != null  and batchId != ''"> and batch_id = #{batchId}</if>
+            <if test="appId != null  and appId != ''"> and app_id = #{appId}</if>
+            <if test="mchId != null  and mchId != ''"> and mch_id = #{mchId}</if>
+            <if test="wathcType != null "> and wathc_type = #{wathcType}</if>
+        </where>
+    </select>
+
+    <select id="selectLiveRedPacketLogByLogId" parameterType="Long" resultMap="LiveRedPacketLogResult">
+        <include refid="selectLiveRedPacketLogVo"/>
+        where log_id = #{logId}
+    </select>
+
+    <insert id="insertLiveRedPacketLog" parameterType="LiveRedPacketLog" useGeneratedKeys="true" keyProperty="logId">
+        insert into live_red_packet_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="outBatchNo != null">out_batch_no,</if>
+            <if test="liveId != null">live_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="amount != null">amount,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="status != null">status,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="watchLogId != null">watch_log_id,</if>
+            <if test="remark != null">remark,</if>
+            <if test="result != null">result,</if>
+            <if test="batchId != null">batch_id,</if>
+            <if test="appId != null">app_id,</if>
+            <if test="mchId != null">mch_id,</if>
+            <if test="wathcType != null">wathc_type,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="outBatchNo != null">#{outBatchNo},</if>
+            <if test="liveId != null">#{liveId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="amount != null">#{amount},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="watchLogId != null">#{watchLogId},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="result != null">#{result},</if>
+            <if test="batchId != null">#{batchId},</if>
+            <if test="appId != null">#{appId},</if>
+            <if test="mchId != null">#{mchId},</if>
+            <if test="wathcType != null">#{wathcType},</if>
+         </trim>
+    </insert>
+
+    <update id="updateLiveRedPacketLog" parameterType="LiveRedPacketLog">
+        update live_red_packet_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="outBatchNo != null">out_batch_no = #{outBatchNo},</if>
+            <if test="liveId != null">live_id = #{liveId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="amount != null">amount = #{amount},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="watchLogId != null">watch_log_id = #{watchLogId},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="result != null">result = #{result},</if>
+            <if test="batchId != null">batch_id = #{batchId},</if>
+            <if test="appId != null">app_id = #{appId},</if>
+            <if test="mchId != null">mch_id = #{mchId},</if>
+            <if test="wathcType != null">wathc_type = #{wathcType},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+
+    <delete id="deleteLiveRedPacketLogByLogId" parameterType="Long">
+        delete from live_red_packet_log where log_id = #{logId}
+    </delete>
+
+    <delete id="deleteLiveRedPacketLogByLogIds" parameterType="String">
+        delete from live_red_packet_log where log_id in
+        <foreach item="logId" collection="array" open="(" separator="," close=")">
+            #{logId}
+        </foreach>
+    </delete>
+</mapper>

+ 15 - 5
fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml

@@ -21,10 +21,14 @@
         <result property="liveFlag"    column="live_flag"    />
         <result property="replayFlag"    column="replay_flag"    />
         <result property="location"    column="location"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="rewardType"    column="reward_type"    />
     </resultMap>
 
     <sql id="selectLiveWatchUserVo">
-        select id, live_id,user_id, msg_status, online, create_time, create_by, update_by, update_time, remark,online_seconds,global_visible,single_visible,live_flag,replay_flag,location from live_watch_user
+        select id, live_id,user_id, msg_status, online, create_time, create_by, update_by, update_time, remark,online_seconds,global_visible,
+               single_visible,live_flag,replay_flag,location,send_type,reward_type
+        from live_watch_user
     </sql>
 
     <select id="selectLiveWatchUserList" parameterType="LiveWatchUser" resultMap="LiveWatchUserResult">
@@ -141,6 +145,8 @@
             <if test="liveFlag != null">live_flag,</if>
             <if test="replayFlag != null">replay_flag,</if>
             <if test="location != null">location,</if>
+            <if test="sendType != null">send_type,</if>
+            <if test="rewardType != null">reward_type,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="liveId != null">#{liveId},</if>
@@ -158,6 +164,8 @@
             <if test="liveFlag != null">#{liveFlag},</if>
             <if test="replayFlag != null">#{replayFlag},</if>
             <if test="location != null">#{location},</if>
+            <if test="sendType != null">#{sendType},</if>
+            <if test="rewardType != null">#{rewardType},</if>
         </trim>
     </insert>
 
@@ -179,6 +187,8 @@
             <if test="liveFlag != null">live_flag = #{liveFlag},</if>
             <if test="replayFlag != null">replay_flag = #{replayFlag},</if>
             <if test="location != null">location = #{location},</if>
+            <if test="sendType != null">send_type = #{sendType},</if>
+            <if test="rewardType != null">reward_type = #{rewardType},</if>
         </trim>
         where id = #{id}
     </update>
@@ -213,20 +223,20 @@
 
     <select id="selectLiveWatchAndRegisterUser" resultType="com.fs.live.domain.LiveWatchUser">
         select a.*,fu.nick_name as nickname from (
-            select lws.* from live_watch_user lws 
+            select lws.* from live_watch_user lws
             where live_id=#{params.liveId} and online = 0
             <if test="params.liveFlag != null "> and live_flag = #{params.liveFlag}</if>
             <if test="params.replayFlag != null "> and replay_flag = #{params.replayFlag}</if>
             and user_id in (
-                select user_id from live_lottery_registration 
-                where live_id = #{params.liveId} and lottery_id=#{params.lotteryId} 
+                select user_id from live_lottery_registration
+                where live_id = #{params.liveId} and lottery_id=#{params.lotteryId}
                 and registration_id >= (SELECT FLOOR(RAND() * (SELECT MAX(registration_id) FROM live_lottery_registration)))
             )
         ) a left join fs_user fu on fu.user_id = a.user_id
     </select>
 
     <select id="selectUserByLiveIdAndUserId" resultType="com.fs.live.domain.LiveWatchUser">
-        select * from live_watch_user 
+        select * from live_watch_user
         where live_id = #{params.liveId} and user_id = #{params.userId}
         <if test="params.liveFlag != null "> and live_flag = #{params.liveFlag}</if>
         <if test="params.replayFlag != null "> and replay_flag = #{params.replayFlag}</if>

+ 6 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseTransferController.java

@@ -55,6 +55,12 @@ public class CourseTransferController {
         return paymentService.v3TransferNotifyApp(notifyData,request);
     }
 
+    @PostMapping( "/live/v3TransferNotify")
+    public String v3TransferNotifyAppLive(@RequestBody String notifyData,HttpServletRequest request, HttpServletResponse response) throws Exception {
+        return paymentService.v3TransferNotifyAppLive(notifyData,request);
+    }
+
+
     @GetMapping("/test")
     public void test(){
         iSopUserLogsInfoService.updateSopUserInfoByExternalId(1L,123456L);

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedController.java

@@ -33,7 +33,7 @@ public class LiveRedController extends AppBaseController {
     private ILiveUserRedRecordService liveUserRedRecordService;
 
     /**
-     * 领取红包
+     * 直播间的积分红包领取记录表
      */
     @Login
     @PostMapping("/claim")

+ 72 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveRedPacketLogController.java

@@ -0,0 +1,72 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.annotation.UserOperationLog;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.his.enums.FsUserOperationEnum;
+import com.fs.live.param.LiveRedPacketParam;
+import com.fs.live.service.ILiveRedPacketLogService;
+import com.fs.live.service.ILiveWatchUserService;
+import com.fs.voice.utils.StringUtil;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 直播红包 记录Controller
+ *
+ * @author fs
+ * @date 2026-06-08
+ */
+@RestController
+@RequestMapping("/app/live/liveRedPacketLog")
+public class LiveRedPacketLogController extends AppBaseController
+{
+    Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Autowired
+    private ILiveRedPacketLogService redPacketLogService;
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @Login
+    @ApiOperation("发放直播红包奖励")
+    @PostMapping("/sendLiveReward")
+    @RepeatSubmit
+    @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
+    public R sendLiveReward(@RequestBody LiveRedPacketParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        logger.info("【发放直播红包奖励】:{}",param);
+
+        if (param.getSource()==1  && StringUtil.strIsNullOrEmpty(param.getAppId())){
+            return R.error("appId不能为空");
+        }
+
+        if (-1==param.getCompanyId() || -1==param.getCompanyUserId()){
+            return R.error("客服信息错误,请从客服处 领取链接观看");
+        }
+
+
+        return redPacketLogService.sendLiveReward(param);
+    }
+
+    @Login
+    @ApiOperation("获取用户看课的直播状态")
+    @GetMapping("/getLiveStatusByUserID/{liveId}")
+    public R getLiveStatusByUserID(@PathVariable("liveId")  Long liveId)
+    {
+        long userId = Long.parseLong(getUserId());
+
+        return   liveWatchUserService.getLiveStatusByUserID(liveId,userId);
+    }
+
+
+
+}