From 38fd2ecc6a9bb57919d76821887f6a13250c1002 Mon Sep 17 00:00:00 2001 From: XinWei <2718030729@qq.com> Date: Tue, 8 Oct 2024 14:10:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BD=99=E9=A2=9D=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BD=BF=E7=94=A8=E8=8A=8B=E9=81=93=E6=96=B0=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/user/UserBalanceUpdateForm.vue | 84 +++++++----- .../enums/wallet/PayWalletBizTypeEnum.java | 4 +- .../admin/wallet/PayWalletController.java | 34 +++-- .../wallet/PayWalletUpdateBalanceReqVO.java | 20 +++ .../pay/dal/redis/RedisKeyConstants.java | 9 +- .../redis/wallet/PayWalletLockRedisDAO.java | 43 ++++++ .../pay/service/wallet/PayWalletService.java | 12 +- .../service/wallet/PayWalletServiceImpl.java | 128 ++++++++++-------- 8 files changed, 224 insertions(+), 110 deletions(-) create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java diff --git a/yudao-admin-vue3/src/views/member/user/UserBalanceUpdateForm.vue b/yudao-admin-vue3/src/views/member/user/UserBalanceUpdateForm.vue index 372ff7a..36137cf 100644 --- a/yudao-admin-vue3/src/views/member/user/UserBalanceUpdateForm.vue +++ b/yudao-admin-vue3/src/views/member/user/UserBalanceUpdateForm.vue @@ -1,20 +1,20 @@ - diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java index a8937fb..61e7f8d 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java @@ -19,8 +19,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable { RECHARGE_REFUND(2, "充值退款"), PAYMENT(3, "支付"), PAYMENT_REFUND(4, "支付退款"), - ADMIN_MODIFY(5, "管理员修改"); - +// ADMIN_MODIFY(5, "管理员修改"); + UPDATE_BALANCE(5, "更新余额"); // TODO 后续增加 /** diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java index cf08e42..f3d8ed0 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java @@ -2,12 +2,10 @@ package cn.iocoder.yudao.module.pay.controller.admin.wallet; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; -import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; -import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserBalanceVo; -import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.*; import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert; import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,7 +18,9 @@ import javax.annotation.Resource; import javax.validation.Valid; import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; @Tag(name = "管理后台 - 用户钱包") @RestController @@ -40,11 +40,27 @@ public class PayWalletController { return success(PayWalletConvert.INSTANCE.convert02(wallet)); } - @PostMapping("/update") - @PreAuthorize("@ss.hasPermission('pay:wallet:update')") - @Operation(summary = "修改用户钱包余额(后台操作)") - public CommonResult updateWallet(@Valid @RequestBody PayWalletUserBalanceVo reqVo){ - payWalletService.updateWallet(reqVo); +// @PostMapping("/update") +// @PreAuthorize("@ss.hasPermission('pay:wallet:update')") +// @Operation(summary = "修改用户钱包余额(后台操作)") +// public CommonResult updateWallet(@Valid @RequestBody PayWalletUserBalanceVo reqVo){ +// payWalletService.updateWallet(reqVo); +// return success(true); +// } + @PutMapping("/update-balance") + @Operation(summary = "更新会员用户余额") + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')") + public CommonResult updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) { + // 获得用户钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue()); + if (wallet == null) { + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO); + throw exception(WALLET_NOT_FOUND); + } + + // 更新钱包余额 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()), + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance()); return success(true); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java new file mode 100644 index 0000000..3fb7eae --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 修改钱包余额 Request VO") +@Data +public class PayWalletUpdateBalanceReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "变动余额不能为空") + private Integer balance; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java index 30081c6..7f7aefa 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java @@ -15,7 +15,14 @@ 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"; /** * 支付序号的缓存 * diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java new file mode 100644 index 0000000..dda2f64 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.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 cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK; + + +/** + * 支付钱包的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayWalletLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public V lock(Long id, Long timeoutMillis, Callable 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(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_WALLET_LOCK, id); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java index db5b784..89b27af 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java @@ -98,10 +98,10 @@ public interface PayWalletService { */ void unfreezePrice(Long id, Integer price); - /** - * 修改钱包余额(后台操作) - * @param reqVo - * @return void - */ - void updateWallet(PayWalletUserBalanceVo reqVo); +// /** +// * 修改钱包余额(后台操作) +// * @param reqVo +// * @return void +// */ +// void updateWallet(PayWalletUserBalanceVo reqVo); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java index 1a011fb..c33a887 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java @@ -2,6 +2,7 @@ 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.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserBalanceVo; @@ -10,10 +11,12 @@ 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 lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -36,7 +39,10 @@ import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYM @Service @Slf4j public class PayWalletServiceImpl implements PayWalletService { - + /** + * 通知超时时间,单位:毫秒 + */ + public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; @Resource private PayWalletMapper walletMapper; @@ -50,6 +56,9 @@ public class PayWalletServiceImpl implements PayWalletService { @Lazy // 延迟加载,避免循环依赖 private PayRefundService refundService; + @Resource + private PayWalletLockRedisDAO lockRedisDAO; + @Override public PayWalletDO getOrCreateWallet(Long userId, Integer userType) { PayWalletDO wallet = walletMapper.selectByUserIdAndType(userId, userType); @@ -161,35 +170,44 @@ public class PayWalletServiceImpl implements PayWalletService { } @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, PayWalletBizTypeEnum bizType, Integer price) { - // 1.1 获取钱包 + // 1. 获取钱包 PayWalletDO payWallet = getWallet(walletId); if (payWallet == null) { - log.error("[addWalletBalance],用户钱包({})不存在.", walletId); + log.error("[addWalletBalance][用户钱包({})不存在]", walletId); throw exception(WALLET_NOT_FOUND); } - // 1.2 更新钱包金额 - switch (bizType) { - case PAYMENT_REFUND: { // 退款更新 - walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price); - break; - } - case RECHARGE: { // 充值更新 - walletMapper.updateWhenRecharge(payWallet.getId(), price); - break; - } - default: { - // TODO 其它类型待实现 - throw new UnsupportedOperationException("待实现"); - } - } - // 2. 生成钱包流水 - 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); + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯) + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> { + // 2. 更新钱包金额 + switch (bizType) { + case PAYMENT_REFUND: { // 退款更新 + walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price); + break; + } + case RECHARGE: { // 充值更新 + walletMapper.updateWhenRecharge(payWallet.getId(), price); + break; + } + case UPDATE_BALANCE: // 更新余额 + walletMapper.updateWhenRecharge(payWallet.getId(), price); + break; + default: { + // TODO 其它类型待实现 + throw new UnsupportedOperationException("待实现"); + } + } + + // 3. 生成钱包流水 + 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 @@ -208,37 +226,37 @@ public class PayWalletServiceImpl implements PayWalletService { } } - @Override - public void updateWallet(PayWalletUserBalanceVo reqVo) { - // 如果 - if (reqVo.getBalance().compareTo(BigDecimal.ZERO) == 0) { - return; - } - // 把单位从元转为分 - BigDecimal change = new BigDecimal("100"); - // 查出对应钱包信息 - PayWalletDO walletDO = walletMapper.selectById(reqVo.getId()); - if (reqVo.getBalance().compareTo(new BigDecimal("21474836.47")) > 0) { - throw exception(WALLET_RECHARGE_RANGE_EXCEPTION); - } - // 总共改变的金额 - long changeBalance = (reqVo.getBalance().multiply(change)).longValue(); - // 总余额 - long totalBalance = walletDO.getBalance() + changeBalance; - // 总充值 - long totalRecharge = walletDO.getTotalRecharge() + changeBalance; - if (totalBalance > 2147483647 || totalRecharge > 2147483647 || totalBalance < 0 || totalRecharge < 0) { - throw exception(WALLET_RECHARGE_RANGE_EXCEPTION); - } - walletDO.setBalance((int) totalBalance); - walletDO.setTotalRecharge((int) totalRecharge); - walletMapper.updateById(walletDO); - String title = "后台操作给用户" + (totalBalance > 0 ? "增加" : "减少") + "余额" + Math.abs(changeBalance)/100.0 + "元"; - WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO() - .setWalletId(reqVo.getId()).setPrice((int) changeBalance).setBalance((int) totalBalance) - .setBizType(PayWalletBizTypeEnum.ADMIN_MODIFY.getType()).setBizId("0").setTitle(title); - walletTransactionService.createWalletTransaction(transactionCreateReqBO); - - } +// @Override +// public void updateWallet(PayWalletUserBalanceVo reqVo) { +// // 如果 +// if (reqVo.getBalance().compareTo(BigDecimal.ZERO) == 0) { +// return; +// } +// // 把单位从元转为分 +// BigDecimal change = new BigDecimal("100"); +// // 查出对应钱包信息 +// PayWalletDO walletDO = walletMapper.selectById(reqVo.getId()); +// if (reqVo.getBalance().compareTo(new BigDecimal("21474836.47")) > 0) { +// throw exception(WALLET_RECHARGE_RANGE_EXCEPTION); +// } +// // 总共改变的金额 +// long changeBalance = (reqVo.getBalance().multiply(change)).longValue(); +// // 总余额 +// long totalBalance = walletDO.getBalance() + changeBalance; +// // 总充值 +// long totalRecharge = walletDO.getTotalRecharge() + changeBalance; +// if (totalBalance > 2147483647 || totalRecharge > 2147483647 || totalBalance < 0 || totalRecharge < 0) { +// throw exception(WALLET_RECHARGE_RANGE_EXCEPTION); +// } +// walletDO.setBalance((int) totalBalance); +// walletDO.setTotalRecharge((int) totalRecharge); +// walletMapper.updateById(walletDO); +// String title = "后台操作给用户" + (totalBalance > 0 ? "增加" : "减少") + "余额" + Math.abs(changeBalance)/100.0 + "元"; +// WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO() +// .setWalletId(reqVo.getId()).setPrice((int) changeBalance).setBalance((int) totalBalance) +// .setBizType(PayWalletBizTypeEnum.ADMIN_MODIFY.getType()).setBizId("0").setTitle(title); +// walletTransactionService.createWalletTransaction(transactionCreateReqBO); +// +// } }