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 1 package cn.iocoder.yudao.framework.common.util.validation;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
4 5 import org.springframework.util.StringUtils;
5 6  
6 7 import javax.validation.ConstraintViolation;
7 8 import javax.validation.ConstraintViolationException;
  9 +import javax.validation.Validation;
8 10 import javax.validation.Validator;
9 11 import java.util.Set;
10 12 import java.util.regex.Pattern;
... ... @@ -37,6 +39,12 @@ public class ValidationUtils {
37 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 48 public static void validate(Validator validator, Object object, Class<?>... groups) {
41 49 Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
42 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 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 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 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 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 11 * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
... ... @@ -22,31 +21,42 @@ public interface PayClient {
22 21 */
23 22 Long getId();
24 23  
  24 + // ============ 支付相关 ==========
  25 +
25 26 /**
26 27 * 调用支付渠道,统一下单
27 28 *
28 29 * @param reqDTO 下单信息
29 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 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 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 43 /**
42 44 * 商品描述信息
43 45 */
44   - @NotEmpty(message = "商品描述信息不能为空")
45 46 @Length(max = 128, message = "商品描述信息长度不能超过128")
46 47 private String body;
47 48 /**
... ... @@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
63 64 */
64 65 @NotNull(message = "支付金额不能为空")
65 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 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 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 59 @NotNull(message = "退款金额不能为空")
65 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 67 @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
72 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 1 package cn.iocoder.yudao.framework.pay.core.client.impl;
2 2  
3 3 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
4 5 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 6 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
  7 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
6 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 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 11 import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
11 12 import lombok.extern.slf4j.Slf4j;
12 13  
13   -import javax.validation.Validation;
  14 +import java.util.Map;
14 15  
15 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 30 /**
30 31 * 渠道编码
31 32 */
  33 + @SuppressWarnings("FieldCanBeLocal")
32 34 private final String channelCode;
33 35 /**
34 36 * 支付配置
... ... @@ -46,7 +48,7 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
46 48 */
47 49 public final void init() {
48 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 61 if (config.equals(this.config)) {
60 62 return;
61 63 }
62   - log.info("[refresh][配置({})发生变化,重新初始化]", config);
  64 + log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
63 65 this.config = config;
64 66 // 初始化
65 67 this.init();
... ... @@ -70,32 +72,47 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
70 72 return channelId;
71 73 }
72 74  
  75 + // ============ 支付相关 ==========
  76 +
73 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 82 try {
79 83 resp = doUnifiedOrder(reqDTO);
80   - } catch (ServiceException ex) {
81   - // 业务异常,都是实现类已经翻译,所以直接抛出即可
82   - throw ex;
83 84 } catch (Throwable ex) {
84 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 90 return resp;
89 91 }
90 92  
91   - protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
  93 + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
92 94 throws Throwable;
93 95  
94 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 116 try {
100 117 resp = doUnifiedRefund(reqDTO);
101 118 } catch (ServiceException ex) {
... ... @@ -103,17 +120,18 @@ public abstract class AbstractPayClient&lt;Config extends PayClientConfig&gt; implemen
103 120 throw ex;
104 121 } catch (Throwable ex) {
105 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 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 135 if (ex instanceof PayException) {
118 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 2  
3 3 import cn.hutool.core.bean.BeanUtil;
4 4 import cn.hutool.core.date.LocalDateTimeUtil;
  5 +import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.StrUtil;
5 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 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 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 18 import com.alipay.api.domain.AlipayTradeRefundModel;
16 19 import com.alipay.api.internal.util.AlipaySignature;
17 20 import com.alipay.api.request.AlipayTradeRefundRequest;
... ... @@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
22 25 import java.nio.charset.StandardCharsets;
23 26 import java.time.LocalDateTime;
24 27 import java.util.Map;
  28 +import java.util.Objects;
  29 +import java.util.function.Supplier;
25 30  
26 31 import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
27   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
28 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 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 91 * 支付宝统一的退款接口 alipay.trade.refund
  92 + *
55 93 * @param reqDTO 退款请求 request DTO
56 94 * @return 退款请求 Response
57 95 */
58 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 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 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 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 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 126 return null;
91 127 }
92 128 }
93 129  
94 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 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 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 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 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
6 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
7 7 import com.alipay.api.AlipayApiException;
... ... @@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
27 27 }
28 28  
29 29 @Override
30   - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  30 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
31 31 // 1.1 构建 AlipayTradeAppPayModel 请求
32 32 AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
33 33 // ① 通用的参数
34   - model.setOutTradeNo(reqDTO.getMerchantOrderId());
  34 + model.setOutTradeNo(reqDTO.getOutTradeNo());
35 35 model.setSubject(reqDTO.getSubject());
36 36 model.setBody(reqDTO.getBody());
37   - model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  37 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
38 38 model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
39 39 // ② 个性化的参数【无】
40 40 // ③ 支付宝扫码支付只有一种展示
... ... @@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
49 49 // 2.1 执行请求
50 50 AlipayTradeAppPayResponse response = client.execute(request);
51 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 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
5 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 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 9 import com.alipay.api.AlipayApiException;
... ... @@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
30 30 }
31 31  
32 32 @Override
33   - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  33 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
34 34 String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
35 35 if (StrUtil.isEmpty(authCode)) {
36 36 throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
... ... @@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
39 39 // 1.1 构建 AlipayTradePayModel 请求
40 40 AlipayTradePayModel model = new AlipayTradePayModel();
41 41 // ① 通用的参数
42   - model.setOutTradeNo(reqDTO.getMerchantOrderId());
  42 + model.setOutTradeNo(reqDTO.getOutTradeNo());
43 43 model.setSubject(reqDTO.getSubject());
44 44 model.setBody(reqDTO.getBody());
45   - model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  45 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
46 46 model.setScene("bar_code"); // 当面付条码支付场景
47 47 // ② 个性化的参数
48 48 model.setAuthCode(authCode);
... ... @@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
55 55 request.setNotifyUrl(reqDTO.getNotifyUrl());
56 56 request.setReturnUrl(reqDTO.getReturnUrl());
57 57  
  58 + // TODO 芋艿:各种边界的处理
58 59 // 2.1 执行请求
59 60 AlipayTradePayResponse response = client.execute(request);
60 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 2  
3 3 import cn.hutool.core.util.ObjectUtil;
4 4 import cn.hutool.http.Method;
  5 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
5 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 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 9 import com.alipay.api.AlipayApiException;
... ... @@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
29 29 }
30 30  
31 31 @Override
32   - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  32 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
33 33 // 1.1 构建 AlipayTradePagePayModel 请求
34 34 AlipayTradePagePayModel model = new AlipayTradePagePayModel();
35 35 // ① 通用的参数
36   - model.setOutTradeNo(reqDTO.getMerchantOrderId());
  36 + model.setOutTradeNo(reqDTO.getOutTradeNo());
37 37 model.setSubject(reqDTO.getSubject());
38 38 model.setBody(reqDTO.getBody());
39   - model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  39 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
40 40 model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
41 41 model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
42 42 // ② 个性化的参数
... ... @@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
60 60 response = client.pageExecute(request, Method.GET.name());
61 61 }
62 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 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 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 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
6 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
7 7 import com.alipay.api.AlipayApiException;
... ... @@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
25 25 }
26 26  
27 27 @Override
28   - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  28 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
29 29 // 1.1 构建 AlipayTradePrecreateModel 请求
30 30 AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
31 31 // ① 通用的参数
32   - model.setOutTradeNo(reqDTO.getMerchantOrderId());
  32 + model.setOutTradeNo(reqDTO.getOutTradeNo());
33 33 model.setSubject(reqDTO.getSubject());
34 34 model.setBody(reqDTO.getBody());
35   - model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  35 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
36 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 47 // 2.1 执行请求
48 48 AlipayTradePrecreateResponse response = client.execute(request);
49 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 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2  
3 3 import cn.hutool.http.Method;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
4 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 6 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
7 7 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
8 8 import com.alipay.api.AlipayApiException;
... ... @@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
26 26 }
27 27  
28 28 @Override
29   - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  29 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
30 30 // 1.1 构建 AlipayTradeWapPayModel 请求
31 31 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
32 32 // ① 通用的参数
33   - model.setOutTradeNo(reqDTO.getMerchantOrderId());
  33 + model.setOutTradeNo(reqDTO.getOutTradeNo());
34 34 model.setSubject(reqDTO.getSubject());
35 35 model.setBody(reqDTO.getBody());
36   - model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  36 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
37 37 model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
38 38 // ② 个性化的参数【无】
39 39 // ③ 支付宝 Wap 支付只有一种展示:URL
... ... @@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
48 48  
49 49 // 2.1 执行请求
50 50 AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
51   -
52 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 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2  
3 3 import cn.hutool.core.bean.BeanUtil;
  4 +import cn.hutool.core.codec.Base64;
4 5 import cn.hutool.core.date.LocalDateTimeUtil;
5 6 import cn.hutool.core.date.TemporalAccessorUtil;
6   -import cn.hutool.core.lang.Assert;
7 7 import cn.hutool.core.util.StrUtil;
8 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 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 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 16 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
16 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 24 import com.github.binarywang.wxpay.config.WxPayConfig;
18 25 import com.github.binarywang.wxpay.exception.WxPayException;
19 26 import com.github.binarywang.wxpay.service.WxPayService;
... ... @@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
22 29  
23 30 import java.time.LocalDateTime;
24 31 import java.time.ZoneId;
  32 +import java.util.Map;
25 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 59 WxPayConfig payConfig = new WxPayConfig();
54 60 BeanUtil.copyProperties(config, payConfig, "keyContent");
55 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 66 if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
57   - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
58 67 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
59 68 }
60 69 if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
61   - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
62 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 75 client.setConfig(payConfig);
68 76 }
69 77  
  78 + // ============ 支付相关 ==========
  79 +
70 80 @Override
71   - protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
  81 + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
72 82 try {
73 83 switch (config.getApiVersion()) {
74   - case WxPayClientConfig.API_VERSION_V2:
  84 + case API_VERSION_V2:
75 85 return doUnifiedOrderV2(reqDTO);
76 86 case WxPayClientConfig.API_VERSION_V3:
77 87 return doUnifiedOrderV3(reqDTO);
... ... @@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
79 89 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
80 90 }
81 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 102 * @param reqDTO 下单信息
90 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 109 * 【V3】调用支付渠道,统一下单
... ... @@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
98 111 * @param reqDTO 下单信息
99 112 * @return 各支付渠道的返回结果
100 113 */
101   - protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
  114 + protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
102 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 154 @Override
106   - public Object parseNotify(PayNotifyReqDTO rawNotify) {
107   - log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
  155 + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
108 156 try {
109   - // 微信支付 v2 回调结果处理
110 157 switch (config.getApiVersion()) {
111   - case WxPayClientConfig.API_VERSION_V2:
112   - return parseOrderNotifyV2(rawNotify);
  158 + case API_VERSION_V2:
  159 + return doUnifiedRefundV2(reqDTO);
113 160 case WxPayClientConfig.API_VERSION_V3:
114   - return parseOrderNotifyV3(rawNotify);
  161 + return doUnifiedRefundV3(reqDTO);
115 162 default:
116 163 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
117 164 }
118 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 285 static String formatDateV2(LocalDateTime time) {
183 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 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 297 static String formatDateV3(LocalDateTime time) {
191 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 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 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 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 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 6 import com.github.binarywang.wxpay.constant.WxPayConstants;
9 7 import com.github.binarywang.wxpay.exception.WxPayException;
... ... @@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
21 19 }
22 20  
23 21 @Override
24   - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
  22 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
25 23 return null;
26 24 }
27 25  
28 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 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 4 import cn.hutool.core.thread.ThreadUtil;
5 5 import cn.hutool.core.util.StrUtil;
6 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 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 9 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
14 10 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
15 11 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
... ... @@ -23,6 +19,7 @@ import java.time.LocalDateTime;
23 19 import java.util.concurrent.TimeUnit;
24 20  
25 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 25 * 微信支付【付款码支付】的 PayClient 实现类
... ... @@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
49 46 }
50 47  
51 48 @Override
52   - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  49 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
53 50 // 由于付款码需要不断轮询,所以需要在较短的时间完成支付
54 51 LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
55 52 if (expireTime.isAfter(reqDTO.getExpireTime())) {
... ... @@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
57 54 }
58 55 // 构建 WxPayMicropayRequest 对象
59 56 WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
60   - .outTradeNo(reqDTO.getMerchantOrderId())
  57 + .outTradeNo(reqDTO.getOutTradeNo())
61 58 .body(reqDTO.getSubject())
62 59 .detail(reqDTO.getBody())
63   - .totalFee(reqDTO.getAmount()) // 单位分
  60 + .totalFee(reqDTO.getPrice()) // 单位分
64 61 .timeExpire(formatDateV2(expireTime))
65 62 .spbillCreateIp(reqDTO.getUserIp())
66 63 .authCode(getAuthCode(reqDTO))
67 64 .build();
68 65 // 执行请求,重试直到失败(过期),或者成功
  66 + WxPayException lastWxPayException = null;
69 67 for (int i = 1; i < Byte.MAX_VALUE; i++) {
70 68 try {
71 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 74 } catch (WxPayException ex) {
  75 + lastWxPayException = ex;
83 76 // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
84 77 // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
85 78 // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
... ... @@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
89 82 }
90 83 // 等待 5 秒,继续下一轮重新发起支付
91 84 log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
92   - JsonUtils.toJsonString(request), ex.getMessage());
  85 + toJsonString(request), ex.getMessage());
93 86 ThreadUtil.sleep(5, TimeUnit.SECONDS);
94 87 }
95 88 }
96   - throw new IllegalStateException("微信 Bar 支付,重试多次失败");
  89 + throw lastWxPayException;
97 90 }
98 91  
99 92 @Override
100   - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  93 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
101 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 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 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 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 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 6 import com.github.binarywang.wxpay.constant.WxPayConstants;
9 7 import com.github.binarywang.wxpay.exception.WxPayException;
... ... @@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
21 19 }
22 20  
23 21 @Override
24   - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
  22 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
25 23 return null;
26 24 }
27 25  
28 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 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 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 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 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
8 6 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
9 7 import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
... ... @@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
34 32 }
35 33  
36 34 @Override
37   - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  35 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
38 36 // 构建 WxPayUnifiedOrderRequest 对象
39 37 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
40   - .outTradeNo(reqDTO.getMerchantOrderId())
  38 + .outTradeNo(reqDTO.getOutTradeNo())
41 39 .body(reqDTO.getSubject())
42 40 .detail(reqDTO.getBody())
43   - .totalFee(reqDTO.getAmount()) // 单位分
44   - .productId(reqDTO.getMerchantOrderId())
  41 + .totalFee(reqDTO.getPrice()) // 单位分
  42 + .productId(reqDTO.getOutTradeNo())
45 43 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
46 44 .spbillCreateIp(reqDTO.getUserIp())
47 45 .notifyUrl(reqDTO.getNotifyUrl())
... ... @@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
50 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 55 @Override
58   - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  56 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
59 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 37 *
38 38 * 只有公众号或小程序需要该字段
39 39 */
  40 + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
40 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 46 private String mchId;
46 47 /**
47 48 * API 版本
48 49 */
49   - @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
  50 + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
50 51 private String apiVersion;
51 52  
52 53 // ========== V2 版本的参数 ==========
... ... @@ -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 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 68 // ========== V3 版本的参数 ==========
68 69 /**
69   - * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
70   - * 对应的字符串
71   - * 注意,可通过 {@link #main(String[])} 读取
  70 + * apiclient_key.pem 证书文件的对应字符串
72 71 */
73 72 @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
74 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 77 @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
82 78 private String privateCertContent;
83 79 /**
84 80 * apiV3 密钥值
85 81 */
86   - @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
  82 + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
87 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 2  
3 3 import cn.hutool.core.map.MapUtil;
4 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 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 7 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
11 8 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
12 9 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
... ... @@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
19 16 import lombok.extern.slf4j.Slf4j;
20 17  
21 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 22 * 微信支付(公众号)的 PayClient 实现类
... ... @@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
44 42 }
45 43  
46 44 @Override
47   - protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  45 + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
48 46 // 构建 WxPayUnifiedOrderRequest 对象
49 47 WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
50   - .outTradeNo(reqDTO.getMerchantOrderId())
  48 + .outTradeNo(reqDTO.getOutTradeNo())
51 49 .body(reqDTO.getSubject())
52 50 .detail(reqDTO.getBody())
53   - .totalFee(reqDTO.getAmount()) // 单位分
  51 + .totalFee(reqDTO.getPrice()) // 单位分
54 52 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
55 53 .spbillCreateIp(reqDTO.getUserIp())
56 54 .openid(getOpenid(reqDTO))
... ... @@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
60 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 65 @Override
68   - protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
  66 + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
69 67 // 构建 WxPayUnifiedOrderRequest 对象
70 68 WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
71   - request.setOutTradeNo(reqDTO.getMerchantOrderId());
  69 + request.setOutTradeNo(reqDTO.getOutTradeNo());
72 70 request.setDescription(reqDTO.getSubject());
73   - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
  71 + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
74 72 request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
75 73 request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
76 74 request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
... ... @@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient {
79 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 18 QR_CODE("qr_code"), // 二维码的文字内容
19 19 QR_CODE_URL("qr_code_url"), // 二维码的图片链接
20 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 3 import lombok.AllArgsConstructor;
4 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 13 @Getter
12 14 @AllArgsConstructor
... ... @@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
20 22 private final Integer status;
21 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 3 import lombok.AllArgsConstructor;
4 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 12 */
11 13 @Getter
12 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 21 private final Integer status;
21 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 121  
122 122 private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
123 123 PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
124   - reqDTO.setAmount(123);
  124 + reqDTO.setPrice(123);
125 125 reqDTO.setSubject("IPhone 13");
126 126 reqDTO.setBody("biubiubiu");
127   - reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
  127 + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
128 128 reqDTO.setUserIp("127.0.0.1");
129 129 reqDTO.setNotifyUrl("http://127.0.0.1:8080");
130 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 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2 import cn.hutool.core.util.ReflectUtil;
3   -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
4 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 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 5 import com.alipay.api.AlipayApiException;
8 6 import com.alipay.api.DefaultAlipayClient;
... ... @@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
74 72 // 这里,设置可以直接随机整个对象。
75 73 Long shopOrderId = System.currentTimeMillis();
76 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 77 reqDTO.setBody("内容:" + shopOrderId);
80 78 reqDTO.setSubject("标题:"+shopOrderId);
81 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 351 public void afterCommit() {
352 352 // 创建退款单
353 353 PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties);
354   - Long payRefundId = payRefundApi.createPayRefund(createReqDTO);
  354 + Long payRefundId = payRefundApi.createRefund(createReqDTO);
355 355 // 更新售后单的退款单号
356 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 18 * @param reqDTO 创建请求
19 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 26 * @param id 退款单编号
27 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 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 45 @NotEmpty(message = "退款描述不能为空")
35   - @Length(max = 128, message = "退款描述长度不能超过128")
  46 + @Length(max = 128, message = "退款描述长度不能超过 128")
36 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 54 @NotNull(message = "退款金额不能为空")
50 55 @Min(value = 1, message = "退款金额必须大于零")
51 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 27 ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
28 28 ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
29 29 ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
  30 + ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
30 31  
31 32 // ========== ORDER 模块(拓展单) 1007003000 ==========
32 33 ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
33 34 ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
34 35  
35 36 // ========== 支付模块(退款) 1007006000 ==========
36   - ErrorCode PAY_PRICE_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
  37 + ErrorCode PAY_REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
37 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 41 ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
  42 + ErrorCode PAY_REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款");
41 43  
42 44 // ========== 示例订单 1007900000 ==========
43 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 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 5  
6 6 import java.util.Objects;
7 7  
  8 +/**
  9 + * 渠道的退款状态枚举
  10 + *
  11 + * @author 芋道源码
  12 + */
8 13 @Getter
9 14 @AllArgsConstructor
10 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 21 private final Integer status;
18 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 22  
23 23 @Override
24 24 public Long createOrder(PayOrderCreateReqDTO reqDTO) {
25   - return payOrderService.createPayOrder(reqDTO);
  25 + return payOrderService.createOrder(reqDTO);
26 26 }
27 27  
28 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 2  
3 3 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
4 4 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
  5 +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
5 6 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
6 7 import org.springframework.stereotype.Service;
7 8 import org.springframework.validation.annotation.Validated;
... ... @@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi {
21 22 private PayRefundService payRefundService;
22 23  
23 24 @Override
24   - public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
  25 + public Long createRefund(PayRefundCreateReqDTO reqDTO) {
25 26 return payRefundService.createPayRefund(reqDTO);
26 27 }
27 28  
28 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 29 @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback")
30 30 @NotNull(message = "支付结果的回调地址不能为空")
31 31 @URL(message = "支付结果的回调地址必须为 URL 格式")
32   - private String payNotifyUrl;
  32 + private String orderNotifyUrl;
33 33  
34 34 @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
35 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 3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
4 4 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 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 8 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
10 9 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
11 10 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll;
19 18 import java.util.Map;
20 19  
21 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 21 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
24 22  
25 23 @Tag(name = "管理后台 - 支付通知")
... ... @@ -37,22 +35,14 @@ public class PayNotifyController {
37 35 @Resource
38 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 40 @PermitAll
51 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 46 // 1. 校验支付渠道是否存在
57 47 PayClient payClient = payClientFactory.getPayClient(channelId);
58 48 if (payClient == null) {
... ... @@ -61,21 +51,30 @@ public class PayNotifyController {
61 51 }
62 52  
63 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 86 @PostMapping("/submit")
87 87 @Operation(summary = "提交支付订单")
88 88 public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
89   - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP());
  89 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
90 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 76 @ExcelProperty("退款成功时间")
77 77 private LocalDateTime successTime;
78 78  
79   - @ExcelProperty("退款通知时间")
80   - private LocalDateTime notifyTime;
81   -
82 79 @ExcelProperty("退款失效时间")
83 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 40 @PostMapping("/submit")
41 41 @Operation(summary = "提交支付订单")
42 42 public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
43   - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP());
  43 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
44 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 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 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 5 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
7 6 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
8 7 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
... ... @@ -93,7 +92,8 @@ public interface PayOrderConvert {
93 92  
94 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 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 1 package cn.iocoder.yudao.module.pay.convert.refund;
2 2  
3 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 6 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
5   -import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
6 7 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
7 8 import org.mapstruct.Mapper;
8   -import org.mapstruct.Mapping;
9   -import org.mapstruct.Mappings;
10 9 import org.mapstruct.factory.Mappers;
11 10  
12 11 import java.math.BigDecimal;
... ... @@ -44,8 +43,6 @@ public interface PayRefundConvert {
44 43  
45 44 PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
46 45  
47   - List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);
48   -
49 46 /**
50 47 * 退款订单DO 转 导出excel VO
51 48 *
... ... @@ -60,20 +57,18 @@ public interface PayRefundConvert {
60 57 PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
61 58  
62 59 payRefundExcelVO.setId(bean.getId());
63   - payRefundExcelVO.setTradeNo(bean.getTradeNo());
  60 + payRefundExcelVO.setTradeNo(bean.getNo());
64 61 payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
65   - payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
  62 + // TODO 芋艿:晚点在改
  63 +// payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
66 64 payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
67 65 payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
68 66 payRefundExcelVO.setStatus(bean.getStatus());
69   - payRefundExcelVO.setType(bean.getType());
70 67 payRefundExcelVO.setReason(bean.getReason());
71 68 payRefundExcelVO.setUserIp(bean.getUserIp());
72 69 payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
73 70 payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
74   - payRefundExcelVO.setExpireTime(bean.getExpireTime());
75 71 payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
76   - payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
77 72 payRefundExcelVO.setCreateTime(bean.getCreateTime());
78 73  
79 74 BigDecimal multiple = new BigDecimal(100);
... ... @@ -85,12 +80,8 @@ public interface PayRefundConvert {
85 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 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 6 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
7 7 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
8 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 10 import com.baomidou.mybatisplus.annotation.KeySequence;
11 11 import com.baomidou.mybatisplus.annotation.TableName;
12 12 import lombok.*;
... ... @@ -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 61 private String merchantOrderId;
61 62 /**
... ... @@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO {
126 127 /**
127 128 * 退款状态
128 129 *
129   - * 枚举 {@link PayRefundTypeEnum}
  130 + * 枚举 {@link PayOrderRefundStatusEnum}
130 131 */
131 132 private Integer refundStatus;
132 133 /**
... ... @@ -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 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 4 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
4 5 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
5 6 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
... ... @@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO {
32 33 */
33 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 42 * 例如说,P202110132239124200055
41 43 */
... ... @@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO {
64 66 * 支付状态
65 67 *
66 68 * 枚举 {@link PayOrderStatusEnum}
67   - * 注意,只包含上述枚举的 WAITING 和 SUCCESS
68 69 */
69 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 76 @TableField(typeHandler = JacksonTypeHandler.class)
76 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 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 1 package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
4 5 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
5 6 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
6 7 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
7 8 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
  9 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
8 10 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
9   -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
10 11 import com.baomidou.mybatisplus.annotation.KeySequence;
11 12 import com.baomidou.mybatisplus.annotation.TableId;
12 13 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
37 38 */
38 39 @TableId
39 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 72 */
64 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 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 91 private String notifyUrl;
103   -
104 92 /**
105 93 * 通知商户退款结果的回调状态
106   - * TODO 0 未发送 1 已发送
  94 + *
  95 + * 枚举 {@link PayNotifyStatusEnum}
107 96 */
108 97 private Integer notifyStatus;
109 98  
... ... @@ -116,12 +105,6 @@ public class PayRefundDO extends BaseDO {
116 105 private Integer status;
117 106  
118 107 /**
119   - * 退款类型(部分退款,全部退款)
120   - *
121   - * 枚举 {@link PayRefundTypeEnum}
122   - */
123   - private Integer type;
124   - /**
125 108 * 支付金额,单位:分
126 109 */
127 110 private Integer payPrice;
... ... @@ -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 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 139 private String channelRefundNo;
  140 + /**
  141 + * 退款成功时间
  142 + */
  143 + private LocalDateTime successTime;
155 144  
156 145 /**
157 146 * 调用渠道的错误码
158 147 */
159 148 private String channelErrorCode;
160   -
161 149 /**
162 150 * 调用渠道报错时,错误信息
163 151 */
164 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 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 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 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 9 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
  10 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
9 11 import org.apache.ibatis.annotations.Mapper;
10 12  
11 13 import java.util.List;
... ... @@ -13,6 +15,34 @@ import java.util.List;
13 15 @Mapper
14 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 46 default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
17 47 return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
18 48 .eqIfPresent("app_id", reqVO.getAppId())
... ... @@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX&lt;PayRefundDO&gt; {
37 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 3 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4 4 import lombok.AllArgsConstructor;
... ... @@ -11,10 +11,10 @@ import lombok.Getter;
11 11 */
12 12 @Getter
13 13 @AllArgsConstructor
14   -public enum PayRefundTypeEnum implements IntArrayValuable {
  14 +public enum PayOrderRefundStatusEnum implements IntArrayValuable {
15 15  
16 16 NO(0, "未退款"),
17   - SOME(10, "部分退款"),
  17 + PART(10, "部分退款"),
18 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 20 public class PayNotifyJob implements JobHandler {
21 21  
22 22 @Resource
23   - private PayNotifyService payNotifyCoreService;
  23 + private PayNotifyService payNotifyService;
24 24  
25 25 @Override
26 26 public String execute(String param) throws Exception {
27   - int notifyCount = payNotifyCoreService.executeNotify();
  27 + int notifyCount = payNotifyService.executeNotify();
28 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 83 */
84 84 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
85 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 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 184 // 1. 校验订单是否可以退款
185 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 193 .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
190   - .setPayOrderId(order.getPayOrderId()) // 支付单号
  194 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
  195 + .setMerchantRefundId(refundId)
191 196 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息
192   - // 2.2 更新退款单到 demo 订单
  197 + // 2.3 更新退款单到 demo 订单
193 198 payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
194 199 .setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
195 200 }
... ... @@ -234,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
234 239 }
235 240  
236 241 // 2.1 校验退款订单
237   - PayRefundRespDTO payRefund = payRefundApi.getPayRefund(payRefundId);
  242 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
238 243 if (payRefund == null) {
239 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 7 import java.util.List;
8 8 import java.util.Map;
9 9  
  10 +// TODO 芋艿:合并到 PayOrder 里;
10 11 /**
11 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 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 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 6 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
8 7 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
9 8 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
... ... @@ -33,6 +32,15 @@ public interface PayOrderService {
33 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 46 * @param appId 应用编号
... ... @@ -71,11 +79,11 @@ public interface PayOrderService {
71 79 /**
72 80 * 根据订单 ID 集合获取订单商品名称Map集合
73 81 *
74   - * @param idList 订单 ID 集合
  82 + * @param ids 订单 ID 集合
75 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 87 return CollectionUtils.convertMap(list, PayOrderDO::getId);
80 88 }
81 89  
... ... @@ -85,7 +93,7 @@ public interface PayOrderService {
85 93 * @param reqDTO 创建请求
86 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 103 * @param userIp 提交 IP
96 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 112 * @param channelId 渠道编号
105 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 4 import cn.hutool.core.lang.Pair;
5 5 import cn.hutool.core.util.ObjectUtil;
6 6 import cn.hutool.core.util.RandomUtil;
  7 +import cn.hutool.core.util.StrUtil;
7 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 9 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
9 10 import cn.iocoder.yudao.framework.pay.config.PayProperties;
10 11 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
11 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 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 16 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
17 17 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
18 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 26 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
27 27 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
28 28 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
29   -import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
30 29 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
31 30 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
  31 +import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
32 32 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
33   -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
34 33 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
35 34 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
36 35 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
... ... @@ -48,6 +47,7 @@ import java.util.Objects;
48 47  
49 48 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
50 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 53 * 支付订单 Service 实现类
... ... @@ -83,6 +83,11 @@ public class PayOrderServiceImpl implements PayOrderService {
83 83 }
84 84  
85 85 @Override
  86 + public PayOrderDO getOrder(Long appId, String merchantOrderId) {
  87 + return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
  88 + }
  89 +
  90 + @Override
86 91 public Long getOrderCountByAppId(Long appId) {
87 92 return orderMapper.selectCountByAppId(appId);
88 93 }
... ... @@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService {
104 109 }
105 110  
106 111 @Override
107   - public Long createPayOrder(PayOrderCreateReqDTO reqDTO) {
  112 + public Long createOrder(PayOrderCreateReqDTO reqDTO) {
108 113 // 校验 App
109 114 PayAppDO app = appService.validPayApp(reqDTO.getAppId());
110 115  
... ... @@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService {
112 117 PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
113 118 reqDTO.getAppId(), reqDTO.getMerchantOrderId());
114 119 if (order != null) {
115   - log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
  120 + log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
116 121 order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
117 122 return order.getId();
118 123 }
... ... @@ -120,20 +125,19 @@ public class PayOrderServiceImpl implements PayOrderService {
120 125 // 创建支付交易单
121 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 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 133 orderMapper.insert(order);
129 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 139 // 1. 获得 PayOrderDO ,并校验其是否存在
136   - PayOrderDO order = validatePayOrderCanSubmit(reqVO.getId());
  140 + PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
137 141 // 1.2 校验支付渠道是否有效
138 142 PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
139 143 PayClient client = payClientFactory.getPayClient(channel.getId());
... ... @@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService {
148 152 // 3. 调用三方接口
149 153 PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
150 154 // 商户相关的字段
151   - .setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
  155 + .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
152 156 .setSubject(order.getSubject()).setBody(order.getBody())
153   - .setNotifyUrl(genChannelPayNotifyUrl(channel))
  157 + .setNotifyUrl(genChannelOrderNotifyUrl(channel))
154 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 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 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 178 PayOrderDO order = orderMapper.selectById(id);
172 179 if (order == null) { // 是否存在
173   - throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
  180 + throw exception(PAY_ORDER_NOT_FOUND);
174 181 }
175 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 185 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
179   - throw exception(ErrorCodeConstants.PAY_ORDER_IS_EXPIRED);
  186 + throw exception(PAY_ORDER_IS_EXPIRED);
180 187 }
181 188 return order;
182 189 }
... ... @@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService {
184 191 private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
185 192 // 校验 App
186 193 appService.validPayApp(appId);
187   -
188 194 // 校验支付渠道是否有效
189 195 PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
190   - // 校验支付客户端是否正确初始化
191 196 PayClient client = payClientFactory.getPayClient(channel.getId());
192 197 if (client == null) {
193 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 201 return channel;
197 202 }
... ... @@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService {
202 207 * @param channel 支付渠道
203 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 214 private String generateOrderExtensionNo() {
... ... @@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService {
226 231  
227 232 @Override
228 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 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 281 // 1. 更新 PayOrderExtensionDO 支付成功
238   - PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify);
  282 + PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
239 283 // 2. 更新 PayOrderDO 支付成功
240   - Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify);
  284 + Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
241 285 if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
242 286 return;
243 287 }
... ... @@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService {
250 294 /**
251 295 * 更新 PayOrderExtensionDO 支付成功
252 296 *
253   - * @param no 支付订单号(支付模块)
254   - * @param rawNotify 通知数据
  297 + * @param notify 通知
255 298 * @return PayOrderExtensionDO 对象
256 299 */
257   - private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) {
  300 + private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
258 301 // 1. 查询 PayOrderExtensionDO
259   - PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no);
  302 + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
260 303 if (orderExtension == null) {
261   - throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
  304 + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
262 305 }
263 306 if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
264   - log.info("[updatePayOrderSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId());
  307 + log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
265 308 return orderExtension;
266 309 }
267 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 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 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 321 return orderExtension;
281 322 }
282 323  
... ... @@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService {
289 330 * @return key:是否之前已经成功回调
290 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 335 // 1. 判断 PayOrderDO 是否处于待支付
295 336 PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
296 337 if (order == null) {
297   - throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
  338 + throw exception(PAY_ORDER_NOT_FOUND);
298 339 }
299 340 if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
300 341 && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) {
301   - log.info("[updatePayOrderSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId());
  342 + log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
302 343 return Pair.of(true, order);
303 344 }
304 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 349 // 2. 更新 PayOrderDO
... ... @@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService {
313 354 .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
314 355 .notifyTime(LocalDateTime.now()).build());
315 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 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 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 5 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
6 6 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
7 7 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
8   -import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 8 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
10 9  
11 10 import java.util.List;
... ... @@ -62,8 +61,7 @@ public interface PayRefundService {
62 61 *
63 62 * @param channelId 渠道编号
64 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 1 package cn.iocoder.yudao.module.pay.service.refund;
2 2  
  3 +import cn.hutool.core.date.DateUtil;
3 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 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 6 import cn.iocoder.yudao.framework.pay.config.PayProperties;
8 7 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
9 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 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 13 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
15 14 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
16 15 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
  16 +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
17 17 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
18 18 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
19 19 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
20 20 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
21 21 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
22   -import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
23 22 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
24 23 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
  24 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
25 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 27 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
28 28 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
29   -import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
30 29 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
31 30 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
32 31 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
... ... @@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated;
41 40 import javax.annotation.Resource;
42 41 import java.time.LocalDateTime;
43 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 48 * 退款订单 Service 实现类
48 49 *
49   - * @author aquan
  50 + * @author jason
50 51 */
51 52 @Service
52 53 @Slf4j
... ... @@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
61 62  
62 63 @Resource
63 64 private PayRefundMapper refundMapper;
64   - @Resource
65   - private PayOrderMapper orderMapper; // TODO @jason:需要改成不直接操作 db;
66 65  
67 66 @Resource
68 67 private PayOrderService orderService;
... ... @@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
82 81  
83 82 @Override
84 83 public Long getRefundCountByAppId(Long appId) {
85   - return refundMapper.selectCountByApp(appId);
  84 + return refundMapper.selectCountByAppId(appId);
86 85 }
87 86  
88 87 @Override
... ... @@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService {
98 97 @Override
99 98 @Transactional(rollbackFor = Exception.class)
100 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 105 PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
111   - // 校验支付客户端是否正确初始化
112 106 PayClient client = payClientFactory.getPayClient(channel.getId());
113 107 if (client == null) {
114 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 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 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 182 * @param channel 支付渠道
186 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 207 @Override
193 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 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 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 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 3 import cn.hutool.core.util.RandomUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6   -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
7 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
8   -import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
9 7 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
10 8 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
11 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 16 import org.springframework.context.annotation.Import;
19 17  
20 18 import javax.annotation.Resource;
21   -import java.time.LocalDateTime;
22   -import java.util.Collections;
23 19 import java.util.Map;
24 20  
25 21 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
... ... @@ -58,7 +54,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
58 54 // 准备参数
59 55 PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o ->
60 56 o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))
61   - .setPayNotifyUrl(randomURL())
  57 + .setOrderNotifyUrl(randomURL())
62 58 .setRefundNotifyUrl(randomURL()));
63 59  
64 60 // 调用
... ... @@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
77 73 // 准备参数
78 74 PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> {
79 75 o.setStatus(CommonStatusEnum.ENABLE.getStatus());
80   - o.setPayNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
  76 + o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
81 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 13 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
14 14 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
15 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 17 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
18 18 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
19 19 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
... ... @@ -85,9 +85,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
85 85 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
86 86 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
87 87 o.setSuccessExtensionId(1L);
88   - o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
  88 + o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
89 89 o.setRefundTimes(0);
90   - o.setRefundPrice(0L);
  90 + o.setRefundPrice(0);
91 91 o.setChannelUserId("1008611");
92 92 o.setChannelOrderNo(channelOrderId);
93 93 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
... ... @@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
106 106 // 测试 status 不匹配
107 107 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
108 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 110 // 测试 createTime 不匹配
111 111 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
112 112 1))));
... ... @@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
118 118 reqVO.setMerchantOrderId(merchantOrderId);
119 119 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
120 120 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
121   - reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
  121 + reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
122 122 reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
123 123 // 调用
124 124 PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO);
... ... @@ -153,9 +153,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
153 153 o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
154 154 o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
155 155 o.setSuccessExtensionId(1L);
156   - o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
  156 + o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
157 157 o.setRefundTimes(0);
158   - o.setRefundPrice(0L);
  158 + o.setRefundPrice(0);
159 159 o.setChannelUserId("1008611");
160 160 o.setChannelOrderNo(channelOrderId);
161 161 o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
... ... @@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
175 175 // 测试 status 不匹配
176 176 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
177 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 179 // 测试 createTime 不匹配
180 180 orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
181 181 1))));
... ... @@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
187 187 reqVO.setMerchantOrderId(merchantOrderId);
188 188 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
189 189 reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
190   - reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
  190 + reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
191 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 11 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
12 12 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
13 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 15 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
16 16 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
17 17 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
... ... @@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
62 62 o.setChannelId(1L);
63 63 o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
64 64 o.setOrderId(1L);
65   - o.setTradeNo("OT0000001");
  65 + o.setNo("OT0000001");
66 66 o.setMerchantOrderId("MOT0000001");
67   - o.setMerchantRefundNo("MRF0000001");
  67 + o.setMerchantRefundId("MRF0000001");
68 68 o.setNotifyUrl("https://www.cancanzi.com");
69 69 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
70 70 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
71   - o.setType(PayRefundTypeEnum.SOME.getStatus());
72 71 o.setPayPrice(100);
73 72 o.setRefundPrice(500);
74 73 o.setReason("就是想退款了,你有意见吗");
... ... @@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
77 76 o.setChannelRefundNo("CHR0000001");
78 77 o.setChannelErrorCode("");
79 78 o.setChannelErrorMsg("");
80   - o.setChannelExtras("");
81   - o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
82 79 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
83   - o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
84 80 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
85 81 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
86 82 });
... ... @@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
90 86 // 测试 channelCode 不匹配
91 87 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
92 88 // 测试 merchantRefundNo 不匹配
93   - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
  89 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
94 90 // 测试 notifyStatus 不匹配
95 91 refundMapper.insert(
96 92 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
97 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 95 // 测试 createTime 不匹配
102 96 refundMapper.insert(cloneIgnoreId(dbRefund, o ->
103 97 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
... ... @@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
108 102 reqVO.setMerchantRefundNo("MRF0000001");
109 103 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
110 104 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
111   - reqVO.setType(PayRefundTypeEnum.SOME.getStatus());
112 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 120 o.setChannelId(1L);
128 121 o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
129 122 o.setOrderId(1L);
130   - o.setTradeNo("OT0000001");
  123 + o.setNo("OT0000001");
131 124 o.setMerchantOrderId("MOT0000001");
132   - o.setMerchantRefundNo("MRF0000001");
  125 + o.setMerchantRefundId("MRF0000001");
133 126 o.setNotifyUrl("https://www.cancanzi.com");
134 127 o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
135 128 o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
136   - o.setType(PayRefundTypeEnum.SOME.getStatus());
137 129 o.setPayPrice(100);
138 130 o.setRefundPrice(500);
139 131 o.setReason("就是想退款了,你有意见吗");
... ... @@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
142 134 o.setChannelRefundNo("CHR0000001");
143 135 o.setChannelErrorCode("");
144 136 o.setChannelErrorMsg("");
145   - o.setChannelExtras("");
146   - o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
147 137 o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
148   - o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
149 138 o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
150 139 o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
151 140 });
... ... @@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
155 144 // 测试 channelCode 不匹配
156 145 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
157 146 // 测试 merchantRefundNo 不匹配
158   - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
  147 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
159 148 // 测试 notifyStatus 不匹配
160 149 refundMapper.insert(
161 150 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
162 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 153 // 测试 createTime 不匹配
167 154 refundMapper.insert(cloneIgnoreId(dbRefund, o ->
168 155 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
... ... @@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
174 161 reqVO.setMerchantRefundNo("MRF0000001");
175 162 reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
176 163 reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
177   - reqVO.setType(PayRefundTypeEnum.SOME.getStatus());
  164 + reqVO.setType(PayOrderRefundStatusEnum.PART.getStatus());
178 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 194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
195 195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
196 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 199 access-log: # 访问日志的配置项
199 200 enable: false
200 201 error-code: # 错误码相关配置项
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -142,7 +142,7 @@ yudao:
142 142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关
143 143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
144 144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
145   - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号
  145 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
146 146 - /jmreport/* # 积木报表,无法携带租户编号
147 147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
148 148 ignore-tables:
... ...
yudao-ui-admin/src/api/pay/channel.js
... ... @@ -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 32 return request({
33   - url: '/pay/channel/get-channel',
  33 + url: '/pay/channel/get',
  34 + method: 'get',
34 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 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 2 \ No newline at end of file
... ...
yudao-ui-admin/src/utils/constants.js
... ... @@ -132,6 +132,14 @@ export const PayChannelEnum = {
132 132 "code": "wx_app",
133 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 143 ALIPAY_PC: {
136 144 "code": "alipay_pc",
137 145 "name": "支付宝 PC 网站支付"
... ...
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
1 1 <template>
2 2 <div>
3 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 5 v-loading="transferParam.loading">
6 6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
7 7 <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
8 8 <template slot="append">%</template>
9 9 </el-input>
10 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 13 </el-input>
14 14 </el-form-item>
15 15 <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
... ... @@ -29,29 +29,41 @@
29 29 </el-radio>
30 30 </el-radio-group>
31 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 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 53 :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
41 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 56 <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
44   - placeholder="请上传apiclient_key.perm证书"
  57 + placeholder="请上传 apiclient_key.perm 证书"
45 58 readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
46 59 </el-form-item>
47 60 <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
48 61 <el-upload ref="privateKeyContentFile"
49 62 :limit="1"
50   - :accept="fileAccept"
51   - :headers="header"
  63 + accept=".pem"
52 64 action=""
53 65 :before-upload="pemFileBeforeUpload"
54   - :http-request="privateKeyUpload"
  66 + :http-request="privateKeyContentUpload"
55 67 >
56 68 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
57 69 </el-upload>
... ... @@ -64,18 +76,17 @@
64 76 <el-form-item label-width="180px" label="" prop="privateCertContentFile">
65 77 <el-upload ref="privateCertContentFile"
66 78 :limit="1"
67   - :accept="fileAccept"
68   - :headers="header"
  79 + accept=".pem"
69 80 action=""
70 81 :before-upload="pemFileBeforeUpload"
71   - :http-request="privateCertUpload"
  82 + :http-request="privateCertContentUpload"
72 83 >
73 84 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
74 85 </el-upload>
75 86 </el-form-item>
76 87 </div>
77 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 90 </el-form-item>
80 91 </el-form>
81 92 <div slot="footer" class="dialog-footer">
... ... @@ -100,6 +111,7 @@ const defaultForm = {
100 111 mchId: '',
101 112 apiVersion: '',
102 113 mchKey: '',
  114 + keyContent: '',
103 115 privateKeyContent: '',
104 116 privateCertContent: '',
105 117 apiV3Key:'',
... ... @@ -159,27 +171,27 @@ export default {
159 171 message: '请输入商户密钥',
160 172 trigger: 'blur'
161 173 }],
  174 + 'weChatConfig.keyContent': [{
  175 + required: true,
  176 + message: '请上传 apiclient_cert.p12 证书',
  177 + trigger: 'blur'
  178 + }],
162 179 'weChatConfig.privateKeyContent': [{
163 180 required: true,
164   - message: '请上传apiclient_key.perm证书',
  181 + message: '请上传 apiclient_key.perm 证书',
165 182 trigger: 'blur'
166 183 }],
167 184 'weChatConfig.privateCertContent': [{
168 185 required: true,
169   - message: '请上传apiclient_cert.perm证书',
  186 + message: '请上传 apiclient_cert.perm证 书',
170 187 trigger: 'blur'
171 188 }],
172 189 'weChatConfig.apiV3Key': [{
173 190 required: true,
174   - message: '请上传apiV3密钥值',
  191 + message: '请上传 api V3 密钥值',
175 192 trigger: 'blur'
176 193 }],
177 194 },
178   - // 文件上传的header
179   - header: {
180   - "Authorization": null
181   - },
182   - fileAccept: ".pem",
183 195 // 渠道状态 数据字典
184 196 statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
185 197 versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
... ... @@ -218,6 +230,7 @@ export default {
218 230 this.form.weChatConfig.apiVersion = config.apiVersion;
219 231 this.form.weChatConfig.mchId = config.mchId;
220 232 this.form.weChatConfig.mchKey = config.mchKey;
  233 + this.form.weChatConfig.keyContent = config.keyContent;
221 234 this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
222 235 this.form.weChatConfig.privateCertContent = config.privateCertContent;
223 236 this.form.weChatConfig.apiV3Key = config.apiV3Key;
... ... @@ -241,7 +254,6 @@ export default {
241 254 this.$modal.msgSuccess("修改成功");
242 255 this.close();
243 256 }
244   -
245 257 })
246 258 } else {
247 259  
... ... @@ -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 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 278 return false;
263 279 }
264 280 let isRightSize = file.size / 1024 / 1024 < 2
... ... @@ -267,19 +283,41 @@ export default {
267 283 }
268 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 296 const readFile = new FileReader()
272 297 readFile.onload = (e) => {
273 298 this.form.weChatConfig.privateKeyContent = e.target.result
274 299 }
275 300 readFile.readAsText(event.file);
276 301 },
277   - privateCertUpload(event) {
  302 + /**
  303 + * 读取 apiclient_cert.pem 到 privateCertContent 字段
  304 + */
  305 + privateCertContentUpload(event) {
278 306 const readFile = new FileReader()
279 307 readFile.onload = (e) => {
280 308 this.form.weChatConfig.privateCertContent = e.target.result
281 309 }
282 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 143 </el-button>
144 144 </template>
145 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 170 </el-table-column>
147 171 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
148 172 <template v-slot="scope">
... ... @@ -173,8 +197,8 @@
173 197 </el-radio-group>
174 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 202 </el-form-item>
179 203 <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
180 204 <el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/>
... ... @@ -235,7 +259,7 @@ export default {
235 259 rules: {
236 260 name: [{required: true, message: "应用名不能为空", trigger: "blur"}],
237 261 status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
238   - payNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
  262 + orderNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
239 263 refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}],
240 264 },
241 265 // 数据字典
... ... @@ -295,7 +319,7 @@ export default {
295 319 name: undefined,
296 320 status: undefined,
297 321 remark: undefined,
298   - payNotifyUrl: undefined,
  322 + orderNotifyUrl: undefined,
299 323 refundNotifyUrl: undefined,
300 324 };
301 325 this.resetForm("form");
... ...
yudao-ui-admin/src/views/pay/cashier/index.vue
... ... @@ -26,7 +26,7 @@
26 26 <!-- 微信支付 -->
27 27 <el-descriptions title="选择微信支付" style="margin-top: 20px;" />
28 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 30 <img :src="channel.icon">
31 31 <div class="title">{{ channel.name }}</div>
32 32 </div>
... ... @@ -126,6 +126,14 @@ export default {
126 126 icon: require("@/assets/images/pay/icon/wx_app.svg"),
127 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 137 name: '模拟支付',
130 138 icon: require("@/assets/images/pay/icon/mock.svg"),
131 139 code: "mock"
... ... @@ -195,6 +203,15 @@ export default {
195 203 }
196 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 217 this.submit0(channelCode)
... ... @@ -207,7 +224,16 @@ export default {
207 224 returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
208 225 ...this.buildSubmitParam(channelCode)
209 226 }).then(response => {
  227 + // 直接返回已支付的情况,例如说扫码支付
210 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 237 if (data.displayMode === PayDisplayModeEnum.URL.mode) {
212 238 this.displayUrl(channelCode, data)
213 239 } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
... ... @@ -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 267 return {}
234 268 },
235 269 /** 提交支付后,URL 的展示形式 */
236 270 displayUrl(channelCode, data) {
237   - // window.open(data.displayContent)
238 271 location.href = data.displayContent
239 272 this.submitLoading = false
240 273 },
... ... @@ -298,6 +331,9 @@ export default {
298 331 * ③ close:支付已关闭
299 332 */
300 333 goReturnUrl(payResult) {
  334 + // 清理任务
  335 + this.clearQueryInterval();
  336 +
301 337 // 未配置的情况下,只能关闭
302 338 if (!this.returnUrl) {
303 339 this.$tab.closePage();
... ...
yudao-ui-admin/src/views/pay/demo/index.vue
... ... @@ -41,7 +41,8 @@
41 41 </el-table-column>
42 42 <el-table-column label="退款时间" align="center" prop="refundTime" width="180">
43 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 46 </template>
46 47 </el-table-column>
47 48 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
... ...