소스 검색

支付退款申请,支付宝手机wap 相应实现

jason 3 년 전
부모
커밋
7d6f205dc0
41개의 변경된 파일1515개의 추가작업 그리고 57개의 파일을 삭제
  1. 35 0
      sql/pay_refund.sql
  2. 38 1
      sql/ruoyi-vue-pro.sql
  3. 26 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/order/PayRefundCoreConvert.java
  4. 2 2
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderDO.java
  5. 93 28
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayRefundDO.java
  6. 21 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayRefundMapper.java
  7. 10 4
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java
  8. 20 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayRefundStatusEnum.java
  9. 1 1
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayRefundTypeEnum.java
  10. 8 1
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/impl/PayNotifyCoreServiceImpl.java
  11. 5 2
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayRefundOrderReqVO.java
  12. 3 3
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java
  13. 40 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundAbstractChannelPostHandler.java
  14. 24 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundChannelPostHandler.java
  15. 20 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java
  16. 96 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundPostReqBO.java
  17. 43 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundReqBO.java
  18. 23 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundRespBO.java
  19. 62 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelFailedHandler.java
  20. 45 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelNotifyHandler.java
  21. 54 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java
  22. 52 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelRetryHandler.java
  23. 64 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelSuccessHandler.java
  24. 203 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java
  25. 50 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/util/PaySeqUtils.java
  26. 9 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  27. 4 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayNotifyDataDTO.java
  28. 78 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java
  29. 73 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java
  30. 36 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  31. 9 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
  32. 68 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
  33. 9 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java
  34. 50 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRespEnum.java
  35. 42 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayRefundController.java
  36. 35 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/vo/PayRefundReqVO.java
  37. 22 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/vo/PayRefundRespVO.java
  38. 22 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/order/PayRefundConvert.java
  39. 6 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/package-info.java
  40. 1 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md
  41. 13 2
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/shop/controller/ShopOrderController.java

+ 35 - 0
sql/pay_refund.sql

@@ -0,0 +1,35 @@
+DROP TABLE IF EXISTS `pay_refund`;
+CREATE TABLE `pay_refund` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '支付退款编号',
+  `req_no` varchar(64) NOT NULL COMMENT '退款单请求号',
+  `merchant_id` bigint NOT NULL COMMENT '商户编号',
+  `app_id` bigint NOT NULL COMMENT '应用编号',
+  `channel_id` bigint NOT NULL COMMENT '渠道编号',
+  `channel_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '渠道编码',
+  `order_id` bigint NOT NULL COMMENT '支付订单编号 pay_order 表id',
+  `trade_no` varchar(64) NOT NULL COMMENT '交易订单号 pay_extension 表no 字段',
+  `merchant_order_id` varchar(64) NOT NULL COMMENT '商户订单编号(商户系统生成)',
+  `merchant_refund_no` varchar(64) NOT NULL COMMENT '商户退款订单号(商户系统生成)',
+  `notify_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '异步通知商户地址',
+  `notify_status` tinyint NOT NULL COMMENT '通知商户退款结果的回调状态',
+  `status` tinyint NOT NULL COMMENT '退款状态',
+  `type` tinyint NOT NULL COMMENT '退款类型(部分退款,全部退款)',
+  `pay_amount` bigint NOT NULL COMMENT '支付金额,单位分',
+  `refund_amount` bigint NOT NULL COMMENT '退款金额,单位分',
+  `reason` VARCHAR(256) NOT NULL COMMENT '退款原因',
+  `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户 IP',
+  `channel_order_no` varchar(64) NOT NULL COMMENT '渠道订单号,pay_order 中的channel_order_no 对应',
+  `channel_refund_no` varchar(64) DEFAULT NULL COMMENT '渠道退款单号,渠道返回',
+  `channel_error_code` varchar(128) DEFAULT NULL COMMENT '渠道调用报错时,错误码',
+  `channel_error_msg` varchar(256) DEFAULT NULL COMMENT '渠道调用报错时,错误信息',
+  `channel_extras` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付渠道的额外参数',
+  `expire_time` datetime  DEFAULT NULL COMMENT '退款失效时间',
+  `success_time` datetime DEFAULT NULL COMMENT '退款成功时间',
+  `notify_time` datetime DEFAULT NULL COMMENT '退款通知时间',
+  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='退款订单';

+ 38 - 1
sql/ruoyi-vue-pro.sql

@@ -953,7 +953,7 @@ CREATE TABLE `pay_app` (
 -- Records of pay_app
 -- ----------------------------
 BEGIN;
-INSERT INTO `pay_app` VALUES (6, '芋道', 0, '我是一个公众号', 'http://127.0.0.1:28080/api/shop/order/pay-notify', 'http://127.0.0.1', 1, '', '2021-10-23 08:49:25', '', '2021-10-27 00:26:35', b'0');
+INSERT INTO `pay_app` VALUES (6, '芋道', 0, '我是一个公众号', 'http://127.0.0.1:28080/api/shop/order/pay-notify', 'http://127.0.0.1:28080/api/shop/order/refund-notify', 1, '', '2021-10-23 08:49:25', '', '2021-10-27 00:26:35', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1240,6 +1240,43 @@ INSERT INTO `pay_order` VALUES (120, 1, 6, 9, 'wx_pub', '1635311949168', '标题
 INSERT INTO `pay_order` VALUES (121, 1, 6, 9, 'wx_pub', '1635312124657', '标题:1635312124656', '内容:1635312124656', 'http://127.0.0.1:28080/api/shop/order/pay-notify', 0, 1, 0, 0, 10, '101.82.233.75', '2021-10-28 13:22:05', '2021-10-27 13:22:15', '2021-10-27 13:22:16', 100, 0, 0, 0, 'ockUAwIZ-0OeMZl9ogcZ4ILrGba0', '4200001174202110278060590766', NULL, '2021-10-27 13:22:05', NULL, '2021-10-27 13:22:16', b'0');
 COMMIT;
 
+DROP TABLE IF EXISTS `pay_refund`;
+CREATE TABLE `pay_refund` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '支付退款编号',
+  `req_no` varchar(64) NOT NULL COMMENT '退款单请求号',
+  `merchant_id` bigint NOT NULL COMMENT '商户编号',
+  `app_id` bigint NOT NULL COMMENT '应用编号',
+  `channel_id` bigint NOT NULL COMMENT '渠道编号',
+  `channel_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '渠道编码',
+  `order_id` bigint NOT NULL COMMENT '支付订单编号 pay_order 表id',
+  `trade_no` varchar(64) NOT NULL COMMENT '交易订单号 pay_extension 表no 字段',
+  `merchant_order_id` varchar(64) NOT NULL COMMENT '商户订单编号(商户系统生成)',
+  `merchant_refund_no` varchar(64) NOT NULL COMMENT '商户退款订单号(商户系统生成)',
+  `notify_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '异步通知商户地址',
+  `notify_status` tinyint NOT NULL COMMENT '通知商户退款结果的回调状态',
+  `status` tinyint NOT NULL COMMENT '退款状态',
+  `type` tinyint NOT NULL COMMENT '退款类型(部分退款,全部退款)',
+  `pay_amount` bigint NOT NULL COMMENT '支付金额,单位分',
+  `refund_amount` bigint NOT NULL COMMENT '退款金额,单位分',
+  `reason` VARCHAR(256) NOT NULL COMMENT '退款原因',
+  `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户 IP',
+  `channel_order_no` varchar(64) NOT NULL COMMENT '渠道订单号,pay_order 中的channel_order_no 对应',
+  `channel_refund_no` varchar(64) DEFAULT NULL COMMENT '渠道退款单号,渠道返回',
+  `channel_error_code` varchar(128) DEFAULT NULL COMMENT '渠道调用报错时,错误码',
+  `channel_error_msg` varchar(256) DEFAULT NULL COMMENT '渠道调用报错时,错误信息',
+  `channel_extras` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付渠道的额外参数',
+  `expire_time` datetime  DEFAULT NULL COMMENT '退款失效时间',
+  `success_time` datetime DEFAULT NULL COMMENT '退款成功时间',
+  `notify_time` datetime DEFAULT NULL COMMENT '退款通知时间',
+  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='退款订单';
+
+
 -- ----------------------------
 -- Table structure for pay_order_extension
 -- ----------------------------

+ 26 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/order/PayRefundCoreConvert.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.coreservice.modules.pay.convert.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+@Mapper
+public interface PayRefundCoreConvert {
+
+    PayRefundCoreConvert INSTANCE = Mappers.getMapper(PayRefundCoreConvert.class);
+
+    PayRefundPostReqBO convert(PayRefundUnifiedRespDTO respDTO);
+
+    //TODO 太多需要处理了, 暂时不用
+    @Mappings(value = {
+            @Mapping(source = "amount", target = "payAmount"),
+            @Mapping(source = "id", target = "orderId"),
+            @Mapping(target = "status",ignore = true)
+    })
+    PayRefundDO convert(PayOrderDO orderDO);
+}

+ 2 - 2
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderDO.java

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
 import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
-import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
 import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
@@ -135,7 +135,7 @@ public class PayOrderDO extends BaseDO {
     /**
      * 退款状态
      *
-     * 枚举 {@link PayOrderRefundStatusEnum}
+     * 枚举 {@link PayRefundTypeEnum}
      */
     private Integer refundStatus;
     /**

+ 93 - 28
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayRefundDO.java

@@ -5,7 +5,9 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChann
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import lombok.Data;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
 
 import java.util.Date;
 
@@ -17,19 +19,34 @@ import java.util.Date;
  *
  * @author 芋道源码
  */
+@TableName("pay_refund")
 @Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
 public class PayRefundDO extends BaseDO {
 
     /**
      * 退款单编号,数据库自增
      */
+    @TableId
     private Long id;
-    /**
-     * 退款单号,根据规则生成
-     *
+
+     /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
+     * 退款请求号。
+     * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
+     * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
+     * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
+     * 退款单请求号,根据规则生成
+      *
      * 例如说,R202109181134287570000
      */
-    private String no;
+    private String reqNo;
+
     /**
      * 商户编号
      *
@@ -61,23 +78,39 @@ public class PayRefundDO extends BaseDO {
      */
     private Long orderId;
 
+
+    /**
+     * 交易订单号,根据规则生成
+     * 调用支付渠道时,使用该字段作为对接的订单号。
+     * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
+     * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
+     *  这里对应 pay_extension 里面的 no
+     * 例如说,P202110132239124200055
+     */
+    private String tradeNo;
+
+
     // ========== 商户相关字段 ==========
     /**
-     * 商户退款订单号
-     * 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一 TODO 芋艿:需要在测试下
+     * 商户订单编号
+     */
+    private String merchantOrderId;
+    /**
+     * 商户退款订单号, 由商户系统产生, 由他们保证唯一,不能为空,通知商户时会传该字段。发送channel 使用 reqNo
+     * 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一
+     * TODO 芋艿:我理解 一个商户退款订单,可以对应多条退款记录, 因为有可能失败。但是 退款请求号 reqNo 必须唯一
+     *
      */
     private String merchantRefundNo;
-//    /**
-//     * 商户拓展参数
-//     */
-//    private String merchantExtra;
+
     /**
      * 异步通知地址
      */
     private String notifyUrl;
+
     /**
      * 通知商户退款结果的回调状态
-     * TODO 芋艿:0 未发送 1 已发送
+     * TODO 0 未发送 1 已发送
      */
     private Integer notifyStatus;
 
@@ -85,44 +118,76 @@ public class PayRefundDO extends BaseDO {
     /**
      * 退款状态
      *
-     * TODO 芋艿:状态枚举
      */
     private Integer status;
+
     /**
-     * 用户 IP
+     * 退款类型(部分退款,全部退款)
      */
-    private String userIp;
+    private Integer type;
     /**
-     * 退款金额,单位:
+     * 支付金额,单位
      */
-    private Long amount;
+    private Long payAmount;
+    /**
+     * 退款金额,单位分
+     */
+    private Long refundAmount;
+
     /**
      * 退款原因
      */
     private String reason;
+
+
     /**
-     * 订单退款成功时间
+     * 用户 IP
      */
-    private Date successTime;
+    private String userIp;
+
+    // ========== 渠道相关字段 ==========
     /**
-     * 退款失效时间
+     * 渠道订单号,pay_order 中的channel_order_no 对应
      */
-    private Date expireTime;
+    private String channelOrderNo;
+    /**
+     * 渠道退款单号,渠道返回
+     */
+    private String channelRefundNo;
+
+    /**
+     * 调用渠道的错误码
+     */
+    private String channelErrorCode;
+
+    /**
+     * 调用渠道报错时,错误信息
+     */
+    private String channelErrorMsg;
+
+
     /**
      * 支付渠道的额外参数
-     *
      * 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
      */
-    private String channelExtra;
+    private String channelExtras;
+
 
-    // ========== 渠道相关字段 ==========
     /**
-     * 渠道订单号
+     * TODO
+     * 退款失效时间
      */
-    private String channelOrderNo;
+    private Date expireTime;
     /**
-     * 渠道退款号
+     * 退款成功时间
      */
-    private String channelRefundNo;
+    private Date successTime;
+    /**
+     * 退款通知时间
+     */
+    private Date notifyTime;
+
+
+
 
 }

+ 21 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayRefundMapper.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
+
+import java.util.*;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+
+import org.apache.ibatis.annotations.Mapper;
+
+
+/**
+ * 退款订单 Mapper
+ *
+ */
+@Mapper
+public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
+
+
+}

+ 10 - 4
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java

@@ -28,6 +28,12 @@ public interface PayErrorCodeCoreConstants {
     ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
     ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007003002, "支付订单不处于已支付");
 
+    // ========== 支付模块(退款) 1-007-006-000 ==========
+    ErrorCode PAY_REFUND_AMOUNT_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
+    ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款");
+    ErrorCode PAY_REFUND_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空");
+    ErrorCode PAY_REFUND_POST_HANDLER_NOT_FOUND = new ErrorCode(1007006002, "未找到对应的退款后置处理类");
+
     /**
      * ========== 支付商户信息 1-007-004-000 ==========
      */
@@ -41,11 +47,11 @@ public interface PayErrorCodeCoreConstants {
 
 
     /**
-     * ========== 支付渠道 1-007-006-000 ==========
+     * ========== 支付渠道 1-007-001-000 ==========
      */
-    ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007006000, "支付渠道不存在");
-    ErrorCode CHANNEL_KEY_READ_ERROR = new ErrorCode(1007006002, "支付渠道秘钥文件读取失败");
+    ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007001003, "支付渠道不存在");
+    ErrorCode CHANNEL_KEY_READ_ERROR = new ErrorCode(1007001004, "支付渠道秘钥文件读取失败");
     // TODO @aquan:下面这个错误码,缺了 CHANNEL 前缀。另外,错误码的分段,上面有啦,合并下进去哈
-    ErrorCode EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007006003, "已存在相同的渠道");
+    ErrorCode EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001005, "已存在相同的渠道");
 
 }

+ 20 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayRefundStatusEnum.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum PayRefundStatusEnum {
+    CREATE(0, "退款订单生成"),
+    SUCCESS(1, "退款成功"),
+    FAILURE(2, "退款失败"),
+    PROCESSING_NOTIFY(3,"退款中, 渠道通知结果"),
+    PROCESSING_QUERY(4,"退款中, 系统查询结果"),
+    UNKNOWN_RETRY(5,"状态未知,需要重试"),
+    UNKNOWN_QUERY(6,"状态未知,系统查询结果"),
+    CLOSE(99, "退款关闭");
+
+    private final Integer status;
+    private final String name;
+}

+ 1 - 1
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderRefundStatusEnum.java → yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayRefundTypeEnum.java

@@ -11,7 +11,7 @@ import lombok.Getter;
  */
 @Getter
 @AllArgsConstructor
-public enum PayOrderRefundStatusEnum implements IntArrayValuable {
+public enum PayRefundTypeEnum implements IntArrayValuable {
 
     NO(0, "未退款"),
     SOME(10, "部分退款"),

+ 8 - 1
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/impl/PayNotifyCoreServiceImpl.java

@@ -5,8 +5,10 @@ import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyLogCoreMapper;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
 import cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify.PayNotifyLockCoreRedisDAO;
 import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
 import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
@@ -72,6 +74,9 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
     @Resource
     private PayNotifyLockCoreRedisDAO payNotifyLockCoreRedisDAO;
 
+    @Resource
+    private PayRefundMapper  payRefundMapper;
+
     @Resource
     @Lazy // 循环依赖(自己依赖自己),避免报错
     private PayNotifyCoreServiceImpl self;
@@ -89,7 +94,9 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
                     setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
         } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
             // TODO 芋艿,需要实现下哈
-            throw new UnsupportedOperationException("需要实现");
+            PayRefundDO refundDO = payRefundMapper.selectById(task.getDataId());
+            task.setMerchantId(refundDO.getMerchantId()).setAppId(refundDO.getAppId())
+                    .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
         }
 
         // 执行插入

+ 5 - 2
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayRefundOrderReqVO.java

@@ -17,12 +17,15 @@ import javax.validation.constraints.NotNull;
 @AllArgsConstructor
 public class PayRefundOrderReqVO {
 
-    @ApiModelProperty(value = "商户单编号", required = true, example = "10")
-    @NotEmpty(message = "商户订单号不能为空")
+    @ApiModelProperty(value = "商户退款单编号", required = true, example = "10")
+    @NotEmpty(message = "商户退款单编号不能为空")
     private String merchantOrderId;
 
     @ApiModelProperty(value = "支付退款编号", required = true, example = "20")
     @NotNull(message = "支付退款编号不能为空")
     private Long payRefundId;
 
+    @ApiModelProperty(value = "退款状态(成功,失败)", required = true, example = "10")
+    private Integer status;
+
 }

+ 3 - 3
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java

@@ -2,9 +2,7 @@ package cn.iocoder.yudao.coreservice.modules.pay.service.order;
 
 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
 
-import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
-import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
-import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.*;
 import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
 
 import javax.validation.Valid;
@@ -50,4 +48,6 @@ public interface PayOrderCoreService {
     */
    void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
 
+
+
 }

+ 40 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundAbstractChannelPostHandler.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+
+/**
+ * 支付退款订单渠道返回后 , 后置处理抽象类, 处理公用的逻辑
+ * @author  jason
+ */
+public abstract  class PayRefundAbstractChannelPostHandler implements PayRefundChannelPostHandler {
+
+    private final PayOrderCoreMapper payOrderCoreMapper;
+    private final PayRefundMapper payRefundMapper;
+
+    public PayRefundAbstractChannelPostHandler(PayOrderCoreMapper payOrderCoreMapper,
+                                               PayRefundMapper payRefundMapper){
+        this.payOrderCoreMapper = payOrderCoreMapper;
+        this.payRefundMapper = payRefundMapper;
+    }
+
+
+    /**
+     * 更新退款单
+     * @param refundDO  需要更新的退款单信息
+     */
+    protected void updatePayRefund(PayRefundDO refundDO){
+        payRefundMapper.updateById(refundDO);
+    }
+
+
+    /**
+     * 更新原始支付订单
+     * @param payOrderDO 支付订单信息
+     */
+    protected void updatePayOrder(PayOrderDO payOrderDO){
+        payOrderCoreMapper.updateById(payOrderDO);
+    }
+}

+ 24 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundChannelPostHandler.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+
+/**
+ * 支付退款订单 ,渠道返回后 后置处理
+ * @author jason
+ */
+public interface PayRefundChannelPostHandler {
+
+    /**
+     * 支持的渠道返回值
+     * @return 支持的渠道返回值数组
+     */
+    PayChannelRespEnum[] supportHandleResp();
+
+
+    /**
+     * 根据渠道返回, 处理支付退款单
+     * @param respBO
+     */
+    void handleRefundChannelResp(PayRefundPostReqBO respBO);
+}

+ 20 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundReqBO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundRespBO;
+
+/**
+ * 退款单 Core Service
+ *
+ * @author jason
+ */
+public interface PayRefundCoreService {
+
+
+    /**
+     * 提交退款申请
+     * @param reqDTO 退款申请信息
+     * @return 退款申请返回信息
+     */
+    PayRefundRespBO refund(PayRefundReqBO reqDTO);
+}

+ 96 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundPostReqBO.java

@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
+
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundPostReqBO {
+
+
+    /**
+     * 渠道的通用返回结果
+     */
+    private PayChannelRespEnum respEnum;
+
+
+
+    private PayRefundTypeEnum refundTypeEnum;
+
+    /**
+     * 已退款的总金额
+     */
+    private Long  refundedAmount;
+
+    /**
+     * 本次退款金额
+     */
+    private Long refundAmount;
+
+   /**
+     * 已退款次数
+     */
+    private Integer refundedTimes;
+
+
+    /**
+     * 订单编号
+     */
+    private Long  orderId;
+
+    /**
+     * 退款单编号
+     */
+    private Long refundId;
+
+
+    /**
+     * 渠道退款单号
+     */
+    private String channelRefundNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
+     * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
+     */
+    private String payTradeNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
+     * 退款请求单号  同一退款请求单号多次请求只退一笔。
+     */
+    private String refundReqNo;
+
+
+
+    /**
+     * 调用异常错误信息
+     */
+    private String exceptionMsg;
+
+
+    /**
+     * 渠道的错误码
+     */
+    private String channelErrCode;
+
+
+    /**
+     * 渠道的错误描述
+     */
+    private String channelErrMsg;
+
+
+}

+ 43 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundReqBO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+/**
+ * 退款申请单 Request DTO
+ */
+@Data
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundReqBO {
+
+    /**
+     * 支付订单编号自增
+     */
+    private Long payOrderId;
+
+    /**
+     * 退款金额
+     */
+    private Long amount;
+
+    /**
+     * 退款原因
+     */
+    private String reason;
+
+
+    /**
+     * 商户退款订单号
+     */
+    private String merchantRefundNo;
+
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+}

+ 23 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/bo/PayRefundRespBO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+/**
+ * 退款申请单 Response DTO
+ */
+@Data
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundRespBO {
+
+    /**
+     * 支付退款单编号, 自增
+     */
+    private Long refundId;
+}

+ 62 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelFailedHandler.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Optional;
+
+/**
+ * 支付退款订单渠道返回失败的后置处理类
+ * {@link PayChannelRespEnum#CALL_EXCEPTION}
+ * {@link PayChannelRespEnum#CAN_NOT_RETRY_FAILURE}
+ */
+@Service
+public class PayRefundChannelFailedHandler extends PayRefundAbstractChannelPostHandler {
+
+    @Resource
+    private PayNotifyCoreService payNotifyCoreService;
+
+    public PayRefundChannelFailedHandler(PayOrderCoreMapper payOrderCoreMapper, PayRefundMapper payRefundMapper) {
+        super(payOrderCoreMapper, payRefundMapper);
+    }
+
+    @Override
+    public PayChannelRespEnum[] supportHandleResp() {
+        return new PayChannelRespEnum[] {PayChannelRespEnum.CALL_EXCEPTION, PayChannelRespEnum.CAN_NOT_RETRY_FAILURE};
+    }
+
+
+    @Override
+    public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
+        //退款失败
+        //更新退款单表
+        PayRefundDO updateRefundDO = new PayRefundDO();
+
+        updateRefundDO.setId(respBO.getRefundId())
+                .setStatus(PayRefundStatusEnum.FAILURE.getStatus())
+                .setChannelErrorCode(respBO.getChannelErrCode())
+                .setChannelErrorMsg(Optional.ofNullable(respBO.getChannelErrMsg())
+                                          .orElse(respBO.getExceptionMsg()));
+        updatePayRefund(updateRefundDO);
+        PayOrderDO updateOrderDO = new PayOrderDO();
+        //更新订单表
+        updateOrderDO.setId(respBO.getOrderId())
+                .setRefundTimes(respBO.getRefundedTimes() + 1);
+        updatePayOrder(updateOrderDO);
+        // 立刻插入退款通知记录
+        // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
+        payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
+                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(respBO.getRefundId()).build());
+    }
+}

+ 45 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelNotifyHandler.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import org.springframework.stereotype.Service;
+
+/**
+ * 支付退款订单渠道返回通知 {@link PayChannelRespEnum#PROCESSING_NOTIFY},后置处理类
+ * 支付宝退款单好像没有回调, 微信会触发回调
+ */
+@Service
+public class PayRefundChannelNotifyHandler extends PayRefundAbstractChannelPostHandler {
+
+    public PayRefundChannelNotifyHandler(PayOrderCoreMapper payOrderCoreMapper,
+                                         PayRefundMapper payRefundMapper) {
+        super(payOrderCoreMapper, payRefundMapper);
+    }
+
+    @Override
+    public PayChannelRespEnum[] supportHandleResp() {
+        return new PayChannelRespEnum[] {PayChannelRespEnum.PROCESSING_NOTIFY};
+    }
+
+    @Override
+    public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
+        PayRefundDO updateRefundDO = new PayRefundDO();
+        //更新退款单表
+        updateRefundDO.setId(respBO.getRefundId())
+                .setStatus(PayRefundStatusEnum.PROCESSING_NOTIFY.getStatus());
+        updatePayRefund(updateRefundDO);
+
+        PayOrderDO updateOrderDO = new PayOrderDO();
+        //更新订单表
+        updateOrderDO.setId(respBO.getOrderId())
+                .setRefundTimes(respBO.getRefundedTimes() + 1);
+        updatePayOrder(updateOrderDO);
+
+    }
+}

+ 54 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+/**
+ * 支付退款订单渠道返回需调用查询接口的后置处理类
+ * {@link PayChannelRespEnum#PROCESSING_QUERY} //TODO 芋道源码 是不是微信有这样的情况
+ * {@link PayChannelRespEnum#READ_TIME_OUT_EXCEPTION}
+ */
+@Service
+public class PayRefundChannelQueryHandler extends PayRefundAbstractChannelPostHandler {
+
+
+    public PayRefundChannelQueryHandler(PayOrderCoreMapper payOrderCoreMapper,
+                                        PayRefundMapper payRefundMapper) {
+        super(payOrderCoreMapper, payRefundMapper);
+    }
+
+    @Override
+    public PayChannelRespEnum[] supportHandleResp() {
+        return new PayChannelRespEnum[]{PayChannelRespEnum.PROCESSING_QUERY, PayChannelRespEnum.READ_TIME_OUT_EXCEPTION};
+    }
+
+    @Override
+    public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
+        final PayChannelRespEnum respEnum = respBO.getRespEnum();
+        PayRefundStatusEnum refundStatus =
+                Objects.equals(PayChannelRespEnum.PROCESSING_QUERY, respEnum) ? PayRefundStatusEnum.PROCESSING_QUERY
+                        : PayRefundStatusEnum.UNKNOWN_QUERY;
+        //更新退款单表
+        PayRefundDO updateRefundDO = new PayRefundDO();
+        updateRefundDO.setId(respBO.getRefundId())
+                .setStatus(refundStatus.getStatus());
+        updatePayRefund(updateRefundDO);
+
+        PayOrderDO updateOrderDO = new PayOrderDO();
+        //更新订单表
+        updateOrderDO.setId(respBO.getOrderId())
+                .setRefundTimes(respBO.getRefundedTimes() + 1);
+        updatePayOrder(updateOrderDO);
+
+        //TODO 发起查询任务
+    }
+}

+ 52 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelRetryHandler.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+/**
+ * 支付退款订单渠道返回重试的后置处理类
+ * {@link PayChannelRespEnum#RETRY_FAILURE}
+ */
+@Service
+public class PayRefundChannelRetryHandler extends PayRefundAbstractChannelPostHandler {
+
+
+    public PayRefundChannelRetryHandler(PayOrderCoreMapper payOrderCoreMapper,
+                                        PayRefundMapper payRefundMapper) {
+        super(payOrderCoreMapper, payRefundMapper);
+    }
+
+    @Override
+    public PayChannelRespEnum[] supportHandleResp() {
+        return new PayChannelRespEnum[] {PayChannelRespEnum.RETRY_FAILURE};
+    }
+
+    @Override
+    public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
+
+        PayRefundDO updateRefundDO = new PayRefundDO();
+        //更新退款单表
+        updateRefundDO.setId(respBO.getRefundId())
+                .setStatus(PayRefundStatusEnum.UNKNOWN_RETRY.getStatus())
+                .setChannelErrorCode(respBO.getChannelErrCode())
+                .setChannelErrorMsg(respBO.getChannelErrMsg());
+        updatePayRefund(updateRefundDO);
+
+        PayOrderDO updateOrderDO = new PayOrderDO();
+        //更新订单表
+        updateOrderDO.setId(respBO.getOrderId())
+                .setRefundTimes(respBO.getRefundedTimes() + 1);
+        updatePayOrder(updateOrderDO);
+
+        //TODO 发起重试任务
+    }
+}

+ 64 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelSuccessHandler.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+/**
+ * 支付退款订单渠道返回退款成功的后置处理类
+ * {@link PayChannelRespEnum#SYNC_SUCCESS}
+ */
+@Service
+public class PayRefundChannelSuccessHandler extends PayRefundAbstractChannelPostHandler {
+
+
+    @Resource
+    private PayNotifyCoreService payNotifyCoreService;
+
+
+    public PayRefundChannelSuccessHandler(PayOrderCoreMapper payOrderCoreMapper,
+                                          PayRefundMapper payRefundMapper) {
+        super(payOrderCoreMapper, payRefundMapper);
+    }
+
+    @Override
+    public PayChannelRespEnum[] supportHandleResp() {
+        return new PayChannelRespEnum[]{PayChannelRespEnum.SYNC_SUCCESS};
+    }
+
+    @Override
+    public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
+        //退款成功
+        PayRefundDO updateRefundDO = new PayRefundDO();
+        //更新退款单表
+        updateRefundDO.setId(respBO.getRefundId())
+                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
+                .setChannelRefundNo(respBO.getChannelRefundNo())
+                .setSuccessTime(new Date());
+        updatePayRefund(updateRefundDO);
+
+        PayOrderDO updateOrderDO = new PayOrderDO();
+        //更新订单表
+        updateOrderDO.setId(respBO.getOrderId())
+                .setRefundTimes(respBO.getRefundedTimes() + 1)
+                .setRefundStatus(respBO.getRefundTypeEnum().getStatus())
+                .setRefundAmount(respBO.getRefundedAmount()+respBO.getRefundAmount());
+         updatePayOrder(updateOrderDO);
+
+         // 立刻插入退款通知记录
+        // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
+        payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
+                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(respBO.getRefundId()).build());
+    }
+}

+ 203 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java

@@ -0,0 +1,203 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.coreservice.modules.pay.convert.order.PayRefundCoreConvert;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensionCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundChannelPostHandler;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundReqBO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundRespBO;
+import cn.iocoder.yudao.coreservice.modules.pay.util.PaySeqUtils;
+import cn.iocoder.yudao.framework.pay.core.client.PayClient;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.*;
+
+import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+@Service
+@Slf4j
+public class PayRefundCoreServiceImpl implements PayRefundCoreService {
+
+    @Resource
+    private PayOrderCoreMapper payOrderCoreMapper;
+
+    @Resource
+    private PayRefundMapper payRefundMapper;
+
+    @Resource
+    private PayOrderExtensionCoreMapper payOrderExtensionCoreMapper;
+
+    @Resource
+    private PayAppCoreService payAppCoreService;
+
+    @Resource
+    private PayChannelCoreService payChannelCoreService;
+
+    @Resource
+    private PayClientFactory payClientFactory;
+
+    /**
+     * 处理渠道返回结果的后置处理器 集合
+     */
+    @Resource
+    private List<PayRefundChannelPostHandler> handlerList;
+
+
+    private final EnumMap<PayChannelRespEnum, PayRefundChannelPostHandler> mapHandler = new EnumMap<>(PayChannelRespEnum.class);
+
+
+
+    @PostConstruct
+    public void init(){
+
+        if (Objects.nonNull(handlerList)) {
+            handlerList.forEach(t->{
+                for (PayChannelRespEnum item : t.supportHandleResp()) {
+                    mapHandler.put(item, t);
+                }
+            });
+        }
+
+    }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayRefundRespBO refund(PayRefundReqBO reqBO) {
+        // 获得 PayOrderDO
+        PayOrderDO order = payOrderCoreMapper.selectById(reqBO.getPayOrderId());
+        // 校验订单是否存在
+        if (Objects.isNull(order) ) {
+            throw exception(PAY_ORDER_NOT_FOUND);
+        }
+        // 校验 App
+        PayAppDO app = payAppCoreService.validPayApp(order.getAppId());
+        // 校验支付渠道是否有效
+        PayChannelDO channel = payChannelCoreService.validPayChannel(order.getChannelId());
+        // 校验支付客户端是否正确初始化
+        PayClient client = payClientFactory.getPayClient(channel.getId());
+        if (client == null) {
+            log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
+            throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
+        }
+
+        //校验退款的条件
+        validatePayRefund(reqBO, order);
+
+        //退款类型
+        PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
+        if (Objects.equals(reqBO.getAmount(), order.getAmount())) {
+            refundType = PayRefundTypeEnum.ALL;
+        }
+
+        //退款单入库 退款单状态:生成,  没有和渠道产生交互
+        PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
+        PayRefundDO refundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
+                .appId(order.getAppId())
+                .channelOrderNo(order.getChannelOrderNo())
+                .channelCode(order.getChannelCode())
+                .channelId(order.getChannelId())
+                .merchantId(order.getMerchantId())
+                .orderId(order.getId())
+                .merchantRefundNo(reqBO.getMerchantRefundNo())
+                .notifyUrl(app.getRefundNotifyUrl())
+                .payAmount(order.getAmount())
+                .refundAmount(reqBO.getAmount())
+                .userIp(reqBO.getUserIp())
+                .merchantOrderId(order.getMerchantOrderId())
+                .tradeNo(orderExtensionDO.getNo())
+                .status(PayRefundStatusEnum.CREATE.getStatus())
+                .reason(reqBO.getReason())
+                .notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
+                .reqNo(PaySeqUtils.genRefundReqNo())
+                .type(refundType.getStatus())
+                .build();
+
+         payRefundMapper.insert(refundDO);
+
+         PayRefundUnifiedReqDTO unifiedReqDTO = PayRefundUnifiedReqDTO.builder()
+                .userIp(reqBO.getUserIp())
+                .channelOrderNo(refundDO.getChannelOrderNo())
+                .payTradeNo(refundDO.getTradeNo())
+                .refundReqNo(refundDO.getReqNo())
+                .amount(reqBO.getAmount())
+                .reason(refundDO.getReason())
+                .build();
+
+         //调用渠道进行退款
+         PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
+
+        //根据渠道返回,获取退款后置处理,由postHandler 进行处理
+         PayRefundChannelPostHandler payRefundChannelPostHandler = mapHandler.get(refundUnifiedRespDTO.getRespEnum());
+
+         if(Objects.isNull(payRefundChannelPostHandler)){
+             throw exception(PAY_REFUND_POST_HANDLER_NOT_FOUND);
+         }
+
+         PayRefundPostReqBO bo = PayRefundCoreConvert.INSTANCE.convert(refundUnifiedRespDTO);
+         bo.setRefundAmount(reqBO.getAmount())
+            .setRefundedAmount(order.getRefundAmount())
+            .setRefundedTimes(order.getRefundTimes())
+            .setRefundId(refundDO.getId())
+            .setOrderId(order.getId())
+            .setRefundTypeEnum(refundType);
+
+         //调用退款的后置处理
+         payRefundChannelPostHandler.handleRefundChannelResp(bo);
+
+         return PayRefundRespBO.builder().refundId(refundDO.getId()).build();
+    }
+
+
+    /**
+     * 校验是否进行退款
+     * @param reqBO 退款申请信息
+     * @param order 原始支付订单信息
+     */
+    private void validatePayRefund(PayRefundReqBO reqBO, PayOrderDO order) {
+
+        // 校验状态,必须是支付状态
+        if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
+            throw exception(PAY_ORDER_STATUS_IS_NOT_SUCCESS);
+        }
+        //是否已经全额退款
+        if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
+            throw exception(PAY_REFUND_ALL_REFUNDED);
+        }
+        // 校验金额 退款金额不能大于 原定的金额
+        if(reqBO.getAmount() + order.getRefundAmount() > order.getAmount()){
+            throw exception(PAY_REFUND_AMOUNT_EXCEED);
+        }
+        //校验渠道订单号
+        if (StrUtil.isEmpty(order.getChannelOrderNo())) {
+            throw exception(PAY_REFUND_CHN_ORDER_NO_IS_NULL);
+        }
+        //TODO  退款的期限  退款次数的控制
+
+
+    }
+}

+ 50 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/util/PaySeqUtils.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.coreservice.modules.pay.util;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.RandomUtil;
+
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 支付相关编号的生产
+ */
+public class PaySeqUtils {
+
+    private static final AtomicLong REFUND_REQ_NO_SEQ = new AtomicLong(0L);
+
+    private static final AtomicLong MER_REFUND_NO_SEQ = new AtomicLong(0L);
+
+    private static final AtomicLong MER_ORDER_NO_SEQ = new AtomicLong(0L);
+
+    /**
+     * 生成商户退款单号,用于测试,应该由商户系统生成
+     * @return 商户退款单
+     */
+    public static String genMerchantRefundNo() {
+        return String.format("%s%s%04d", "MR",
+                DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
+                (int) MER_REFUND_NO_SEQ.getAndIncrement() % 10000);
+    }
+
+    /**
+     * 生成退款请求号
+     * @return 退款请求号
+     */
+    public static String genRefundReqNo() {
+        return String.format("%s%s%04d", "RR",
+                DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
+                (int) REFUND_REQ_NO_SEQ.getAndIncrement() % 10000);
+    }
+
+    /**
+     * 生成商户订单编号号 用于测试,应该由商户系统生成
+     * @return 商户订单编号
+     */
+    public static String genMerchantOrderNo() {
+        return String.format("%s%s%04d", "MO",
+                DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
+                (int) MER_ORDER_NO_SEQ.getAndIncrement() % 10000);
+    }
+}

+ 9 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client;
 
 
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.*;
 
 /**
  * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
@@ -36,4 +34,12 @@ public interface PayClient {
      */
     PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
 
+
+    /**
+     * 调用支付渠道,进行退款
+     * @param reqDTO  统一退款请求信息
+     * @return 各支付渠道的统一返回结果
+     */
+    PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
+
 }

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayNotifyDataDTO.java

@@ -6,7 +6,10 @@ import lombok.ToString;
 
 import java.util.Map;
 
-// TODO @jason:注释要写下哈。字段不要使用 // 注释,非标准
+
+/**
+ * 支付订单回调,渠道的统一通知请求数据
+ */
 @Data
 @ToString
 @Builder

+ 78 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 统一 退款 Request DTO
+ *
+ * @author jason
+ */
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class PayRefundUnifiedReqDTO {
+
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
+     * 渠道订单号
+     */
+    private String channelOrderNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
+     * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
+     */
+    private String payTradeNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
+     * 退款请求单号  同一退款请求单号多次请求只退一笔。
+     */
+    @NotEmpty(message = "退款请求单号")
+    private String refundReqNo;
+
+
+    /**
+     * 退款原因
+     */
+    @NotEmpty(message = "退款原因不能为空")
+    private String reason;
+
+
+    /**
+     * 退款金额,单位:分
+     */
+    @NotNull(message = "退款金额不能为空")
+    @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+    private Long amount;
+
+
+
+
+    /**
+     * 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
+     */
+    @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
+    private String notifyUrl;
+}

+ 73 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java

@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto;
+
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotEmpty;
+/**
+ * 统一 退款 Response DTO
+ *
+ * @author jason
+ */
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class PayRefundUnifiedRespDTO {
+
+
+
+    /**
+     * 渠道的通用返回结果
+     */
+    private PayChannelRespEnum respEnum;
+
+
+
+    /**
+     * 渠道退款单号
+     */
+    private String channelRefundNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
+     * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
+     */
+    private String payTradeNo;
+
+
+    /**
+     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
+     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
+     * 退款请求单号  同一退款请求单号多次请求只退一笔。
+     */
+    private String refundReqNo;
+
+
+
+    /**
+     * 调用异常错误信息
+     */
+    private String exceptionMsg;
+
+
+    /**
+     * 渠道的错误码
+     */
+    private String channelErrCode;
+
+
+    /**
+     * 渠道的错误描述
+     */
+    private String channelErrMsg;
+
+    //TODO 退款资金渠 ???
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -6,8 +6,13 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
 import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
 import lombok.extern.slf4j.Slf4j;
 
+import java.net.SocketTimeoutException;
+
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
@@ -22,6 +27,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
      * 渠道编号
      */
     private final Long channelId;
+
     /**
      * 渠道编码
      */
@@ -91,7 +97,37 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         return result;
     }
 
+
+
     protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
             throws Throwable;
 
+
+    @Override
+    public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
+
+        PayRefundUnifiedRespDTO resp;
+        try {
+            resp = doUnifiedRefund(reqDTO);
+        }catch (SocketTimeoutException ex){
+            //网络 read time out 异常
+            log.error("[unifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), ex);
+            return PayRefundUnifiedRespDTO.builder()
+                    .exceptionMsg(ex.getMessage())
+                    .respEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION)
+                    .build();
+        } catch (Throwable ex) {
+            // 打印异常日志
+            log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex);
+            return PayRefundUnifiedRespDTO.builder()
+                    .exceptionMsg(ex.getMessage())
+                    .respEnum(PayChannelRespEnum.CALL_EXCEPTION)
+                    .build();
+        }
+        return resp;
+    }
+
+
+    protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
+
 }

+ 9 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java

@@ -3,9 +3,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.*;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
 import com.alipay.api.AlipayApiException;
@@ -71,6 +69,8 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
         return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
     }
 
+
+
     @Override
     public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
         //结果转换
@@ -82,4 +82,10 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
                 .data(data.getBody()).build();
 
     }
+
+    @Override
+    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+        //TODO 需要实现
+        throw new UnsupportedOperationException();
+    }
 }

+ 68 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java

@@ -3,28 +3,35 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.*;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
 import com.alipay.api.AlipayApiException;
 import com.alipay.api.AlipayConfig;
 import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AlipayTradeRefundModel;
 import com.alipay.api.domain.AlipayTradeWapPayModel;
+import com.alipay.api.request.AlipayTradeRefundRequest;
 import com.alipay.api.request.AlipayTradeWapPayRequest;
+import com.alipay.api.response.AlipayTradeRefundResponse;
 import com.alipay.api.response.AlipayTradeWapPayResponse;
 import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
 
+import java.net.SocketTimeoutException;
 import java.util.Map;
 import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+
 /**
  * 支付宝【手机网站】的 PayClient 实现类
  * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
  *
  * @author 芋道源码
  */
+@Slf4j
 public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
 
     private DefaultAlipayClient client;
@@ -96,4 +103,62 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
                 .data(data.getBody()).build();
     }
 
+
+    @Override
+    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO)  {
+        AlipayTradeRefundModel model=new AlipayTradeRefundModel();
+        model.setTradeNo(reqDTO.getChannelOrderNo());
+        model.setOutTradeNo(reqDTO.getPayTradeNo());
+        model.setOutRequestNo(reqDTO.getRefundReqNo());
+        model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
+        model.setRefundReason(reqDTO.getReason());
+        AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
+        refundRequest.setBizModel(model);
+        PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
+        try {
+            AlipayTradeRefundResponse response =  client.execute(refundRequest);
+            log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
+            if (response.isSuccess()) {
+                //退款成功
+                //TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
+                respDTO.setRespEnum(PayChannelRespEnum.SYNC_SUCCESS)
+                        .setChannelRefundNo(response.getTradeNo())
+                        .setPayTradeNo(response.getOutTradeNo());
+            }else{
+                //特殊处理 sub_code  ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务
+                //沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain
+                if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
+                    respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
+                            .setChannelErrMsg(response.getSubMsg())
+                            .setChannelErrCode(response.getSubCode());
+                }else{
+                    //其他当做不可以重试的错误
+                    respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
+                            .setChannelErrCode(response.getSubCode())
+                            .setChannelErrMsg(response.getSubMsg());
+                }
+            }
+            return respDTO;
+        } catch (AlipayApiException e) {
+            //TODO 记录异常日志
+            log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
+            Throwable cause = e.getCause();
+            //网络 read time out 异常, 退款状态未知
+            if (cause instanceof SocketTimeoutException) {
+                respDTO.setExceptionMsg(e.getMessage())
+                        .setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
+            }else{
+                respDTO.setExceptionMsg(e.getMessage())
+                        .setChannelErrCode(e.getErrCode())
+                        .setChannelErrMsg(e.getErrMsg())
+                        .setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
+            }
+
+            return respDTO;
+
+        }
+
+
+    }
+
 }

+ 9 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java

@@ -8,9 +8,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.*;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
@@ -92,6 +90,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
         return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
     }
 
+
     private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 构建 WxPayUnifiedOrderRequest 对象
         WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
@@ -142,4 +141,11 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
                 .data(data.getBody()).build();
     }
 
+
+    @Override
+    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+        //TODO 需要实现
+        throw new UnsupportedOperationException();
+    }
+
 }

+ 50 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRespEnum.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.framework.pay.core.enums;
+
+/**
+ * 统一的渠道返回结果
+ * @author  jason
+ */
+public enum PayChannelRespEnum {
+
+    /**
+     * 接口通讯正常返回, 并明确处理成功 ,不需要通过查询或者回调接口 进行下一步处理
+     */
+    SYNC_SUCCESS,
+
+    /**
+     * 接口通讯正常返回, 但返回错误,并且不能通过重试解决的错误,
+     * 如提交失败, 业务错误(余额不足), 或者参数错误, 签名错误, 需要干预后才能处理
+     */
+    CAN_NOT_RETRY_FAILURE,
+
+
+    /**
+     * 接口通讯正常返回,
+     * 可以通过重试解决的错误. 如系统超时, 系统繁忙。状态未知 不能改变请求参数,如退款单请求号,重发请求
+     */
+    RETRY_FAILURE,
+
+
+    /**
+     * 接口通讯正常返回,但是处理结果 需要渠道回调进行下一步处理
+     */
+    PROCESSING_NOTIFY,
+
+
+    /**
+     * 接口通讯正常返回, 但是处理结果,需要调用查询接口 进行查询
+     */
+    PROCESSING_QUERY,
+
+
+    /**
+     * 本系统调用渠道接口异常, 渠道接口请求未正常发送, 本系统不可预知的异常,较少发生, 可认为失败。  不用重试.
+     */
+    CALL_EXCEPTION,
+
+
+    /**
+     * 本系统调用渠道接口成功, 但是未接受到请求结果,较少发生(需合理设置read time out )  结果未知。 需要调用查询接口进行查询
+     */
+    READ_TIME_OUT_EXCEPTION;
+}

+ 42 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayRefundController.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.userserver.modules.pay.controller.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundReqBO;
+import cn.iocoder.yudao.coreservice.modules.pay.util.PaySeqUtils;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayRefundReqVO;
+import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayRefundRespVO;
+import cn.iocoder.yudao.userserver.modules.pay.convert.order.PayRefundConvert;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
+
+@Api(tags = "退款订单")
+@RestController
+@RequestMapping("/pay/order")
+@Validated
+@Slf4j
+public class PayRefundController {
+
+    @Resource
+    private PayRefundCoreService payRefundCoreService;
+
+    @PostMapping("/refund")
+    @ApiOperation("提交退款订单")
+    public CommonResult<PayRefundRespVO> refund(@RequestBody  PayRefundReqVO reqVO){
+        PayRefundReqBO reqBO = PayRefundConvert.INSTANCE.convert(reqVO);
+        reqBO.setUserIp(getClientIP());
+        //TODO 测试暂时模拟生成商户退款订单
+        reqBO.setMerchantRefundNo(PaySeqUtils.genMerchantRefundNo());
+        return CommonResult.success( PayRefundConvert.INSTANCE.convert(payRefundCoreService.refund(reqBO)));
+    }
+}

+ 35 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/vo/PayRefundReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.userserver.modules.pay.controller.order.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+
+@ApiModel("退款订单 Req VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundReqVO {
+
+    @ApiModelProperty(value = "支付订单编号自增", required = true, example = "10")
+    @NotEmpty(message = "支付订单编号自增")
+    private Long payOrderId;
+
+    @ApiModelProperty(value = "退款金额", required = true, example = "1")
+    @NotEmpty(message = "退款金额")
+    private Long amount;
+
+
+    @ApiModelProperty(value = "退款原因", required = true, example = "不喜欢")
+    @NotEmpty(message = "退款原因")
+    private String reason;
+
+    @ApiModelProperty(value = "商户退款订单号", required = true, example = "MR202111180000000001")
+    //TODO 测试暂时模拟生成
+    //@NotEmpty(message = "商户退款订单号")
+    private String merchantRefundNo;
+
+}

+ 22 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/vo/PayRefundRespVO.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.userserver.modules.pay.controller.order.vo;
+
+import io.swagger.annotations.ApiModel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@ApiModel("提交退款订单 Response VO")
+@Data
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundRespVO {
+
+    /**
+     * 支付退款单编号, 自增
+     */
+    private Long refundId;
+}

+ 22 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/order/PayRefundConvert.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.userserver.modules.pay.convert.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundReqBO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundRespBO;
+import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayRefundReqVO;
+import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayRefundRespVO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * 支付退款 Convert
+ * @author jason
+ */
+@Mapper
+public interface PayRefundConvert {
+
+    PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class);
+
+    PayRefundReqBO convert(PayRefundReqVO reqVO);
+
+    PayRefundRespVO convert(PayRefundRespBO respBO);
+}

+ 6 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 提供 POJO 类的实体转换
+ *
+ * 目前使用 MapStruct 框架
+ */
+package cn.iocoder.yudao.userserver.modules.pay.convert;

+ 1 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md

@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>

+ 13 - 2
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/shop/controller/ShopOrderController.java

@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.userserver.modules.shop.controller;
 
 import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayNotifyOrderReqVO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayRefundOrderReqVO;
 import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
 import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.util.PaySeqUtils;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.userserver.modules.shop.controller.vo.ShopOrderCreateRespVO;
@@ -43,10 +45,10 @@ public class ShopOrderController {
         PayOrderCreateReqDTO reqDTO = new PayOrderCreateReqDTO();
         reqDTO.setAppId(6L);
         reqDTO.setUserIp(getClientIP());
-        reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
+        reqDTO.setMerchantOrderId(PaySeqUtils.genMerchantOrderNo());
         reqDTO.setSubject("标题:" + shopOrderId);
         reqDTO.setBody("内容:" + shopOrderId);
-        reqDTO.setAmount(1); // 单位:分
+        reqDTO.setAmount(200); // 单位:分
         reqDTO.setExpireTime(DateUtils.addTime(Duration.ofDays(1)));
         Long payOrderId = payOrderCoreService.createPayOrder(reqDTO);
 
@@ -55,6 +57,8 @@ public class ShopOrderController {
                 .payOrderId(payOrderId).build());
     }
 
+
+
     @PostMapping("/pay-notify")
     @ApiOperation("支付回调")
     public CommonResult<Boolean> payNotify(@RequestBody @Valid PayNotifyOrderReqVO reqVO) {
@@ -62,4 +66,11 @@ public class ShopOrderController {
         return success(true);
     }
 
+    @PostMapping("/refund-notify")
+    @ApiOperation("退款回调")
+    public CommonResult<Boolean> refundNotify(@RequestBody @Valid PayRefundOrderReqVO reqVO) {
+        log.info("[refundNotify][回调成功]");
+        return success(true);
+    }
+
 }