Commit d80bf5a368310cfdfe7356f15b3466eee026066a
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<Config extends PayClientConfig> 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<Config extends PayClientConfig> 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<Config extends PayClientConfig> 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<Config extends PayClientConfig> 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<Config extends PayClientConfig> 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<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<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<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<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<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<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<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<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<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
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
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<PayRefundDO> { |
| 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
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("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</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("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</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"> | ... | ... |