Browse Source

1、解决货币钱包和钱包退款流水不正确的问题

rayson 8 months ago
parent
commit
091bda24cd

+ 7 - 10
citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/enums/wallet/PayWalletBizTypeEnum.java

@@ -25,11 +25,11 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     RECOMMENDED_POSITIONS(21, "推荐职位"),
     DELIVERY_PERSON(22, "投递人"),
     GIFT(23, "赠与"),
-    NOT_RECOMMENDED(24,"无推荐人,推荐人佣金给到平台")
-    ;
+    NOT_RECOMMENDED(24, "无推荐人,推荐人佣金给到平台");
 
     // TODO 后续增加
 
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayWalletBizTypeEnum::getType).toArray();
     /**
      * 业务分类
      */
@@ -39,14 +39,6 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
      */
     private final String description;
 
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayWalletBizTypeEnum::getType).toArray();
-
-    @Override
-    public int[] array() {
-         return ARRAYS;
-    }
-
-
     /**
      * 根据业务分类获取枚举
      *
@@ -62,4 +54,9 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
         return null;
     }
 
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
 }

+ 3 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/app/currency/vo/transaction/AppPayCurrencyTransactionRespVO.java

@@ -15,6 +15,9 @@ public class AppPayCurrencyTransactionRespVO {
     @Schema(description = "交易金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
     private Long price;
 
+    @Schema(description = "交易后的余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    private Long balance;
+
     @Schema(description = "流水标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆土豆")
     private String title;
 

+ 18 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/redis/RedisKeyConstants.java

@@ -16,6 +16,24 @@ public interface RedisKeyConstants {
      */
     String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
 
+    /**
+     * 支付钱包的分布式锁
+     *
+     * KEY 格式:pay_wallet:lock:%d
+     * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
+     * 过期时间:不固定
+     */
+    String PAY_WALLET_LOCK = "pay_wallet:lock:%d";
+
+    /**
+     * 货币钱包的分布式锁
+     *
+     * KEY 格式:pay_wallet:lock:%d
+     * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
+     * 过期时间:不固定
+     */
+    String PAY_CURRENCY_LOCK = "pay_currency:lock:%d";
+
     /**
      * 支付序号的缓存
      *

+ 43 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/redis/currency/PayCurrencyLockRedisDAO.java

@@ -0,0 +1,43 @@
+package com.citu.module.pay.dal.redis.currency;
+
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import static com.citu.module.pay.dal.redis.RedisKeyConstants.PAY_CURRENCY_LOCK;
+
+
+/**
+ * 支付钱包的锁 Redis DAO
+ *
+ * @author Rayson
+ */
+@Repository
+public class PayCurrencyLockRedisDAO {
+
+    @Resource
+    private RedissonClient redissonClient;
+
+    private static String formatKey(Long id) {
+        return String.format(PAY_CURRENCY_LOCK, id);
+    }
+
+    public <V> V lock(Long id, Long timeoutMillis, Callable<V> callable) throws Exception {
+        String lockKey = formatKey(id);
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw e;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+}

+ 43 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java

@@ -0,0 +1,43 @@
+package com.citu.module.pay.dal.redis.wallet;
+
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import static com.citu.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK;
+
+
+/**
+ * 支付钱包的锁 Redis DAO
+ *
+ * @author Rayson
+ */
+@Repository
+public class PayWalletLockRedisDAO {
+
+    @Resource
+    private RedissonClient redissonClient;
+
+    private static String formatKey(Long id) {
+        return String.format(PAY_WALLET_LOCK, id);
+    }
+
+    public <V> V lock(Long id, Long timeoutMillis, Callable<V> callable) throws Exception {
+        String lockKey = formatKey(id);
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw e;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+}

+ 39 - 26
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/currency/PayCurrencyServiceImpl.java

@@ -2,23 +2,25 @@ package com.citu.module.pay.service.currency;
 
 import cn.hutool.core.lang.Assert;
 import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.date.DateUtils;
 import com.citu.module.pay.controller.admin.currency.vo.currency.PayCurrencyPageReqVO;
 import com.citu.module.pay.dal.dataobject.currency.PayCurrencyDO;
 import com.citu.module.pay.dal.dataobject.currency.PayCurrencyTransactionDO;
 import com.citu.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import com.citu.module.pay.dal.dataobject.refund.PayRefundDO;
 import com.citu.module.pay.dal.mysql.currency.PayCurrencyMapper;
+import com.citu.module.pay.dal.redis.currency.PayCurrencyLockRedisDAO;
 import com.citu.module.pay.enums.currency.PayCurrencyBizTypeEnum;
 import com.citu.module.pay.service.currency.bo.CurrencyTransactionCreateReqBO;
 import com.citu.module.pay.service.order.PayOrderService;
 import com.citu.module.pay.service.refund.PayRefundService;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
 
 import static com.citu.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static com.citu.module.pay.enums.ErrorCodeConstants.*;
@@ -33,9 +35,14 @@ import static com.citu.module.pay.enums.currency.PayCurrencyBizTypeEnum.PAYMENT_
 @Service
 @Slf4j
 public class PayCurrencyServiceImpl implements PayCurrencyService {
-
+    /**
+     * 通知超时时间,单位:毫秒
+     */
+    public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
     @Resource
     private PayCurrencyMapper currencyMapper;
+    @Resource
+    private PayCurrencyLockRedisDAO lockRedisDAO;
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
@@ -120,6 +127,8 @@ public class PayCurrencyServiceImpl implements PayCurrencyService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @SneakyThrows
     public PayCurrencyTransactionDO reduceCurrencyBalance(Long currencyId, Long bizId,
                                                           PayCurrencyBizTypeEnum bizType, Long price) {
         // 1. 获取货币账户
@@ -128,32 +137,36 @@ public class PayCurrencyServiceImpl implements PayCurrencyService {
             log.error("[reduceCurrencyBalance],用户货币账户({})不存在.", currencyId);
             throw exception(WALLET_NOT_FOUND);
         }
-
-        // 2.1 扣除余额
-        int updateCounts;
-        switch (bizType) {
-            case PAYMENT: {
-                updateCounts = currencyMapper.updateWhenConsumption(payCurrency.getId(), price);
-                break;
+        // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
+        return lockRedisDAO.lock(currencyId, UPDATE_TIMEOUT_MILLIS, () -> {
+            //  扣除余额
+            int updateCounts;
+            switch (bizType) {
+                case PAYMENT: {
+                    updateCounts = currencyMapper.updateWhenConsumption(payCurrency.getId(), price);
+                    break;
+                }
+                case RECHARGE_REFUND: {
+                    updateCounts = currencyMapper.updateWhenRechargeRefund(payCurrency.getId(), price);
+                    break;
+                }
+                default: {
+                    // TODO 其它类型待实现
+                    throw new UnsupportedOperationException("待实现");
+                }
             }
-            case RECHARGE_REFUND: {
-                updateCounts = currencyMapper.updateWhenRechargeRefund(payCurrency.getId(), price);
-                break;
+            if (updateCounts == 0) {
+                throw exception(WALLET_BALANCE_NOT_ENOUGH);
             }
-            default: {
-                // TODO 其它类型待实现
-                throw new UnsupportedOperationException("待实现");
-            }
-        }
-        if (updateCounts == 0) {
-            throw exception(WALLET_BALANCE_NOT_ENOUGH);
-        }
-        // 2.2 生成货币账户流水
-        Long afterBalance = payCurrency.getBalance() - price;
-        CurrencyTransactionCreateReqBO bo = new CurrencyTransactionCreateReqBO().setCurrencyId(payCurrency.getId())
-                .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
-                .setBizType(bizType.getType()).setTitle(bizType.getDescription());
-        return currencyTransactionService.createCurrencyTransaction(bo);
+            // 2.2 生成货币账户流水
+            // TODO 加上payCurrency.getFreezePrice()冻结金额是因为创建退款单的时候就已经冻结的;
+            //  并且扣了payCurrency.getBalance(),如果这个时候不加则会导致扣除两笔钱的流水记录(但实际扣了一笔钱)
+            Long afterBalance = (payCurrency.getBalance() + payCurrency.getFreezePrice()) - price;
+            CurrencyTransactionCreateReqBO bo = new CurrencyTransactionCreateReqBO().setCurrencyId(payCurrency.getId())
+                    .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
+                    .setBizType(bizType.getType()).setTitle(bizType.getDescription());
+            return currencyTransactionService.createCurrencyTransaction(bo);
+        });
     }
 
     @Override

+ 40 - 24
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/wallet/PayWalletServiceImpl.java

@@ -2,16 +2,19 @@ package com.citu.module.pay.service.wallet;
 
 import cn.hutool.core.lang.Assert;
 import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.date.DateUtils;
 import com.citu.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
 import com.citu.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import com.citu.module.pay.dal.dataobject.refund.PayRefundDO;
 import com.citu.module.pay.dal.dataobject.wallet.PayWalletDO;
 import com.citu.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
 import com.citu.module.pay.dal.mysql.wallet.PayWalletMapper;
+import com.citu.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
 import com.citu.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import com.citu.module.pay.service.order.PayOrderService;
 import com.citu.module.pay.service.refund.PayRefundService;
 import com.citu.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
@@ -34,8 +37,15 @@ import static com.citu.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT_REFU
 @Slf4j
 public class PayWalletServiceImpl implements PayWalletService {
 
+    /**
+     * 通知超时时间,单位:毫秒
+     */
+    public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
+
     @Resource
     private PayWalletMapper walletMapper;
+    @Resource
+    private PayWalletLockRedisDAO lockRedisDAO;
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
@@ -121,6 +131,8 @@ public class PayWalletServiceImpl implements PayWalletService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @SneakyThrows
     public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
                                                       PayWalletBizTypeEnum bizType, Integer price) {
         // 1. 获取钱包
@@ -129,32 +141,36 @@ public class PayWalletServiceImpl implements PayWalletService {
             log.error("[reduceWalletBalance],用户钱包({})不存在.", walletId);
             throw exception(WALLET_NOT_FOUND);
         }
-
-        // 2.1 扣除余额
-        int updateCounts;
-        switch (bizType) {
-            case PAYMENT: {
-                updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
-                break;
+        // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
+        return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
+            // 扣除余额
+            int updateCounts;
+            switch (bizType) {
+                case PAYMENT: {
+                    updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
+                    break;
+                }
+                case RECHARGE_REFUND: {
+                    updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
+                    break;
+                }
+                default: {
+                    // TODO 其它类型待实现
+                    throw new UnsupportedOperationException("待实现");
+                }
             }
-            case RECHARGE_REFUND: {
-                updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
-                break;
+            if (updateCounts == 0) {
+                throw exception(WALLET_BALANCE_NOT_ENOUGH);
             }
-            default: {
-                // TODO 其它类型待实现
-                throw new UnsupportedOperationException("待实现");
-            }
-        }
-        if (updateCounts == 0) {
-            throw exception(WALLET_BALANCE_NOT_ENOUGH);
-        }
-        // 2.2 生成钱包流水
-        Integer afterBalance = payWallet.getBalance() - price;
-        WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
-                .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
-                .setBizType(bizType.getType()).setTitle(bizType.getDescription());
-        return walletTransactionService.createWalletTransaction(bo);
+            // 2.2 生成钱包流水
+            // TODO 加上payWallet.getFreezePrice()冻结金额是因为创建退款单的时候就已经冻结的;
+            //  并且扣了payWallet.getBalance(),如果这个时候不加则会导致扣除两笔钱的流水记录(但实际扣了一笔钱)
+            Integer afterBalance = (payWallet.getBalance()+payWallet.getFreezePrice()) - price;
+            WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
+                    .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
+                    .setBizType(bizType.getType()).setTitle(bizType.getDescription());
+            return walletTransactionService.createWalletTransaction(bo);
+        });
     }
 
     @Override