Commit d80bf5a368310cfdfe7356f15b3466eee026066a

Authored by puhui999
2 parents 147cd271 6f475f8c

Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product

Showing 77 changed files with 1616 additions and 1092 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
1 package cn.iocoder.yudao.framework.common.util.validation; 1 package cn.iocoder.yudao.framework.common.util.validation;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
4 import org.springframework.util.StringUtils; 5 import org.springframework.util.StringUtils;
5 6
6 import javax.validation.ConstraintViolation; 7 import javax.validation.ConstraintViolation;
7 import javax.validation.ConstraintViolationException; 8 import javax.validation.ConstraintViolationException;
  9 +import javax.validation.Validation;
8 import javax.validation.Validator; 10 import javax.validation.Validator;
9 import java.util.Set; 11 import java.util.Set;
10 import java.util.regex.Pattern; 12 import java.util.regex.Pattern;
@@ -37,6 +39,12 @@ public class ValidationUtils { @@ -37,6 +39,12 @@ public class ValidationUtils {
37 && PATTERN_XML_NCNAME.matcher(str).matches(); 39 && PATTERN_XML_NCNAME.matcher(str).matches();
38 } 40 }
39 41
  42 + public static void validate(Object object, Class<?>... groups) {
  43 + Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  44 + Assert.notNull(validator);
  45 + validate(validator, object, groups);
  46 + }
  47 +
40 public static void validate(Validator validator, Object object, Class<?>... groups) { 48 public static void validate(Validator validator, Object object, Class<?>... groups) {
41 Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); 49 Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
42 if (CollUtil.isNotEmpty(constraintViolations)) { 50 if (CollUtil.isNotEmpty(constraintViolations)) {
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java
@@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty; @@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
13 public class PayProperties { 13 public class PayProperties {
14 14
15 /** 15 /**
16 - * 回调地址 16 + * 支付回调地址
17 * 17 *
18 - * 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL 18 + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
19 * 19 *
20 - * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址 20 + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
21 */ 21 */
22 - @NotEmpty(message = "回调地址不能为空")  
23 - @URL(message = "回调地址的格式必须是 URL")  
24 - private String callbackUrl; 22 + @NotEmpty(message = "支付回调地址不能为空")
  23 + @URL(message = "支付回调地址的格式必须是 URL")
  24 + private String orderNotifyUrl;
  25 +
  26 + /**
  27 + * 退款回调地址
  28 + *
  29 + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
  30 + *
  31 + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
  32 + */
  33 + @NotEmpty(message = "支付回调地址不能为空")
  34 + @URL(message = "支付回调地址的格式必须是 URL")
  35 + private String refundNotifyUrl;
25 36
26 } 37 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client; 1 package cn.iocoder.yudao.framework.pay.core.client;
2 2
3 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;  
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; 3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
7 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 5 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
8 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 6 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
9 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO; 7 +
  8 +import java.util.Map;
10 9
11 /** 10 /**
12 * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 11 * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
@@ -22,31 +21,42 @@ public interface PayClient { @@ -22,31 +21,42 @@ public interface PayClient {
22 */ 21 */
23 Long getId(); 22 Long getId();
24 23
  24 + // ============ 支付相关 ==========
  25 +
25 /** 26 /**
26 * 调用支付渠道,统一下单 27 * 调用支付渠道,统一下单
27 * 28 *
28 * @param reqDTO 下单信息 29 * @param reqDTO 下单信息
29 * @return 各支付渠道的返回结果 30 * @return 各支付渠道的返回结果
30 */ 31 */
31 - PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); 32 + PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
  33 +
  34 + /**
  35 + * 解析 order 回调数据
  36 + *
  37 + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
  38 + * @param body HTTP 回调接口的 request body
  39 + * @return 支付订单信息
  40 + */
  41 + PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
  42 +
  43 + // ============ 退款相关 ==========
32 44
33 /** 45 /**
34 * 调用支付渠道,进行退款 46 * 调用支付渠道,进行退款
  47 + *
35 * @param reqDTO 统一退款请求信息 48 * @param reqDTO 统一退款请求信息
36 - * @return 各支付渠道的统一返回结果 49 + * @return 退款信息
37 */ 50 */
38 - PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); 51 + PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
39 52
40 /** 53 /**
41 - * 解析回调数据 54 + * 解析 refund 回调数据
42 * 55 *
43 - * @param rawNotify 通知内容  
44 - * @return 回调对象  
45 - * 1. {@link PayRefundNotifyRespDTO} 退款通知  
46 - * 2. {@link PayOrderNotifyRespDTO} 支付通知 56 + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
  57 + * @param body HTTP 回调接口的 request body
  58 + * @return 支付订单信息
47 */ 59 */
48 - default Object parseNotify(PayNotifyReqDTO rawNotify) {  
49 - throw new UnsupportedOperationException("未实现 parseNotify 方法!");  
50 - } 60 + PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
51 61
52 } 62 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.client.dto.notify;  
2 -  
3 -import lombok.Builder;  
4 -import lombok.Data;  
5 -import lombok.ToString;  
6 -  
7 -import java.util.Map;  
8 -  
9 -  
10 -/**  
11 - * 支付订单,退款订单回调,渠道的统一通知请求数据  
12 - */  
13 -@Data  
14 -@ToString  
15 -@Builder  
16 -public class PayNotifyReqDTO {  
17 -  
18 -  
19 - /**  
20 - * HTTP 回调接口的 request body  
21 - */  
22 - private String body;  
23 -  
24 - /**  
25 - * HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数  
26 - */  
27 - private Map<String,String> params;  
28 -  
29 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.client.dto.notify;  
2 -  
3 -import lombok.AllArgsConstructor;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.NoArgsConstructor;  
7 -  
8 -import java.time.LocalDateTime;  
9 -  
10 -/**  
11 - * 支付通知 Response DTO  
12 - *  
13 - * @author 芋道源码  
14 - */  
15 -@Data  
16 -@Builder  
17 -@NoArgsConstructor  
18 -@AllArgsConstructor  
19 -public class PayOrderNotifyRespDTO {  
20 -  
21 - /**  
22 - * 支付订单号(支付模块的)  
23 - */  
24 - private String orderExtensionNo;  
25 - /**  
26 - * 支付渠道编号  
27 - */  
28 - private String channelOrderNo;  
29 - /**  
30 - * 支付渠道用户编号  
31 - */  
32 - private String channelUserId;  
33 - /**  
34 - * 支付成功时间  
35 - */  
36 - private LocalDateTime successTime;  
37 -  
38 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.client.dto.notify;  
2 -  
3 -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.ToString;  
7 -  
8 -import java.time.LocalDateTime;  
9 -  
10 -/**  
11 - * 从渠道返回数据中解析得到的支付退款通知的Notify DTO  
12 - *  
13 - * @author jason  
14 - */  
15 -@Data  
16 -@ToString  
17 -@Builder  
18 -public class PayRefundNotifyRespDTO {  
19 -  
20 - /**  
21 - * 支付渠道编号  
22 - */  
23 - private String channelOrderNo;  
24 -  
25 - /**  
26 - * 交易订单号,根据规则生成  
27 - * 调用支付渠道时,使用该字段作为对接的订单号。  
28 - * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no  
29 - * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no  
30 - * 这里对应 pay_extension 里面的 no  
31 - * 例如说,P202110132239124200055  
32 - */  
33 - private String tradeNo;  
34 -  
35 - /**  
36 - * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no  
37 - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no  
38 - * 退款请求号。  
39 - * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。  
40 - * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,  
41 - * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。  
42 - * 退款单请求号,根据规则生成  
43 - *  
44 - * 例如说,RR202109181134287570000  
45 - */  
46 - private String reqNo;  
47 -  
48 - /**  
49 - * 退款是否成功  
50 - */  
51 - private PayNotifyRefundStatusEnum status;  
52 -  
53 - /**  
54 - * 退款成功时间  
55 - */  
56 - private LocalDateTime refundSuccessTime;  
57 -  
58 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.pay.core.client.dto.order;
  2 +
  3 +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
  4 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
  5 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
  6 +import lombok.Data;
  7 +
  8 +import java.time.LocalDateTime;
  9 +
  10 +/**
  11 + * 渠道支付订单 Response DTO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +public class PayOrderRespDTO {
  17 +
  18 + /**
  19 + * 支付状态
  20 + *
  21 + * 枚举:{@link PayOrderStatusRespEnum}
  22 + */
  23 + private Integer status;
  24 +
  25 + /**
  26 + * 外部订单号
  27 + *
  28 + * 对应 PayOrderExtensionDO 的 no 字段
  29 + */
  30 + private String outTradeNo;
  31 +
  32 + /**
  33 + * 支付渠道编号
  34 + */
  35 + private String channelOrderNo;
  36 + /**
  37 + * 支付渠道用户编号
  38 + */
  39 + private String channelUserId;
  40 +
  41 + /**
  42 + * 支付成功时间
  43 + */
  44 + private LocalDateTime successTime;
  45 +
  46 + /**
  47 + * 原始的同步/异步通知结果
  48 + */
  49 + private Object rawData;
  50 +
  51 + // ========== 主动发起支付时,会返回的字段 ==========
  52 +
  53 + /**
  54 + * 展示模式
  55 + *
  56 + * 枚举 {@link PayOrderDisplayModeEnum} 类
  57 + */
  58 + private String displayMode;
  59 + /**
  60 + * 展示内容
  61 + */
  62 + private String displayContent;
  63 +
  64 + /**
  65 + * 调用渠道的错误码
  66 + *
  67 + * 注意:这里返回的是业务异常,而是不系统异常。
  68 + * 如果是系统异常,则会抛出 {@link PayException}
  69 + */
  70 + private String channelErrorCode;
  71 + /**
  72 + * 调用渠道报错时,错误信息
  73 + */
  74 + private String channelErrorMsg;
  75 +
  76 + public PayOrderRespDTO() {
  77 + }
  78 +
  79 + /**
  80 + * 创建【WAITING】状态的订单返回
  81 + */
  82 + public PayOrderRespDTO(String displayMode, String displayContent,
  83 + String outTradeNo, Object rawData) {
  84 + this.status = PayOrderStatusRespEnum.WAITING.getStatus();
  85 + this.displayMode = displayMode;
  86 + this.displayContent = displayContent;
  87 + // 相对通用的字段
  88 + this.outTradeNo = outTradeNo;
  89 + this.rawData = rawData;
  90 + }
  91 +
  92 + /**
  93 + * 创建【SUCCESS】状态的订单返回
  94 + */
  95 + public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime,
  96 + String outTradeNo, Object rawData) {
  97 + this.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
  98 + this.channelOrderNo = channelOrderNo;
  99 + this.channelUserId = channelUserId;
  100 + this.successTime = successTime;
  101 + // 相对通用的字段
  102 + this.outTradeNo = outTradeNo;
  103 + this.rawData = rawData;
  104 + }
  105 +
  106 + /**
  107 + * 创建【SUCCESS】或【CLOSED】状态的订单返回,适合支付渠道回调时
  108 + */
  109 + public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
  110 + String outTradeNo, Object rawData) {
  111 + this.status = status;
  112 + this.channelOrderNo = channelOrderNo;
  113 + this.channelUserId = channelUserId;
  114 + this.successTime = successTime;
  115 + // 相对通用的字段
  116 + this.outTradeNo = outTradeNo;
  117 + this.rawData = rawData;
  118 + }
  119 +
  120 + /**
  121 + * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
  122 + *
  123 + * 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突,所以独立个方法出来
  124 + */
  125 + public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg,
  126 + String outTradeNo, Object rawData) {
  127 + PayOrderRespDTO respDTO = new PayOrderRespDTO();
  128 + respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
  129 + respDTO.channelErrorCode = channelErrorCode;
  130 + respDTO.channelErrorMsg = channelErrorMsg;
  131 + // 相对通用的字段
  132 + respDTO.outTradeNo = outTradeNo;
  133 + respDTO.rawData = rawData;
  134 + return respDTO;
  135 + }
  136 +
  137 +}
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java
@@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO { @@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
28 // ========== 商户相关字段 ========== 28 // ========== 商户相关字段 ==========
29 29
30 /** 30 /**
31 - * 商户订单编号 31 + * 外部订单号
  32 + *
  33 + * 对应 PayOrderExtensionDO 的 no 字段
32 */ 34 */
33 - @NotEmpty(message = "商户订单编号不能为空")  
34 - private String merchantOrderId; 35 + @NotEmpty(message = "外部订单编号不能为空")
  36 + private String outTradeNo;
35 /** 37 /**
36 * 商品标题 38 * 商品标题
37 */ 39 */
@@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO { @@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
41 /** 43 /**
42 * 商品描述信息 44 * 商品描述信息
43 */ 45 */
44 - @NotEmpty(message = "商品描述信息不能为空")  
45 @Length(max = 128, message = "商品描述信息长度不能超过128") 46 @Length(max = 128, message = "商品描述信息长度不能超过128")
46 private String body; 47 private String body;
47 /** 48 /**
@@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO { @@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
63 */ 64 */
64 @NotNull(message = "支付金额不能为空") 65 @NotNull(message = "支付金额不能为空")
65 @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") 66 @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
66 - private Integer amount; 67 + private Integer price;
67 68
68 /** 69 /**
69 * 支付过期时间 70 * 支付过期时间
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.client.dto.order;  
2 -  
3 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;  
4 -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;  
5 -import lombok.Data;  
6 -  
7 -/**  
8 - * 统一下单 Response DTO  
9 - *  
10 - * @author 芋道源码  
11 - */  
12 -@Data  
13 -public class PayOrderUnifiedRespDTO {  
14 -  
15 - /**  
16 - * 展示模式  
17 - *  
18 - * 枚举 {@link PayOrderDisplayModeEnum} 类  
19 - */  
20 - private String displayMode;  
21 - /**  
22 - * 展示内容  
23 - */  
24 - private String displayContent;  
25 -  
26 - /**  
27 - * 同步的通知信息  
28 - *  
29 - * 目前只有 bar 条码支付才会出现,它是支付发起时,直接返回是否支付成功的,而其它支付还是异步通知  
30 - */  
31 - private PayOrderNotifyRespDTO notify;  
32 -  
33 - public PayOrderUnifiedRespDTO(String displayMode, String displayContent) {  
34 - this.displayMode = displayMode;  
35 - this.displayContent = displayContent;  
36 - }  
37 -  
38 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
  2 +
  3 +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +/**
  9 + * 渠道退款订单 Response DTO
  10 + *
  11 + * @author jason
  12 + */
  13 +@Data
  14 +public class PayRefundRespDTO {
  15 +
  16 + /**
  17 + * 退款状态
  18 + *
  19 + * 枚举 {@link PayRefundStatusRespEnum}
  20 + */
  21 + private Integer status;
  22 +
  23 + /**
  24 + * 外部退款号
  25 + *
  26 + * 对应 PayRefundDO 的 no 字段
  27 + */
  28 + private String outRefundNo;
  29 +
  30 + /**
  31 + * 渠道退款单号
  32 + *
  33 + * 对应 PayRefundDO.channelRefundNo 字段
  34 + */
  35 + private String channelRefundNo;
  36 +
  37 + /**
  38 + * 退款成功时间
  39 + */
  40 + private LocalDateTime successTime;
  41 +
  42 + /**
  43 + * 原始的异步通知结果
  44 + */
  45 + private Object rawData;
  46 +
  47 +}
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java
@@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull; @@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
24 public class PayRefundUnifiedReqDTO { 24 public class PayRefundUnifiedReqDTO {
25 25
26 /** 26 /**
27 - * 用户 IP 27 + * 外部订单号
  28 + *
  29 + * 对应 PayOrderExtensionDO 的 no 字段
28 */ 30 */
29 - private String userIp; 31 + @NotEmpty(message = "外部订单编号不能为空")
  32 + private String outTradeNo;
30 33
31 - // TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简  
32 /** 34 /**
33 - * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id  
34 - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no  
35 - * 渠道订单号 35 + * 外部退款号
  36 + *
  37 + * 对应 PayRefundDO 的 no 字段
36 */ 38 */
37 - private String channelOrderNo;  
38 -  
39 - /**  
40 - * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no  
41 - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no  
42 - * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空  
43 - */  
44 - private String payTradeNo;  
45 -  
46 - /**  
47 - * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no  
48 - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no  
49 - * 退款请求单号 同一退款请求单号多次请求只退一笔。  
50 - * 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}  
51 - */  
52 - @NotEmpty(message = "退款请求单号")  
53 - private String merchantRefundId; 39 + @NotEmpty(message = "退款请求单号不能为空")
  40 + private String outRefundNo;
54 41
55 /** 42 /**
56 * 退款原因 43 * 退款原因
@@ -59,15 +46,24 @@ public class PayRefundUnifiedReqDTO { @@ -59,15 +46,24 @@ public class PayRefundUnifiedReqDTO {
59 private String reason; 46 private String reason;
60 47
61 /** 48 /**
  49 + * 支付金额,单位:分
  50 + *
  51 + * 目前微信支付在退款的时候,必须传递该字段
  52 + */
  53 + @NotNull(message = "支付金额不能为空")
  54 + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
  55 + private Integer payPrice;
  56 + /**
62 * 退款金额,单位:分 57 * 退款金额,单位:分
63 */ 58 */
64 @NotNull(message = "退款金额不能为空") 59 @NotNull(message = "退款金额不能为空")
65 @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") 60 @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
66 - private Integer amount; 61 + private Integer refundPrice;
67 62
68 /** 63 /**
69 - * 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要 64 + * 退款结果的 notify 回调地址
70 */ 65 */
  66 + @NotEmpty(message = "支付结果的回调地址不能为空")
71 @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") 67 @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
72 private String notifyUrl; 68 private String notifyUrl;
73 69
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.client.dto.refund;  
2 -  
3 -import lombok.AllArgsConstructor;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.NoArgsConstructor;  
7 -import lombok.experimental.Accessors;  
8 -/**  
9 - * 统一退款 Response DTO  
10 - *  
11 - * @author jason  
12 - */  
13 -@Accessors(chain = true)  
14 -@Builder  
15 -@NoArgsConstructor  
16 -@AllArgsConstructor  
17 -@Data  
18 -public class PayRefundUnifiedRespDTO {  
19 -  
20 - /**  
21 - * 渠道退款单编号  
22 - */  
23 - private String channelRefundId;  
24 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl; 1 package cn.iocoder.yudao.framework.pay.core.client.impl;
2 2
3 import cn.iocoder.yudao.framework.common.exception.ServiceException; 3 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
4 import cn.iocoder.yudao.framework.pay.core.client.PayClient; 5 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; 6 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
  7 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 8 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
7 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 9 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
8 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 10 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
9 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
10 import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; 11 import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
11 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
12 13
13 -import javax.validation.Validation; 14 +import java.util.Map;
14 15
15 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; 16 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
16 17
@@ -29,6 +30,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen @@ -29,6 +30,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
29 /** 30 /**
30 * 渠道编码 31 * 渠道编码
31 */ 32 */
  33 + @SuppressWarnings("FieldCanBeLocal")
32 private final String channelCode; 34 private final String channelCode;
33 /** 35 /**
34 * 支付配置 36 * 支付配置
@@ -46,7 +48,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen @@ -46,7 +48,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
46 */ 48 */
47 public final void init() { 49 public final void init() {
48 doInit(); 50 doInit();
49 - log.info("[init][配置({}) 初始化完成]", config); 51 + log.info("[init][客户端({}) 初始化完成]", getId());
50 } 52 }
51 53
52 /** 54 /**
@@ -59,7 +61,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen @@ -59,7 +61,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
59 if (config.equals(this.config)) { 61 if (config.equals(this.config)) {
60 return; 62 return;
61 } 63 }
62 - log.info("[refresh][配置({})发生变化,重新初始化]", config); 64 + log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
63 this.config = config; 65 this.config = config;
64 // 初始化 66 // 初始化
65 this.init(); 67 this.init();
@@ -70,32 +72,47 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen @@ -70,32 +72,47 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
70 return channelId; 72 return channelId;
71 } 73 }
72 74
  75 + // ============ 支付相关 ==========
  76 +
73 @Override 77 @Override
74 - public final PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {  
75 - Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); 78 + public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
  79 + ValidationUtils.validate(reqDTO);
76 // 执行统一下单 80 // 执行统一下单
77 - PayOrderUnifiedRespDTO resp; 81 + PayOrderRespDTO resp;
78 try { 82 try {
79 resp = doUnifiedOrder(reqDTO); 83 resp = doUnifiedOrder(reqDTO);
80 - } catch (ServiceException ex) {  
81 - // 业务异常,都是实现类已经翻译,所以直接抛出即可  
82 - throw ex;  
83 } catch (Throwable ex) { 84 } catch (Throwable ex) {
84 // 系统异常,则包装成 PayException 异常抛出 85 // 系统异常,则包装成 PayException 异常抛出
85 - log.error("[unifiedRefund][request({}) 发起支付异常]", toJsonString(reqDTO), ex);  
86 - throw buildException(ex); 86 + log.error("[unifiedRefund][客户端({}) request({}) 发起支付异常]",
  87 + getId(), toJsonString(reqDTO), ex);
  88 + throw buildPayException(ex);
87 } 89 }
88 return resp; 90 return resp;
89 } 91 }
90 92
91 - protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) 93 + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
92 throws Throwable; 94 throws Throwable;
93 95
94 @Override 96 @Override
95 - public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {  
96 - Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); 97 + public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
  98 + try {
  99 + return doParseOrderNotify(params, body);
  100 + } catch (Throwable ex) {
  101 + log.error("[parseOrderNotify][params({}) body({}) 解析失败]", params, body, ex);
  102 + throw buildPayException(ex);
  103 + }
  104 + }
  105 +
  106 + protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
  107 + throws Throwable;
  108 +
  109 + // ============ 退款相关 ==========
  110 +
  111 + @Override
  112 + public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
  113 + ValidationUtils.validate(reqDTO);
97 // 执行统一退款 114 // 执行统一退款
98 - PayRefundUnifiedRespDTO resp; 115 + PayRefundRespDTO resp;
99 try { 116 try {
100 resp = doUnifiedRefund(reqDTO); 117 resp = doUnifiedRefund(reqDTO);
101 } catch (ServiceException ex) { 118 } catch (ServiceException ex) {
@@ -103,17 +120,18 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen @@ -103,17 +120,18 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
103 throw ex; 120 throw ex;
104 } catch (Throwable ex) { 121 } catch (Throwable ex) {
105 // 系统异常,则包装成 PayException 异常抛出 122 // 系统异常,则包装成 PayException 异常抛出
106 - log.error("[unifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), ex);  
107 - throw buildException(ex); 123 + log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
  124 + getId(), toJsonString(reqDTO), ex);
  125 + throw buildPayException(ex);
108 } 126 }
109 return resp; 127 return resp;
110 } 128 }
111 129
112 - protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; 130 + protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
113 131
114 // ========== 各种工具方法 ========== 132 // ========== 各种工具方法 ==========
115 133
116 - private PayException buildException(Throwable ex) { 134 + private PayException buildPayException(Throwable ex) {
117 if (ex instanceof PayException) { 135 if (ex instanceof PayException) {
118 return (PayException) ex; 136 return (PayException) ex;
119 } 137 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; @@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
3 import cn.hutool.core.bean.BeanUtil; 3 import cn.hutool.core.bean.BeanUtil;
4 import cn.hutool.core.date.LocalDateTimeUtil; 4 import cn.hutool.core.date.LocalDateTimeUtil;
  5 +import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.StrUtil;
5 import cn.hutool.http.HttpUtil; 7 import cn.hutool.http.HttpUtil;
6 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
7 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
8 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;  
9 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; 8 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
  9 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
  10 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
10 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 11 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
11 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
12 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; 12 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
13 -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;  
14 -import com.alipay.api.*; 13 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
  14 +import com.alipay.api.AlipayApiException;
  15 +import com.alipay.api.AlipayConfig;
  16 +import com.alipay.api.AlipayResponse;
  17 +import com.alipay.api.DefaultAlipayClient;
15 import com.alipay.api.domain.AlipayTradeRefundModel; 18 import com.alipay.api.domain.AlipayTradeRefundModel;
16 import com.alipay.api.internal.util.AlipaySignature; 19 import com.alipay.api.internal.util.AlipaySignature;
17 import com.alipay.api.request.AlipayTradeRefundRequest; 20 import com.alipay.api.request.AlipayTradeRefundRequest;
@@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j; @@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
22 import java.nio.charset.StandardCharsets; 25 import java.nio.charset.StandardCharsets;
23 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
24 import java.util.Map; 27 import java.util.Map;
  28 +import java.util.Objects;
  29 +import java.util.function.Supplier;
25 30
26 import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; 31 import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
27 -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;  
28 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; 32 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
29 -import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR;  
30 33
31 /** 34 /**
32 * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) 35 * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
@@ -50,72 +53,88 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -50,72 +53,88 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
50 this.client = new DefaultAlipayClient(alipayConfig); 53 this.client = new DefaultAlipayClient(alipayConfig);
51 } 54 }
52 55
  56 + // ============ 支付相关 ==========
  57 +
  58 + /**
  59 + * 构造支付关闭的 {@link PayOrderRespDTO} 对象
  60 + *
  61 + * @return 支付关闭的 {@link PayOrderRespDTO} 对象
  62 + */
  63 + protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
  64 + Assert.isFalse(response.isSuccess());
  65 + return PayOrderRespDTO.build(response.getSubCode(), response.getSubMsg(),
  66 + reqDTO.getOutTradeNo(), response);
  67 + }
  68 +
  69 + @Override
  70 + public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
  71 + // 1. 校验回调数据
  72 + Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
  73 + AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
  74 + StandardCharsets.UTF_8.name(), config.getSignType());
  75 +
  76 + // 2. 解析订单的状态
  77 + String tradeStatus = bodyObj.get("trade_status");
  78 + Integer status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
  79 + : Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS.getStatus()
  80 + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
  81 + Assert.notNull(status, (Supplier<Throwable>) () -> {
  82 + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
  83 + });
  84 + return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
  85 + bodyObj.get("out_trade_no"), body);
  86 + }
  87 +
  88 + // ============ 退款相关 ==========
  89 +
53 /** 90 /**
54 * 支付宝统一的退款接口 alipay.trade.refund 91 * 支付宝统一的退款接口 alipay.trade.refund
  92 + *
55 * @param reqDTO 退款请求 request DTO 93 * @param reqDTO 退款请求 request DTO
56 * @return 退款请求 Response 94 * @return 退款请求 Response
57 */ 95 */
58 @Override 96 @Override
59 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {  
60 - AlipayTradeRefundModel model=new AlipayTradeRefundModel();  
61 - model.setTradeNo(reqDTO.getChannelOrderNo());  
62 - model.setOutTradeNo(reqDTO.getPayTradeNo());  
63 -  
64 - model.setOutRequestNo(reqDTO.getMerchantRefundId());  
65 - model.setRefundAmount(formatAmount(reqDTO.getAmount())); 97 + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
  98 + // 1.1 构建 AlipayTradeRefundModel 请求
  99 + AlipayTradeRefundModel model = new AlipayTradeRefundModel();
  100 + model.setOutTradeNo(reqDTO.getOutTradeNo());
  101 + model.setOutRequestNo(reqDTO.getOutRefundNo());
  102 + model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
66 model.setRefundReason(reqDTO.getReason()); 103 model.setRefundReason(reqDTO.getReason());
67 -  
68 - AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();  
69 - refundRequest.setBizModel(model);  
70 - refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());  
71 - refundRequest.setReturnUrl(reqDTO.getNotifyUrl()); 104 + // 1.2 构建 AlipayTradePayRequest 请求
  105 + AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
  106 + request.setBizModel(model);
72 try { 107 try {
73 - AlipayTradeRefundResponse response = client.execute(refundRequest);  
74 - log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response)); 108 + // 2.1 执行请求
  109 + AlipayTradeRefundResponse response = client.execute(request);
  110 + // 2.2 创建返回结果
  111 + PayRefundRespDTO refund = new PayRefundRespDTO()
  112 + .setOutRefundNo(reqDTO.getOutRefundNo())
  113 + .setRawData(response);
  114 + // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
  115 + // 另外,支付宝没有退款单号,所以不用设置
75 if (response.isSuccess()) { 116 if (response.isSuccess()) {
76 - //退款导致触发的异步通知是发送到支付接口中设置的notify_url  
77 - //支付宝不返回退款单号,设置为空  
78 - PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();  
79 - respDTO.setChannelRefundId("");  
80 -// return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO  
81 - return null; 117 + refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
  118 + .setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
  119 + Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
  120 + } else {
  121 + refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
82 } 122 }
83 - // 失败。需要抛出异常  
84 -// return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO  
85 - return null; 123 + return refund;
86 } catch (AlipayApiException e) { 124 } catch (AlipayApiException e) {
87 - // TODO 记录异常日志  
88 - log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);  
89 -// return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO 125 + log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
90 return null; 126 return null;
91 } 127 }
92 } 128 }
93 129
94 @Override 130 @Override
95 - @SneakyThrows  
96 - public Object parseNotify(PayNotifyReqDTO rawNotify) {  
97 - // 1. 校验回调数据  
98 - String body = rawNotify.getBody();  
99 - Map<String, String> params = rawNotify.getParams();  
100 - Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);  
101 - AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),  
102 - StandardCharsets.UTF_8.name(), "RSA2");  
103 -  
104 - // 2.1 退款的情况  
105 - if (bodyObj.containsKey("refund_fee")) {  
106 - return PayRefundNotifyRespDTO.builder().channelOrderNo(bodyObj.get("trade_no"))  
107 - .tradeNo(bodyObj.get("out_trade_no")).reqNo(bodyObj.get("out_biz_no"))  
108 - .status(PayNotifyRefundStatusEnum.SUCCESS)  
109 - .refundSuccessTime(parseTime(params.get("gmt_refund")))  
110 - .build();  
111 - }  
112 - // 2.2 支付的情况  
113 - return PayOrderNotifyRespDTO.builder()  
114 - .orderExtensionNo(bodyObj.get("out_trade_no"))  
115 - .channelOrderNo(bodyObj.get("trade_no"))  
116 - .channelUserId(bodyObj.get("seller_id"))  
117 - .successTime(parseTime(params.get("notify_time")))  
118 - .build(); 131 + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
  132 + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
  133 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
  134 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
  135 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
  136 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
  137 + throw new UnsupportedOperationException("支付宝无退款回调");
119 } 138 }
120 139
121 // ========== 各种工具方法 ========== 140 // ========== 各种工具方法 ==========
@@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
132 return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); 151 return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
133 } 152 }
134 153
135 - /**  
136 - * 校验支付宝统一下单的响应  
137 - *  
138 - * 如果校验不通过,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 异常  
139 - *  
140 - * @param request 请求  
141 - * @param response 响应  
142 - */  
143 - protected void validateUnifiedOrderResponse(Object request, AlipayResponse response) {  
144 - if (response.isSuccess()) {  
145 - return;  
146 - }  
147 - log.error("[validateUnifiedOrderResponse][发起支付失败,request({}),response({})]",  
148 - JsonUtils.toJsonString(request), JsonUtils.toJsonString(response));  
149 - throw exception0(ORDER_UNIFIED_ERROR.getCode(), response.getSubMsg());  
150 - }  
151 -  
152 } 154 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
7 import com.alipay.api.AlipayApiException; 7 import com.alipay.api.AlipayApiException;
@@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient { @@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
27 } 27 }
28 28
29 @Override 29 @Override
30 - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { 30 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
31 // 1.1 构建 AlipayTradeAppPayModel 请求 31 // 1.1 构建 AlipayTradeAppPayModel 请求
32 AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); 32 AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
33 // ① 通用的参数 33 // ① 通用的参数
34 - model.setOutTradeNo(reqDTO.getMerchantOrderId()); 34 + model.setOutTradeNo(reqDTO.getOutTradeNo());
35 model.setSubject(reqDTO.getSubject()); 35 model.setSubject(reqDTO.getSubject());
36 model.setBody(reqDTO.getBody()); 36 model.setBody(reqDTO.getBody());
37 - model.setTotalAmount(formatAmount(reqDTO.getAmount())); 37 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
38 model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品 38 model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
39 // ② 个性化的参数【无】 39 // ② 个性化的参数【无】
40 // ③ 支付宝扫码支付只有一种展示 40 // ③ 支付宝扫码支付只有一种展示
@@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient { @@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
49 // 2.1 执行请求 49 // 2.1 执行请求
50 AlipayTradeAppPayResponse response = client.execute(request); 50 AlipayTradeAppPayResponse response = client.execute(request);
51 // 2.2 处理结果 51 // 2.2 处理结果
52 - validateUnifiedOrderResponse(request, response);  
53 - return new PayOrderUnifiedRespDTO(displayMode, ""); 52 + if (!response.isSuccess()) {
  53 + return buildClosedPayOrderRespDTO(reqDTO, response);
  54 + }
  55 + return new PayOrderRespDTO(displayMode, "",
  56 + reqDTO.getOutTradeNo(), response);
54 } 57 }
55 58
56 } 59 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java
@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; @@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
5 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 import com.alipay.api.AlipayApiException; 9 import com.alipay.api.AlipayApiException;
@@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient { @@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
30 } 30 }
31 31
32 @Override 32 @Override
33 - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { 33 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
34 String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); 34 String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
35 if (StrUtil.isEmpty(authCode)) { 35 if (StrUtil.isEmpty(authCode)) {
36 throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); 36 throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
@@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient { @@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
39 // 1.1 构建 AlipayTradePayModel 请求 39 // 1.1 构建 AlipayTradePayModel 请求
40 AlipayTradePayModel model = new AlipayTradePayModel(); 40 AlipayTradePayModel model = new AlipayTradePayModel();
41 // ① 通用的参数 41 // ① 通用的参数
42 - model.setOutTradeNo(reqDTO.getMerchantOrderId()); 42 + model.setOutTradeNo(reqDTO.getOutTradeNo());
43 model.setSubject(reqDTO.getSubject()); 43 model.setSubject(reqDTO.getSubject());
44 model.setBody(reqDTO.getBody()); 44 model.setBody(reqDTO.getBody());
45 - model.setTotalAmount(formatAmount(reqDTO.getAmount())); 45 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
46 model.setScene("bar_code"); // 当面付条码支付场景 46 model.setScene("bar_code"); // 当面付条码支付场景
47 // ② 个性化的参数 47 // ② 个性化的参数
48 model.setAuthCode(authCode); 48 model.setAuthCode(authCode);
@@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient { @@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
55 request.setNotifyUrl(reqDTO.getNotifyUrl()); 55 request.setNotifyUrl(reqDTO.getNotifyUrl());
56 request.setReturnUrl(reqDTO.getReturnUrl()); 56 request.setReturnUrl(reqDTO.getReturnUrl());
57 57
  58 + // TODO 芋艿:各种边界的处理
58 // 2.1 执行请求 59 // 2.1 执行请求
59 AlipayTradePayResponse response = client.execute(request); 60 AlipayTradePayResponse response = client.execute(request);
60 // 2.2 处理结果 61 // 2.2 处理结果
61 - validateUnifiedOrderResponse(request, response);  
62 - return new PayOrderUnifiedRespDTO(displayMode, ""); 62 + if (!response.isSuccess()) {
  63 + return buildClosedPayOrderRespDTO(reqDTO, response);
  64 + }
  65 + return new PayOrderRespDTO(displayMode, "",
  66 + reqDTO.getOutTradeNo(), response);
63 } 67 }
64 68
65 } 69 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java
@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; @@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
3 import cn.hutool.core.util.ObjectUtil; 3 import cn.hutool.core.util.ObjectUtil;
4 import cn.hutool.http.Method; 4 import cn.hutool.http.Method;
  5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
5 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 import com.alipay.api.AlipayApiException; 9 import com.alipay.api.AlipayApiException;
@@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient { @@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
29 } 29 }
30 30
31 @Override 31 @Override
32 - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { 32 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
33 // 1.1 构建 AlipayTradePagePayModel 请求 33 // 1.1 构建 AlipayTradePagePayModel 请求
34 AlipayTradePagePayModel model = new AlipayTradePagePayModel(); 34 AlipayTradePagePayModel model = new AlipayTradePagePayModel();
35 // ① 通用的参数 35 // ① 通用的参数
36 - model.setOutTradeNo(reqDTO.getMerchantOrderId()); 36 + model.setOutTradeNo(reqDTO.getOutTradeNo());
37 model.setSubject(reqDTO.getSubject()); 37 model.setSubject(reqDTO.getSubject());
38 model.setBody(reqDTO.getBody()); 38 model.setBody(reqDTO.getBody());
39 - model.setTotalAmount(formatAmount(reqDTO.getAmount())); 39 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
40 model.setTimeExpire(formatTime(reqDTO.getExpireTime())); 40 model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
41 model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY 41 model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
42 // ② 个性化的参数 42 // ② 个性化的参数
@@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient { @@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
60 response = client.pageExecute(request, Method.GET.name()); 60 response = client.pageExecute(request, Method.GET.name());
61 } 61 }
62 // 2.2 处理结果 62 // 2.2 处理结果
63 - validateUnifiedOrderResponse(request, response);  
64 - return new PayOrderUnifiedRespDTO(displayMode, response.getBody()); 63 + if (!response.isSuccess()) {
  64 + return buildClosedPayOrderRespDTO(reqDTO, response);
  65 + }
  66 + return new PayOrderRespDTO(displayMode, response.getBody(),
  67 + reqDTO.getOutTradeNo(), response);
65 } 68 }
66 69
67 } 70 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
7 import com.alipay.api.AlipayApiException; 7 import com.alipay.api.AlipayApiException;
@@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient { @@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
25 } 25 }
26 26
27 @Override 27 @Override
28 - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { 28 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
29 // 1.1 构建 AlipayTradePrecreateModel 请求 29 // 1.1 构建 AlipayTradePrecreateModel 请求
30 AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); 30 AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
31 // ① 通用的参数 31 // ① 通用的参数
32 - model.setOutTradeNo(reqDTO.getMerchantOrderId()); 32 + model.setOutTradeNo(reqDTO.getOutTradeNo());
33 model.setSubject(reqDTO.getSubject()); 33 model.setSubject(reqDTO.getSubject());
34 model.setBody(reqDTO.getBody()); 34 model.setBody(reqDTO.getBody());
35 - model.setTotalAmount(formatAmount(reqDTO.getAmount())); 35 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
36 model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT 36 model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
37 // ② 个性化的参数【无】 37 // ② 个性化的参数【无】
38 // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开 38 // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
@@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient { @@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
47 // 2.1 执行请求 47 // 2.1 执行请求
48 AlipayTradePrecreateResponse response = client.execute(request); 48 AlipayTradePrecreateResponse response = client.execute(request);
49 // 2.2 处理结果 49 // 2.2 处理结果
50 - validateUnifiedOrderResponse(request, response);  
51 - return new PayOrderUnifiedRespDTO(displayMode, response.getQrCode()); 50 + if (!response.isSuccess()) {
  51 + return buildClosedPayOrderRespDTO(reqDTO, response);
  52 + }
  53 + return new PayOrderRespDTO(displayMode, response.getQrCode(),
  54 + reqDTO.getOutTradeNo(), response);
52 } 55 }
53 56
54 } 57 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2
3 import cn.hutool.http.Method; 3 import cn.hutool.http.Method;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 5 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
6 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 6 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
7 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 7 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
8 import com.alipay.api.AlipayApiException; 8 import com.alipay.api.AlipayApiException;
@@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient { @@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
26 } 26 }
27 27
28 @Override 28 @Override
29 - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { 29 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
30 // 1.1 构建 AlipayTradeWapPayModel 请求 30 // 1.1 构建 AlipayTradeWapPayModel 请求
31 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); 31 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
32 // ① 通用的参数 32 // ① 通用的参数
33 - model.setOutTradeNo(reqDTO.getMerchantOrderId()); 33 + model.setOutTradeNo(reqDTO.getOutTradeNo());
34 model.setSubject(reqDTO.getSubject()); 34 model.setSubject(reqDTO.getSubject());
35 model.setBody(reqDTO.getBody()); 35 model.setBody(reqDTO.getBody());
36 - model.setTotalAmount(formatAmount(reqDTO.getAmount())); 36 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
37 model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY 37 model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
38 // ② 个性化的参数【无】 38 // ② 个性化的参数【无】
39 // ③ 支付宝 Wap 支付只有一种展示:URL 39 // ③ 支付宝 Wap 支付只有一种展示:URL
@@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient { @@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
48 48
49 // 2.1 执行请求 49 // 2.1 执行请求
50 AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); 50 AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
51 -  
52 // 2.2 处理结果 51 // 2.2 处理结果
53 - validateUnifiedOrderResponse(request, response);  
54 - return new PayOrderUnifiedRespDTO(displayMode, response.getBody()); 52 + if (!response.isSuccess()) {
  53 + return buildClosedPayOrderRespDTO(reqDTO, response);
  54 + }
  55 + return new PayOrderRespDTO(displayMode, response.getBody(),
  56 + reqDTO.getOutTradeNo(), response);
55 } 57 }
56 58
57 } 59 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
3 import cn.hutool.core.bean.BeanUtil; 3 import cn.hutool.core.bean.BeanUtil;
  4 +import cn.hutool.core.codec.Base64;
4 import cn.hutool.core.date.LocalDateTimeUtil; 5 import cn.hutool.core.date.LocalDateTimeUtil;
5 import cn.hutool.core.date.TemporalAccessorUtil; 6 import cn.hutool.core.date.TemporalAccessorUtil;
6 -import cn.hutool.core.lang.Assert;  
7 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
8 import cn.iocoder.yudao.framework.common.util.io.FileUtils; 8 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
9 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
10 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; 9 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
11 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 10 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
12 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 11 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
  12 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
13 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; 13 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
14 -import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; 14 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
  15 +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
15 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; 16 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
16 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; 17 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
  18 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
  19 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
  20 +import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
  21 +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
  22 +import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
  23 +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
17 import com.github.binarywang.wxpay.config.WxPayConfig; 24 import com.github.binarywang.wxpay.config.WxPayConfig;
18 import com.github.binarywang.wxpay.exception.WxPayException; 25 import com.github.binarywang.wxpay.exception.WxPayException;
19 import com.github.binarywang.wxpay.service.WxPayService; 26 import com.github.binarywang.wxpay.service.WxPayService;
@@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j; @@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
22 29
23 import java.time.LocalDateTime; 30 import java.time.LocalDateTime;
24 import java.time.ZoneId; 31 import java.time.ZoneId;
  32 +import java.util.Map;
25 import java.util.Objects; 33 import java.util.Objects;
26 34
27 -import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;  
28 -import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;  
29 -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;  
30 -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; 35 +import static cn.hutool.core.date.DatePattern.*;
  36 +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
31 37
32 /** 38 /**
33 * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) 39 * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
@@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
53 WxPayConfig payConfig = new WxPayConfig(); 59 WxPayConfig payConfig = new WxPayConfig();
54 BeanUtil.copyProperties(config, payConfig, "keyContent"); 60 BeanUtil.copyProperties(config, payConfig, "keyContent");
55 payConfig.setTradeType(tradeType); 61 payConfig.setTradeType(tradeType);
  62 + // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
  63 + if (Base64.isBase64(config.getKeyContent())) {
  64 + payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
  65 + }
56 if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { 66 if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
57 - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决  
58 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); 67 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
59 } 68 }
60 if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { 69 if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
61 - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决  
62 payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); 70 payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
63 } 71 }
64 72
@@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
67 client.setConfig(payConfig); 75 client.setConfig(payConfig);
68 } 76 }
69 77
  78 + // ============ 支付相关 ==========
  79 +
70 @Override 80 @Override
71 - protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { 81 + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
72 try { 82 try {
73 switch (config.getApiVersion()) { 83 switch (config.getApiVersion()) {
74 - case WxPayClientConfig.API_VERSION_V2: 84 + case API_VERSION_V2:
75 return doUnifiedOrderV2(reqDTO); 85 return doUnifiedOrderV2(reqDTO);
76 case WxPayClientConfig.API_VERSION_V3: 86 case WxPayClientConfig.API_VERSION_V3:
77 return doUnifiedOrderV3(reqDTO); 87 return doUnifiedOrderV3(reqDTO);
@@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
79 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); 89 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
80 } 90 }
81 } catch (WxPayException e) { 91 } catch (WxPayException e) {
82 - throw buildUnifiedOrderException(reqDTO, e); 92 + String errorCode = getErrorCode(e);
  93 + String errorMessage = getErrorMessage(e);
  94 + return PayOrderRespDTO.build(errorCode, errorMessage,
  95 + reqDTO.getOutTradeNo(), e.getXmlString());
83 } 96 }
84 } 97 }
85 98
@@ -89,8 +102,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -89,8 +102,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
89 * @param reqDTO 下单信息 102 * @param reqDTO 下单信息
90 * @return 各支付渠道的返回结果 103 * @return 各支付渠道的返回结果
91 */ 104 */
92 - protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)  
93 - throws WxPayException; 105 + protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
  106 + throws Exception;
94 107
95 /** 108 /**
96 * 【V3】调用支付渠道,统一下单 109 * 【V3】调用支付渠道,统一下单
@@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
98 * @param reqDTO 下单信息 111 * @param reqDTO 下单信息
99 * @return 各支付渠道的返回结果 112 * @return 各支付渠道的返回结果
100 */ 113 */
101 - protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) 114 + protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
102 throws WxPayException; 115 throws WxPayException;
103 116
  117 + @Override
  118 + public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
  119 + // 微信支付 v2 回调结果处理
  120 + switch (config.getApiVersion()) {
  121 + case API_VERSION_V2:
  122 + return doParseOrderNotifyV2(body);
  123 + case WxPayClientConfig.API_VERSION_V3:
  124 + return doParseOrderNotifyV3(body);
  125 + default:
  126 + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
  127 + }
  128 + }
  129 +
  130 + private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
  131 + // 1. 解析回调
  132 + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
  133 + // 2. 构建结果
  134 + Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
  135 + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
  136 + return new PayOrderRespDTO(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
  137 + response.getOutTradeNo(), body);
  138 + }
  139 +
  140 + private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
  141 + // 1. 解析回调
  142 + WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
  143 + WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
  144 + // 2. 构建结果
  145 + Integer status = Objects.equals(result.getTradeState(), "SUCCESS") ?
  146 + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
  147 + String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
  148 + return new PayOrderRespDTO(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
  149 + result.getOutTradeNo(), body);
  150 + }
  151 +
  152 + // ============ 退款相关 ==========
104 153
105 @Override 154 @Override
106 - public Object parseNotify(PayNotifyReqDTO rawNotify) {  
107 - log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody()); 155 + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
108 try { 156 try {
109 - // 微信支付 v2 回调结果处理  
110 switch (config.getApiVersion()) { 157 switch (config.getApiVersion()) {
111 - case WxPayClientConfig.API_VERSION_V2:  
112 - return parseOrderNotifyV2(rawNotify); 158 + case API_VERSION_V2:
  159 + return doUnifiedRefundV2(reqDTO);
113 case WxPayClientConfig.API_VERSION_V3: 160 case WxPayClientConfig.API_VERSION_V3:
114 - return parseOrderNotifyV3(rawNotify); 161 + return doUnifiedRefundV3(reqDTO);
115 default: 162 default:
116 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); 163 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
117 } 164 }
118 } catch (WxPayException e) { 165 } catch (WxPayException e) {
119 - log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e);  
120 -// throw buildPayException(e);  
121 - throw new RuntimeException(e);  
122 - // TODO 芋艿:缺一个异常翻译 166 + // todo 芋艿:异常的处理;
  167 +// throw buildUnifiedOrderException(null, e);
  168 + return null;
123 } 169 }
124 } 170 }
125 171
126 - private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException {  
127 - WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());  
128 - Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");  
129 - // 转换结果  
130 - return PayOrderNotifyRespDTO  
131 - .builder()  
132 - .orderExtensionNo(notifyResult.getOutTradeNo())  
133 - .channelOrderNo(notifyResult.getTransactionId())  
134 - .channelUserId(notifyResult.getOpenid())  
135 - .successTime(parseDateV2(notifyResult.getTimeEnd()))  
136 - .build();  
137 - }  
138 -  
139 - private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException {  
140 - WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null);  
141 - WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();  
142 - // 转换结果  
143 - Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),  
144 - "支付结果非 SUCCESS");  
145 - return PayOrderNotifyRespDTO.builder()  
146 - .orderExtensionNo(result.getOutTradeNo())  
147 - .channelOrderNo(result.getTradeState())  
148 - .channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null)  
149 - .successTime(parseDateV3(result.getSuccessTime()))  
150 - .build(); 172 + private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
  173 + // 1. 构建 WxPayRefundRequest 请求
  174 + WxPayRefundRequest request = new WxPayRefundRequest()
  175 + .setOutTradeNo(reqDTO.getOutTradeNo())
  176 + .setOutRefundNo(reqDTO.getOutRefundNo())
  177 + .setRefundFee(reqDTO.getRefundPrice())
  178 + .setRefundDesc(reqDTO.getReason())
  179 + .setTotalFee(reqDTO.getPayPrice())
  180 + .setNotifyUrl(reqDTO.getNotifyUrl());
  181 + // 2.1 执行请求
  182 + WxPayRefundResult response = client.refundV2(request);
  183 + // 2.2 创建返回结果
  184 + PayRefundRespDTO refund = new PayRefundRespDTO()
  185 + .setOutRefundNo(reqDTO.getOutRefundNo())
  186 + .setRawData(response);
  187 + if (Objects.equals("SUCCESS", response.getResultCode())) {
  188 + refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
  189 + .setChannelRefundNo(response.getRefundId());
  190 + } else {
  191 + refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
  192 + }
  193 + // TODO 芋艿;异常的处理;
  194 + return refund;
151 } 195 }
152 196
153 - // ========== 各种工具方法 ========== 197 + private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
  198 + // 1. 构建 WxPayRefundRequest 请求
  199 + WxPayRefundV3Request request = new WxPayRefundV3Request()
  200 + .setOutTradeNo(reqDTO.getOutTradeNo())
  201 + .setOutRefundNo(reqDTO.getOutRefundNo())
  202 + .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
  203 + .setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
  204 + .setReason(reqDTO.getReason())
  205 + .setNotifyUrl(reqDTO.getNotifyUrl());
  206 + // 2.1 执行请求
  207 + WxPayRefundV3Result response = client.refundV3(request);
  208 + // 2.2 创建返回结果
  209 + PayRefundRespDTO refund = new PayRefundRespDTO()
  210 + .setOutRefundNo(reqDTO.getOutRefundNo())
  211 + .setRawData(response);
  212 + if (Objects.equals("SUCCESS", response.getStatus())) {
  213 + refund.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
  214 + .setChannelRefundNo(response.getRefundId())
  215 + .setSuccessTime(parseDateV3(response.getSuccessTime()));
  216 + } else if (Objects.equals("PROCESSING", response.getStatus())) {
  217 + refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
  218 + .setChannelRefundNo(response.getRefundId());
  219 + } else {
  220 + refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
  221 + }
  222 + // TODO 芋艿;异常的处理;
  223 + return refund;
  224 + }
154 225
155 - /**  
156 - * 构建统一下单的异常  
157 - *  
158 - * 目的:将参数不正确等异常,转换成 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常  
159 - *  
160 - * @param reqDTO 请求  
161 - * @param e 微信的支付异常  
162 - * @return 转换后的异常  
163 - *  
164 - */  
165 - static Exception buildUnifiedOrderException(PayOrderUnifiedReqDTO reqDTO, WxPayException e) {  
166 - // 情况一:业务结果为 FAIL  
167 - if (Objects.equals(e.getResultCode(), "FAIL")) {  
168 - log.error("[buildUnifiedOrderException][request({}) 发起支付失败]", toJsonString(reqDTO), e);  
169 - if (Objects.equals(e.getErrCode(), "PARAM_ERROR")) {  
170 - throw invalidParamException(e.getErrCodeDes()); 226 + @Override
  227 + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
  228 + try {
  229 + // 微信支付 v2 回调结果处理
  230 + switch (config.getApiVersion()) {
  231 + case API_VERSION_V2:
  232 + return parseRefundNotifyV2(body);
  233 + case WxPayClientConfig.API_VERSION_V3:
  234 + return parseRefundNotifyV3(body);
  235 + default:
  236 + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
171 } 237 }
172 - throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getErrCodeDes()); 238 + } catch (WxPayException e) {
  239 + log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
  240 + throw new RuntimeException(e);
  241 + // TODO 芋艿:缺一个异常翻译
  242 + }
  243 + }
  244 +
  245 + @SuppressWarnings("DuplicatedCode")
  246 + private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
  247 + // 1. 解析回调
  248 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
  249 + WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo();
  250 + // 2. 构建结果
  251 + PayRefundRespDTO notify = new PayRefundRespDTO()
  252 + .setChannelRefundNo(responseResult.getRefundId())
  253 + .setOutRefundNo(responseResult.getOutRefundNo())
  254 + .setRawData(response);
  255 + if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
  256 + notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
  257 + .setSuccessTime(parseDateV2B(responseResult.getSuccessTime()));
  258 + } else {
  259 + notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
173 } 260 }
174 - // 情况二:状态码结果为 FAIL  
175 - if (Objects.equals(e.getReturnCode(), "FAIL")) {  
176 - throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getReturnMsg()); 261 + return notify;
  262 + }
  263 +
  264 + @SuppressWarnings("DuplicatedCode")
  265 + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
  266 + // 1. 解析回调
  267 + WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
  268 + WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult();
  269 + // 2. 构建结果
  270 + PayRefundRespDTO notify = new PayRefundRespDTO()
  271 + .setChannelRefundNo(responseResult.getRefundId())
  272 + .setOutRefundNo(responseResult.getOutRefundNo())
  273 + .setRawData(response);
  274 + if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
  275 + notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
  276 + .setSuccessTime(parseDateV3(responseResult.getSuccessTime()));
  277 + } else {
  278 + notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
177 } 279 }
178 - // 情况三:系统异常,这里暂时不打,交给上层的 AbstractPayClient 统一打  
179 - return e; 280 + return notify;
180 } 281 }
181 282
  283 + // ========== 各种工具方法 ==========
  284 +
182 static String formatDateV2(LocalDateTime time) { 285 static String formatDateV2(LocalDateTime time) {
183 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); 286 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
184 } 287 }
@@ -187,6 +290,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -187,6 +290,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
187 return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); 290 return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
188 } 291 }
189 292
  293 + static LocalDateTime parseDateV2B(String time) {
  294 + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
  295 + }
  296 +
190 static String formatDateV3(LocalDateTime time) { 297 static String formatDateV3(LocalDateTime time) {
191 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); 298 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
192 } 299 }
@@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
195 return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); 302 return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
196 } 303 }
197 304
  305 + static String getErrorCode(WxPayException e) {
  306 + if (StrUtil.isNotEmpty(e.getErrCode())) {
  307 + return e.getErrCode();
  308 + }
  309 + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
  310 + return "CUSTOM_ERROR";
  311 + }
  312 + return e.getReturnCode();
  313 + }
  314 +
  315 + static String getErrorMessage(WxPayException e) {
  316 + if (StrUtil.isNotEmpty(e.getErrCode())) {
  317 + return e.getErrCodeDes();
  318 + }
  319 + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
  320 + return e.getCustomErrorMsg();
  321 + }
  322 + return e.getReturnMsg();
  323 + }
  324 +
198 } 325 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;  
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 import com.github.binarywang.wxpay.constant.WxPayConstants; 6 import com.github.binarywang.wxpay.constant.WxPayConstants;
9 import com.github.binarywang.wxpay.exception.WxPayException; 7 import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient { @@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
21 } 19 }
22 20
23 @Override 21 @Override
24 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { 22 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
25 return null; 23 return null;
26 } 24 }
27 25
28 @Override 26 @Override
29 - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {  
30 - return null;  
31 - }  
32 -  
33 - @Override  
34 - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 27 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
35 return null; 28 return null;
36 } 29 }
37 30
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java
@@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil; @@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil;
4 import cn.hutool.core.thread.ThreadUtil; 4 import cn.hutool.core.thread.ThreadUtil;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 6 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
7 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
8 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; 7 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
9 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 8 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
10 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
11 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;  
12 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
13 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 9 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
14 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 10 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
15 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; 11 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
@@ -23,6 +19,7 @@ import java.time.LocalDateTime; @@ -23,6 +19,7 @@ import java.time.LocalDateTime;
23 import java.util.concurrent.TimeUnit; 19 import java.util.concurrent.TimeUnit;
24 20
25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; 21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  22 +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
26 23
27 /** 24 /**
28 * 微信支付【付款码支付】的 PayClient 实现类 25 * 微信支付【付款码支付】的 PayClient 实现类
@@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient { @@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
49 } 46 }
50 47
51 @Override 48 @Override
52 - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 49 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
53 // 由于付款码需要不断轮询,所以需要在较短的时间完成支付 50 // 由于付款码需要不断轮询,所以需要在较短的时间完成支付
54 LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); 51 LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
55 if (expireTime.isAfter(reqDTO.getExpireTime())) { 52 if (expireTime.isAfter(reqDTO.getExpireTime())) {
@@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient { @@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
57 } 54 }
58 // 构建 WxPayMicropayRequest 对象 55 // 构建 WxPayMicropayRequest 对象
59 WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() 56 WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
60 - .outTradeNo(reqDTO.getMerchantOrderId()) 57 + .outTradeNo(reqDTO.getOutTradeNo())
61 .body(reqDTO.getSubject()) 58 .body(reqDTO.getSubject())
62 .detail(reqDTO.getBody()) 59 .detail(reqDTO.getBody())
63 - .totalFee(reqDTO.getAmount()) // 单位分 60 + .totalFee(reqDTO.getPrice()) // 单位分
64 .timeExpire(formatDateV2(expireTime)) 61 .timeExpire(formatDateV2(expireTime))
65 .spbillCreateIp(reqDTO.getUserIp()) 62 .spbillCreateIp(reqDTO.getUserIp())
66 .authCode(getAuthCode(reqDTO)) 63 .authCode(getAuthCode(reqDTO))
67 .build(); 64 .build();
68 // 执行请求,重试直到失败(过期),或者成功 65 // 执行请求,重试直到失败(过期),或者成功
  66 + WxPayException lastWxPayException = null;
69 for (int i = 1; i < Byte.MAX_VALUE; i++) { 67 for (int i = 1; i < Byte.MAX_VALUE; i++) {
70 try { 68 try {
71 WxPayMicropayResult response = client.micropay(request); 69 WxPayMicropayResult response = client.micropay(request);
72 - // 支付成功(例如说,用户输入了密码)  
73 - PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder()  
74 - .orderExtensionNo(response.getOutTradeNo())  
75 - .channelOrderNo(response.getTransactionId())  
76 - .channelUserId(response.getOpenid())  
77 - .successTime(parseDateV2(response.getTimeEnd()))  
78 - .build();  
79 - return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),  
80 - JsonUtils.toJsonString(response))  
81 - .setNotify(notify); 70 + // 支付成功,例如说:1)用户输入了密码;2)
  71 + return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
  72 + response.getOutTradeNo(), response)
  73 + .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
82 } catch (WxPayException ex) { 74 } catch (WxPayException ex) {
  75 + lastWxPayException = ex;
83 // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理 76 // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
84 // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。 77 // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
85 // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 78 // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
@@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient { @@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
89 } 82 }
90 // 等待 5 秒,继续下一轮重新发起支付 83 // 等待 5 秒,继续下一轮重新发起支付
91 log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, 84 log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
92 - JsonUtils.toJsonString(request), ex.getMessage()); 85 + toJsonString(request), ex.getMessage());
93 ThreadUtil.sleep(5, TimeUnit.SECONDS); 86 ThreadUtil.sleep(5, TimeUnit.SECONDS);
94 } 87 }
95 } 88 }
96 - throw new IllegalStateException("微信 Bar 支付,重试多次失败"); 89 + throw lastWxPayException;
97 } 90 }
98 91
99 @Override 92 @Override
100 - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 93 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
101 return doUnifiedOrderV2(reqDTO); 94 return doUnifiedOrderV2(reqDTO);
102 } 95 }
103 96
104 - @Override  
105 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {  
106 - return null;  
107 - }  
108 -  
109 // ========== 各种工具方法 ========== 97 // ========== 各种工具方法 ==========
110 98
111 static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { 99 static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxH5PayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;  
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 import com.github.binarywang.wxpay.constant.WxPayConstants; 6 import com.github.binarywang.wxpay.constant.WxPayConstants;
9 import com.github.binarywang.wxpay.exception.WxPayException; 7 import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient { @@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
21 } 19 }
22 20
23 @Override 21 @Override
24 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { 22 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
25 return null; 23 return null;
26 } 24 }
27 25
28 @Override 26 @Override
29 - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {  
30 - return null;  
31 - }  
32 -  
33 - @Override  
34 - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 27 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
35 return null; 28 return null;
36 } 29 }
37 30
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;  
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; 7 import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
@@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient { @@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
34 } 32 }
35 33
36 @Override 34 @Override
37 - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 35 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
38 // 构建 WxPayUnifiedOrderRequest 对象 36 // 构建 WxPayUnifiedOrderRequest 对象
39 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() 37 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
40 - .outTradeNo(reqDTO.getMerchantOrderId()) 38 + .outTradeNo(reqDTO.getOutTradeNo())
41 .body(reqDTO.getSubject()) 39 .body(reqDTO.getSubject())
42 .detail(reqDTO.getBody()) 40 .detail(reqDTO.getBody())
43 - .totalFee(reqDTO.getAmount()) // 单位分  
44 - .productId(reqDTO.getMerchantOrderId()) 41 + .totalFee(reqDTO.getPrice()) // 单位分
  42 + .productId(reqDTO.getOutTradeNo())
45 .timeExpire(formatDateV2(reqDTO.getExpireTime())) 43 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
46 .spbillCreateIp(reqDTO.getUserIp()) 44 .spbillCreateIp(reqDTO.getUserIp())
47 .notifyUrl(reqDTO.getNotifyUrl()) 45 .notifyUrl(reqDTO.getNotifyUrl())
@@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient { @@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
50 WxPayNativeOrderResult response = client.createOrder(request); 48 WxPayNativeOrderResult response = client.createOrder(request);
51 49
52 // 转换结果 50 // 转换结果
53 - return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),  
54 - response.getCodeUrl()); 51 + return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
  52 + reqDTO.getOutTradeNo(), response);
55 } 53 }
56 54
57 @Override 55 @Override
58 - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 56 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
59 // 构建 WxPayUnifiedOrderRequest 对象 57 // 构建 WxPayUnifiedOrderRequest 对象
60 - WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();  
61 - request.setOutTradeNo(reqDTO.getMerchantOrderId());  
62 - request.setDescription(reqDTO.getBody());  
63 - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分  
64 - request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));  
65 - request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));  
66 - request.setNotifyUrl(reqDTO.getNotifyUrl()); 58 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
  59 + .setOutTradeNo(reqDTO.getOutTradeNo())
  60 + .setDescription(reqDTO.getSubject())
  61 + .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())) // 单位分
  62 + .setTimeExpire(formatDateV3(reqDTO.getExpireTime()))
  63 + .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()))
  64 + .setNotifyUrl(reqDTO.getNotifyUrl());
67 // 执行请求 65 // 执行请求
68 - WxPayNativeOrderResult response = client.createOrderV3(TradeTypeEnum.NATIVE, request); 66 + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
69 67
70 // 转换结果 68 // 转换结果
71 - return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),  
72 - response.getCodeUrl());  
73 - }  
74 -  
75 - @Override  
76 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {  
77 - return null; 69 + return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
  70 + reqDTO.getOutTradeNo(), response);
78 } 71 }
79 72
80 } 73 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java
@@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig { @@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
37 * 37 *
38 * 只有公众号或小程序需要该字段 38 * 只有公众号或小程序需要该字段
39 */ 39 */
  40 + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
40 private String appId; 41 private String appId;
41 /** 42 /**
42 * 商户号 43 * 商户号
43 */ 44 */
44 - @NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class}) 45 + @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
45 private String mchId; 46 private String mchId;
46 /** 47 /**
47 * API 版本 48 * API 版本
48 */ 49 */
49 - @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class}) 50 + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
50 private String apiVersion; 51 private String apiVersion;
51 52
52 // ========== V2 版本的参数 ========== 53 // ========== V2 版本的参数 ==========
@@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig { @@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
54 /** 55 /**
55 * 商户密钥 56 * 商户密钥
56 */ 57 */
57 - @NotBlank(message = "商户密钥 不能为空", groups = V2.class) 58 + @NotBlank(message = "商户密钥不能为空", groups = V2.class)
58 private String mchKey; 59 private String mchKey;
59 /** 60 /**
60 - * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.  
61 - * 对应的字符串 61 + * apiclient_cert.p12 证书文件的对应字符串【base64 格式】
62 * 62 *
63 - * 注意,可通过 {@link #main(String[])} 读取 63 + * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
64 */ 64 */
65 - /// private String keyContent; 65 + @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
  66 + private String keyContent;
66 67
67 // ========== V3 版本的参数 ========== 68 // ========== V3 版本的参数 ==========
68 /** 69 /**
69 - * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.  
70 - * 对应的字符串  
71 - * 注意,可通过 {@link #main(String[])} 读取 70 + * apiclient_key.pem 证书文件的对应字符串
72 */ 71 */
73 @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) 72 @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
74 private String privateKeyContent; 73 private String privateKeyContent;
75 /** 74 /**
76 - * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.  
77 - * 对应的字符串  
78 - * <p>  
79 - * 注意,可通过 {@link #main(String[])} 读取 75 + * apiclient_cert.pem 证书文件的对应的字符串
80 */ 76 */
81 @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) 77 @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
82 private String privateCertContent; 78 private String privateCertContent;
83 /** 79 /**
84 * apiV3 密钥值 80 * apiV3 密钥值
85 */ 81 */
86 - @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class) 82 + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
87 private String apiV3Key; 83 private String apiV3Key;
88 84
89 /** 85 /**
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java
@@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; @@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
5 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
7 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
8 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;  
9 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;  
10 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
11 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
12 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; 9 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
@@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException; @@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
19 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
20 17
21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; 18 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  19 +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
22 20
23 /** 21 /**
24 * 微信支付(公众号)的 PayClient 实现类 22 * 微信支付(公众号)的 PayClient 实现类
@@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient { @@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
44 } 42 }
45 43
46 @Override 44 @Override
47 - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 45 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
48 // 构建 WxPayUnifiedOrderRequest 对象 46 // 构建 WxPayUnifiedOrderRequest 对象
49 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() 47 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
50 - .outTradeNo(reqDTO.getMerchantOrderId()) 48 + .outTradeNo(reqDTO.getOutTradeNo())
51 .body(reqDTO.getSubject()) 49 .body(reqDTO.getSubject())
52 .detail(reqDTO.getBody()) 50 .detail(reqDTO.getBody())
53 - .totalFee(reqDTO.getAmount()) // 单位分 51 + .totalFee(reqDTO.getPrice()) // 单位分
54 .timeExpire(formatDateV2(reqDTO.getExpireTime())) 52 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
55 .spbillCreateIp(reqDTO.getUserIp()) 53 .spbillCreateIp(reqDTO.getUserIp())
56 .openid(getOpenid(reqDTO)) 54 .openid(getOpenid(reqDTO))
@@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient { @@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
60 WxPayMpOrderResult response = client.createOrder(request); 58 WxPayMpOrderResult response = client.createOrder(request);
61 59
62 // 转换结果 60 // 转换结果
63 - return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),  
64 - JsonUtils.toJsonString(response)); 61 + return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
  62 + reqDTO.getOutTradeNo(), response);
65 } 63 }
66 64
67 @Override 65 @Override
68 - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { 66 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
69 // 构建 WxPayUnifiedOrderRequest 对象 67 // 构建 WxPayUnifiedOrderRequest 对象
70 WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); 68 WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
71 - request.setOutTradeNo(reqDTO.getMerchantOrderId()); 69 + request.setOutTradeNo(reqDTO.getOutTradeNo());
72 request.setDescription(reqDTO.getSubject()); 70 request.setDescription(reqDTO.getSubject());
73 - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 71 + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
74 request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); 72 request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
75 request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); 73 request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
76 request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); 74 request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
@@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient { @@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient {
79 WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); 77 WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
80 78
81 // 转换结果 79 // 转换结果
82 - return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),  
83 - JsonUtils.toJsonString(response));  
84 - }  
85 -  
86 - @Override  
87 - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {  
88 - // TODO 需要实现  
89 - throw new UnsupportedOperationException(); 80 + return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
  81 + reqDTO.getOutTradeNo(), response);
90 } 82 }
91 83
92 // ========== 各种工具方法 ========== 84 // ========== 各种工具方法 ==========
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.enums;  
2 -  
3 -import cn.iocoder.yudao.framework.common.exception.ErrorCode;  
4 -  
5 -/**  
6 - * 支付框架的错误码枚举  
7 - *  
8 - * 支付框架,使用 2-002-000-000 段  
9 - *  
10 - * @author 芋道源码  
11 - */  
12 -public interface PayFrameworkErrorCodeConstants {  
13 -  
14 - ErrorCode ORDER_UNIFIED_ERROR = new ErrorCode(2002000000, "发起支付失败,原因:{}");  
15 -  
16 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java
@@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum { @@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum {
18 QR_CODE("qr_code"), // 二维码的文字内容 18 QR_CODE("qr_code"), // 二维码的文字内容
19 QR_CODE_URL("qr_code_url"), // 二维码的图片链接 19 QR_CODE_URL("qr_code_url"), // 二维码的图片链接
20 BAR_CODE("bar_code"), // 条形码 20 BAR_CODE("bar_code"), // 条形码
21 - APP("app"), // 应用【目前暂时用不到】  
22 - CUSTOM("custom"), // 自定义:每种支付方式,做个性化处理;例如说,微信公众号支付时,调用 JSAPI 接口 21 + APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
23 ; 22 ;
24 23
25 /** 24 /**
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java
@@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order; @@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
3 import lombok.AllArgsConstructor; 3 import lombok.AllArgsConstructor;
4 import lombok.Getter; 4 import lombok.Getter;
5 5
  6 +import java.util.Objects;
  7 +
6 /** 8 /**
7 * 渠道的支付状态枚举 9 * 渠道的支付状态枚举
8 * 10 *
9 - * @author 遇到源码 11 + * @author 芋道源码
10 */ 12 */
11 @Getter 13 @Getter
12 @AllArgsConstructor 14 @AllArgsConstructor
@@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum { @@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
20 private final Integer status; 22 private final Integer status;
21 private final String name; 23 private final String name;
22 24
  25 + /**
  26 + * 判断是否支付成功
  27 + *
  28 + * @param status 状态
  29 + * @return 是否支付成功
  30 + */
  31 + public static boolean isSuccess(Integer status) {
  32 + return Objects.equals(status, SUCCESS.getStatus());
  33 + }
  34 +
  35 + /**
  36 + * 判断是否支付关闭
  37 + *
  38 + * @param status 状态
  39 + * @return 是否支付关闭
  40 + */
  41 + public static boolean isClosed(Integer status) {
  42 + return Objects.equals(status, CLOSED.getStatus());
  43 + }
  44 +
23 } 45 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java deleted
1 -package cn.iocoder.yudao.framework.pay.core.enums.refund;  
2 -  
3 -// TODO 芋艿:看看能不能去掉  
4 -/**  
5 - * 退款通知, 统一的渠道退款状态  
6 - *  
7 - * @author jason  
8 - */  
9 -public enum PayNotifyRefundStatusEnum {  
10 -  
11 - /**  
12 - * 支付宝 中 全额退款 trade_status=TRADE_CLOSED, 部分退款 trade_status=TRADE_SUCCESS  
13 - * 退款成功  
14 - */  
15 - SUCCESS,  
16 -  
17 - /**  
18 - * 支付宝退款通知没有这个状态  
19 - * 退款异常  
20 - */  
21 - ABNORMAL;  
22 -  
23 -}  
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java renamed to yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.pay.core.enums.refund; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.pay.core.enums.refund;
3 import lombok.AllArgsConstructor; 3 import lombok.AllArgsConstructor;
4 import lombok.Getter; 4 import lombok.Getter;
5 5
  6 +import java.util.Objects;
  7 +
6 /** 8 /**
7 * 渠道的退款状态枚举 9 * 渠道的退款状态枚举
8 * 10 *
@@ -10,14 +12,17 @@ import lombok.Getter; @@ -10,14 +12,17 @@ import lombok.Getter;
10 */ 12 */
11 @Getter 13 @Getter
12 @AllArgsConstructor 14 @AllArgsConstructor
13 -public enum PayRefundRespEnum { 15 +public enum PayRefundStatusRespEnum {
14 16
15 - SUCCESS(1, "退款成功"),  
16 - FAILURE(2, "退款失败"),  
17 - PROCESSING(3,"退款处理中"),  
18 - CLOSED(4, "退款关闭"); 17 + WAITING(0, "未退款"),
  18 + SUCCESS(10, "退款成功"),
  19 + FAILURE(20, "退款失败");
19 20
20 private final Integer status; 21 private final Integer status;
21 private final String name; 22 private final String name;
22 23
  24 + public static boolean isSuccess(Integer status) {
  25 + return Objects.equals(status, SUCCESS.getStatus());
  26 + }
  27 +
23 } 28 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java
@@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest { @@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
121 121
122 private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { 122 private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
123 PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); 123 PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
124 - reqDTO.setAmount(123); 124 + reqDTO.setPrice(123);
125 reqDTO.setSubject("IPhone 13"); 125 reqDTO.setSubject("IPhone 13");
126 reqDTO.setBody("biubiubiu"); 126 reqDTO.setBody("biubiubiu");
127 - reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); 127 + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
128 reqDTO.setUserIp("127.0.0.1"); 128 reqDTO.setUserIp("127.0.0.1");
129 reqDTO.setNotifyUrl("http://127.0.0.1:8080"); 129 reqDTO.setNotifyUrl("http://127.0.0.1:8080");
130 return reqDTO; 130 return reqDTO;
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java
1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 import cn.hutool.core.util.ReflectUtil; 2 import cn.hutool.core.util.ReflectUtil;
3 -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;  
4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 3 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 import com.alipay.api.AlipayApiException; 5 import com.alipay.api.AlipayApiException;
8 import com.alipay.api.DefaultAlipayClient; 6 import com.alipay.api.DefaultAlipayClient;
@@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest { @@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
74 // 这里,设置可以直接随机整个对象。 72 // 这里,设置可以直接随机整个对象。
75 Long shopOrderId = System.currentTimeMillis(); 73 Long shopOrderId = System.currentTimeMillis();
76 PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO(); 74 PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
77 - reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));  
78 - reqDTO.setAmount(1); 75 + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
  76 + reqDTO.setPrice(1);
79 reqDTO.setBody("内容:" + shopOrderId); 77 reqDTO.setBody("内容:" + shopOrderId);
80 reqDTO.setSubject("标题:"+shopOrderId); 78 reqDTO.setSubject("标题:"+shopOrderId);
81 String notify="http://niubi.natapp1.cc/api/pay/order/notify"; 79 String notify="http://niubi.natapp1.cc/api/pay/order/notify";
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
  6 +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
  7 +import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
  8 +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
  9 +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
  10 +import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
  11 +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
  12 +import com.github.binarywang.wxpay.config.WxPayConfig;
  13 +import com.github.binarywang.wxpay.exception.WxPayException;
  14 +import com.github.binarywang.wxpay.service.WxPayService;
  15 +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
  16 +import org.junit.jupiter.api.Disabled;
  17 +import org.junit.jupiter.api.Test;
  18 +
  19 +import java.time.Duration;
  20 +
  21 +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2;
  22 +
  23 +/**
  24 + * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Disabled
  29 +public class WxBarPayClientIntegrationTest {
  30 +
  31 + @Test
  32 + public void testPayV2() throws WxPayException {
  33 + // 创建 config 配置
  34 + WxPayConfig config = buildWxPayConfigV2();
  35 + // 创建 WxPayService 客户端
  36 + WxPayService client = new WxPayServiceImpl();
  37 + client.setConfig(config);
  38 +
  39 + // 执行发起支付
  40 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
  41 + .outTradeNo(String.valueOf(System.currentTimeMillis()))
  42 + .body("测试支付-body")
  43 + .detail("测试支付-detail")
  44 + .totalFee(1) // 单位分
  45 + .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
  46 + .spbillCreateIp("127.0.0.1")
  47 + .authCode("134298744426278497")
  48 + .build();
  49 + System.out.println("========= request ==========");
  50 + System.out.println(JsonUtils.toJsonPrettyString(request));
  51 + WxPayMicropayResult response = client.micropay(request);
  52 + System.out.println("========= response ==========");
  53 + System.out.println(JsonUtils.toJsonPrettyString(response));
  54 + }
  55 +
  56 + @Test
  57 + public void testParseRefundNotifyV2() throws WxPayException {
  58 + // 创建 config 配置
  59 + WxPayConfig config = buildWxPayConfigV2();
  60 + // 创建 WxPayService 客户端
  61 + WxPayService client = new WxPayServiceImpl();
  62 + client.setConfig(config);
  63 +
  64 + // 执行解析
  65 + String xml = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx62056c0d5e8db250]]></appid><mch_id><![CDATA[1545083881]]></mch_id><nonce_str><![CDATA[ed8f02c21d15635cede114a42d0525a0]]></nonce_str><req_info><![CDATA[bGp+wB9DAHjoOO9Nw1iSmmIFdN2zZDhsoRWZBYdf/8bcpjowr4T8i2qjLsbMtvKQeVC5kBZOL/Agal3be6UPwnoantil+L+ojZgvLch7dXFKs/AcoxIYcVYyGka+wmnRJfUmuFRBgzt++8HOFsmJz6e2brYv1EAz+93fP2AsJtRuw1FEzodcg8eXm52hbE0KhLNqC2OyNVkn8AbOOrwIxSYobg2jVbuJ4JllYbEGIQ/6kWzNbVmMKhGJGYBy/NbUGKoQsoe4QeTQqcqQqVp08muxaOfJGThaN3B9EEMFSrog/3yT7ykVV6WQ5+Ygt89LplOf5ucWa4Ird7VJhHWtzI92ZePj4Omy1XkT1TRlwtDegA0S5MeQpM4WZ1taMrhxgmNkTUJ0JXFncx5e2KLQvbvD/HOcccx48Xv1c16JBz6G3501k8E++LWXgZ2TeNXwGsk6FyRZb0ApLyQHIx5ZtPo/UET9z3AmJCPXkrUsZ4WK46fDtbzxVPU2r8nTOcGCPbO0LUsGT6wpsuQVC4CisXDJwoZmL6kKwHfKs6mmUL2YZYzNfgoB/KgpJYSpC96kcpQyFvw+xuwqK2SXGZbAl9lADT+a83z04feQHSSIG3PCrX4QEWzpCZZ4+ySEz1Y34aoU20X9GtX+1LSwUjmQgwHrMBSvFm3/B7+IFM8OUqDB+Uvkr9Uvy7P2/KDvfy3Ih7GFcGd0C5NXpSvVTTfu1IlK/T3/t6MR/8iq78pp/2ZTYvO6eNDRJWaXYU+x6sl2dTs9n+2Z4W4AfYTvEyuxlx+aI19SqCJh7WmaFcAxidFl/9iqDjWiplb9+C6ijZv2hJtVjSCuoptIWpGDYItH7RAqlKHrx6flJD+M/5BceMHBv2w4OWCD9vPRLo8gl9o06ip0iflzO1dixhOAgLFjsQmQHNGFtR3EvCID+iS4FUlilwK+hcKNxrr0wp9Btkl9W1R9aTo289CUiIxx45skfCYzHwb+7Hqj3uTiXnep6zhCKZBAnPsDOvISXfBgXKufcFsTNtts09jX8H5/uMc9wyJ179H1cp+At1mIK2duwfo4Q9asfEoffl6Zn1olGdtEruxHGeVU0NwJ8V7RflC/Cx5RXtJ3sPJ/sHmVnBlVyR0=]]></req_info></xml>";
  66 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml);
  67 + System.out.println(response.getReqInfo());
  68 + }
  69 +
  70 + @Test
  71 + public void testRefundV2() throws WxPayException {
  72 + // 创建 config 配置
  73 + WxPayConfig config = buildWxPayConfigV2();
  74 + // 创建 WxPayService 客户端
  75 + WxPayService client = new WxPayServiceImpl();
  76 + client.setConfig(config);
  77 +
  78 + // 执行发起退款
  79 + WxPayRefundRequest request = new WxPayRefundRequest()
  80 + .setOutTradeNo("1689545667276")
  81 + .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
  82 + .setRefundFee(1)
  83 + .setRefundDesc("就是想退了")
  84 + .setTotalFee(1);
  85 + System.out.println("========= request ==========");
  86 + System.out.println(JsonUtils.toJsonPrettyString(request));
  87 + WxPayRefundResult response = client.refund(request);
  88 + System.out.println("========= response ==========");
  89 + System.out.println(JsonUtils.toJsonPrettyString(response));
  90 + }
  91 +
  92 + @Test
  93 + public void testRefundV3() throws WxPayException {
  94 + // 创建 config 配置
  95 + WxPayConfig config = buildWxPayConfigV2();
  96 + // 创建 WxPayService 客户端
  97 + WxPayService client = new WxPayServiceImpl();
  98 + client.setConfig(config);
  99 +
  100 + // 执行发起退款
  101 + WxPayRefundV3Request request = new WxPayRefundV3Request()
  102 + .setOutTradeNo("1689506325635")
  103 + .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
  104 + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
  105 + .setReason("就是想退了");
  106 + System.out.println("========= request ==========");
  107 + System.out.println(JsonUtils.toJsonPrettyString(request));
  108 + WxPayRefundV3Result response = client.refundV3(request);
  109 + System.out.println("========= response ==========");
  110 + System.out.println(JsonUtils.toJsonPrettyString(response));
  111 + }
  112 +
  113 + private WxPayConfig buildWxPayConfigV2() {
  114 + WxPayConfig config = new WxPayConfig();
  115 + config.setAppId("wx62056c0d5e8db250");
  116 + config.setMchId("1545083881");
  117 + config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
  118 +// config.setSignType(WxPayConstants.SignType.MD5);
  119 + config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
  120 + return config;
  121 + }
  122 +
  123 +}
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
  6 +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
  7 +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
  8 +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
  9 +import com.github.binarywang.wxpay.config.WxPayConfig;
  10 +import com.github.binarywang.wxpay.exception.WxPayException;
  11 +import com.github.binarywang.wxpay.service.WxPayService;
  12 +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
  13 +import org.junit.jupiter.api.Disabled;
  14 +import org.junit.jupiter.api.Test;
  15 +
  16 +import java.time.Duration;
  17 +
  18 +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3;
  19 +
  20 +/**
  21 + * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Disabled
  26 +public class WxNativePayClientIntegrationTest {
  27 +
  28 + @Test
  29 + public void testPayV3() throws WxPayException {
  30 + // 创建 config 配置
  31 + WxPayConfig config = buildWxPayConfigV3();
  32 + // 创建 WxPayService 客户端
  33 + WxPayService client = new WxPayServiceImpl();
  34 + client.setConfig(config);
  35 +
  36 + // 执行发起支付
  37 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
  38 + .setOutTradeNo(String.valueOf(System.currentTimeMillis()))
  39 + .setDescription("测试支付-body")
  40 + .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分
  41 + .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
  42 + .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1"))
  43 + .setNotifyUrl("http://127.0.0.1:48080");
  44 + System.out.println("========= request ==========");
  45 + System.out.println(JsonUtils.toJsonPrettyString(request));
  46 + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
  47 + System.out.println("========= response ==========");
  48 + System.out.println(JsonUtils.toJsonPrettyString(response));
  49 + }
  50 +
  51 + @Test
  52 + public void testRefundV3() throws WxPayException {
  53 + // 创建 config 配置
  54 + WxPayConfig config = buildWxPayConfigV3();
  55 + // 创建 WxPayService 客户端
  56 + WxPayService client = new WxPayServiceImpl();
  57 + client.setConfig(config);
  58 +
  59 + // 执行发起退款
  60 + WxPayRefundV3Request request = new WxPayRefundV3Request()
  61 + .setOutTradeNo("1689545729695")
  62 + .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
  63 + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
  64 + .setReason("就是想退了");
  65 + System.out.println("========= request ==========");
  66 + System.out.println(JsonUtils.toJsonPrettyString(request));
  67 + WxPayRefundV3Result response = client.refundV3(request);
  68 + System.out.println("========= response ==========");
  69 + System.out.println(JsonUtils.toJsonPrettyString(response));
  70 + }
  71 +
  72 + private WxPayConfig buildWxPayConfigV3() {
  73 + WxPayConfig config = new WxPayConfig();
  74 + config.setAppId("wx62056c0d5e8db250");
  75 + config.setMchId("1545083881");
  76 + config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
  77 +// config.setCertSerialNo(serialNo);
  78 + config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
  79 + config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
  80 + return config;
  81 + }
  82 +
  83 +}
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java
@@ -351,7 +351,7 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa @@ -351,7 +351,7 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa
351 public void afterCommit() { 351 public void afterCommit() {
352 // 创建退款单 352 // 创建退款单
353 PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties); 353 PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties);
354 - Long payRefundId = payRefundApi.createPayRefund(createReqDTO); 354 + Long payRefundId = payRefundApi.createRefund(createReqDTO);
355 // 更新售后单的退款单号 355 // 更新售后单的退款单号
356 tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId)); 356 tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
357 } 357 }
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java
@@ -18,7 +18,7 @@ public interface PayRefundApi { @@ -18,7 +18,7 @@ public interface PayRefundApi {
18 * @param reqDTO 创建请求 18 * @param reqDTO 创建请求
19 * @return 退款单编号 19 * @return 退款单编号
20 */ 20 */
21 - Long createPayRefund(@Valid PayRefundCreateReqDTO reqDTO); 21 + Long createRefund(@Valid PayRefundCreateReqDTO reqDTO);
22 22
23 /** 23 /**
24 * 获得退款单 24 * 获得退款单
@@ -26,6 +26,6 @@ public interface PayRefundApi { @@ -26,6 +26,6 @@ public interface PayRefundApi {
26 * @param id 退款单编号 26 * @param id 退款单编号
27 * @return 退款单 27 * @return 退款单
28 */ 28 */
29 - PayRefundRespDTO getPayRefund(Long id); 29 + PayRefundRespDTO getRefund(Long id);
30 30
31 } 31 }
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
@@ -27,26 +27,32 @@ public class PayRefundCreateReqDTO { @@ -27,26 +27,32 @@ public class PayRefundCreateReqDTO {
27 private String userIp; 27 private String userIp;
28 28
29 // ========== 商户相关字段 ========== 29 // ========== 商户相关字段 ==========
  30 + /**
  31 + * 商户订单编号
  32 + */
  33 + @NotEmpty(message = "商户订单编号不能为空")
  34 + private String merchantOrderId;
  35 +
  36 + /**
  37 + * 商户退款编号
  38 + */
  39 + @NotEmpty(message = "商户退款编号不能为空")
  40 + private String merchantRefundId;
30 41
31 /** 42 /**
32 * 退款描述 43 * 退款描述
33 */ 44 */
34 @NotEmpty(message = "退款描述不能为空") 45 @NotEmpty(message = "退款描述不能为空")
35 - @Length(max = 128, message = "退款描述长度不能超过128") 46 + @Length(max = 128, message = "退款描述长度不能超过 128")
36 private String reason; 47 private String reason;
37 48
38 // ========== 订单相关字段 ========== 49 // ========== 订单相关字段 ==========
39 50
40 /** 51 /**
41 - * 支付单号  
42 - */  
43 - @NotNull(message = "支付单号不能为空")  
44 - private Long payOrderId;  
45 -  
46 - /**  
47 * 退款金额,单位:分 52 * 退款金额,单位:分
48 */ 53 */
49 @NotNull(message = "退款金额不能为空") 54 @NotNull(message = "退款金额不能为空")
50 @Min(value = 1, message = "退款金额必须大于零") 55 @Min(value = 1, message = "退款金额必须大于零")
51 private Integer price; 56 private Integer price;
  57 +
52 } 58 }
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
@@ -27,17 +27,19 @@ public interface ErrorCodeConstants { @@ -27,17 +27,19 @@ public interface ErrorCodeConstants {
27 ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付"); 27 ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
28 ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付"); 28 ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
29 ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期"); 29 ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
  30 + ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
30 31
31 // ========== ORDER 模块(拓展单) 1007003000 ========== 32 // ========== ORDER 模块(拓展单) 1007003000 ==========
32 ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在"); 33 ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
33 ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付"); 34 ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
34 35
35 // ========== 支付模块(退款) 1007006000 ========== 36 // ========== 支付模块(退款) 1007006000 ==========
36 - ErrorCode PAY_PRICE_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额"); 37 + ErrorCode PAY_REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
37 ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款"); 38 ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款");
38 - ErrorCode PAY_REFUND_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空");  
39 - ErrorCode PAY_REFUND_SUCCEED = new ErrorCode(1007006003, "已经退款成功"); 39 + ErrorCode PAY_REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中");
  40 + ErrorCode PAY_REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单");
40 ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在"); 41 ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
  42 + ErrorCode PAY_REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款");
41 43
42 // ========== 示例订单 1007900000 ========== 44 // ========== 示例订单 1007900000 ==========
43 ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在"); 45 ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在");
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java
@@ -38,4 +38,14 @@ public enum PayOrderStatusEnum implements IntArrayValuable { @@ -38,4 +38,14 @@ public enum PayOrderStatusEnum implements IntArrayValuable {
38 return Objects.equals(status, SUCCESS.getStatus()); 38 return Objects.equals(status, SUCCESS.getStatus());
39 } 39 }
40 40
  41 + /**
  42 + * 判断是否支付关闭
  43 + *
  44 + * @param status 状态
  45 + * @return 是否支付关闭
  46 + */
  47 + public static boolean isClosed(Integer status) {
  48 + return Objects.equals(status, CLOSED.getStatus());
  49 + }
  50 +
41 } 51 }
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java
@@ -5,14 +5,18 @@ import lombok.Getter; @@ -5,14 +5,18 @@ import lombok.Getter;
5 5
6 import java.util.Objects; 6 import java.util.Objects;
7 7
  8 +/**
  9 + * 渠道的退款状态枚举
  10 + *
  11 + * @author 芋道源码
  12 + */
8 @Getter 13 @Getter
9 @AllArgsConstructor 14 @AllArgsConstructor
10 public enum PayRefundStatusEnum { 15 public enum PayRefundStatusEnum {
11 16
12 - CREATE(0, "退款订单生成"),  
13 - SUCCESS(1, "退款成功"),  
14 - FAILURE(2, "退款失败"),  
15 - CLOSE(99, "退款关闭"); 17 + WAITING(0, "未退款"),
  18 + SUCCESS(10, "退款成功"),
  19 + FAILURE(20, "退款失败");
16 20
17 private final Integer status; 21 private final Integer status;
18 private final String name; 22 private final String name;
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java
@@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi { @@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi {
22 22
23 @Override 23 @Override
24 public Long createOrder(PayOrderCreateReqDTO reqDTO) { 24 public Long createOrder(PayOrderCreateReqDTO reqDTO) {
25 - return payOrderService.createPayOrder(reqDTO); 25 + return payOrderService.createOrder(reqDTO);
26 } 26 }
27 27
28 @Override 28 @Override
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.refund; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.refund;
2 2
3 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; 3 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
4 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; 4 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
  5 +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
5 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 6 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
6 import org.springframework.stereotype.Service; 7 import org.springframework.stereotype.Service;
7 import org.springframework.validation.annotation.Validated; 8 import org.springframework.validation.annotation.Validated;
@@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi { @@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi {
21 private PayRefundService payRefundService; 22 private PayRefundService payRefundService;
22 23
23 @Override 24 @Override
24 - public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { 25 + public Long createRefund(PayRefundCreateReqDTO reqDTO) {
25 return payRefundService.createPayRefund(reqDTO); 26 return payRefundService.createPayRefund(reqDTO);
26 } 27 }
27 28
28 @Override 29 @Override
29 - public PayRefundRespDTO getPayRefund(Long id) {  
30 - // TODO 芋艿:暂未实现  
31 - return null; 30 + public PayRefundRespDTO getRefund(Long id) {
  31 + return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
32 } 32 }
33 33
34 } 34 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
@@ -29,7 +29,7 @@ public class PayAppBaseVO { @@ -29,7 +29,7 @@ public class PayAppBaseVO {
29 @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback") 29 @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback")
30 @NotNull(message = "支付结果的回调地址不能为空") 30 @NotNull(message = "支付结果的回调地址不能为空")
31 @URL(message = "支付结果的回调地址必须为 URL 格式") 31 @URL(message = "支付结果的回调地址必须为 URL 格式")
32 - private String payNotifyUrl; 32 + private String orderNotifyUrl;
33 33
34 @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback") 34 @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
35 @NotNull(message = "退款结果的回调地址不能为空") 35 @NotNull(message = "退款结果的回调地址不能为空")
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
@@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify; @@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; 3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
4 import cn.iocoder.yudao.framework.pay.core.client.PayClient; 4 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; 5 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
7 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;  
8 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; 6 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
  7 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
9 import cn.iocoder.yudao.module.pay.service.order.PayOrderService; 8 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
10 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 9 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
11 import io.swagger.v3.oas.annotations.Operation; 10 import io.swagger.v3.oas.annotations.Operation;
@@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll; @@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll;
19 import java.util.Map; 18 import java.util.Map;
20 19
21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
22 -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;  
23 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; 21 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
24 22
25 @Tag(name = "管理后台 - 支付通知") 23 @Tag(name = "管理后台 - 支付通知")
@@ -37,22 +35,14 @@ public class PayNotifyController { @@ -37,22 +35,14 @@ public class PayNotifyController {
37 @Resource 35 @Resource
38 private PayClientFactory payClientFactory; 36 private PayClientFactory payClientFactory;
39 37
40 - /**  
41 - * 统一的渠道支付回调,支付宝的退款回调  
42 - *  
43 - * @param channelId 渠道编号  
44 - * @param params form 参数  
45 - * @param body request body  
46 - * @return 成功返回 "success"  
47 - */  
48 - @PostMapping(value = "/callback/{channelId}")  
49 - @Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调") 38 + @PostMapping(value = "/order/{channelId}")
  39 + @Operation(summary = "支付渠道的统一【支付】回调")
50 @PermitAll 40 @PermitAll
51 @OperateLog(enable = false) // 回调地址,无需记录操作日志 41 @OperateLog(enable = false) // 回调地址,无需记录操作日志
52 - public String notifyCallback(@PathVariable("channelId") Long channelId,  
53 - @RequestParam(required = false) Map<String, String> params,  
54 - @RequestBody(required = false) String body) {  
55 - log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); 42 + public String notifyOrder(@PathVariable("channelId") Long channelId,
  43 + @RequestParam(required = false) Map<String, String> params,
  44 + @RequestBody(required = false) String body) {
  45 + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
56 // 1. 校验支付渠道是否存在 46 // 1. 校验支付渠道是否存在
57 PayClient payClient = payClientFactory.getPayClient(channelId); 47 PayClient payClient = payClientFactory.getPayClient(channelId);
58 if (payClient == null) { 48 if (payClient == null) {
@@ -61,21 +51,30 @@ public class PayNotifyController { @@ -61,21 +51,30 @@ public class PayNotifyController {
61 } 51 }
62 52
63 // 2. 解析通知数据 53 // 2. 解析通知数据
64 - PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(params).body(body).build();  
65 - Object notify = payClient.parseNotify(rawNotify); 54 + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
  55 + orderService.notifyOrder(channelId, notify);
  56 + return "success";
  57 + }
66 58
67 - // 3. 处理通知  
68 - // 3.1:退款通知  
69 - if (notify instanceof PayRefundNotifyRespDTO) {  
70 - refundService.notifyPayRefund(channelId, (PayRefundNotifyRespDTO) notify, rawNotify);  
71 - return "success";  
72 - }  
73 - // 3.2:支付通知  
74 - if (notify instanceof PayOrderNotifyRespDTO) {  
75 - orderService.notifyPayOrder(channelId, (PayOrderNotifyRespDTO) notify, rawNotify);  
76 - return "success"; 59 + @PostMapping(value = "/refund/{channelId}")
  60 + @Operation(summary = "支付渠道的统一【退款】回调")
  61 + @PermitAll
  62 + @OperateLog(enable = false) // 回调地址,无需记录操作日志
  63 + public String notifyRefund(@PathVariable("channelId") Long channelId,
  64 + @RequestParam(required = false) Map<String, String> params,
  65 + @RequestBody(required = false) String body) {
  66 + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
  67 + // 1. 校验支付渠道是否存在
  68 + PayClient payClient = payClientFactory.getPayClient(channelId);
  69 + if (payClient == null) {
  70 + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
  71 + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
77 } 72 }
78 - throw new UnsupportedOperationException("未知通知:" + toJsonString(notify)); 73 +
  74 + // 2. 解析通知数据
  75 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
  76 + refundService.notifyRefund(channelId, notify);
  77 + return "success";
79 } 78 }
80 79
81 } 80 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java
@@ -86,7 +86,7 @@ public class PayOrderController { @@ -86,7 +86,7 @@ public class PayOrderController {
86 @PostMapping("/submit") 86 @PostMapping("/submit")
87 @Operation(summary = "提交支付订单") 87 @Operation(summary = "提交支付订单")
88 public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { 88 public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
89 - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); 89 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
90 return success(respVO); 90 return success(respVO);
91 } 91 }
92 92
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java
@@ -76,9 +76,6 @@ public class PayRefundExcelVO { @@ -76,9 +76,6 @@ public class PayRefundExcelVO {
76 @ExcelProperty("退款成功时间") 76 @ExcelProperty("退款成功时间")
77 private LocalDateTime successTime; 77 private LocalDateTime successTime;
78 78
79 - @ExcelProperty("退款通知时间")  
80 - private LocalDateTime notifyTime;  
81 -  
82 @ExcelProperty("退款失效时间") 79 @ExcelProperty("退款失效时间")
83 private LocalDateTime expireTime; 80 private LocalDateTime expireTime;
84 81
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java
@@ -40,7 +40,7 @@ public class AppPayOrderController { @@ -40,7 +40,7 @@ public class AppPayOrderController {
40 @PostMapping("/submit") 40 @PostMapping("/submit")
41 @Operation(summary = "提交支付订单") 41 @Operation(summary = "提交支付订单")
42 public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { 42 public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
43 - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); 43 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
44 return success(PayOrderConvert.INSTANCE.convert3(respVO)); 44 return success(PayOrderConvert.INSTANCE.convert3(respVO));
45 } 45 }
46 46
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.pay.convert.order; @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.pay.convert.order;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 4 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;  
6 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 5 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
7 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; 6 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
8 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; 7 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
@@ -93,7 +92,8 @@ public interface PayOrderConvert { @@ -93,7 +92,8 @@ public interface PayOrderConvert {
93 92
94 PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp); 93 PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp);
95 94
96 - PayOrderSubmitRespVO convert(PayOrderDO order, PayOrderUnifiedRespDTO unifiedRespDTO); 95 + @Mapping(source = "order.status", target = "status")
  96 + PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO);
97 97
98 AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); 98 AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean);
99 99
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java
1 package cn.iocoder.yudao.module.pay.convert.refund; 1 package cn.iocoder.yudao.module.pay.convert.refund;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
  5 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
4 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*; 6 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
5 -import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;  
6 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 7 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
7 import org.mapstruct.Mapper; 8 import org.mapstruct.Mapper;
8 -import org.mapstruct.Mapping;  
9 -import org.mapstruct.Mappings;  
10 import org.mapstruct.factory.Mappers; 9 import org.mapstruct.factory.Mappers;
11 10
12 import java.math.BigDecimal; 11 import java.math.BigDecimal;
@@ -44,8 +43,6 @@ public interface PayRefundConvert { @@ -44,8 +43,6 @@ public interface PayRefundConvert {
44 43
45 PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page); 44 PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
46 45
47 - List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);  
48 -  
49 /** 46 /**
50 * 退款订单DO 转 导出excel VO 47 * 退款订单DO 转 导出excel VO
51 * 48 *
@@ -60,20 +57,18 @@ public interface PayRefundConvert { @@ -60,20 +57,18 @@ public interface PayRefundConvert {
60 PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO(); 57 PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
61 58
62 payRefundExcelVO.setId(bean.getId()); 59 payRefundExcelVO.setId(bean.getId());
63 - payRefundExcelVO.setTradeNo(bean.getTradeNo()); 60 + payRefundExcelVO.setTradeNo(bean.getNo());
64 payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId()); 61 payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
65 - payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo()); 62 + // TODO 芋艿:晚点在改
  63 +// payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
66 payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl()); 64 payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
67 payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus()); 65 payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
68 payRefundExcelVO.setStatus(bean.getStatus()); 66 payRefundExcelVO.setStatus(bean.getStatus());
69 - payRefundExcelVO.setType(bean.getType());  
70 payRefundExcelVO.setReason(bean.getReason()); 67 payRefundExcelVO.setReason(bean.getReason());
71 payRefundExcelVO.setUserIp(bean.getUserIp()); 68 payRefundExcelVO.setUserIp(bean.getUserIp());
72 payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo()); 69 payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
73 payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo()); 70 payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
74 - payRefundExcelVO.setExpireTime(bean.getExpireTime());  
75 payRefundExcelVO.setSuccessTime(bean.getSuccessTime()); 71 payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
76 - payRefundExcelVO.setNotifyTime(bean.getNotifyTime());  
77 payRefundExcelVO.setCreateTime(bean.getCreateTime()); 72 payRefundExcelVO.setCreateTime(bean.getCreateTime());
78 73
79 BigDecimal multiple = new BigDecimal(100); 74 BigDecimal multiple = new BigDecimal(100);
@@ -85,12 +80,8 @@ public interface PayRefundConvert { @@ -85,12 +80,8 @@ public interface PayRefundConvert {
85 return payRefundExcelVO; 80 return payRefundExcelVO;
86 } 81 }
87 82
88 - //TODO 太多需要处理了, 暂时不用  
89 - @Mappings(value = {  
90 - @Mapping(source = "price", target = "payPrice"),  
91 - @Mapping(source = "id", target = "orderId"),  
92 - @Mapping(target = "status",ignore = true)  
93 - })  
94 - PayRefundDO convert(PayOrderDO orderDO); 83 + PayRefundDO convert(PayRefundCreateReqDTO bean);
  84 +
  85 + PayRefundRespDTO convert02(PayRefundDO bean);
95 86
96 } 87 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
@@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO { @@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO {
48 /** 48 /**
49 * 支付结果的回调地址 49 * 支付结果的回调地址
50 */ 50 */
51 - private String payNotifyUrl; 51 + private String orderNotifyUrl;
52 /** 52 /**
53 * 退款结果的回调地址 53 * 退款结果的回调地址
54 */ 54 */
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java
@@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
6 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; 6 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
7 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; 7 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
8 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 8 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
9 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; 9 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
10 import com.baomidou.mybatisplus.annotation.KeySequence; 10 import com.baomidou.mybatisplus.annotation.KeySequence;
11 import com.baomidou.mybatisplus.annotation.TableName; 11 import com.baomidou.mybatisplus.annotation.TableName;
12 import lombok.*; 12 import lombok.*;
@@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO { @@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO {
55 55
56 /** 56 /**
57 * 商户订单编号 57 * 商户订单编号
58 - * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 58 + *
  59 + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
59 */ 60 */
60 private String merchantOrderId; 61 private String merchantOrderId;
61 /** 62 /**
@@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO { @@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO {
126 /** 127 /**
127 * 退款状态 128 * 退款状态
128 * 129 *
129 - * 枚举 {@link PayRefundTypeEnum} 130 + * 枚举 {@link PayOrderRefundStatusEnum}
130 */ 131 */
131 private Integer refundStatus; 132 private Integer refundStatus;
132 /** 133 /**
@@ -136,7 +137,7 @@ public class PayOrderDO extends BaseDO { @@ -136,7 +137,7 @@ public class PayOrderDO extends BaseDO {
136 /** 137 /**
137 * 退款总金额,单位:分 138 * 退款总金额,单位:分
138 */ 139 */
139 - private Long refundPrice; 140 + private Integer refundPrice;
140 141
141 // ========== 渠道相关字段 ========== 142 // ========== 渠道相关字段 ==========
142 /** 143 /**
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java
1 package cn.iocoder.yudao.module.pay.dal.dataobject.order; 1 package cn.iocoder.yudao.module.pay.dal.dataobject.order;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
3 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; 4 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
4 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 5 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
5 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 6 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO { @@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO {
32 */ 33 */
33 private Long id; 34 private Long id;
34 /** 35 /**
35 - * 支付订单号,根据规则生成  
36 - * 调用支付渠道时,使用该字段作为对接的订单号。  
37 - * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no  
38 - * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no 36 + * 外部订单号,根据规则生成
  37 + *
  38 + * 调用支付渠道时,使用该字段作为对接的订单号:
  39 + * 1. 微信支付:对应 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 支付</a> 的 out_trade_no 字段
  40 + * 2. 支付宝支付:对应 <a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> 的 out_trade_no 字段
39 * 41 *
40 * 例如说,P202110132239124200055 42 * 例如说,P202110132239124200055
41 */ 43 */
@@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO { @@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO {
64 * 支付状态 66 * 支付状态
65 * 67 *
66 * 枚举 {@link PayOrderStatusEnum} 68 * 枚举 {@link PayOrderStatusEnum}
67 - * 注意,只包含上述枚举的 WAITING 和 SUCCESS  
68 */ 69 */
69 private Integer status; 70 private Integer status;
70 /** 71 /**
71 * 支付渠道的额外参数 72 * 支付渠道的额外参数
72 * 73 *
73 - * 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html 74 + * 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
74 */ 75 */
75 @TableField(typeHandler = JacksonTypeHandler.class) 76 @TableField(typeHandler = JacksonTypeHandler.class)
76 private Map<String, String> channelExtras; 77 private Map<String, String> channelExtras;
  78 +
  79 + /**
  80 + * 调用渠道的错误码
  81 + */
  82 + private String channelErrorCode;
  83 + /**
  84 + * 调用渠道报错时,错误信息
  85 + */
  86 + private String channelErrorMsg;
  87 +
77 /** 88 /**
78 - * 支付渠道异步通知的内容 89 + * 支付渠道的同步/异步通知的内容
79 * 90 *
80 - * 在支持成功后,会记录回调的数据 91 + * 对应 {@link PayOrderRespDTO#getRawData()}
81 */ 92 */
82 private String channelNotifyData; 93 private String channelNotifyData;
83 94
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java
1 package cn.iocoder.yudao.module.pay.dal.dataobject.refund; 1 package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
4 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
5 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; 6 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
6 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; 7 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
7 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; 8 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
  9 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
8 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; 10 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
9 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;  
10 import com.baomidou.mybatisplus.annotation.KeySequence; 11 import com.baomidou.mybatisplus.annotation.KeySequence;
11 import com.baomidou.mybatisplus.annotation.TableId; 12 import com.baomidou.mybatisplus.annotation.TableId;
12 import com.baomidou.mybatisplus.annotation.TableName; 13 import com.baomidou.mybatisplus.annotation.TableName;
@@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO { @@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
37 */ 38 */
38 @TableId 39 @TableId
39 private Long id; 40 private Long id;
  41 + /**
  42 + * 外部退款号,根据规则生成
  43 + *
  44 + * 调用支付渠道时,使用该字段作为对接的退款号:
  45 + * 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 out_refund_no 字段
  46 + * 2. 支付宝退款:对应 <a href="https://opendocs.alipay.com/open/02e7go"统一收单交易退款接口></a> 的 out_request_no 字段
  47 + */
  48 + private String no;
40 49
41 /** 50 /**
42 * 应用编号 51 * 应用编号
@@ -63,47 +72,27 @@ public class PayRefundDO extends BaseDO { @@ -63,47 +72,27 @@ public class PayRefundDO extends BaseDO {
63 */ 72 */
64 private Long orderId; 73 private Long orderId;
65 74
66 - /**  
67 - * 交易订单号,根据规则生成  
68 - * 调用支付渠道时,使用该字段作为对接的订单号。  
69 - * 1. 调用微信支付 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 时,使用该字段作为 out_trade_no  
70 - * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no  
71 - * 这里对应 pay_extension 里面的 no  
72 - * 例如说,P202110132239124200055  
73 - */  
74 - private String tradeNo;  
75 -  
76 // ========== 商户相关字段 ========== 75 // ========== 商户相关字段 ==========
77 /** 76 /**
78 * 商户订单编号 77 * 商户订单编号
  78 + *
  79 + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
79 */ 80 */
80 private String merchantOrderId; 81 private String merchantOrderId;
81 -  
82 /** 82 /**
83 - * 商户退款订单号, 由商户系统产生, 由他们保证唯一,不能为空,通知商户时会传该字段。  
84 - * 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一  
85 - * 个商户退款订单,对应一条退款请求记录。可多次提交。 渠道保持幂等  
86 - * 使用商户退款单,作为退款请求号  
87 - * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml 中的 out_refund_no  
88 - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no  
89 - * 退款请求号。  
90 - * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。  
91 - * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,  
92 - * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。  
93 - * 退款单请求号,根据规则生成  
94 - * 例如说,R202109181134287570000  
95 - */  
96 - // TODO @jason:merchantRefundNo =》merchantRefundOId  
97 - private String merchantRefundNo;  
98 - 83 + * 商户退款订单号
  84 + *
  85 + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
  86 + */
  87 + private String merchantRefundId;
99 /** 88 /**
100 * 异步通知地址 89 * 异步通知地址
101 */ 90 */
102 private String notifyUrl; 91 private String notifyUrl;
103 -  
104 /** 92 /**
105 * 通知商户退款结果的回调状态 93 * 通知商户退款结果的回调状态
106 - * TODO 0 未发送 1 已发送 94 + *
  95 + * 枚举 {@link PayNotifyStatusEnum}
107 */ 96 */
108 private Integer notifyStatus; 97 private Integer notifyStatus;
109 98
@@ -116,12 +105,6 @@ public class PayRefundDO extends BaseDO { @@ -116,12 +105,6 @@ public class PayRefundDO extends BaseDO {
116 private Integer status; 105 private Integer status;
117 106
118 /** 107 /**
119 - * 退款类型(部分退款,全部退款)  
120 - *  
121 - * 枚举 {@link PayRefundTypeEnum}  
122 - */  
123 - private Integer type;  
124 - /**  
125 * 支付金额,单位:分 108 * 支付金额,单位:分
126 */ 109 */
127 private Integer payPrice; 110 private Integer payPrice;
@@ -142,45 +125,37 @@ public class PayRefundDO extends BaseDO { @@ -142,45 +125,37 @@ public class PayRefundDO extends BaseDO {
142 125
143 // ========== 渠道相关字段 ========== 126 // ========== 渠道相关字段 ==========
144 /** 127 /**
145 - * 渠道订单号,pay_order 中的channel_order_no 对应 128 + * 渠道订单号
  129 + *
  130 + * 冗余 {@link PayOrderDO#getChannelOrderNo()}
146 */ 131 */
147 private String channelOrderNo; 132 private String channelOrderNo;
148 /** 133 /**
149 - * 微信中的 refund_id  
150 - * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml  
151 - * 支付宝没有  
152 - * 渠道退款单号,渠道返回 134 + * 渠道退款单号
  135 + *
  136 + * 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 refund_id 字段
  137 + * 2. 支付宝退款:没有字段
153 */ 138 */
154 private String channelRefundNo; 139 private String channelRefundNo;
  140 + /**
  141 + * 退款成功时间
  142 + */
  143 + private LocalDateTime successTime;
155 144
156 /** 145 /**
157 * 调用渠道的错误码 146 * 调用渠道的错误码
158 */ 147 */
159 private String channelErrorCode; 148 private String channelErrorCode;
160 -  
161 /** 149 /**
162 * 调用渠道报错时,错误信息 150 * 调用渠道报错时,错误信息
163 */ 151 */
164 private String channelErrorMsg; 152 private String channelErrorMsg;
165 153
166 /** 154 /**
167 - * 支付渠道的额外参数  
168 - * 参见 https://www.pingxx.com/api/Refunds%20退款概述.html  
169 - */  
170 - private String channelExtras;  
171 -  
172 - /**  
173 - * TODO  
174 - * 退款失效时间  
175 - */  
176 - private LocalDateTime expireTime;  
177 - /**  
178 - * 退款成功时间  
179 - */  
180 - private LocalDateTime successTime;  
181 - /**  
182 - * 退款通知时间 155 + * 支付渠道的同步/异步通知的内容
  156 + *
  157 + * 对应 {@link PayRefundRespDTO#getRawData()}
183 */ 158 */
184 - private LocalDateTime notifyTime; 159 + private String channelNotifyData;
185 160
186 } 161 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java
1 package cn.iocoder.yudao.module.pay.dal.mysql.refund; 1 package cn.iocoder.yudao.module.pay.dal.mysql.refund;
2 2
3 -import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;  
4 -import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;  
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  7 +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
  8 +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
8 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 9 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
  10 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
9 import org.apache.ibatis.annotations.Mapper; 11 import org.apache.ibatis.annotations.Mapper;
10 12
11 import java.util.List; 13 import java.util.List;
@@ -13,6 +15,34 @@ import java.util.List; @@ -13,6 +15,34 @@ import java.util.List;
13 @Mapper 15 @Mapper
14 public interface PayRefundMapper extends BaseMapperX<PayRefundDO> { 16 public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
15 17
  18 + default Long selectCountByAppId(Long appId) {
  19 + return selectCount(PayRefundDO::getAppId, appId);
  20 + }
  21 +
  22 + default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) {
  23 + return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
  24 + .eq(PayRefundDO::getAppId, appId)
  25 + .eq(PayRefundDO::getMerchantRefundId, merchantRefundId));
  26 + }
  27 +
  28 + default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) {
  29 + return selectCount(new LambdaQueryWrapperX<PayRefundDO>()
  30 + .eq(PayRefundDO::getAppId, appId)
  31 + .eq(PayRefundDO::getOrderId, orderId)
  32 + .eq(PayRefundDO::getStatus, status));
  33 + }
  34 +
  35 + default PayRefundDO selectByAppIdAndNo(Long appId, String no) {
  36 + return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
  37 + .eq(PayRefundDO::getAppId, appId)
  38 + .eq(PayRefundDO::getNo, no));
  39 + }
  40 +
  41 + default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) {
  42 + return update(update, new LambdaQueryWrapper<PayRefundDO>()
  43 + .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status));
  44 + }
  45 +
16 default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) { 46 default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
17 return selectPage(reqVO, new QueryWrapperX<PayRefundDO>() 47 return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
18 .eqIfPresent("app_id", reqVO.getAppId()) 48 .eqIfPresent("app_id", reqVO.getAppId())
@@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX&lt;PayRefundDO&gt; { @@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX&lt;PayRefundDO&gt; {
37 .orderByDesc("id")); 67 .orderByDesc("id"));
38 } 68 }
39 69
40 - default Long selectCountByApp(Long appId) {  
41 - return selectCount(PayRefundDO::getAppId, appId);  
42 - }  
43 -  
44 - default PayRefundDO selectByReqNo(String reqNo) {  
45 - return selectOne("req_no", reqNo);  
46 - }  
47 -  
48 - default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){  
49 - return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);  
50 - }  
51 -  
52 } 70 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundTypeEnum.java renamed to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java
1 -package cn.iocoder.yudao.module.pay.enums.refund; 1 +package cn.iocoder.yudao.module.pay.enums.order;
2 2
3 import cn.iocoder.yudao.framework.common.core.IntArrayValuable; 3 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4 import lombok.AllArgsConstructor; 4 import lombok.AllArgsConstructor;
@@ -11,10 +11,10 @@ import lombok.Getter; @@ -11,10 +11,10 @@ import lombok.Getter;
11 */ 11 */
12 @Getter 12 @Getter
13 @AllArgsConstructor 13 @AllArgsConstructor
14 -public enum PayRefundTypeEnum implements IntArrayValuable { 14 +public enum PayOrderRefundStatusEnum implements IntArrayValuable {
15 15
16 NO(0, "未退款"), 16 NO(0, "未退款"),
17 - SOME(10, "部分退款"), 17 + PART(10, "部分退款"),
18 ALL(20, "全部退款") 18 ALL(20, "全部退款")
19 ; 19 ;
20 20
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java
@@ -20,11 +20,11 @@ import javax.annotation.Resource; @@ -20,11 +20,11 @@ import javax.annotation.Resource;
20 public class PayNotifyJob implements JobHandler { 20 public class PayNotifyJob implements JobHandler {
21 21
22 @Resource 22 @Resource
23 - private PayNotifyService payNotifyCoreService; 23 + private PayNotifyService payNotifyService;
24 24
25 @Override 25 @Override
26 public String execute(String param) throws Exception { 26 public String execute(String param) throws Exception {
27 - int notifyCount = payNotifyCoreService.executeNotify(); 27 + int notifyCount = payNotifyService.executeNotify();
28 return String.format("执行支付通知 %s 个", notifyCount); 28 return String.format("执行支付通知 %s 个", notifyCount);
29 } 29 }
30 30
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java
@@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService { @@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService {
83 */ 83 */
84 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) 84 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
85 public void refreshLocalCache() { 85 public void refreshLocalCache() {
86 - // 情况一:如果缓存里没有数据,则直接刷新缓存  
87 - if (CollUtil.isEmpty(channelCache)) {  
88 - initLocalCache();  
89 - return;  
90 - }  
91 -  
92 - // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存  
93 - LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);  
94 - if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {  
95 - initLocalCache();  
96 - } 86 + // 注意:忽略自动多租户,因为要全局初始化缓存
  87 + TenantUtils.executeIgnore(() -> {
  88 + // 情况一:如果缓存里没有数据,则直接刷新缓存
  89 + if (CollUtil.isEmpty(channelCache)) {
  90 + initLocalCache();
  91 + return;
  92 + }
  93 +
  94 + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
  95 + LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
  96 + if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
  97 + initLocalCache();
  98 + }
  99 + });
97 } 100 }
98 101
99 @Override 102 @Override
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
@@ -184,12 +184,17 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { @@ -184,12 +184,17 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
184 // 1. 校验订单是否可以退款 184 // 1. 校验订单是否可以退款
185 PayDemoOrderDO order = validateDemoOrderCanRefund(id); 185 PayDemoOrderDO order = validateDemoOrderCanRefund(id);
186 186
187 - // 2.1 创建退款单  
188 - Long payRefundId = payRefundApi.createPayRefund(new PayRefundCreateReqDTO() 187 + // 2.1 生成退款单号
  188 + // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
  189 + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示
  190 + String refundId = order.getId() + "-refund";
  191 + // 2.2 创建退款单
  192 + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
189 .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 193 .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
190 - .setPayOrderId(order.getPayOrderId()) // 支付单号 194 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
  195 + .setMerchantRefundId(refundId)
191 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 196 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息
192 - // 2.2 更新退款单到 demo 订单 197 + // 2.3 更新退款单到 demo 订单
193 payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) 198 payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
194 .setPayRefundId(payRefundId).setRefundPrice(order.getPrice())); 199 .setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
195 } 200 }
@@ -234,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { @@ -234,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
234 } 239 }
235 240
236 // 2.1 校验退款订单 241 // 2.1 校验退款订单
237 - PayRefundRespDTO payRefund = payRefundApi.getPayRefund(payRefundId); 242 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
238 if (payRefund == null) { 243 if (payRefund == null) {
239 throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); 244 throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
240 } 245 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java
@@ -7,6 +7,7 @@ import java.util.Collection; @@ -7,6 +7,7 @@ import java.util.Collection;
7 import java.util.List; 7 import java.util.List;
8 import java.util.Map; 8 import java.util.Map;
9 9
  10 +// TODO 芋艿:合并到 PayOrder 里;
10 /** 11 /**
11 * 支付订单 Service 接口 12 * 支付订单 Service 接口
12 * 13 *
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java
@@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.pay.service.order; @@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.pay.service.order;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; 5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
7 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 6 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
8 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO; 7 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
9 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; 8 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
@@ -33,6 +32,15 @@ public interface PayOrderService { @@ -33,6 +32,15 @@ public interface PayOrderService {
33 PayOrderDO getOrder(Long id); 32 PayOrderDO getOrder(Long id);
34 33
35 /** 34 /**
  35 + * 获得支付订单
  36 + *
  37 + * @param appId 应用编号
  38 + * @param merchantOrderId 商户订单编号
  39 + * @return 支付订单
  40 + */
  41 + PayOrderDO getOrder(Long appId, String merchantOrderId);
  42 +
  43 + /**
36 * 获得指定应用的订单数量 44 * 获得指定应用的订单数量
37 * 45 *
38 * @param appId 应用编号 46 * @param appId 应用编号
@@ -71,11 +79,11 @@ public interface PayOrderService { @@ -71,11 +79,11 @@ public interface PayOrderService {
71 /** 79 /**
72 * 根据订单 ID 集合获取订单商品名称Map集合 80 * 根据订单 ID 集合获取订单商品名称Map集合
73 * 81 *
74 - * @param idList 订单 ID 集合 82 + * @param ids 订单 ID 集合
75 * @return 订单商品 map 集合 83 * @return 订单商品 map 集合
76 */ 84 */
77 - default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> idList) {  
78 - List<PayOrderDO> list = getOrderSubjectList(idList); 85 + default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> ids) {
  86 + List<PayOrderDO> list = getOrderSubjectList(ids);
79 return CollectionUtils.convertMap(list, PayOrderDO::getId); 87 return CollectionUtils.convertMap(list, PayOrderDO::getId);
80 } 88 }
81 89
@@ -85,7 +93,7 @@ public interface PayOrderService { @@ -85,7 +93,7 @@ public interface PayOrderService {
85 * @param reqDTO 创建请求 93 * @param reqDTO 创建请求
86 * @return 支付单编号 94 * @return 支付单编号
87 */ 95 */
88 - Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO); 96 + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
89 97
90 /** 98 /**
91 * 提交支付 99 * 提交支付
@@ -95,16 +103,23 @@ public interface PayOrderService { @@ -95,16 +103,23 @@ public interface PayOrderService {
95 * @param userIp 提交 IP 103 * @param userIp 提交 IP
96 * @return 提交结果 104 * @return 提交结果
97 */ 105 */
98 - PayOrderSubmitRespVO submitPayOrder(@Valid PayOrderSubmitReqVO reqVO,  
99 - @NotEmpty(message = "提交 IP 不能为空") String userIp); 106 + PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
  107 + @NotEmpty(message = "提交 IP 不能为空") String userIp);
100 108
101 /** 109 /**
102 * 通知支付单成功 110 * 通知支付单成功
103 * 111 *
104 * @param channelId 渠道编号 112 * @param channelId 渠道编号
105 * @param notify 通知 113 * @param notify 通知
106 - * @param rawNotify 通知数据  
107 */ 114 */
108 - void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify); 115 + void notifyOrder(Long channelId, PayOrderRespDTO notify);
  116 +
  117 + /**
  118 + * 更新支付订单的退款金额
  119 + *
  120 + * @param id 编号
  121 + * @param incrRefundPrice 增加的退款金额
  122 + */
  123 + void updateOrderRefundPrice(Long id, Integer incrRefundPrice);
109 124
110 } 125 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
@@ -4,15 +4,15 @@ import cn.hutool.core.date.DateUtil; @@ -4,15 +4,15 @@ import cn.hutool.core.date.DateUtil;
4 import cn.hutool.core.lang.Pair; 4 import cn.hutool.core.lang.Pair;
5 import cn.hutool.core.util.ObjectUtil; 5 import cn.hutool.core.util.ObjectUtil;
6 import cn.hutool.core.util.RandomUtil; 6 import cn.hutool.core.util.RandomUtil;
  7 +import cn.hutool.core.util.StrUtil;
7 import cn.iocoder.yudao.framework.common.pojo.PageResult; 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 9 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
9 import cn.iocoder.yudao.framework.pay.config.PayProperties; 10 import cn.iocoder.yudao.framework.pay.config.PayProperties;
10 import cn.iocoder.yudao.framework.pay.core.client.PayClient; 11 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
11 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; 12 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
12 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
13 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; 13 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
14 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 14 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
15 -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 15 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
16 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 16 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
17 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 17 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
18 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO; 18 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
@@ -26,11 +26,10 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; @@ -26,11 +26,10 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
26 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; 26 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
27 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper; 27 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
28 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper; 28 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
29 -import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;  
30 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; 29 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
31 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; 30 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
  31 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
32 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 32 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
33 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;  
34 import cn.iocoder.yudao.module.pay.service.app.PayAppService; 33 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
35 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; 34 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
36 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; 35 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -48,6 +47,7 @@ import java.util.Objects; @@ -48,6 +47,7 @@ import java.util.Objects;
48 47
49 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 48 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
50 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; 49 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
  50 +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
51 51
52 /** 52 /**
53 * 支付订单 Service 实现类 53 * 支付订单 Service 实现类
@@ -83,6 +83,11 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -83,6 +83,11 @@ public class PayOrderServiceImpl implements PayOrderService {
83 } 83 }
84 84
85 @Override 85 @Override
  86 + public PayOrderDO getOrder(Long appId, String merchantOrderId) {
  87 + return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
  88 + }
  89 +
  90 + @Override
86 public Long getOrderCountByAppId(Long appId) { 91 public Long getOrderCountByAppId(Long appId) {
87 return orderMapper.selectCountByAppId(appId); 92 return orderMapper.selectCountByAppId(appId);
88 } 93 }
@@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService {
104 } 109 }
105 110
106 @Override 111 @Override
107 - public Long createPayOrder(PayOrderCreateReqDTO reqDTO) { 112 + public Long createOrder(PayOrderCreateReqDTO reqDTO) {
108 // 校验 App 113 // 校验 App
109 PayAppDO app = appService.validPayApp(reqDTO.getAppId()); 114 PayAppDO app = appService.validPayApp(reqDTO.getAppId());
110 115
@@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService {
112 PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( 117 PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
113 reqDTO.getAppId(), reqDTO.getMerchantOrderId()); 118 reqDTO.getAppId(), reqDTO.getMerchantOrderId());
114 if (order != null) { 119 if (order != null) {
115 - log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), 120 + log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
116 order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况 121 order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
117 return order.getId(); 122 return order.getId();
118 } 123 }
@@ -120,20 +125,19 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -120,20 +125,19 @@ public class PayOrderServiceImpl implements PayOrderService {
120 // 创建支付交易单 125 // 创建支付交易单
121 order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) 126 order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId())
122 // 商户相关字段 127 // 商户相关字段
123 - .setNotifyUrl(app.getPayNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) 128 + .setNotifyUrl(app.getOrderNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
124 // 订单相关字段 129 // 订单相关字段
125 .setStatus(PayOrderStatusEnum.WAITING.getStatus()) 130 .setStatus(PayOrderStatusEnum.WAITING.getStatus())
126 // 退款相关字段 131 // 退款相关字段
127 - .setRefundStatus(PayRefundTypeEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0L); 132 + .setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0);
128 orderMapper.insert(order); 133 orderMapper.insert(order);
129 return order.getId(); 134 return order.getId();
130 } 135 }
131 136
132 - @Override  
133 - @Transactional(rollbackFor = Exception.class)  
134 - public PayOrderSubmitRespVO submitPayOrder(PayOrderSubmitReqVO reqVO, String userIp) { 137 + @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
  138 + public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
135 // 1. 获得 PayOrderDO ,并校验其是否存在 139 // 1. 获得 PayOrderDO ,并校验其是否存在
136 - PayOrderDO order = validatePayOrderCanSubmit(reqVO.getId()); 140 + PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
137 // 1.2 校验支付渠道是否有效 141 // 1.2 校验支付渠道是否有效
138 PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); 142 PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
139 PayClient client = payClientFactory.getPayClient(channel.getId()); 143 PayClient client = payClientFactory.getPayClient(channel.getId());
@@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService {
148 // 3. 调用三方接口 152 // 3. 调用三方接口
149 PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp) 153 PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
150 // 商户相关的字段 154 // 商户相关的字段
151 - .setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性! 155 + .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
152 .setSubject(order.getSubject()).setBody(order.getBody()) 156 .setSubject(order.getSubject()).setBody(order.getBody())
153 - .setNotifyUrl(genChannelPayNotifyUrl(channel)) 157 + .setNotifyUrl(genChannelOrderNotifyUrl(channel))
154 .setReturnUrl(reqVO.getReturnUrl()) 158 .setReturnUrl(reqVO.getReturnUrl())
155 // 订单相关字段 159 // 订单相关字段
156 - .setAmount(order.getPrice()).setExpireTime(order.getExpireTime());  
157 - PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO); 160 + .setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
  161 + PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
158 162
159 // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功 163 // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
160 - if (unifiedOrderRespDTO.getNotify() != null) {  
161 - notifyPayOrderSuccess(channel, unifiedOrderRespDTO.getNotify(), null); 164 + if (unifiedOrderResp != null) {
  165 + notifyPayOrder(channel, unifiedOrderResp);
  166 + // 如有渠道错误码,则抛出业务异常,提示用户
  167 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) {
  168 + throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(),
  169 + unifiedOrderResp.getChannelErrorMsg());
  170 + }
162 // 此处需要读取最新的状态 171 // 此处需要读取最新的状态
163 order = orderMapper.selectById(order.getId()); 172 order = orderMapper.selectById(order.getId());
164 } 173 }
165 -  
166 - // 返回成功  
167 - return PayOrderConvert.INSTANCE.convert(order, unifiedOrderRespDTO); 174 + return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
168 } 175 }
169 176
170 - private PayOrderDO validatePayOrderCanSubmit(Long id) { 177 + private PayOrderDO validateOrderCanSubmit(Long id) {
171 PayOrderDO order = orderMapper.selectById(id); 178 PayOrderDO order = orderMapper.selectById(id);
172 if (order == null) { // 是否存在 179 if (order == null) { // 是否存在
173 - throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND); 180 + throw exception(PAY_ORDER_NOT_FOUND);
174 } 181 }
175 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 182 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
176 - throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); 183 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
177 } 184 }
178 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 185 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
179 - throw exception(ErrorCodeConstants.PAY_ORDER_IS_EXPIRED); 186 + throw exception(PAY_ORDER_IS_EXPIRED);
180 } 187 }
181 return order; 188 return order;
182 } 189 }
@@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService {
184 private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) { 191 private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
185 // 校验 App 192 // 校验 App
186 appService.validPayApp(appId); 193 appService.validPayApp(appId);
187 -  
188 // 校验支付渠道是否有效 194 // 校验支付渠道是否有效
189 PayChannelDO channel = channelService.validPayChannel(appId, channelCode); 195 PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
190 - // 校验支付客户端是否正确初始化  
191 PayClient client = payClientFactory.getPayClient(channel.getId()); 196 PayClient client = payClientFactory.getPayClient(channel.getId());
192 if (client == null) { 197 if (client == null) {
193 log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); 198 log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
194 - throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND); 199 + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
195 } 200 }
196 return channel; 201 return channel;
197 } 202 }
@@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService {
202 * @param channel 支付渠道 207 * @param channel 支付渠道
203 * @return 支付渠道的回调地址 配置地址 + "/" + channel id 208 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
204 */ 209 */
205 - private String genChannelPayNotifyUrl(PayChannelDO channel) {  
206 - return payProperties.getCallbackUrl() + "/" + channel.getId(); 210 + private String genChannelOrderNotifyUrl(PayChannelDO channel) {
  211 + return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
207 } 212 }
208 213
209 private String generateOrderExtensionNo() { 214 private String generateOrderExtensionNo() {
@@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService {
226 231
227 @Override 232 @Override
228 @Transactional(rollbackFor = Exception.class) 233 @Transactional(rollbackFor = Exception.class)
229 - public void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { 234 + public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
230 // 校验支付渠道是否有效 235 // 校验支付渠道是否有效
231 PayChannelDO channel = channelService.validPayChannel(channelId); 236 PayChannelDO channel = channelService.validPayChannel(channelId);
232 // 更新支付订单为已支付 237 // 更新支付订单为已支付
233 - TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrderSuccess(channel, notify, rawNotify)); 238 + TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrder(channel, notify));
  239 + }
  240 +
  241 + @Override
  242 + public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) {
  243 + PayOrderDO order = orderMapper.selectById(id);
  244 + if (order == null) {
  245 + throw exception(PAY_ORDER_NOT_FOUND);
  246 + }
  247 + if (!PayOrderStatusEnum.isSuccess(order.getStatus())) {
  248 + throw exception(PAY_REFUND_PRICE_EXCEED);
  249 + }
  250 + if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) {
  251 + throw exception(PAY_REFUND_PRICE_EXCEED);
  252 + }
  253 +
  254 + // 更新订单
  255 + PayOrderDO updateObj = new PayOrderDO()
  256 + .setRefundPrice(order.getRefundPrice() + incrRefundPrice)
  257 + .setRefundTimes(order.getRefundTimes() + 1);
  258 + if (Objects.equals(updateObj.getRefundPrice(), order.getPrice())) {
  259 + updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
  260 + .setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus());
  261 + } else {
  262 + updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
  263 + .setRefundStatus(PayOrderRefundStatusEnum.PART.getStatus());
  264 + }
  265 + orderMapper.updateByIdAndStatus(id, PayOrderStatusEnum.SUCCESS.getStatus(), updateObj);
234 } 266 }
235 267
236 - private void notifyPayOrderSuccess(PayChannelDO channel, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { 268 + private void notifyPayOrder(PayChannelDO channel, PayOrderRespDTO notify) {
  269 + // 情况一:支付成功的回调
  270 + if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
  271 + notifyOrderSuccess(channel, notify);
  272 + return;
  273 + }
  274 + // 情况二:支付失败的回调
  275 + if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) {
  276 + notifyOrderClosed(channel, notify);
  277 + }
  278 + }
  279 +
  280 + private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) {
237 // 1. 更新 PayOrderExtensionDO 支付成功 281 // 1. 更新 PayOrderExtensionDO 支付成功
238 - PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify); 282 + PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
239 // 2. 更新 PayOrderDO 支付成功 283 // 2. 更新 PayOrderDO 支付成功
240 - Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify); 284 + Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
241 if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调 285 if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
242 return; 286 return;
243 } 287 }
@@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService {
250 /** 294 /**
251 * 更新 PayOrderExtensionDO 支付成功 295 * 更新 PayOrderExtensionDO 支付成功
252 * 296 *
253 - * @param no 支付订单号(支付模块)  
254 - * @param rawNotify 通知数据 297 + * @param notify 通知
255 * @return PayOrderExtensionDO 对象 298 * @return PayOrderExtensionDO 对象
256 */ 299 */
257 - private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) { 300 + private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
258 // 1. 查询 PayOrderExtensionDO 301 // 1. 查询 PayOrderExtensionDO
259 - PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no); 302 + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
260 if (orderExtension == null) { 303 if (orderExtension == null) {
261 - throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND); 304 + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
262 } 305 }
263 if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 306 if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
264 - log.info("[updatePayOrderSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId()); 307 + log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
265 return orderExtension; 308 return orderExtension;
266 } 309 }
267 if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 310 if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
268 - throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); 311 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
269 } 312 }
270 313
271 // 2. 更新 PayOrderExtensionDO 314 // 2. 更新 PayOrderExtensionDO
272 - int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),  
273 - PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())  
274 - .status(PayOrderStatusEnum.SUCCESS.getStatus())  
275 - .channelNotifyData(toJsonString(rawNotify)).build()); 315 + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
  316 + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
276 if (updateCounts == 0) { // 校验状态,必须是待支付 317 if (updateCounts == 0) { // 校验状态,必须是待支付
277 - throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); 318 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
278 } 319 }
279 - log.info("[updatePayOrderSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId()); 320 + log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
280 return orderExtension; 321 return orderExtension;
281 } 322 }
282 323
@@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService {
289 * @return key:是否之前已经成功回调 330 * @return key:是否之前已经成功回调
290 * value:PayOrderDO 对象 331 * value:PayOrderDO 对象
291 */ 332 */
292 - private Pair<Boolean, PayOrderDO> updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,  
293 - PayOrderNotifyRespDTO notify) { 333 + private Pair<Boolean, PayOrderDO> updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
  334 + PayOrderRespDTO notify) {
294 // 1. 判断 PayOrderDO 是否处于待支付 335 // 1. 判断 PayOrderDO 是否处于待支付
295 PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); 336 PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
296 if (order == null) { 337 if (order == null) {
297 - throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND); 338 + throw exception(PAY_ORDER_NOT_FOUND);
298 } 339 }
299 if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新 340 if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
300 && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) { 341 && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) {
301 - log.info("[updatePayOrderSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId()); 342 + log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
302 return Pair.of(true, order); 343 return Pair.of(true, order);
303 } 344 }
304 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 345 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
305 - throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); 346 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
306 } 347 }
307 348
308 // 2. 更新 PayOrderDO 349 // 2. 更新 PayOrderDO
@@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService {
313 .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId()) 354 .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
314 .notifyTime(LocalDateTime.now()).build()); 355 .notifyTime(LocalDateTime.now()).build());
315 if (updateCounts == 0) { // 校验状态,必须是待支付 356 if (updateCounts == 0) { // 校验状态,必须是待支付
316 - throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); 357 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
317 } 358 }
318 - log.info("[updatePayOrderSuccess][支付订单({}) 更新为已支付]", order.getId()); 359 + log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
319 return Pair.of(false, order); 360 return Pair.of(false, order);
320 } 361 }
321 362
  363 + private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) {
  364 + updateOrderExtensionClosed(channel, notify);
  365 + }
  366 +
  367 + private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) {
  368 + // 1. 查询 PayOrderExtensionDO
  369 + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
  370 + if (orderExtension == null) {
  371 + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
  372 + }
  373 + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新
  374 + log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId());
  375 + return;
  376 + }
  377 + // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
  378 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
  379 + log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
  380 + return;
  381 + }
  382 + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
  383 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
  384 + }
  385 +
  386 + // 2. 更新 PayOrderExtensionDO
  387 + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
  388 + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify))
  389 + .channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build());
  390 + if (updateCounts == 0) { // 校验状态,必须是待支付
  391 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
  392 + }
  393 + log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId());
  394 + }
  395 +
322 } 396 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java
1 package cn.iocoder.yudao.module.pay.service.refund; 1 package cn.iocoder.yudao.module.pay.service.refund;
2 2
3 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
4 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; 3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
5 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; 5 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
6 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; 6 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
7 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; 7 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
8 -import cn.iocoder.yudao.framework.common.pojo.PageResult;  
9 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 8 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
10 9
11 import java.util.List; 10 import java.util.List;
@@ -62,8 +61,7 @@ public interface PayRefundService { @@ -62,8 +61,7 @@ public interface PayRefundService {
62 * 61 *
63 * @param channelId 渠道编号 62 * @param channelId 渠道编号
64 * @param notify 通知 63 * @param notify 通知
65 - * @param rawNotify 通知数据  
66 */ 64 */
67 - void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify); 65 + void notifyRefund(Long channelId, PayRefundRespDTO notify);
68 66
69 } 67 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
1 package cn.iocoder.yudao.module.pay.service.refund; 1 package cn.iocoder.yudao.module.pay.service.refund;
2 2
  3 +import cn.hutool.core.date.DateUtil;
3 import cn.hutool.core.util.RandomUtil; 4 import cn.hutool.core.util.RandomUtil;
4 -import cn.hutool.core.util.StrUtil;  
5 -import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;  
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 import cn.iocoder.yudao.framework.pay.config.PayProperties; 6 import cn.iocoder.yudao.framework.pay.config.PayProperties;
8 import cn.iocoder.yudao.framework.pay.core.client.PayClient; 7 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
9 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; 8 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
10 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;  
11 -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; 9 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
12 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 10 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
13 -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum; 11 +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
  12 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
14 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; 13 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
15 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; 14 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
16 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; 15 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
  16 +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
17 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; 17 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
18 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; 18 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
19 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; 19 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
20 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; 20 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
21 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 21 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
22 -import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;  
23 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; 22 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
24 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants; 23 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
  24 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
25 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; 25 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
26 -import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; 26 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
27 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 27 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
28 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; 28 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
29 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;  
30 import cn.iocoder.yudao.module.pay.service.app.PayAppService; 29 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
31 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; 30 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
32 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; 31 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated; @@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated;
41 import javax.annotation.Resource; 40 import javax.annotation.Resource;
42 import java.time.LocalDateTime; 41 import java.time.LocalDateTime;
43 import java.util.List; 42 import java.util.List;
44 -import java.util.Objects; 43 +
  44 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  45 +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
45 46
46 /** 47 /**
47 * 退款订单 Service 实现类 48 * 退款订单 Service 实现类
48 * 49 *
49 - * @author aquan 50 + * @author jason
50 */ 51 */
51 @Service 52 @Service
52 @Slf4j 53 @Slf4j
@@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
61 62
62 @Resource 63 @Resource
63 private PayRefundMapper refundMapper; 64 private PayRefundMapper refundMapper;
64 - @Resource  
65 - private PayOrderMapper orderMapper; // TODO @jason:需要改成不直接操作 db;  
66 65
67 @Resource 66 @Resource
68 private PayOrderService orderService; 67 private PayOrderService orderService;
@@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
82 81
83 @Override 82 @Override
84 public Long getRefundCountByAppId(Long appId) { 83 public Long getRefundCountByAppId(Long appId) {
85 - return refundMapper.selectCountByApp(appId); 84 + return refundMapper.selectCountByAppId(appId);
86 } 85 }
87 86
88 @Override 87 @Override
@@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService {
98 @Override 97 @Override
99 @Transactional(rollbackFor = Exception.class) 98 @Transactional(rollbackFor = Exception.class)
100 public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { 99 public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
101 - // 获得 PayOrderDO  
102 - PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId());  
103 - // 校验订单是否存在  
104 - if (Objects.isNull(order) ) {  
105 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);  
106 - }  
107 - // 校验 App  
108 - PayAppDO app = appService.validPayApp(order.getAppId());  
109 - // 校验支付渠道是否有效 100 + // 1.1 校验 App
  101 + PayAppDO app = appService.validPayApp(reqDTO.getAppId());
  102 + // 1.2 校验支付订单
  103 + PayOrderDO order = validatePayOrderCanRefund(reqDTO);
  104 + // 1.3 校验支付渠道是否有效
110 PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); 105 PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
111 - // 校验支付客户端是否正确初始化  
112 PayClient client = payClientFactory.getPayClient(channel.getId()); 106 PayClient client = payClientFactory.getPayClient(channel.getId());
113 if (client == null) { 107 if (client == null) {
114 log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); 108 log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
115 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND); 109 + throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
116 } 110 }
117 -  
118 - // TODO 芋艿:待实现  
119 - String merchantRefundId = RandomUtil.randomNumbers(16);  
120 -  
121 - // 校验退款的条件  
122 - validatePayRefund(reqDTO, order);  
123 - // 退款类型  
124 - PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;  
125 - if (Objects.equals(reqDTO.getPrice(), order.getPrice())) {  
126 - refundType = PayRefundTypeEnum.ALL; 111 + // 1.4 校验退款订单是否已经存在
  112 + PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
  113 + app.getId(), reqDTO.getMerchantRefundId());
  114 + if (refund != null) {
  115 + throw exception(ErrorCodeConstants.PAY_REFUND_EXISTS);
127 } 116 }
128 - PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());  
129 - PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),  
130 - merchantRefundId); // TODO 芋艿:需要优化  
131 - if(Objects.nonNull(payRefundDO)){  
132 - // 退款订单已经提交过。  
133 - //TODO 校验相同退款单的金额  
134 - // TODO @jason:咱要不封装一个 ObjectUtils.equalsAny  
135 - if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())  
136 - || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {  
137 - //已成功退款  
138 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);  
139 - }  
140 - //可以重复提交,保证 退款请求号 一致,由渠道保证幂等  
141 - } else {  
142 - // 成功,插入退款单 状态为生成.没有和渠道交互  
143 - // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;  
144 - payRefundDO = PayRefundDO.builder()  
145 - .appId(order.getAppId())  
146 - .channelOrderNo(order.getChannelOrderNo())  
147 - .channelCode(order.getChannelCode())  
148 - .channelId(order.getChannelId())  
149 - .orderId(order.getId())  
150 - .merchantRefundNo(merchantRefundId) // TODO 芋艿:需要优化  
151 - .notifyUrl(app.getRefundNotifyUrl())  
152 - .payPrice(order.getPrice())  
153 - .refundPrice(reqDTO.getPrice())  
154 - .userIp(reqDTO.getUserIp())  
155 - .merchantOrderId(order.getMerchantOrderId())  
156 - .tradeNo(orderExtensionDO.getNo())  
157 - .status(PayRefundStatusEnum.CREATE.getStatus())  
158 - .reason(reqDTO.getReason())  
159 - .notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())  
160 - .type(refundType.getStatus())  
161 - .build();  
162 - refundMapper.insert(payRefundDO);  
163 - }  
164 - // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;  
165 - PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();  
166 - unifiedReqDTO.setUserIp(reqDTO.getUserIp())  
167 - .setAmount(reqDTO.getPrice()) 117 +
  118 + // 2.1 插入退款单
  119 + refund = PayRefundConvert.INSTANCE.convert(reqDTO)
  120 + .setNo(generateRefundNo()).setOrderId(order.getId())
  121 + .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
  122 + // 商户相关的字段
  123 + .setNotifyUrl(app.getRefundNotifyUrl()).setNotifyStatus(PayNotifyStatusEnum.WAITING.getStatus())
  124 + // 渠道相关字段
168 .setChannelOrderNo(order.getChannelOrderNo()) 125 .setChannelOrderNo(order.getChannelOrderNo())
169 - .setPayTradeNo(orderExtensionDO.getNo())  
170 - .setMerchantRefundId(merchantRefundId) // TODO 芋艿:需要优化  
171 - .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl 126 + // 退款相关字段
  127 + .setStatus(PayRefundStatusEnum.WAITING.getStatus())
  128 + .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice());
  129 + refundMapper.insert(refund);
  130 + // 2.2 向渠道发起退款申请
  131 + PayOrderExtensionDO orderExtension = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
  132 + PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
  133 + .setPayPrice(order.getPrice())
  134 + .setRefundPrice(reqDTO.getPrice())
  135 + .setOutTradeNo(orderExtension.getNo())
  136 + .setOutRefundNo(refund.getNo())
  137 + .setNotifyUrl(genChannelRefundNotifyUrl(channel))
172 .setReason(reqDTO.getReason()); 138 .setReason(reqDTO.getReason());
173 - // 向渠道发起退款申请  
174 - client.unifiedRefund(unifiedReqDTO);  
175 - // 检查是否失败,失败抛出业务异常。  
176 - // TODO 渠道的异常记录。  
177 - // TODO @jason:可以先打个 warn log 哈; 139 + PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
  140 + // 2.3 处理退款返回
  141 + notifyRefund(channel, refundRespDTO);
  142 +
178 // 成功在 退款回调中处理 143 // 成功在 退款回调中处理
179 - return payRefundDO.getId(); 144 + return refund.getId();
  145 + }
  146 +
  147 + /**
  148 + * 校验支付订单是否可以退款
  149 + *
  150 + * @param reqDTO 退款申请信息
  151 + * @return 支付订单
  152 + */
  153 + private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
  154 + PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
  155 + if (order == null) {
  156 + throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
  157 + }
  158 + // 校验状态,必须是支付状态
  159 + if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
  160 + throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
  161 + }
  162 +
  163 + // 是否已经全额退款
  164 + if (PayOrderRefundStatusEnum.ALL.getStatus().equals(order.getRefundStatus())) {
  165 + throw exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
  166 + }
  167 + // 校验金额 退款金额不能大于原定的金额
  168 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
  169 + throw exception(ErrorCodeConstants.PAY_REFUND_PRICE_EXCEED);
  170 + }
  171 + // 是否有退款中的订单
  172 + if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
  173 + PayRefundStatusEnum.WAITING.getStatus()) > 0) {
  174 + throw exception(ErrorCodeConstants.PAY_REFUND_HAS_REFUNDING);
  175 + }
  176 + return order;
180 } 177 }
181 178
182 /** 179 /**
@@ -185,91 +182,84 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -185,91 +182,84 @@ public class PayRefundServiceImpl implements PayRefundService {
185 * @param channel 支付渠道 182 * @param channel 支付渠道
186 * @return 支付渠道的回调地址 配置地址 + "/" + channel id 183 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
187 */ 184 */
188 - private String genChannelPayNotifyUrl(PayChannelDO channel) {  
189 - return payProperties.getCallbackUrl() + "/" + channel.getId(); 185 + private String genChannelRefundNotifyUrl(PayChannelDO channel) {
  186 + return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
  187 + }
  188 +
  189 + private String generateRefundNo() {
  190 +// wx
  191 +// 2014
  192 +// 10
  193 +// 27
  194 +// 20
  195 +// 09
  196 +// 39
  197 +// 5522657
  198 +// a690389285100
  199 + // 目前的算法
  200 + // 时间序列,年月日时分秒 14 位
  201 + // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
  202 + return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
  203 + RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
  204 + ;
190 } 205 }
191 206
192 @Override 207 @Override
193 @Transactional(rollbackFor = Exception.class) 208 @Transactional(rollbackFor = Exception.class)
194 - public void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { 209 + public void notifyRefund(Long channelId, PayRefundRespDTO notify) {
  210 + // 校验支付渠道是否有效
  211 + channelService.validPayChannel(channelId);
  212 + // 通知结果
  213 +
195 // 校验支付渠道是否有效 214 // 校验支付渠道是否有效
196 - // TODO 芋艿:需要重构下这块的逻辑  
197 PayChannelDO channel = channelService.validPayChannel(channelId); 215 PayChannelDO channel = channelService.validPayChannel(channelId);
198 - if (Objects.equals(PayNotifyRefundStatusEnum.SUCCESS, notify.getStatus())){  
199 - payRefundSuccess(notify); 216 + // 更新退款订单
  217 + TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
  218 + }
  219 +
  220 + private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
  221 + if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
  222 + notifyRefundSuccess(channel, notify);
200 } else { 223 } else {
201 - //TODO 支付异常, 支付宝似乎没有支付异常的通知。  
202 - // TODO @jason:那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知 224 + notifyRefundFailure(channel, notify);
203 } 225 }
204 } 226 }
205 227
206 - private void payRefundSuccess(PayRefundNotifyRespDTO refundNotify) {  
207 - // 校验退款单存在  
208 - PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),  
209 - refundNotify.getReqNo());  
210 - if (refundDO == null) {  
211 - log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());  
212 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND); 228 + private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
  229 + // 1.1 查询 PayRefundDO
  230 + PayRefundDO refund = refundMapper.selectByAppIdAndNo(
  231 + channel.getAppId(), notify.getOutRefundNo());
  232 + if (refund == null) {
  233 + throw exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
  234 + }
  235 + if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
  236 + return;
  237 + }
  238 + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
  239 + throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
213 } 240 }
214 241
215 - // 得到已退金额  
216 - PayOrderDO payOrderDO = orderService.getOrder(refundDO.getOrderId());  
217 - Long refundedAmount = payOrderDO.getRefundPrice();  
218 -  
219 - PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;  
220 - if(Objects.equals(payOrderDO.getPrice(), refundedAmount+ refundDO.getRefundPrice())){  
221 - //支付金额 = 已退金额 + 本次退款金额。  
222 - orderStatus = PayOrderStatusEnum.CLOSED; 242 + // 1.2 更新 PayRefundDO
  243 + PayRefundDO updateRefundObj = new PayRefundDO()
  244 + .setSuccessTime(notify.getSuccessTime())
  245 + .setChannelRefundNo(notify.getChannelRefundNo())
  246 + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
  247 + .setChannelNotifyData(toJsonString(notify));
  248 + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
  249 + if (updateCounts == 0) { // 校验状态,必须是等待状态
  250 + throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
223 } 251 }
224 - // 更新支付订单  
225 - PayOrderDO updateOrderDO = new PayOrderDO();  
226 - updateOrderDO.setId(refundDO.getOrderId())  
227 - .setRefundPrice(refundedAmount + refundDO.getRefundPrice())  
228 - .setStatus(orderStatus.getStatus())  
229 - .setRefundTimes(payOrderDO.getRefundTimes() + 1)  
230 - .setRefundStatus(refundDO.getType());  
231 - orderMapper.updateById(updateOrderDO);  
232 252
233 - // 更新退款订单  
234 - PayRefundDO updateRefundDO = new PayRefundDO();  
235 - updateRefundDO.setId(refundDO.getId())  
236 - .setSuccessTime(refundNotify.getRefundSuccessTime())  
237 - .setChannelRefundNo(refundNotify.getChannelOrderNo())  
238 - .setTradeNo(refundNotify.getTradeNo())  
239 - .setNotifyTime(LocalDateTime.now())  
240 - .setStatus(PayRefundStatusEnum.SUCCESS.getStatus());  
241 - refundMapper.updateById(updateRefundDO); 253 + // 2. 更新订单
  254 + orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
242 255
243 - // 插入退款通知记录  
244 - // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调 256 + // 3. 插入退款通知记录
245 notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder() 257 notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
246 - .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build()); 258 + .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
247 } 259 }
248 260
249 - /**  
250 - * 校验是否进行退款  
251 - *  
252 - * @param reqDTO 退款申请信息  
253 - * @param order 原始支付订单信息  
254 - */  
255 - private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {  
256 - // 校验状态,必须是支付状态  
257 - if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {  
258 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);  
259 - }  
260 - // 是否已经全额退款  
261 - if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {  
262 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);  
263 - }  
264 - // 校验金额 退款金额不能大于 原定的金额  
265 - if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){  
266 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_PRICE_PRICE_EXCEED);  
267 - }  
268 - // 校验渠道订单号  
269 - if (StrUtil.isEmpty(order.getChannelOrderNo())) {  
270 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);  
271 - }  
272 - //TODO 退款的期限 退款次数的控制 261 + private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
  262 + // TODO 芋艿:未实现
273 } 263 }
274 264
275 } 265 }
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java
@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app; @@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app;
3 import cn.hutool.core.util.RandomUtil; 3 import cn.hutool.core.util.RandomUtil;
4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;  
7 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
8 -import cn.iocoder.yudao.framework.test.core.util.RandomUtils;  
9 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; 7 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
10 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; 8 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
11 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; 9 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
@@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; @@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean;
18 import org.springframework.context.annotation.Import; 16 import org.springframework.context.annotation.Import;
19 17
20 import javax.annotation.Resource; 18 import javax.annotation.Resource;
21 -import java.time.LocalDateTime;  
22 -import java.util.Collections;  
23 import java.util.Map; 19 import java.util.Map;
24 20
25 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; 21 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
@@ -58,7 +54,7 @@ public class PayAppServiceTest extends BaseDbUnitTest { @@ -58,7 +54,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
58 // 准备参数 54 // 准备参数
59 PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o -> 55 PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o ->
60 o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus())) 56 o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))
61 - .setPayNotifyUrl(randomURL()) 57 + .setOrderNotifyUrl(randomURL())
62 .setRefundNotifyUrl(randomURL())); 58 .setRefundNotifyUrl(randomURL()));
63 59
64 // 调用 60 // 调用
@@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest { @@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
77 // 准备参数 73 // 准备参数
78 PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> { 74 PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> {
79 o.setStatus(CommonStatusEnum.ENABLE.getStatus()); 75 o.setStatus(CommonStatusEnum.ENABLE.getStatus());
80 - o.setPayNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL()); 76 + o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
81 o.setId(dbApp.getId()); // 设置更新的 ID 77 o.setId(dbApp.getId()); // 设置更新的 ID
82 }); 78 });
83 79
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
@@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; @@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
13 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper; 13 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
14 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; 14 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
15 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 15 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
16 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; 16 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
17 import cn.iocoder.yudao.module.pay.service.app.PayAppService; 17 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
18 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; 18 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
19 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; 19 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -85,9 +85,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -85,9 +85,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
85 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2)); 85 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
86 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); 86 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
87 o.setSuccessExtensionId(1L); 87 o.setSuccessExtensionId(1L);
88 - o.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); 88 + o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
89 o.setRefundTimes(0); 89 o.setRefundTimes(0);
90 - o.setRefundPrice(0L); 90 + o.setRefundPrice(0);
91 o.setChannelUserId("1008611"); 91 o.setChannelUserId("1008611");
92 o.setChannelOrderNo(channelOrderId); 92 o.setChannelOrderNo(channelOrderId);
93 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); 93 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
106 // 测试 status 不匹配 106 // 测试 status 不匹配
107 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); 107 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
108 // 测试 refundStatus 不匹配 108 // 测试 refundStatus 不匹配
109 - orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus()))); 109 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
110 // 测试 createTime 不匹配 110 // 测试 createTime 不匹配
111 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10, 111 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
112 1)))); 112 1))));
@@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
118 reqVO.setMerchantOrderId(merchantOrderId); 118 reqVO.setMerchantOrderId(merchantOrderId);
119 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 119 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
120 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); 120 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
121 - reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); 121 + reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
122 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)})); 122 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
123 // 调用 123 // 调用
124 PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO); 124 PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO);
@@ -153,9 +153,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -153,9 +153,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
153 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2)); 153 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
154 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); 154 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
155 o.setSuccessExtensionId(1L); 155 o.setSuccessExtensionId(1L);
156 - o.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); 156 + o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
157 o.setRefundTimes(0); 157 o.setRefundTimes(0);
158 - o.setRefundPrice(0L); 158 + o.setRefundPrice(0);
159 o.setChannelUserId("1008611"); 159 o.setChannelUserId("1008611");
160 o.setChannelOrderNo(channelOrderId); 160 o.setChannelOrderNo(channelOrderId);
161 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); 161 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
175 // 测试 status 不匹配 175 // 测试 status 不匹配
176 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); 176 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
177 // 测试 refundStatus 不匹配 177 // 测试 refundStatus 不匹配
178 - orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus()))); 178 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
179 // 测试 createTime 不匹配 179 // 测试 createTime 不匹配
180 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10, 180 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
181 1)))); 181 1))));
@@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest { @@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
187 reqVO.setMerchantOrderId(merchantOrderId); 187 reqVO.setMerchantOrderId(merchantOrderId);
188 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 188 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
189 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); 189 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
190 - reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); 190 + reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
191 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)})); 191 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
192 192
193 // 调用 193 // 调用
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; @@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
11 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; 11 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
12 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; 12 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
13 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; 13 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
14 -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; 14 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
15 import cn.iocoder.yudao.module.pay.service.app.PayAppService; 15 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
16 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; 16 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
17 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; 17 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
62 o.setChannelId(1L); 62 o.setChannelId(1L);
63 o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); 63 o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
64 o.setOrderId(1L); 64 o.setOrderId(1L);
65 - o.setTradeNo("OT0000001"); 65 + o.setNo("OT0000001");
66 o.setMerchantOrderId("MOT0000001"); 66 o.setMerchantOrderId("MOT0000001");
67 - o.setMerchantRefundNo("MRF0000001"); 67 + o.setMerchantRefundId("MRF0000001");
68 o.setNotifyUrl("https://www.cancanzi.com"); 68 o.setNotifyUrl("https://www.cancanzi.com");
69 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 69 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
70 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); 70 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
71 - o.setType(PayRefundTypeEnum.SOME.getStatus());  
72 o.setPayPrice(100); 71 o.setPayPrice(100);
73 o.setRefundPrice(500); 72 o.setRefundPrice(500);
74 o.setReason("就是想退款了,你有意见吗"); 73 o.setReason("就是想退款了,你有意见吗");
@@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
77 o.setChannelRefundNo("CHR0000001"); 76 o.setChannelRefundNo("CHR0000001");
78 o.setChannelErrorCode(""); 77 o.setChannelErrorCode("");
79 o.setChannelErrorMsg(""); 78 o.setChannelErrorMsg("");
80 - o.setChannelExtras("");  
81 - o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));  
82 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); 79 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
83 - o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));  
84 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); 80 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
85 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); 81 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
86 }); 82 });
@@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
90 // 测试 channelCode 不匹配 86 // 测试 channelCode 不匹配
91 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); 87 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
92 // 测试 merchantRefundNo 不匹配 88 // 测试 merchantRefundNo 不匹配
93 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112"))); 89 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
94 // 测试 notifyStatus 不匹配 90 // 测试 notifyStatus 不匹配
95 refundMapper.insert( 91 refundMapper.insert(
96 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus()))); 92 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
97 // 测试 status 不匹配 93 // 测试 status 不匹配
98 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));  
99 - // 测试 type 不匹配  
100 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus()))); 94 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
101 // 测试 createTime 不匹配 95 // 测试 createTime 不匹配
102 refundMapper.insert(cloneIgnoreId(dbRefund, o -> 96 refundMapper.insert(cloneIgnoreId(dbRefund, o ->
103 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); 97 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
108 reqVO.setMerchantRefundNo("MRF0000001"); 102 reqVO.setMerchantRefundNo("MRF0000001");
109 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 103 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
110 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); 104 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
111 - reqVO.setType(PayRefundTypeEnum.SOME.getStatus());  
112 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); 105 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
113 106
114 // 调用 107 // 调用
@@ -127,13 +120,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -127,13 +120,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
127 o.setChannelId(1L); 120 o.setChannelId(1L);
128 o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); 121 o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
129 o.setOrderId(1L); 122 o.setOrderId(1L);
130 - o.setTradeNo("OT0000001"); 123 + o.setNo("OT0000001");
131 o.setMerchantOrderId("MOT0000001"); 124 o.setMerchantOrderId("MOT0000001");
132 - o.setMerchantRefundNo("MRF0000001"); 125 + o.setMerchantRefundId("MRF0000001");
133 o.setNotifyUrl("https://www.cancanzi.com"); 126 o.setNotifyUrl("https://www.cancanzi.com");
134 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 127 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
135 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); 128 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
136 - o.setType(PayRefundTypeEnum.SOME.getStatus());  
137 o.setPayPrice(100); 129 o.setPayPrice(100);
138 o.setRefundPrice(500); 130 o.setRefundPrice(500);
139 o.setReason("就是想退款了,你有意见吗"); 131 o.setReason("就是想退款了,你有意见吗");
@@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
142 o.setChannelRefundNo("CHR0000001"); 134 o.setChannelRefundNo("CHR0000001");
143 o.setChannelErrorCode(""); 135 o.setChannelErrorCode("");
144 o.setChannelErrorMsg(""); 136 o.setChannelErrorMsg("");
145 - o.setChannelExtras("");  
146 - o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));  
147 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); 137 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
148 - o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));  
149 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); 138 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
150 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); 139 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
151 }); 140 });
@@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
155 // 测试 channelCode 不匹配 144 // 测试 channelCode 不匹配
156 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); 145 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
157 // 测试 merchantRefundNo 不匹配 146 // 测试 merchantRefundNo 不匹配
158 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112"))); 147 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
159 // 测试 notifyStatus 不匹配 148 // 测试 notifyStatus 不匹配
160 refundMapper.insert( 149 refundMapper.insert(
161 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus()))); 150 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
162 // 测试 status 不匹配 151 // 测试 status 不匹配
163 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));  
164 - // 测试 type 不匹配  
165 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus()))); 152 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
166 // 测试 createTime 不匹配 153 // 测试 createTime 不匹配
167 refundMapper.insert(cloneIgnoreId(dbRefund, o -> 154 refundMapper.insert(cloneIgnoreId(dbRefund, o ->
168 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); 155 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest { @@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
174 reqVO.setMerchantRefundNo("MRF0000001"); 161 reqVO.setMerchantRefundNo("MRF0000001");
175 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); 162 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
176 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); 163 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
177 - reqVO.setType(PayRefundTypeEnum.SOME.getStatus()); 164 + reqVO.setType(PayOrderRefundStatusEnum.PART.getStatus());
178 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); 165 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
179 166
180 // 调用 167 // 调用
yudao-server/src/main/resources/application-local.yaml
@@ -194,7 +194,8 @@ yudao: @@ -194,7 +194,8 @@ yudao:
194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
196 pay: 196 pay:
197 - callback-url: http://yunai.natapp1.cc/admin-api/pay/notify/callback 197 + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
  198 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
198 access-log: # 访问日志的配置项 199 access-log: # 访问日志的配置项
199 enable: false 200 enable: false
200 error-code: # 错误码相关配置项 201 error-code: # 错误码相关配置项
yudao-server/src/main/resources/application.yaml
@@ -142,7 +142,7 @@ yudao: @@ -142,7 +142,7 @@ yudao:
142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关
143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
145 - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号 145 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
146 - /jmreport/* # 积木报表,无法携带租户编号 146 - /jmreport/* # 积木报表,无法携带租户编号
147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
148 ignore-tables: 148 ignore-tables:
yudao-ui-admin/src/api/pay/channel.js
@@ -28,14 +28,14 @@ export function deleteChannel(id) { @@ -28,14 +28,14 @@ export function deleteChannel(id) {
28 } 28 }
29 29
30 // 获得支付渠道 30 // 获得支付渠道
31 -export function getChannel(appId,code) { 31 +export function getChannel(appId, code) {
32 return request({ 32 return request({
33 - url: '/pay/channel/get-channel', 33 + url: '/pay/channel/get',
  34 + method: 'get',
34 params:{ 35 params:{
35 - appId:appId,  
36 - code:code 36 + appId,
  37 + code
37 }, 38 },
38 - method: 'get'  
39 }) 39 })
40 } 40 }
41 41
yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg 0 → 100644
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2&quot;) format(&quot;woff2&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff&quot;) format(&quot;woff&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf&quot;) format(&quot;truetype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont&quot;) format(&quot;svg&quot;); }</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#04C361"/><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#04C361"/></svg>
0 \ No newline at end of file 2 \ No newline at end of file
yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg 0 → 100644
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2&quot;) format(&quot;woff2&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff&quot;) format(&quot;woff&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf&quot;) format(&quot;truetype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont&quot;) format(&quot;svg&quot;); }</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"/></svg>
0 \ No newline at end of file 2 \ No newline at end of file
yudao-ui-admin/src/utils/constants.js
@@ -132,6 +132,14 @@ export const PayChannelEnum = { @@ -132,6 +132,14 @@ export const PayChannelEnum = {
132 "code": "wx_app", 132 "code": "wx_app",
133 "name": "微信 APP 支付" 133 "name": "微信 APP 支付"
134 }, 134 },
  135 + WX_NATIVE: {
  136 + "code": "wx_native",
  137 + "name": "微信扫码支付"
  138 + },
  139 + WX_BAR: {
  140 + "code": "wx_bar",
  141 + "name": "微信条码支付"
  142 + },
135 ALIPAY_PC: { 143 ALIPAY_PC: {
136 "code": "alipay_pc", 144 "code": "alipay_pc",
137 "name": "支付宝 PC 网站支付" 145 "name": "支付宝 PC 网站支付"
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
1 <template> 1 <template>
2 <div> 2 <div>
3 <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px"> 3 <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px">
4 - <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="100px" 4 + <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px"
5 v-loading="transferParam.loading"> 5 v-loading="transferParam.loading">
6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate"> 6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
7 <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}"> 7 <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
8 <template slot="append">%</template> 8 <template slot="append">%</template>
9 </el-input> 9 </el-input>
10 </el-form-item> 10 </el-form-item>
11 - <el-form-item label-width="180px" label="公众号APPID" prop="weChatConfig.appId">  
12 - <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号APPID" clearable :style="{width: '100%'}"> 11 + <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
  12 + <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
13 </el-input> 13 </el-input>
14 </el-form-item> 14 </el-form-item>
15 <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId"> 15 <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
@@ -29,29 +29,41 @@ @@ -29,29 +29,41 @@
29 </el-radio> 29 </el-radio>
30 </el-radio-group> 30 </el-radio-group>
31 </el-form-item> 31 </el-form-item>
32 - <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey"  
33 - v-if="form.weChatConfig.apiVersion === 'v2'">  
34 - <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable  
35 - :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>  
36 - </el-form-item> 32 + <div v-if="form.weChatConfig.apiVersion === 'v2'">
  33 + <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
  34 + <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
  35 + :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
  36 + </el-form-item>
  37 + <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
  38 + <el-input v-model="form.weChatConfig.keyContent" type="textarea"
  39 + placeholder="请上传 apiclient_cert.p12 证书"
  40 + readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
  41 + </el-form-item>
  42 + <el-form-item label-width="180px" label="">
  43 + <el-upload :limit="1" accept=".p12" action=""
  44 + :before-upload="p12FileBeforeUpload"
  45 + :http-request="keyContentUpload">
  46 + <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
  47 + </el-upload>
  48 + </el-form-item>
  49 + </div>
37 <div v-if="form.weChatConfig.apiVersion === 'v3'"> 50 <div v-if="form.weChatConfig.apiVersion === 'v3'">
38 - <el-form-item label-width="180px" label="API V3密钥" prop="weChatConfig.apiV3Key">  
39 - <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入API V3密钥" clearable 51 + <el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
  52 + <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
40 :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input> 53 :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
41 </el-form-item> 54 </el-form-item>
42 - <el-form-item label-width="180px" label="apiclient_key.perm证书" prop="weChatConfig.privateKeyContent"> 55 + <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
43 <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea" 56 <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
44 - placeholder="请上传apiclient_key.perm证书" 57 + placeholder="请上传 apiclient_key.perm 证书"
45 readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input> 58 readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
46 </el-form-item> 59 </el-form-item>
47 <el-form-item label-width="180px" label="" prop="privateKeyContentFile"> 60 <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
48 <el-upload ref="privateKeyContentFile" 61 <el-upload ref="privateKeyContentFile"
49 :limit="1" 62 :limit="1"
50 - :accept="fileAccept"  
51 - :headers="header" 63 + accept=".pem"
52 action="" 64 action=""
53 :before-upload="pemFileBeforeUpload" 65 :before-upload="pemFileBeforeUpload"
54 - :http-request="privateKeyUpload" 66 + :http-request="privateKeyContentUpload"
55 > 67 >
56 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button> 68 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
57 </el-upload> 69 </el-upload>
@@ -64,18 +76,17 @@ @@ -64,18 +76,17 @@
64 <el-form-item label-width="180px" label="" prop="privateCertContentFile"> 76 <el-form-item label-width="180px" label="" prop="privateCertContentFile">
65 <el-upload ref="privateCertContentFile" 77 <el-upload ref="privateCertContentFile"
66 :limit="1" 78 :limit="1"
67 - :accept="fileAccept"  
68 - :headers="header" 79 + accept=".pem"
69 action="" 80 action=""
70 :before-upload="pemFileBeforeUpload" 81 :before-upload="pemFileBeforeUpload"
71 - :http-request="privateCertUpload" 82 + :http-request="privateCertContentUpload"
72 > 83 >
73 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button> 84 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
74 </el-upload> 85 </el-upload>
75 </el-form-item> 86 </el-form-item>
76 </div> 87 </div>
77 <el-form-item label-width="180px" label="备注" prop="remark"> 88 <el-form-item label-width="180px" label="备注" prop="remark">
78 - <el-input v-model="form.remark" :style="{width: '100%'}"></el-input> 89 + <el-input v-model="form.remark" :style="{width: '100%'}" />
79 </el-form-item> 90 </el-form-item>
80 </el-form> 91 </el-form>
81 <div slot="footer" class="dialog-footer"> 92 <div slot="footer" class="dialog-footer">
@@ -100,6 +111,7 @@ const defaultForm = { @@ -100,6 +111,7 @@ const defaultForm = {
100 mchId: '', 111 mchId: '',
101 apiVersion: '', 112 apiVersion: '',
102 mchKey: '', 113 mchKey: '',
  114 + keyContent: '',
103 privateKeyContent: '', 115 privateKeyContent: '',
104 privateCertContent: '', 116 privateCertContent: '',
105 apiV3Key:'', 117 apiV3Key:'',
@@ -159,27 +171,27 @@ export default { @@ -159,27 +171,27 @@ export default {
159 message: '请输入商户密钥', 171 message: '请输入商户密钥',
160 trigger: 'blur' 172 trigger: 'blur'
161 }], 173 }],
  174 + 'weChatConfig.keyContent': [{
  175 + required: true,
  176 + message: '请上传 apiclient_cert.p12 证书',
  177 + trigger: 'blur'
  178 + }],
162 'weChatConfig.privateKeyContent': [{ 179 'weChatConfig.privateKeyContent': [{
163 required: true, 180 required: true,
164 - message: '请上传apiclient_key.perm证书', 181 + message: '请上传 apiclient_key.perm 证书',
165 trigger: 'blur' 182 trigger: 'blur'
166 }], 183 }],
167 'weChatConfig.privateCertContent': [{ 184 'weChatConfig.privateCertContent': [{
168 required: true, 185 required: true,
169 - message: '请上传apiclient_cert.perm证书', 186 + message: '请上传 apiclient_cert.perm证 书',
170 trigger: 'blur' 187 trigger: 'blur'
171 }], 188 }],
172 'weChatConfig.apiV3Key': [{ 189 'weChatConfig.apiV3Key': [{
173 required: true, 190 required: true,
174 - message: '请上传apiV3密钥值', 191 + message: '请上传 api V3 密钥值',
175 trigger: 'blur' 192 trigger: 'blur'
176 }], 193 }],
177 }, 194 },
178 - // 文件上传的header  
179 - header: {  
180 - "Authorization": null  
181 - },  
182 - fileAccept: ".pem",  
183 // 渠道状态 数据字典 195 // 渠道状态 数据字典
184 statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS), 196 statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
185 versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION), 197 versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
@@ -218,6 +230,7 @@ export default { @@ -218,6 +230,7 @@ export default {
218 this.form.weChatConfig.apiVersion = config.apiVersion; 230 this.form.weChatConfig.apiVersion = config.apiVersion;
219 this.form.weChatConfig.mchId = config.mchId; 231 this.form.weChatConfig.mchId = config.mchId;
220 this.form.weChatConfig.mchKey = config.mchKey; 232 this.form.weChatConfig.mchKey = config.mchKey;
  233 + this.form.weChatConfig.keyContent = config.keyContent;
221 this.form.weChatConfig.privateKeyContent = config.privateKeyContent; 234 this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
222 this.form.weChatConfig.privateCertContent = config.privateCertContent; 235 this.form.weChatConfig.privateCertContent = config.privateCertContent;
223 this.form.weChatConfig.apiV3Key = config.apiV3Key; 236 this.form.weChatConfig.apiV3Key = config.apiV3Key;
@@ -241,7 +254,6 @@ export default { @@ -241,7 +254,6 @@ export default {
241 this.$modal.msgSuccess("修改成功"); 254 this.$modal.msgSuccess("修改成功");
242 this.close(); 255 this.close();
243 } 256 }
244 -  
245 }) 257 })
246 } else { 258 } else {
247 259
@@ -255,10 +267,14 @@ export default { @@ -255,10 +267,14 @@ export default {
255 } 267 }
256 }); 268 });
257 }, 269 },
258 - pemFileBeforeUpload(file) { 270 + /**
  271 + * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
  272 + */
  273 + fileBeforeUpload(file, fileAccept) {
259 let format = '.' + file.name.split(".")[1]; 274 let format = '.' + file.name.split(".")[1];
260 - if (format !== this.fileAccept) {  
261 - this.$message.error('请上传指定格式"' + this.fileAccept + '"文件'); 275 + if (format !== fileAccept) {
  276 + debugger
  277 + this.$message.error('请上传指定格式"' + fileAccept + '"文件');
262 return false; 278 return false;
263 } 279 }
264 let isRightSize = file.size / 1024 / 1024 < 2 280 let isRightSize = file.size / 1024 / 1024 < 2
@@ -267,19 +283,41 @@ export default { @@ -267,19 +283,41 @@ export default {
267 } 283 }
268 return isRightSize 284 return isRightSize
269 }, 285 },
270 - privateKeyUpload(event) { 286 + p12FileBeforeUpload(file) {
  287 + this.fileBeforeUpload(file, '.p12')
  288 + },
  289 + pemFileBeforeUpload(file) {
  290 + this.fileBeforeUpload(file, '.pem')
  291 + },
  292 + /**
  293 + * 读取 apiclient_key.pem 到 privateKeyContent 字段
  294 + */
  295 + privateKeyContentUpload(event) {
271 const readFile = new FileReader() 296 const readFile = new FileReader()
272 readFile.onload = (e) => { 297 readFile.onload = (e) => {
273 this.form.weChatConfig.privateKeyContent = e.target.result 298 this.form.weChatConfig.privateKeyContent = e.target.result
274 } 299 }
275 readFile.readAsText(event.file); 300 readFile.readAsText(event.file);
276 }, 301 },
277 - privateCertUpload(event) { 302 + /**
  303 + * 读取 apiclient_cert.pem 到 privateCertContent 字段
  304 + */
  305 + privateCertContentUpload(event) {
278 const readFile = new FileReader() 306 const readFile = new FileReader()
279 readFile.onload = (e) => { 307 readFile.onload = (e) => {
280 this.form.weChatConfig.privateCertContent = e.target.result 308 this.form.weChatConfig.privateCertContent = e.target.result
281 } 309 }
282 readFile.readAsText(event.file); 310 readFile.readAsText(event.file);
  311 + },
  312 + /**
  313 + * 读取 apiclient_cert.p12 到 keyContent 字段
  314 + */
  315 + keyContentUpload(event) {
  316 + const readFile = new FileReader()
  317 + readFile.onload = (e) => {
  318 + this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
  319 + }
  320 + readFile.readAsDataURL(event.file); // 读成 base64
283 } 321 }
284 } 322 }
285 } 323 }
yudao-ui-admin/src/views/pay/app/index.vue
@@ -143,6 +143,30 @@ @@ -143,6 +143,30 @@
143 </el-button> 143 </el-button>
144 </template> 144 </template>
145 </el-table-column> 145 </el-table-column>
  146 + <el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
  147 + <template v-slot="scope">
  148 + <el-button type="success" icon="el-icon-check" circle
  149 + v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
  150 + @click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
  151 + </el-button>
  152 + <el-button v-else
  153 + type="danger" icon="el-icon-close" circle
  154 + @click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
  155 + </el-button>
  156 + </template>
  157 + </el-table-column>
  158 + <el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
  159 + <template v-slot="scope">
  160 + <el-button type="success" icon="el-icon-check" circle
  161 + v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
  162 + @click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
  163 + </el-button>
  164 + <el-button v-else
  165 + type="danger" icon="el-icon-close" circle
  166 + @click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
  167 + </el-button>
  168 + </template>
  169 + </el-table-column>
146 </el-table-column> 170 </el-table-column>
147 <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> 171 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
148 <template v-slot="scope"> 172 <template v-slot="scope">
@@ -173,8 +197,8 @@ @@ -173,8 +197,8 @@
173 </el-radio-group> 197 </el-radio-group>
174 </el-form-item> 198 </el-form-item>
175 199
176 - <el-form-item label="支付结果的回调地址" prop="payNotifyUrl">  
177 - <el-input v-model="form.payNotifyUrl" placeholder="请输入支付结果的回调地址"/> 200 + <el-form-item label="支付结果的回调地址" prop="orderNotifyUrl">
  201 + <el-input v-model="form.orderNotifyUrl" placeholder="请输入支付结果的回调地址"/>
178 </el-form-item> 202 </el-form-item>
179 <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl"> 203 <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
180 <el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/> 204 <el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/>
@@ -235,7 +259,7 @@ export default { @@ -235,7 +259,7 @@ export default {
235 rules: { 259 rules: {
236 name: [{required: true, message: "应用名不能为空", trigger: "blur"}], 260 name: [{required: true, message: "应用名不能为空", trigger: "blur"}],
237 status: [{required: true, message: "开启状态不能为空", trigger: "blur"}], 261 status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
238 - payNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}], 262 + orderNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
239 refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}], 263 refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}],
240 }, 264 },
241 // 数据字典 265 // 数据字典
@@ -295,7 +319,7 @@ export default { @@ -295,7 +319,7 @@ export default {
295 name: undefined, 319 name: undefined,
296 status: undefined, 320 status: undefined,
297 remark: undefined, 321 remark: undefined,
298 - payNotifyUrl: undefined, 322 + orderNotifyUrl: undefined,
299 refundNotifyUrl: undefined, 323 refundNotifyUrl: undefined,
300 }; 324 };
301 this.resetForm("form"); 325 this.resetForm("form");
yudao-ui-admin/src/views/pay/cashier/index.vue
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 <!-- 微信支付 --> 26 <!-- 微信支付 -->
27 <el-descriptions title="选择微信支付" style="margin-top: 20px;" /> 27 <el-descriptions title="选择微信支付" style="margin-top: 20px;" />
28 <div class="pay-channel-container"> 28 <div class="pay-channel-container">
29 - <div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code"> 29 + <div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code" @click="submit(channel.code)">
30 <img :src="channel.icon"> 30 <img :src="channel.icon">
31 <div class="title">{{ channel.name }}</div> 31 <div class="title">{{ channel.name }}</div>
32 </div> 32 </div>
@@ -126,6 +126,14 @@ export default { @@ -126,6 +126,14 @@ export default {
126 icon: require("@/assets/images/pay/icon/wx_app.svg"), 126 icon: require("@/assets/images/pay/icon/wx_app.svg"),
127 code: "wx_app" 127 code: "wx_app"
128 }, { 128 }, {
  129 + name: '微信扫码支付',
  130 + icon: require("@/assets/images/pay/icon/wx_native.svg"),
  131 + code: "wx_native"
  132 + }, {
  133 + name: '微信条码支付',
  134 + icon: require("@/assets/images/pay/icon/wx_bar.svg"),
  135 + code: "wx_bar"
  136 + }, {
129 name: '模拟支付', 137 name: '模拟支付',
130 icon: require("@/assets/images/pay/icon/mock.svg"), 138 icon: require("@/assets/images/pay/icon/mock.svg"),
131 code: "mock" 139 code: "mock"
@@ -195,6 +203,15 @@ export default { @@ -195,6 +203,15 @@ export default {
195 } 203 }
196 return; 204 return;
197 } 205 }
  206 + if (channelCode === PayChannelEnum.WX_BAR.code) {
  207 + this.barCode = {
  208 + channelCode: channelCode,
  209 + value: '',
  210 + title: '“微信”条码支付',
  211 + visible: true
  212 + }
  213 + return;
  214 + }
198 215
199 // 默认的提交处理 216 // 默认的提交处理
200 this.submit0(channelCode) 217 this.submit0(channelCode)
@@ -207,7 +224,16 @@ export default { @@ -207,7 +224,16 @@ export default {
207 returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址 224 returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
208 ...this.buildSubmitParam(channelCode) 225 ...this.buildSubmitParam(channelCode)
209 }).then(response => { 226 }).then(response => {
  227 + // 直接返回已支付的情况,例如说扫码支付
210 const data = response.data 228 const data = response.data
  229 + if (data.status === PayOrderStatusEnum.SUCCESS.status) {
  230 + this.clearQueryInterval();
  231 + this.$message.success('支付成功!');
  232 + this.goReturnUrl();
  233 + return
  234 + }
  235 +
  236 + // 展示对应的界面
211 if (data.displayMode === PayDisplayModeEnum.URL.mode) { 237 if (data.displayMode === PayDisplayModeEnum.URL.mode) {
212 this.displayUrl(channelCode, data) 238 this.displayUrl(channelCode, data)
213 } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) { 239 } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
@@ -230,11 +256,18 @@ export default { @@ -230,11 +256,18 @@ export default {
230 } 256 }
231 } 257 }
232 } 258 }
  259 + // ② 微信 BarCode 支付时,需要传递 authCode 条形码
  260 + if (channelCode === PayChannelEnum.WX_BAR.code) {
  261 + return {
  262 + "channelExtras": {
  263 + "authCode": this.barCode.value
  264 + }
  265 + }
  266 + }
233 return {} 267 return {}
234 }, 268 },
235 /** 提交支付后,URL 的展示形式 */ 269 /** 提交支付后,URL 的展示形式 */
236 displayUrl(channelCode, data) { 270 displayUrl(channelCode, data) {
237 - // window.open(data.displayContent)  
238 location.href = data.displayContent 271 location.href = data.displayContent
239 this.submitLoading = false 272 this.submitLoading = false
240 }, 273 },
@@ -298,6 +331,9 @@ export default { @@ -298,6 +331,9 @@ export default {
298 * ③ close:支付已关闭 331 * ③ close:支付已关闭
299 */ 332 */
300 goReturnUrl(payResult) { 333 goReturnUrl(payResult) {
  334 + // 清理任务
  335 + this.clearQueryInterval();
  336 +
301 // 未配置的情况下,只能关闭 337 // 未配置的情况下,只能关闭
302 if (!this.returnUrl) { 338 if (!this.returnUrl) {
303 this.$tab.closePage(); 339 this.$tab.closePage();
yudao-ui-admin/src/views/pay/demo/index.vue
@@ -41,7 +41,8 @@ @@ -41,7 +41,8 @@
41 </el-table-column> 41 </el-table-column>
42 <el-table-column label="退款时间" align="center" prop="refundTime" width="180"> 42 <el-table-column label="退款时间" align="center" prop="refundTime" width="180">
43 <template v-slot="scope"> 43 <template v-slot="scope">
44 - <span>{{ parseTime(scope.row.refundTime) }}</span> 44 + <span v-if="scope.row.refundTime">{{ parseTime(scope.row.refundTime) }}</span>
  45 + <span v-else-if="scope.row.payRefundId">退款中,等待退款结果</span>
45 </template> 46 </template>
46 </el-table-column> 47 </el-table-column>
47 <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> 48 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">