Explorar o código

公司余额缓存记录到redis

xgb hai 1 día
pai
achega
4d51a774e0

+ 60 - 0
fs-admin/src/main/java/com/fs/course/task/CompanyBalanceTask.java

@@ -0,0 +1,60 @@
+package com.fs.course.task;
+
+import com.fs.course.service.BalanceRollbackErrorService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * @description: 公司余额同步定时任务
+ * @author: Xgb
+ * @createDate: 2025/10/22
+ * @version: 1.0
+ */
+@Component("companyBalanceTask")
+public class CompanyBalanceTask {
+
+
+    @Autowired
+    private BalanceRollbackErrorService balanceRollbackErrorService;
+
+
+    /**
+     * @Description: 每 ? 秒从缓存获取余额同步到公司账户中 todo 待完善
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/22 10:56
+     */
+    public void syncCompanyBalance() {
+
+    }
+
+    /**
+     * @Description: 定时回滚公司余额数据
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/22 11:48
+     */
+    public void processBatchRollbackByCompanyId() {
+        balanceRollbackErrorService.processBatchRollbackByCompanyId();
+    }
+
+    /**
+     * @Description: spring启动执行 查询余额报存到缓存中 当缓存没数据时
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/22 11:52
+     */
+    @EventListener(ApplicationReadyEvent.class)
+    public void initCompanyBalance() {
+        balanceRollbackErrorService.initCompanyBalance();
+
+    }
+
+
+
+}

+ 5 - 0
fs-common/src/main/java/com/fs/common/constant/FsConstants.java

@@ -11,4 +11,9 @@ public interface FsConstants {
 
     String FRIEND_WELCOME_VIDEO_KEY = "friend:welcome:";
     String REDIS_INTEGRAL_ORDER_UNPAY = "integral:order:unpay:";
+
+    // 公司余额redis key "company:money:" + company.getCompanyId()
+    String COMPANY_MONEY_KEY = "company:money:";
+    // 公司余额redis 锁
+    String COMPANY_MONEY_LOCK = "company_money_lock:";
 }

+ 267 - 0
fs-service/src/main/java/com/fs/course/domain/BalanceRollbackError.java

@@ -0,0 +1,267 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * Redis余额数据回滚异常登记表
+ * @TableName balance_rollback_error
+ */
+@TableName(value ="balance_rollback_error")
+public class BalanceRollbackError {
+    /**
+     * 自增主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 公司ID
+     */
+    private Long companyId;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 看客记录ID
+     */
+    private Long logId;
+
+    /**
+     * 视频ID
+     */
+    private Long videoId;
+
+    /**
+     * 状态:0-回滚异常登记,1-已重新回滚
+     */
+    private Integer status;
+
+    /**
+     * 异常金额
+     */
+    private BigDecimal money;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 备注信息
+     */
+    private String remark;
+
+    /**
+     * 自增主键ID
+     */
+    public Long getId() {
+        return id;
+    }
+
+    /**
+     * 自增主键ID
+     */
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    /**
+     * 公司ID
+     */
+    public Long getCompanyId() {
+        return companyId;
+    }
+
+    /**
+     * 公司ID
+     */
+    public void setCompanyId(Long companyId) {
+        this.companyId = companyId;
+    }
+
+    /**
+     * 用户ID
+     */
+    public Long getUserId() {
+        return userId;
+    }
+
+    /**
+     * 用户ID
+     */
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    /**
+     * 看客记录ID
+     */
+    public Long getLogId() {
+        return logId;
+    }
+
+    /**
+     * 看客记录ID
+     */
+    public void setLogId(Long logId) {
+        this.logId = logId;
+    }
+
+    /**
+     * 视频ID
+     */
+    public Long getVideoId() {
+        return videoId;
+    }
+
+    /**
+     * 视频ID
+     */
+    public void setVideoId(Long videoId) {
+        this.videoId = videoId;
+    }
+
+    /**
+     * 状态:0-回滚异常登记,1-已重新回滚
+     */
+    public Integer getStatus() {
+        return status;
+    }
+
+    /**
+     * 状态:0-回滚异常登记,1-已重新回滚
+     */
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    /**
+     * 异常金额
+     */
+    public BigDecimal getMoney() {
+        return money;
+    }
+
+    /**
+     * 异常金额
+     */
+    public void setMoney(BigDecimal money) {
+        this.money = money;
+    }
+
+    /**
+     * 创建时间
+     */
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    /**
+     * 创建时间
+     */
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    /**
+     * 更新时间
+     */
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    /**
+     * 更新时间
+     */
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    /**
+     * 备注信息
+     */
+    public String getRemark() {
+        return remark;
+    }
+
+    /**
+     * 备注信息
+     */
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        BalanceRollbackError other = (BalanceRollbackError) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getCompanyId() == null ? other.getCompanyId() == null : this.getCompanyId().equals(other.getCompanyId()))
+            && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
+            && (this.getLogId() == null ? other.getLogId() == null : this.getLogId().equals(other.getLogId()))
+            && (this.getVideoId() == null ? other.getVideoId() == null : this.getVideoId().equals(other.getVideoId()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getMoney() == null ? other.getMoney() == null : this.getMoney().equals(other.getMoney()))
+            && (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
+            && (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()))
+            && (this.getRemark() == null ? other.getRemark() == null : this.getRemark().equals(other.getRemark()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getCompanyId() == null) ? 0 : getCompanyId().hashCode());
+        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
+        result = prime * result + ((getLogId() == null) ? 0 : getLogId().hashCode());
+        result = prime * result + ((getVideoId() == null) ? 0 : getVideoId().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getMoney() == null) ? 0 : getMoney().hashCode());
+        result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
+        result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
+        result = prime * result + ((getRemark() == null) ? 0 : getRemark().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", companyId=").append(companyId);
+        sb.append(", userId=").append(userId);
+        sb.append(", logId=").append(logId);
+        sb.append(", videoId=").append(videoId);
+        sb.append(", status=").append(status);
+        sb.append(", money=").append(money);
+        sb.append(", createTime=").append(createTime);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append(", remark=").append(remark);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/course/mapper/BalanceRollbackErrorMapper.java

@@ -0,0 +1,18 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.BalanceRollbackError;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author Administrator
+* @description 针对表【balance_rollback_error(Redis余额数据回滚异常登记表)】的数据库操作Mapper
+* @createDate 2025-10-22 10:29:23
+* @Entity com.fs.course.domain.BalanceRollbackError
+*/
+public interface BalanceRollbackErrorMapper extends BaseMapper<BalanceRollbackError> {
+
+}
+
+
+
+

+ 16 - 0
fs-service/src/main/java/com/fs/course/service/BalanceRollbackErrorService.java

@@ -0,0 +1,16 @@
+package com.fs.course.service;
+
+import com.fs.course.domain.BalanceRollbackError;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author Administrator
+* @description 针对表【balance_rollback_error(Redis余额数据回滚异常登记表)】的数据库操作Service
+* @createDate 2025-10-22 10:29:23
+*/
+public interface BalanceRollbackErrorService extends IService<BalanceRollbackError> {
+
+    public void processBatchRollbackByCompanyId();
+
+    void initCompanyBalance();
+}

+ 163 - 0
fs-service/src/main/java/com/fs/course/service/impl/BalanceRollbackErrorServiceImpl.java

@@ -0,0 +1,163 @@
+package com.fs.course.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.constant.FsConstants;
+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.course.domain.BalanceRollbackError;
+import com.fs.course.mapper.BalanceRollbackErrorMapper;
+import com.fs.course.service.BalanceRollbackErrorService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+* @author Administrator
+* @description 针对表【balance_rollback_error(Redis余额数据回滚异常登记表)】的数据库操作Service实现
+* @createDate 2025-10-22 10:29:23
+*/
+@Service
+public class BalanceRollbackErrorServiceImpl extends ServiceImpl<BalanceRollbackErrorMapper, BalanceRollbackError>
+    implements BalanceRollbackErrorService{
+
+
+    private static final Logger logger = LoggerFactory.getLogger(BalanceRollbackErrorServiceImpl.class);
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+    @Override
+    public void processBatchRollbackByCompanyId() {
+        // 按companyId分组查询 余额异常表 中的companyId数据
+        List<Long> companyIds = this.list(
+                        new QueryWrapper<BalanceRollbackError>()
+                                .select("DISTINCT company_id")
+                                .eq("status", 0)
+                ).stream()
+                .map(BalanceRollbackError::getCompanyId) // 假设 getCompanyId 返回 Long 类型
+                .distinct()
+                .collect(Collectors.toList());
+
+
+        for(Long companyId : companyIds){
+            // 按公司查询余额异常表数据 状态为0 缓存回滚 每次查询1000条一次处理
+            int pageSize = 1000;
+            int offset = 0;
+            List<BalanceRollbackError> errorList;
+
+            do {
+                // 分页查询,每次只处理1000条记录
+                errorList = this.list(
+                        new QueryWrapper<BalanceRollbackError>()
+                                .eq("status", 0).eq("company_id", companyId)
+                                .last("LIMIT " + pageSize + " OFFSET " + offset)
+                );
+
+                // 每一千条回滚一次
+                if (!errorList.isEmpty()) {
+                    // 计算 预扣减总金额
+                    BigDecimal totalAmount = errorList.stream()
+                            .map(BalanceRollbackError::getMoney)
+                            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+                    // 执行回滚操作
+                    processBatchRollback(companyId,errorList, totalAmount);
+                }
+
+                offset += pageSize;
+            } while (errorList.size() == pageSize);
+        }
+    }
+
+    @Override
+    public void initCompanyBalance() {
+
+        // 查询公司表 Company
+        List<Company> companyList = companyMapper.selectCompanyAllList();
+        for (Company company : companyList) {
+            String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
+            redisCache.setCacheObject(companyMoneyKey, company.getMoney().toString());
+
+        }
+    }
+
+    /**
+     * @Description: 按公司批量回滚
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/22 11:39
+     */
+    private void processBatchRollback(Long companyId,List<BalanceRollbackError> errorList, BigDecimal totalAmount) {
+        String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + companyId;
+
+        // 加锁:扣减余额
+        RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + companyId);
+        try {
+            if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
+                try {
+                    BigDecimal originalMoney;
+                    // 获取当前余额
+                    String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+                    if (StringUtils.isNotEmpty(moneyStr)) {
+                        originalMoney = new BigDecimal(moneyStr);
+                    }else {
+                        logger.error("获取公司余额缓存异常公司id{},",companyId);
+                        logger.error("获取公司余额缓存异常数据{},",errorList);
+                        throw new RuntimeException("获取公司余额缓存异常");
+                    }
+
+                    // 扣减金额
+                    BigDecimal newMoney = originalMoney.subtract(totalAmount);
+                    redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+
+                } finally {
+                    lock1.unlock();
+                }
+
+                // 批量更新余额异常数据 状态改为1
+                for (BalanceRollbackError error : errorList) {
+                    error.setStatus(1);
+                }
+
+                try {
+                    this.updateBatchById(errorList);
+                } catch(Exception  e){
+                    logger.error("批量更新余额异常数据异常公司id{},",companyId);
+                    logger.error("批量更新余额异常数据异常数据{},",errorList);
+                    throw new RuntimeException("批量更新余额异常数据异常",e);
+                }
+            } else {
+                logger.error("获取锁异常公司id{},",companyId);
+                logger.error("获取锁异常数据{},",errorList);
+                throw new RuntimeException("获取锁失败");
+            }
+        } catch (InterruptedException e) {
+            logger.error("InterruptedException异常公司id{},",companyId);
+            logger.error("InterruptedException异常数据{},",errorList);
+            throw new RuntimeException(e);
+        }
+
+    }
+}
+
+
+
+

+ 123 - 17
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.BeanCopyUtils;
+import com.fs.common.constant.FsConstants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.domain.entity.SysDictData;
@@ -31,7 +32,10 @@ import com.fs.course.domain.*;
 import com.fs.course.dto.CoursePackageDTO;
 import com.fs.course.mapper.*;
 import com.fs.course.param.*;
-import com.fs.course.param.newfs.*;
+import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
+import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
+import com.fs.course.param.newfs.FsUserCourseVideoUParam;
+import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.service.IFsVideoResourceService;
@@ -62,38 +66,29 @@ import com.fs.qwApi.service.QwApiService;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.SopUserLogsInfoMapper;
 import com.fs.sop.service.ISopUserLogsInfoService;
-import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
-import com.google.common.collect.Sets;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang.exception.ExceptionUtils;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
-import org.jetbrains.annotations.NotNull;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
-import org.redisson.client.RedisClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.text.SimpleDateFormat;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
 import java.util.*;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
@@ -232,6 +227,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
     @Autowired
     ConfigUtil configUtil;
 
+    @Autowired
+    private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
+
 
 
     /**
@@ -1485,14 +1483,76 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         //2025.6.19 红包金额为0的时候
         if (amount.compareTo(BigDecimal.ZERO)>0){
 
-            Company company = companyMapper.selectCompanyById(param.getCompanyId());
-            BigDecimal money = company.getMoney();
-            if (money.compareTo(BigDecimal.ZERO)<=0) {
-                return R.error("服务商余额不足,请联系群主服务器充值!");
+            // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
+            // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
+            // 2 另起定时任务 同步缓存余额到redis中
+            // 3 启动系统时查询公司账户余额(这个时候要保证余额正确)保存到redis缓存中
+
+            if(packetParam.getCompanyId()== null){
+                logger.error("发送红包参数错误,公司不能为空,异常请求参数{}",packetParam);
+                return R.error("发送红包失败,请联系管理员");
             }
+            String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();
 
-            // 发送红包
-            R sendRedPacket = paymentService.sendRedPacket(packetParam);
+            // 第一次加锁:预扣减余额
+            RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
+            try {
+                if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
+                    try {
+                        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("服务商余额不足,请联系群主服务器充值!");
+                        }
+
+                        // 预扣减金额
+                        BigDecimal newMoney = originalMoney.subtract(amount);
+                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString(), 2, TimeUnit.HOURS);
+
+                    } finally {
+                        lock1.unlock();
+                    }
+                } else {
+                    logger.error("获取redis锁失败,异常请求参数{}",packetParam);
+                    return R.error("系统繁忙,请稍后重试");
+                }
+            } catch (Exception e) {
+                logger.error("预扣减余额失败: 异常请求参数{},异常信息{}", packetParam, e.getMessage(), e);
+                return R.error("系统异常,请稍后重试");
+            }
+
+            // 预设值异常对象
+            BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
+            balanceRollbackError.setCompanyId(packetParam.getCompanyId());
+            balanceRollbackError.setUserId(user.getUserId());
+            balanceRollbackError.setLogId(log.getLogId());
+            balanceRollbackError.setVideoId(log.getVideoId());
+            balanceRollbackError.setStatus(0);
+            balanceRollbackError.setMoney(amount);
+
+            // 调用第三方接口(锁外操作)
+            R sendRedPacket;
+            try {
+                sendRedPacket= paymentService.sendRedPacket(packetParam);
+            } catch (Exception e) {
+                logger.error("红包发送异常: 异常请求参数{}",packetParam, e);
+                // 异常时回滚余额
+
+                rollbackBalance(balanceRollbackError);
+                return R.error("奖励发送失败,请联系客服");
+            }
+
+            // 红包发送成功处理
             if (sendRedPacket.get("code").equals(200)) {
                 FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                 TransferBillsResult transferBillsResult;
@@ -1524,11 +1584,15 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
                 // 更新观看记录的奖励类型
                 log.setRewardType(config.getRewardType());
                 courseWatchLogMapper.updateFsCourseWatchLog(log);
-
+                // 发送成功,记录日志等操作
                 return sendRedPacket;
             } else {
+                // 发送失败,回滚余额
+                rollbackBalance(balanceRollbackError);
                 return R.error("奖励发送失败,请联系客服");
             }
+
+            // ===================== 本次修改目的为了实时扣减公司余额=====================
         } else {
             FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
             // 添加红包记录
@@ -1555,6 +1619,48 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     }
 
+    /**
+     * @Description: 回滚redis缓存中的余额 异常登记回滚异常表,定时重新回滚
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @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());
+
+        try {
+            if (lock2.tryLock(3, 10, TimeUnit.SECONDS)) {
+                try {
+                    // 获取当前余额
+                    String currentMoneyStr = redisCache.getCacheObject(companyMoneyKey);
+                    if (StringUtils.isNotEmpty(currentMoneyStr)) {
+                        throw new RuntimeException("回滚余额异常");
+                    }
+
+                    // 回滚金额(加回之前扣减的金额)
+                    BigDecimal rollbackMoney = new BigDecimal(currentMoneyStr).add(balanceRollbackError.getMoney());
+                    redisCache.setCacheObject(companyMoneyKey, rollbackMoney.toString(), 2, TimeUnit.HOURS);
+
+                    logger.info("余额回滚成功: companyId={}, amount={}", balanceRollbackError.getCompanyId(), balanceRollbackError.getMoney());
+                } finally {
+                    lock2.unlock();
+                }
+            } else {
+                logger.warn("回滚余额时获取锁失败: companyId={}", balanceRollbackError.getCompanyId());
+                // 登记回滚余额异常表
+                balanceRollbackErrorMapper.insert(balanceRollbackError);
+            }
+        } catch (Exception e) {
+            logger.error("回滚余额时发生异常: companyId={}", balanceRollbackError.getCompanyId(), e);
+            // 登记回滚余额异常表
+            balanceRollbackErrorMapper.insert(balanceRollbackError);
+
+        }
+    }
+
+
     /**
      * 直接发送奖励
      *

+ 24 - 0
fs-service/src/main/resources/mapper/course/BalanceRollbackErrorMapper.xml

@@ -0,0 +1,24 @@
+<?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.course.mapper.BalanceRollbackErrorMapper">
+
+    <resultMap id="BaseResultMap" type="com.fs.course.domain.BalanceRollbackError">
+            <id property="id" column="id" />
+            <result property="companyId" column="company_id" />
+            <result property="userId" column="user_id" />
+            <result property="logId" column="log_id" />
+            <result property="videoId" column="video_id" />
+            <result property="status" column="status" />
+            <result property="money" column="money" />
+            <result property="createTime" column="create_time" />
+            <result property="updateTime" column="update_time" />
+            <result property="remark" column="remark" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,company_id,user_id,log_id,video_id,status,
+        money,create_time,update_time,remark
+    </sql>
+</mapper>