2026-06-25 a26b31cc9f3ee9b21b1a754e80fa7359e8a7a8f8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package cn.iocoder.yudao.module.pay.service.wallet;
 
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper;
import cn.iocoder.yudao.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
import jakarta.annotation.Resource;
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 java.time.LocalDateTime;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT;
import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT_REFUND;
 
/**
 * 钱包 Service 实现类
 *
 * @author jason
 */
@Service
@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 // 延迟加载,避免循环依赖
    private PayWalletTransactionService walletTransactionService;
    @Resource
    @Lazy // 延迟加载,避免循环依赖
    private PayOrderService orderService;
    @Resource
    @Lazy // 延迟加载,避免循环依赖
    private PayRefundService refundService;
 
    @Override
    @SneakyThrows
    public PayWalletDO getOrCreateWallet(Long userId, Integer userType) {
        PayWalletDO wallet = walletMapper.selectByUserIdAndType(userId, userType);
        if (wallet == null) {
            // 使用双重检查锁,保证钱包创建并发问题
            // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1475/files
            wallet = lockRedisDAO.lock(userId, UPDATE_TIMEOUT_MILLIS, () -> {
                PayWalletDO newWallet = walletMapper.selectByUserIdAndType(userId, userType);
                if (newWallet == null) {
                    newWallet = new PayWalletDO().setUserId(userId).setUserType(userType)
                            .setBalance(0).setTotalExpense(0).setTotalRecharge(0);
                    newWallet.setCreateTime(LocalDateTime.now());
                    walletMapper.insert(newWallet);
                }
                return newWallet;
            });
        }
        return wallet;
    }
 
    @Override
    public PayWalletDO getWallet(Long walletId) {
        return walletMapper.selectById(walletId);
    }
 
    @Override
    public PageResult<PayWalletDO> getWalletPage(PayWalletPageReqVO pageReqVO) {
        return walletMapper.selectPage(pageReqVO);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayWalletTransactionDO orderPay(Long walletId, String outTradeNo, Integer price) {
        // 1. 判断支付交易拓展单是否存
        PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo);
        if (orderExtension == null) {
            throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
        }
        PayWalletDO wallet = walletMapper.selectById(walletId);
        // 2. 扣减余额
        return reduceWalletBalance(wallet.getId(), orderExtension.getOrderId(), PAYMENT, price);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayWalletTransactionDO orderRefund(String outRefundNo, Integer refundPrice, String reason) {
        // 1.1 判断退款单是否存在
        PayRefundDO payRefund = refundService.getRefundByNo(outRefundNo);
        if (payRefund == null) {
            throw exception(REFUND_NOT_FOUND);
        }
        // 1.2 校验是否可以退款
        Long walletId = validateWalletCanRefund(payRefund.getId(), payRefund.getChannelOrderNo());
        PayWalletDO wallet = walletMapper.selectById(walletId);
        Assert.notNull(wallet, "钱包 {} 不存在", walletId);
 
        // 2. 增加余额
        return addWalletBalance(walletId, String.valueOf(payRefund.getId()), PAYMENT_REFUND, refundPrice);
    }
 
    /**
     * 校验是否能退款
     *
     * @param refundId 支付退款单 id
     * @param walletPayNo 钱包支付 no
     */
    private Long validateWalletCanRefund(Long refundId, String walletPayNo) {
        // 1. 校验钱包支付交易存在
        PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransactionByNo(walletPayNo);
        if (walletTransaction == null) {
            throw exception(WALLET_TRANSACTION_NOT_FOUND);
        }
        // 2. 校验退款是否存在
        PayWalletTransactionDO refundTransaction = walletTransactionService.getWalletTransaction(
                String.valueOf(refundId), PAYMENT_REFUND);
        if (refundTransaction != null) {
            throw exception(WALLET_REFUND_EXIST);
        }
        return walletTransaction.getWalletId();
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    @SneakyThrows
    public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
                                                      PayWalletBizTypeEnum bizType, Integer price) {
        // 1. 获取钱包
        PayWalletDO payWallet = getWallet(walletId);
        if (payWallet == null) {
            log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId);
            throw exception(WALLET_NOT_FOUND);
        }
 
        // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
        return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
            // 2. 扣除余额
            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("待实现");
                }
            }
            if (updateCounts == 0) {
                throw exception(WALLET_BALANCE_NOT_ENOUGH);
            }
 
            // 3. 生成钱包流水
            // 情况一:充值退款:balance 在冻结时已扣,updateWhenRechargeRefund 只扣 freeze_price,所以 afterBalance 不变。https://t.zsxq.com/OJk9m
            // 情况二:消费支付:updateWhenConsumption 从 balance 扣,所以 afterBalance = balance - price
            Integer afterBalance = bizType == PayWalletBizTypeEnum.RECHARGE_REFUND
                    ? payWallet.getBalance()
                    : 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);
        });
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    @SneakyThrows
    public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
                                                   PayWalletBizTypeEnum bizType, Integer price) {
        // 1. 获取钱包
        PayWalletDO payWallet = getWallet(walletId);
        if (payWallet == null) {
            log.error("[addWalletBalance][用户钱包({})不存在]", walletId);
            throw exception(WALLET_NOT_FOUND);
        }
 
        // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
        return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
            // 3. 更新钱包金额
            switch (bizType) {
                case PAYMENT_REFUND: { // 退款更新
                    walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
                    break;
                }
                case RECHARGE: { // 充值更新
                    walletMapper.updateWhenRecharge(payWallet.getId(), price);
                    break;
                }
                case UPDATE_BALANCE: // 更新余额
                case TRANSFER: // 分佣提现
                    walletMapper.updateWhenAdd(payWallet.getId(), price);
                    break;
                default: {
                    throw new UnsupportedOperationException("待实现:" + bizType);
                }
            }
 
            // 4. 生成钱包流水
            WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
                    .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
                    .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());
            return walletTransactionService.createWalletTransaction(transactionCreateReqBO);
        });
    }
 
    @Override
    public void freezePrice(Long id, Integer price) {
        int updateCounts = walletMapper.freezePrice(id, price);
        if (updateCounts == 0) {
            throw exception(WALLET_BALANCE_NOT_ENOUGH);
        }
    }
 
    @Override
    public void unfreezePrice(Long id, Integer price) {
        int updateCounts = walletMapper.unFreezePrice(id, price);
        if (updateCounts == 0) {
            throw exception(WALLET_FREEZE_PRICE_NOT_ENOUGH);
        }
    }
 
}