From 910ffa7a5ac82cf6ba2b7e1f1219e95e69aaf95f Mon Sep 17 00:00:00 2001 From: jason <2667446@qq.com> Date: Fri, 22 Sep 2023 19:04:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E9=92=B1=E5=8C=85=E5=85=85=E5=80=BC?= =?UTF-8?q?=E9=80=80=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/pay_wallet.sql | 2 + .../module/pay/enums/ErrorCodeConstants.java | 8 +- .../wallet/PayWalletRechargeController.java | 48 ++++++++ .../AppPayWalletRechargeController.java | 3 - .../dal/dataobject/wallet/PayWalletDO.java | 5 + .../wallet/PayWalletRechargeDO.java | 8 ++ .../pay/dal/mysql/wallet/PayWalletMapper.java | 46 ++++++- .../mysql/wallet/PayWalletRechargeMapper.java | 5 + .../wallet/PayWalletRechargeService.java | 13 ++ .../wallet/PayWalletRechargeServiceImpl.java | 116 ++++++++++++++++++ .../pay/service/wallet/PayWalletService.java | 21 +++- .../service/wallet/PayWalletServiceImpl.java | 40 +++++- 12 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java diff --git a/sql/mysql/pay_wallet.sql b/sql/mysql/pay_wallet.sql index e3d92239c..840da078b 100644 --- a/sql/mysql/pay_wallet.sql +++ b/sql/mysql/pay_wallet.sql @@ -10,6 +10,7 @@ CREATE TABLE `pay_wallet` `balance` int NOT NULL DEFAULT 0 COMMENT '余额,单位分', `total_expense` int NOT NULL DEFAULT 0 COMMENT '累计支出,单位分', `total_recharge` int NOT NULL DEFAULT 0 COMMENT '累计充值,单位分', + `freeze_price` int NOT NULL DEFAULT 0 COMMENT '冻结金额,单位分', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', @@ -62,6 +63,7 @@ CREATE TABLE `pay_wallet_recharge` `refund_pay_price` int NOT NULL DEFAULT 0 COMMENT '退款支付金额', `refund_bonus_price` int NOT NULL DEFAULT 0 COMMENT '退款钱包赠送金额', `refund_time` datetime NULL COMMENT '退款时间', + `refund_status` int NOT NULL DEFAULT 0 COMMENT '退款状态', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 87b046a63..815c18fb9 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -46,6 +46,7 @@ public interface ErrorCodeConstants { ErrorCode WALLET_TRANSACTION_NOT_FOUND = new ErrorCode(1007007002, "未找到对应的钱包交易"); ErrorCode WALLET_REFUND_AMOUNT_ERROR = new ErrorCode(1007007003, "钱包退款金额不对"); ErrorCode WALLET_REFUND_EXIST = new ErrorCode(1007007004, "已经存在钱包退款"); + ErrorCode WALLET_FREEZE_PRICE_NOT_ENOUGH = new ErrorCode(1007007005, "钱包冻结余额不足"); // TODO @jason:把钱包充值,单独搞个错误码段哈; @@ -54,7 +55,12 @@ public interface ErrorCodeConstants { ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR = new ErrorCode(1007007007, "钱包充值更新支付状态失败,支付单编号不匹配"); ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1007007008, "钱包充值更新支付状态失败,支付单状态不是【支付成功】状态"); ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH = new ErrorCode(1007007009, "钱包充值更新支付状态失败,支付单金额不匹配"); - + ErrorCode WALLET_RECHARGE_REFUND_FAIL_NOT_PAID = new ErrorCode(1007900010, "钱包发起退款失败,钱包充值订单未支付"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUNDED = new ErrorCode(1007900011, "钱包发起退款失败,钱包充值订单已退款"); + ErrorCode WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH = new ErrorCode(1007900012, "钱包发起退款失败,钱包余额不足"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1007900013, "钱包退款更新失败,钱包退款单编号不匹配"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1007900014, "钱包退款更新失败,退款订单不存在"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1007900015, "钱包退款更新失败,退款单金额不匹配"); // ========== 示例订单 1007900000 ========== ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在"); ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1007900001, "示例订单更新支付状态失败,订单不是【未支付】状态"); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java new file mode 100644 index 000000000..feaa13447 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.pay.controller.admin.wallet; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.iocoder.yudao.module.pay.service.wallet.PayWalletRechargeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "管理后台 - 钱包充值") +@RestController +@RequestMapping("/pay/wallet-recharge") +@Validated +@Slf4j +public class PayWalletRechargeController { + + @Resource + private PayWalletRechargeService walletRechargeService; + + + @GetMapping("/refund") + @Operation(summary = "发起钱包充值退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundWalletRecharge(@RequestParam("id") Long id) { + walletRechargeService.refundWalletRecharge(id,getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新钱包充值为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录, 内部校验实现 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人 + public CommonResult updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + walletRechargeService.updateWalletRechargeRefunded( + Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayRefundId()); + return success(true); + } +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/AppPayWalletRechargeController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/AppPayWalletRechargeController.java index 6ce0cff3f..d0bb0cfce 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/AppPayWalletRechargeController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/AppPayWalletRechargeController.java @@ -53,7 +53,4 @@ public class AppPayWalletRechargeController { notifyReqDTO.getPayOrderId()); return success(true); } - - // TODO @jason:管理后台,是不是可以搞个发起退款; - } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java index 4536ae635..a3c54c969 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java @@ -42,6 +42,11 @@ public class PayWalletDO extends BaseDO { */ private Integer balance; + /** + * 冻结金额,单位分 + */ + private Integer freezePrice; + /** * 累计支出,单位分 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java index dc56e5614..728d6eaab 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.dal.dataobject.wallet; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -99,4 +100,11 @@ public class PayWalletRechargeDO extends BaseDO { */ private LocalDateTime refundTime; + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer refundStatus; + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java index 47c4c2009..11fc6edbb 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java @@ -56,7 +56,51 @@ public interface PayWalletMapper extends BaseMapperX { .eq(PayWalletDO::getId, id); return update(null, lambdaUpdateWrapper); } - + + /** + * 冻结钱包部分余额 + * @param id 钱包 id + * @param price 冻结金额 + */ + default int freezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price + + ", freeze_price = freeze_price + " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 解冻钱包余额 + * @param id 钱包 id + * @param price 解冻金额 + */ + default int unFreezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", freeze_price = freeze_price - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 当充值退款时, 更新钱包 + * @param id 钱包 id + * @param price 退款金额 + */ + default int updateWhenRechargeRefund(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" freeze_price = freeze_price - " + price + + ", total_recharge = total_recharge - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price) + .ge(PayWalletDO::getTotalRecharge, price);// cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java index 506d978dc..65994b2f3 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java @@ -13,6 +13,11 @@ public interface PayWalletRechargeMapper extends BaseMapperX() + .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getRefundStatus, whereRefundStatus)); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java index 1a167474b..908a77f78 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java @@ -29,4 +29,17 @@ public interface PayWalletRechargeService { */ void updateWalletRechargerPaid(Long id, Long payOrderId); + /** + * 发起钱包充值退款 + * @param id 钱包充值编号 + * @param userIp 用户 ip 地址 + */ + void refundWalletRecharge(Long id, String userIp); + + /** + * 更新钱包充值记录为已退款 + * @param id 钱包充值 id + * @param payRefundId 退款单id + */ + void updateWalletRechargeRefunded(Long id, Long payRefundId); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index 0259627c7..ed9b77c1d 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -1,15 +1,21 @@ package cn.iocoder.yudao.module.pay.service.wallet; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletRechargeConvert; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +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.PayWalletRechargeDO; import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper; import cn.iocoder.yudao.module.pay.enums.member.PayWalletBizTypeEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +30,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.add import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*; /** * 钱包充值 Service 实现类 @@ -47,6 +54,8 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { private PayWalletService payWalletService; @Resource private PayOrderService payOrderService; + @Resource + private PayRefundService payRefundService; @Override @Transactional(rollbackFor = Exception.class) @@ -98,6 +107,113 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { PayWalletBizTypeEnum.RECHARGE, walletRecharge.getTotalPrice()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void refundWalletRecharge(Long id, String userIp) { + // 1.1 获取钱包充值记录 + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[refundWalletRecharge][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以发起退款 + PayWalletDO wallet = validateWalletRechargeCanRefund(walletRecharge); + // 2 冻结退款的余额, 暂时只处理赠送的余额也全部退回 + payWalletService.freezePrice(wallet.getId(), walletRecharge.getTotalPrice()); + // 3 创建退款单 + String walletRechargeId = String.valueOf(id); + String refundId = walletRechargeId + "-refund"; + Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO() + .setAppId(WALLET_PAY_APP_ID).setUserIp(userIp) + .setMerchantOrderId(walletRechargeId) + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); + // 4 更新充值记录退款单号 + walletRechargeMapper.updateById(new PayWalletRechargeDO().setPayRefundId(payRefundId) + .setRefundStatus(WAITING.getStatus()).setId(walletRecharge.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWalletRechargeRefunded(Long id, Long payRefundId) { + // 1.1 获取钱包充值记录 + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[updateWalletRechargerPaid][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以更新已退款 + PayRefundDO payRefund = validateWalletRechargeCanRefunded(walletRecharge, payRefundId); + + PayWalletRechargeDO updateObj = new PayWalletRechargeDO().setId(id); + // 退款成功 + if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + // 2.1 更新钱包余额 + payWalletService.reduceWalletBalance(walletRecharge.getWalletId(), id, + PayWalletBizTypeEnum.RECHARGE_REFUND, walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(SUCCESS.getStatus()).setRefundTime(payRefund.getSuccessTime()) + .setRefundTotalPrice(walletRecharge.getTotalPrice()).setRefundPayPrice(walletRecharge.getPayPrice()) + .setRefundBonusPrice(walletRecharge.getBonusPrice()); + } + // 退款失败 + if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) { + // 2.2 解冻余额 + payWalletService.unFreezePrice(walletRecharge.getWalletId(), walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(FAILURE.getStatus()); + } + // 3. 更新钱包充值的退款字段 + walletRechargeMapper.updateByIdAndRefunded(id, WAITING.getStatus(), updateObj); + } + + private PayRefundDO validateWalletRechargeCanRefunded(PayWalletRechargeDO walletRecharge, Long payRefundId) { + // 1. 校验退款订单匹配 + if (notEqual(walletRecharge.getPayRefundId(), payRefundId)) { + log.error("[validateWalletRechargeCanRefunded][钱包充值({}) 退款单不匹配({}),请进行处理!钱包充值的数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundDO payRefund = payRefundService.getRefund(payRefundId); + if (payRefund == null) { + log.error("[validateWalletRechargeCanRefunded][payRefund({})不存在]", payRefundId); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), walletRecharge.getPayPrice())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) payRefund({}) 退款金额不匹配,请进行处理!钱包数据是:{},payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge), toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.3 校验退款订单商户订单是否匹配 + if (notEqual(payRefund.getMerchantOrderId(), walletRecharge.getId().toString())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + + private PayWalletDO validateWalletRechargeCanRefund(PayWalletRechargeDO walletRecharge) { + // 校验充值订单是否支付 + if (!walletRecharge.getPayStatus()) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_NOT_PAID); + } + // 校验充值订单是否已退款 + if (walletRecharge.getPayRefundId() != null) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUNDED); + } + // 校验钱包余额是否足够 + PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId()); + Assert.notNull(wallet, "用户钱包({}) 不存在", wallet.getId()); + if (wallet.getBalance() < walletRecharge.getTotalPrice()) { + throw exception(WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH); + } + return wallet; + } + private PayOrderDO validateWalletRechargerCanPaid(PayWalletRechargeDO walletRecharge, Long payOrderId) { // 1.1 校验充值记录的支付状态 if (walletRecharge.getPayStatus()) { 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 ed8dcee9f..1703d8e1b 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 @@ -50,15 +50,14 @@ public interface PayWalletService { /** * 扣减钱包余额 * - * @param userId 用户 id - * @param userType 用户类型 + * @param walletId 钱包 id * @param bizId 业务关联 id * @param bizType 业务关联分类 * @param price 扣减金额 * @return 钱包流水 */ - PayWalletTransactionDO reduceWalletBalance(Long userId, Integer userType, - Long bizId, PayWalletBizTypeEnum bizType, Integer price); + PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price); /** * 增加钱包余额 @@ -72,4 +71,18 @@ public interface PayWalletService { PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, PayWalletBizTypeEnum bizType, Integer price); + /** + * 冻结钱包部分余额 + * + * @param id 钱包编号 + * @param price 冻结金额 + */ + void freezePrice(Long id, Integer price); + + /** + * 解冻钱包余额 + * @param id 钱包编号 + * @param price 解冻金额 + */ + void unFreezePrice(Long id, Integer price); } 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 542e567ed..9a83ffb83 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 @@ -68,8 +68,9 @@ public class PayWalletServiceImpl implements PayWalletService { if (orderExtension == null) { throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); } + PayWalletDO wallet = getOrCreateWallet(userId, userType); // 2. 扣减余额 - return reduceWalletBalance(userId, userType, orderExtension.getOrderId(), PAYMENT, price); + return reduceWalletBalance(wallet.getId(), orderExtension.getOrderId(), PAYMENT, price); } @Override @@ -116,10 +117,14 @@ public class PayWalletServiceImpl implements PayWalletService { } @Override - public PayWalletTransactionDO reduceWalletBalance(Long userId, Integer userType, - Long bizId, PayWalletBizTypeEnum bizType, Integer price) { + public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price) { // 1. 获取钱包 - PayWalletDO payWallet = getOrCreateWallet(userId, userType); + PayWalletDO payWallet = getWallet(walletId); + if (payWallet == null) { + log.error("[reduceWalletBalance],用户钱包({})不存在.", walletId); + throw exception(WALLET_NOT_FOUND); + } // 2.1 扣除余额 int updateCounts = 0 ; @@ -129,9 +134,13 @@ public class PayWalletServiceImpl implements PayWalletService { break; } case RECHARGE_REFUND: { - // TODO + updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price); break; } + default: { + // TODO 其它类型待实现 + throw new UnsupportedOperationException("待实现"); + } } if (updateCounts == 0) { throw exception(WALLET_BALANCE_NOT_ENOUGH); @@ -163,7 +172,10 @@ public class PayWalletServiceImpl implements PayWalletService { walletMapper.updateWhenRecharge(payWallet.getId(), price); break; } - // TODO 其它类型;这里可以先跑异常;避免有业务搞错; + default: { + // TODO 其它类型待实现 + throw new UnsupportedOperationException("待实现"); + } } // 2. 生成钱包流水 @@ -173,4 +185,20 @@ public class PayWalletServiceImpl implements PayWalletService { 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); + } + } + } From 7c4fbc63dc21f71ad62ea9c9a05af3e8acafbf50 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 22 Sep 2023 21:56:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?trade=EF=BC=9A=E7=89=A9=E6=B5=81=E8=B4=B9?= =?UTF-8?q?=E7=94=A8=E8=AE=A1=E7=AE=97=EF=BC=8C=E5=A2=9E=E5=8A=A0=E9=87=91?= =?UTF-8?q?=E9=A2=9D=20divide=20=E5=88=86=E6=8B=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeliveryExpressChargeModeEnum.java | 2 +- .../TradeDeliveryPriceCalculator.java | 165 ++++++------------ .../TradeDeliveryPriceCalculatorTest.java | 6 +- 3 files changed, 61 insertions(+), 112 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java index a4b5252ee..7503dd322 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java @@ -16,7 +16,7 @@ import java.util.Arrays; @Getter public enum DeliveryExpressChargeModeEnum implements IntArrayValuable { - PIECE(1, "按件"), + COUNT(1, "按件"), WEIGHT(2,"按重量"), VOLUME(3, "按体积"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java index 7a4669801..20c3feee3 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.member.api.address.AddressApi; import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO; @@ -55,13 +56,13 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { return; } if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { - calculateByPickUp(param, result); + calculateByPickUp(param); } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { calculateExpress(param, result); } } - private void calculateByPickUp(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + private void calculateByPickUp(TradePriceCalculateReqBO param) { if (param.getPickUpStoreId() == null) { throw exception(PRICE_CALCULATE_DELIVERY_PRICE_PICK_UP_STORE_IS_EMPTY); } @@ -82,12 +83,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); // 情况一:全局包邮 - if (isGlobalExpressFree(param, result)) { + if (isGlobalExpressFree(result)) { return; } - // 情况二: - // 2.1 过滤出已选中的商品SKU + // 情况二:快递模版 + // 2.1 过滤出已选中的商品 SKU List selectedItem = filterList(result.getItems(), OrderItem::getSelected); Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); Map expressTemplateMap = @@ -103,11 +104,10 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { /** * 是否全局包邮 * - * @param param 计算信息 * @param result 计算结果 * @return 是否包邮 */ - private boolean isGlobalExpressFree(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) { TradeConfigDO config = tradeConfigService.getTradeConfig(); return config != null && Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮 @@ -118,9 +118,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { Map expressTemplateMap, TradePriceCalculateRespBO result) { // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU - Map> tplIdItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); + Map> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); // 依次计算快递运费 - for (Map.Entry> entry : tplIdItemMap.entrySet()) { + for (Map.Entry> entry : template2ItemMap.entrySet()) { Long templateId = entry.getKey(); List orderItems = entry.getValue(); DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); @@ -128,30 +128,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); continue; } - // 总件数, 总金额, 总重量, 总体积 - int totalCount = 0; - int totalPrice = 0; - double totalWeight = 0; - double totalVolume = 0; - for (OrderItem orderItem : orderItems) { - totalCount += orderItem.getCount(); - totalPrice += orderItem.getPayPrice(); - if (orderItem.getWeight() != null) { - totalWeight += totalWeight + orderItem.getWeight() * orderItem.getCount(); - } - if (orderItem.getVolume() != null) { - totalVolume += totalVolume + orderItem.getVolume() * orderItem.getCount(); - } - } - // 优先判断是否包邮. 如果包邮不计算快递运费 - if (isExpressFree(templateBO.getChargeMode(), totalCount, totalWeight, - totalVolume, totalPrice, templateBO.getFree())) { + // 1. 优先判断是否包邮。如果包邮不计算快递运费 + if (isExpressTemplateFree(orderItems, templateBO.getChargeMode(), templateBO.getFree())) { continue; } - // 计算快递运费 - calculateExpressFeeByChargeMode(totalCount, totalWeight, totalVolume, - templateBO.getChargeMode(), templateBO.getCharge(), orderItems); - + // 2. 计算快递运费 + calculateExpressFeeByChargeMode(orderItems, templateBO.getChargeMode(), templateBO.getCharge()); } TradePriceCalculatorHelper.recountAllPrice(result); } @@ -159,73 +141,44 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { /** * 按配送方式来计算运费 * - * @param totalCount 总件数 - * @param totalWeight 总重量 - * @param totalVolume 总体积 + * @param orderItems SKU 商品项目 * @param chargeMode 配送计费方式 * @param templateCharge 快递运费配置 - * @param orderItems SKU 商品项目 */ - private void calculateExpressFeeByChargeMode(double totalCount, double totalWeight, double totalVolume, - int chargeMode, DeliveryExpressTemplateRespBO.Charge templateCharge, - List orderItems) { + private void calculateExpressFeeByChargeMode(List orderItems, Integer chargeMode, + DeliveryExpressTemplateRespBO.Charge templateCharge) { if (templateCharge == null) { log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); return; } - DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); - switch (chargeModeEnum) { - case PIECE: { - calculateExpressFee(totalCount, templateCharge, orderItems); - break; - } - case WEIGHT: { - calculateExpressFee(totalWeight, templateCharge, orderItems); - break; - } - case VOLUME: { - calculateExpressFee(totalVolume, templateCharge, orderItems); - break; - } - } - } - - /** - * 计算 SKU 商品快递费用 - * - * @param total 总件数/总重量/总体积 - * @param templateCharge 快递运费配置 - * @param orderItems SKU 商品项目 - */ - private void calculateExpressFee(double total, DeliveryExpressTemplateRespBO.Charge templateCharge, List orderItems) { + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); + // 1. 计算 SKU 商品快递费用 int deliveryPrice; - if (total <= templateCharge.getStartCount()) { + if (totalChargeValue <= templateCharge.getStartCount()) { deliveryPrice = templateCharge.getStartPrice(); } else { - double remainWeight = total - templateCharge.getStartCount(); + double remainWeight = totalChargeValue - templateCharge.getStartCount(); // 剩余重量/ 续件 = 续件的次数. 向上取整 int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); int extraPrice = templateCharge.getExtraPrice() * extraNum; deliveryPrice = templateCharge.getStartPrice() + extraPrice; } - // 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 - divideDeliveryPrice(deliveryPrice, orderItems); - } - /** - * 快递运费分摊到每个 SKU 商品上 - * - * @param deliveryPrice 快递运费 - * @param orderItems SKU 商品 - */ - private void divideDeliveryPrice(int deliveryPrice, List orderItems) { - // TODO @jason:分摊的话,是不是要按照比例呀?重量、价格、数量等等, - // 按比例是不是有点复杂。后面看看是否需要; - // TODO 可以看看别的项目怎么搞的哈。 - int dividePrice = deliveryPrice / orderItems.size(); - for (OrderItem item : orderItems) { + // 2. 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 + int remainPrice = deliveryPrice; + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem item = orderItems.get(i); + int partPrice; + double chargeValue = getChargeValue(item, chargeMode); + if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + partPrice = (int) (deliveryPrice * (chargeValue / totalChargeValue)); + remainPrice -= partPrice; + } else { + partPrice = remainPrice; + } + Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); // 更新快递运费 - item.setDeliveryPrice(dividePrice); + item.setDeliveryPrice(partPrice); TradePriceCalculatorHelper.recountPayPrice(item); } } @@ -234,42 +187,38 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { * 检查是否包邮 * * @param chargeMode 配送计费方式 - * @param totalCount 总件数 - * @param totalWeight 总重量 - * @param totalVolume 总体积 - * @param totalPrice 总金额 * @param templateFree 包邮配置 */ - private boolean isExpressFree(Integer chargeMode, int totalCount, double totalWeight, - double totalVolume, int totalPrice, DeliveryExpressTemplateRespBO.Free templateFree) { + private boolean isExpressTemplateFree(List orderItems, Integer chargeMode, + DeliveryExpressTemplateRespBO.Free templateFree) { if (templateFree == null) { return false; } + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); + double totalPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + return totalChargeValue >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice(); + } + + private double getTotalChargeValue(List orderItems, Integer chargeMode) { + double total = 0; + for (OrderItem orderItem : orderItems) { + total += getChargeValue(orderItem, chargeMode); + } + return total; + } + + private double getChargeValue(OrderItem orderItem, Integer chargeMode) { DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); switch (chargeModeEnum) { - case PIECE: - // 两个条件都满足才包邮 - if (totalCount >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice()) { - return true; - } - break; + case COUNT: + return orderItem.getCount(); case WEIGHT: - // freeCount 是不是应该是 double ?? - // TODO @jason:要不配置的时候,把它的单位和商品对齐?到底是 kg、还是斤 - // TODO @芋艿 目前 包邮 件数/重量/体积 都用的是这个字段 - // TODO @jason:那要不快递模版也改成 kg?这样是不是就不用 double ? - if (totalWeight >= templateFree.getFreeCount() - && totalPrice >= templateFree.getFreePrice()) { - return true; - } - break; + return orderItem.getWeight() != null ? orderItem.getWeight() * orderItem.getCount() : 0; case VOLUME: - if (totalVolume >= templateFree.getFreeCount() - && totalPrice >= templateFree.getFreePrice()) { - return true; - } - break; + return orderItem.getVolume() != null ? orderItem.getVolume() * orderItem.getCount() : 0; + default: + throw new IllegalArgumentException(StrUtil.format("未知的计费模式({})", chargeMode)); } - return false; } + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java index 5aa75fd94..3c1c107ed 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java @@ -91,7 +91,7 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { item -> item.setFreeCount(20).setFreePrice(100)); // 准备 SP 运费模板数据 templateRespBO = randomPojo(DeliveryExpressTemplateRespBO.class, - item -> item.setChargeMode(DeliveryExpressChargeModeEnum.PIECE.getType()) + item -> item.setChargeMode(DeliveryExpressChargeModeEnum.COUNT.getType()) .setCharge(chargeBO).setFree(freeBO)); } @@ -144,11 +144,11 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { // 断言:SKU1 assertThat(resultBO.getItems().get(0)) .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") - .containsExactly(100, 2, 0, 0, 0, 1500, 1700); + .containsExactly(100, 2, 0, 0, 0, 500, 700); // 断言:SKU2 assertThat(resultBO.getItems().get(1)) .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") - .containsExactly(200, 10, 0, 0, 0, 1500, 3500); + .containsExactly(200, 10, 0, 0, 0, 2500, 4500); // 断言:SKU3 未选中 assertThat(resultBO.getItems().get(2)) .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")