浏览代码

Merge remote-tracking branch 'origin/feature/mall_product' into brokerate

owen 1 年之前
父节点
当前提交
dc46dff62c
共有 15 个文件被更改,包括 361 次插入127 次删除
  1. 2 0
      sql/mysql/pay_wallet.sql
  2. 1 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java
  3. 57 108
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
  4. 3 3
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java
  5. 7 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
  6. 48 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java
  7. 0 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/AppPayWalletRechargeController.java
  8. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java
  9. 8 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java
  10. 45 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java
  11. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java
  12. 13 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java
  13. 116 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
  14. 17 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java
  15. 34 6
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java

+ 2 - 0
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 '更新者',

+ 1 - 1
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, "按体积");
 

+ 57 - 108
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<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
         Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
         Map<Long, DeliveryExpressTemplateRespBO> 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<Long, DeliveryExpressTemplateRespBO> expressTemplateMap,
                                         TradePriceCalculateRespBO result) {
         // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU
-        Map<Long, List<OrderItem>> tplIdItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
+        Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
         // 依次计算快递运费
-        for (Map.Entry<Long, List<OrderItem>> entry : tplIdItemMap.entrySet()) {
+        for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
             Long templateId  = entry.getKey();
             List<OrderItem> 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<OrderItem> orderItems) {
+    private void calculateExpressFeeByChargeMode(List<OrderItem> 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<OrderItem> 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<OrderItem> 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<OrderItem> 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<OrderItem> 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;
     }
+
 }

+ 3 - 3
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")

+ 7 - 1
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, "示例订单更新支付状态失败,订单不是【未支付】状态");

+ 48 - 0
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<Boolean> 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<Boolean> updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
+        walletRechargeService.updateWalletRechargeRefunded(
+                Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayRefundId());
+        return success(true);
+    }
+}

+ 0 - 3
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:管理后台,是不是可以搞个发起退款;
-
 }

+ 5 - 0
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;
+
     /**
      * 累计支出,单位分
      */

+ 8 - 0
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;
+
 }

+ 45 - 1
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<PayWalletDO> {
                 .eq(PayWalletDO::getId, id);
         return update(null, lambdaUpdateWrapper);
     }
-    
+
+    /**
+     * 冻结钱包部分余额
+     * @param id 钱包 id
+     * @param price 冻结金额
+     */
+    default int freezePrice(Long id, Integer price){
+        LambdaUpdateWrapper<PayWalletDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<PayWalletDO>()
+                .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<PayWalletDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<PayWalletDO>()
+                .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<PayWalletDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<PayWalletDO>()
+                .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);
+    }
+
+
 }
 
 

+ 5 - 0
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<PayWalletRechargeDO
                 .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getPayStatus, wherePayStatus));
     }
 
+    default  int updateByIdAndRefunded(Long id, Integer whereRefundStatus, PayWalletRechargeDO updateObj){
+        return update(updateObj, new LambdaQueryWrapperX<PayWalletRechargeDO>()
+                .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getRefundStatus, whereRefundStatus));
+    }
+
 }
 
 

+ 13 - 0
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);
 }

+ 116 - 0
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()) {

+ 17 - 4
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);
 }

+ 34 - 6
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);
+        }
+    }
+
 }