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

This commit is contained in:
puhui999
2023-07-18 09:06:02 +08:00
78 changed files with 1638 additions and 1114 deletions

View File

@@ -1,10 +1,12 @@
package cn.iocoder.yudao.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
import java.util.regex.Pattern;
@@ -37,6 +39,12 @@ public class ValidationUtils {
&& PATTERN_XML_NCNAME.matcher(str).matches();
}
public static void validate(Object object, Class<?>... groups) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Assert.notNull(validator);
validate(validator, object, groups);
}
public static void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) {

View File

@@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
public class PayProperties {
/**
* 回调地址
* 支付回调地址
*
* 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL
* 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
*
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
*/
@NotEmpty(message = "回调地址不能为空")
@URL(message = "回调地址的格式必须是 URL")
private String callbackUrl;
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String orderNotifyUrl;
/**
* 退款回调地址
*
* 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
*
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String refundNotifyUrl;
}

View File

@@ -1,12 +1,11 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import java.util.Map;
/**
* 支付客户端,用于对接各支付渠道的 SDK实现发起支付、退款等功能
@@ -22,31 +21,42 @@ public interface PayClient {
*/
Long getId();
// ============ 支付相关 ==========
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
// ============ 退款相关 ==========
/**
* 调用支付渠道,进行退款
*
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
* @return 退款信息
*/
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析回调数据
* 解析 refund 回调数据
*
* @param rawNotify 通知内容
* @return 回调对象
* 1. {@link PayRefundNotifyRespDTO} 退款通知
* 2. {@link PayOrderNotifyRespDTO} 支付通知
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
default Object parseNotify(PayNotifyReqDTO rawNotify) {
throw new UnsupportedOperationException("未实现 parseNotify 方法!");
}
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
}

View File

@@ -1,29 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Map;
/**
* 支付订单,退款订单回调,渠道的统一通知请求数据
*/
@Data
@ToString
@Builder
public class PayNotifyReqDTO {
/**
* HTTP 回调接口的 request body
*/
private String body;
/**
* HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
*/
private Map<String,String> params;
}

View File

@@ -1,38 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 支付通知 Response DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderNotifyRespDTO {
/**
* 支付订单号(支付模块的)
*/
private String orderExtensionNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
}

View File

@@ -1,58 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
*
* @author jason
*/
@Data
@ToString
@Builder
public class PayRefundNotifyRespDTO {
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 交易订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号。
* 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
* 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
* 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
* 退款单请求号,根据规则生成
*
* 例如说RR202109181134287570000
*/
private String reqNo;
/**
* 退款是否成功
*/
private PayNotifyRefundStatusEnum status;
/**
* 退款成功时间
*/
private LocalDateTime refundSuccessTime;
}

View File

@@ -0,0 +1,137 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderRespDTO {
/**
* 支付状态
*
* 枚举:{@link PayOrderStatusRespEnum}
*/
private Integer status;
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
private String outTradeNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的同步/异步通知结果
*/
private Object rawData;
// ========== 主动发起支付时,会返回的字段 ==========
/**
* 展示模式
*
* 枚举 {@link PayOrderDisplayModeEnum} 类
*/
private String displayMode;
/**
* 展示内容
*/
private String displayContent;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link PayException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
public PayOrderRespDTO() {
}
/**
* 创建【WAITING】状态的订单返回
*/
public PayOrderRespDTO(String displayMode, String displayContent,
String outTradeNo, Object rawData) {
this.status = PayOrderStatusRespEnum.WAITING.getStatus();
this.displayMode = displayMode;
this.displayContent = displayContent;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建【SUCCESS】状态的订单返回
*/
public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
this.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
this.channelOrderNo = channelOrderNo;
this.channelUserId = channelUserId;
this.successTime = successTime;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建【SUCCESS】或【CLOSED】状态的订单返回适合支付渠道回调时
*/
public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
this.status = status;
this.channelOrderNo = channelOrderNo;
this.channelUserId = channelUserId;
this.successTime = successTime;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建【CLOSED】状态的订单返回适合调用支付渠道失败时
*
* 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突,所以独立个方法出来
*/
public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
}

View File

@@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
// ========== 商户相关字段 ==========
/**
* 商户订单
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty(message = "商户订单编号不能为空")
private String merchantOrderId;
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 商品标题
*/
@@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
/**
* 商品描述信息
*/
@NotEmpty(message = "商品描述信息不能为空")
@Length(max = 128, message = "商品描述信息长度不能超过128")
private String body;
/**
@@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
private Integer price;
/**
* 支付过期时间

View File

@@ -1,38 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import lombok.Data;
/**
* 统一下单 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderUnifiedRespDTO {
/**
* 展示模式
*
* 枚举 {@link PayOrderDisplayModeEnum} 类
*/
private String displayMode;
/**
* 展示内容
*/
private String displayContent;
/**
* 同步的通知信息
*
* 目前只有 bar 条码支付才会出现,它是支付发起时,直接返回是否支付成功的,而其它支付还是异步通知
*/
private PayOrderNotifyRespDTO notify;
public PayOrderUnifiedRespDTO(String displayMode, String displayContent) {
this.displayMode = displayMode;
this.displayContent = displayContent;
}
}

View File

@@ -0,0 +1,47 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
public class PayRefundRespDTO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusRespEnum}
*/
private Integer status;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
private String outRefundNo;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
}

View File

@@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
public class PayRefundUnifiedReqDTO {
/**
* 用户 IP
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
private String userIp;
// TODO @jason这个是否为非必传字段呀只需要传递 payTradeNo 字段即可。尽可能精简
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
* 渠道订单号
*/
private String channelOrderNo;
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
* 外部退款号
*
* 对应 PayRefundDO no 字段
*/
private String payTradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 退款请求单号 同一退款请求单号多次请求只退一笔。
* 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
*/
@NotEmpty(message = "退款请求单号")
private String merchantRefundId;
@NotEmpty(message = "退款请求单号不能为空")
private String outRefundNo;
/**
* 退款原因
@@ -58,16 +45,25 @@ public class PayRefundUnifiedReqDTO {
@NotEmpty(message = "退款原因不能为空")
private String reason;
/**
* 支付金额,单位:分
*
* 目前微信支付在退款的时候,必须传递该字段
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer payPrice;
/**
* 退款金额,单位:分
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
private Integer refundPrice;
/**
* 退款结果 notify 回调地址 支付宝退款不需要回调地址, 微信需要
* 退款结果 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;

View File

@@ -1,24 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 统一退款 Response DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
/**
* 渠道退款单编号
*/
private String channelRefundId;
}

View File

@@ -1,16 +1,17 @@
package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
import lombok.extern.slf4j.Slf4j;
import javax.validation.Validation;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@@ -29,6 +30,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
/**
* 渠道编码
*/
@SuppressWarnings("FieldCanBeLocal")
private final String channelCode;
/**
* 支付配置
@@ -46,7 +48,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", config);
log.info("[init][客户端({}) 初始化完成]", getId());
}
/**
@@ -59,7 +61,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
if (config.equals(this.config)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", config);
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
this.config = config;
// 初始化
this.init();
@@ -70,32 +72,47 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
return channelId;
}
// ============ 支付相关 ==========
@Override
public final PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一下单
PayOrderUnifiedRespDTO resp;
PayOrderRespDTO resp;
try {
resp = doUnifiedOrder(reqDTO);
} catch (ServiceException ex) {
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedRefund][request({}) 发起支付异常]", toJsonString(reqDTO), ex);
throw buildException(ex);
log.error("[unifiedRefund][客户端({}) request({}) 发起支付异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
@Override
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
try {
return doParseOrderNotify(params, body);
} catch (Throwable ex) {
log.error("[parseOrderNotify][params({}) body({}) 解析失败]", params, body, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
throws Throwable;
// ============ 退款相关 ==========
@Override
public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一退款
PayRefundUnifiedRespDTO resp;
PayRefundRespDTO resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (ServiceException ex) {
@@ -103,17 +120,18 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), ex);
throw buildException(ex);
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
// ========== 各种工具方法 ==========
private PayException buildException(Throwable ex) {
private PayException buildPayException(Throwable ex) {
if (ex instanceof PayException) {
return (PayException) ex;
}

View File

@@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
import com.alipay.api.*;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.AlipayResponse;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeRefundRequest;
@@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR;
/**
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
@@ -50,72 +53,88 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
this.client = new DefaultAlipayClient(alipayConfig);
}
// ============ 支付相关 ==========
/**
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
*
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
*/
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
Assert.isFalse(response.isSuccess());
return PayOrderRespDTO.build(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTradeNo(), response);
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
// 1. 校验回调数据
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), config.getSignType());
// 2. 解析订单的状态
String tradeStatus = bodyObj.get("trade_status");
Integer status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
: Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS.getStatus()
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
Assert.notNull(status, (Supplier<Throwable>) () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
});
return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
bodyObj.get("out_trade_no"), body);
}
// ============ 退款相关 ==========
/**
* 支付宝统一的退款接口 alipay.trade.refund
*
* @param reqDTO 退款请求 request DTO
* @return 退款请求 Response
*/
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
model.setTradeNo(reqDTO.getChannelOrderNo());
model.setOutTradeNo(reqDTO.getPayTradeNo());
model.setOutRequestNo(reqDTO.getMerchantRefundId());
model.setRefundAmount(formatAmount(reqDTO.getAmount()));
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setOutRequestNo(reqDTO.getOutRefundNo());
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
model.setRefundReason(reqDTO.getReason());
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model);
refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());
refundRequest.setReturnUrl(reqDTO.getNotifyUrl());
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizModel(model);
try {
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
// 2.1 执行请求
AlipayTradeRefundResponse response = client.execute(request);
// 2.2 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
// 支付宝只要退款调用返回 success就认为退款成功不需要回调。具体可见 parseNotify 方法的说明。
// 另外,支付宝没有退款单号,所以不用设置
if (response.isSuccess()) {
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
//支付宝不返回退款单号,设置为空
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
respDTO.setChannelRefundId("");
// return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO
return null;
refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
} else {
refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
}
// 失败。需要抛出异常
// return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO
return null;
return refund;
} catch (AlipayApiException e) {
// TODO 记录异常日志
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
// return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO
log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
return null;
}
}
@Override
@SneakyThrows
public Object parseNotify(PayNotifyReqDTO rawNotify) {
// 1. 校验回调数据
String body = rawNotify.getBody();
Map<String, String> params = rawNotify.getParams();
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), "RSA2");
// 2.1 退款的情况
if (bodyObj.containsKey("refund_fee")) {
return PayRefundNotifyRespDTO.builder().channelOrderNo(bodyObj.get("trade_no"))
.tradeNo(bodyObj.get("out_trade_no")).reqNo(bodyObj.get("out_biz_no"))
.status(PayNotifyRefundStatusEnum.SUCCESS)
.refundSuccessTime(parseTime(params.get("gmt_refund")))
.build();
}
// 2.2 支付的情况
return PayOrderNotifyRespDTO.builder()
.orderExtensionNo(bodyObj.get("out_trade_no"))
.channelOrderNo(bodyObj.get("trade_no"))
.channelUserId(bodyObj.get("seller_id"))
.successTime(parseTime(params.get("notify_time")))
.build();
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
throw new UnsupportedOperationException("支付宝无退款回调");
}
// ========== 各种工具方法 ==========
@@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
}
/**
* 校验支付宝统一下单的响应
*
* 如果校验不通过,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 异常
*
* @param request 请求
* @param response 响应
*/
protected void validateUnifiedOrderResponse(Object request, AlipayResponse response) {
if (response.isSuccess()) {
return;
}
log.error("[validateUnifiedOrderResponse][发起支付失败request({})response({})]",
JsonUtils.toJsonString(request), JsonUtils.toJsonString(response));
throw exception0(ORDER_UNIFIED_ERROR.getCode(), response.getSubMsg());
}
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
@@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
}
@Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示
@@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求
AlipayTradeAppPayResponse response = client.execute(request);
// 2.2 处理结果
validateUnifiedOrderResponse(request, response);
return new PayOrderUnifiedRespDTO(displayMode, "");
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, "",
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
@@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
}
@Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
if (StrUtil.isEmpty(authCode)) {
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
@@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel model = new AlipayTradePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setScene("bar_code"); // 当面付条码支付场景
// ② 个性化的参数
model.setAuthCode(authCode);
@@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// TODO 芋艿:各种边界的处理
// 2.1 执行请求
AlipayTradePayResponse response = client.execute(request);
// 2.2 处理结果
validateUnifiedOrderResponse(request, response);
return new PayOrderUnifiedRespDTO(displayMode, "");
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, "",
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.Method;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
@@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
}
@Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// ② 个性化的参数
@@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
response = client.pageExecute(request, Method.GET.name());
}
// 2.2 处理结果
validateUnifiedOrderResponse(request, response);
return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
@@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
}
@Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
@@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求
AlipayTradePrecreateResponse response = client.execute(request);
// 2.2 处理结果
validateUnifiedOrderResponse(request, response);
return new PayOrderUnifiedRespDTO(displayMode, response.getQrCode());
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getQrCode(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.http.Method;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
@@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
}
@Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// ② 个性化的参数【无】
// ③ 支付宝 Wap 支付只有一种展示URL
@@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
// 2.2 处理结果
validateUnifiedOrderResponse(request, response);
return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -1,19 +1,26 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.hutool.core.date.DatePattern.*;
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
/**
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
@@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Base64.isBase64(config.getKeyContent())) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
@@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
client.setConfig(payConfig);
}
// ============ 支付相关 ==========
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
try {
switch (config.getApiVersion()) {
case WxPayClientConfig.API_VERSION_V2:
case API_VERSION_V2:
return doUnifiedOrderV2(reqDTO);
case WxPayClientConfig.API_VERSION_V3:
return doUnifiedOrderV3(reqDTO);
@@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
throw buildUnifiedOrderException(reqDTO, e);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.build(errorCode, errorMessage,
reqDTO.getOutTradeNo(), e.getXmlString());
}
}
@@ -89,8 +102,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException;
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws Exception;
/**
* 【V3】调用支付渠道统一下单
@@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException;
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return doParseOrderNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
return new PayOrderRespDTO(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = Objects.equals(result.getTradeState(), "SUCCESS") ?
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
return new PayOrderRespDTO(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
result.getOutTradeNo(), body);
}
// ============ 退款相关 ==========
@Override
public Object parseNotify(PayNotifyReqDTO rawNotify) {
log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case WxPayClientConfig.API_VERSION_V2:
return parseOrderNotifyV2(rawNotify);
case API_VERSION_V2:
return doUnifiedRefundV2(reqDTO);
case WxPayClientConfig.API_VERSION_V3:
return parseOrderNotifyV3(rawNotify);
return doUnifiedRefundV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e);
// throw buildPayException(e);
// todo 芋艿:异常的处理;
// throw buildUnifiedOrderException(null, e);
return null;
}
}
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRefundFee(reqDTO.getRefundPrice())
.setRefundDesc(reqDTO.getReason())
.setTotalFee(reqDTO.getPayPrice())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundResult response = client.refundV2(request);
// 2.2 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", response.getResultCode())) {
refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
.setChannelRefundNo(response.getRefundId());
} else {
refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
// TODO 芋艿;异常的处理;
return refund;
}
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
.setReason(reqDTO.getReason())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundV3Result response = client.refundV3(request);
// 2.2 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", response.getStatus())) {
refund.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setChannelRefundNo(response.getRefundId())
.setSuccessTime(parseDateV3(response.getSuccessTime()));
} else if (Objects.equals("PROCESSING", response.getStatus())) {
refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
.setChannelRefundNo(response.getRefundId());
} else {
refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
// TODO 芋艿;异常的处理;
return refund;
}
@Override
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case API_VERSION_V2:
return parseRefundNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return parseRefundNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
throw new RuntimeException(e);
// TODO 芋艿:缺一个异常翻译
}
}
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO
.builder()
.orderExtensionNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId())
.channelUserId(notifyResult.getOpenid())
.successTime(parseDateV2(notifyResult.getTimeEnd()))
.build();
@SuppressWarnings("DuplicatedCode")
private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo();
// 2. 构建结果
PayRefundRespDTO notify = new PayRefundRespDTO()
.setChannelRefundNo(responseResult.getRefundId())
.setOutRefundNo(responseResult.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(parseDateV2B(responseResult.getSuccessTime()));
} else {
notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
return notify;
}
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException {
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
// 转换结果
Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
"支付结果非 SUCCESS");
return PayOrderNotifyRespDTO.builder()
.orderExtensionNo(result.getOutTradeNo())
.channelOrderNo(result.getTradeState())
.channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null)
.successTime(parseDateV3(result.getSuccessTime()))
.build();
@SuppressWarnings("DuplicatedCode")
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult();
// 2. 构建结果
PayRefundRespDTO notify = new PayRefundRespDTO()
.setChannelRefundNo(responseResult.getRefundId())
.setOutRefundNo(responseResult.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(parseDateV3(responseResult.getSuccessTime()));
} else {
notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
return notify;
}
// ========== 各种工具方法 ==========
/**
* 构建统一下单的异常
*
* 目的:将参数不正确等异常,转换成 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常
*
* @param reqDTO 请求
* @param e 微信的支付异常
* @return 转换后的异常
*
*/
static Exception buildUnifiedOrderException(PayOrderUnifiedReqDTO reqDTO, WxPayException e) {
// 情况一:业务结果为 FAIL
if (Objects.equals(e.getResultCode(), "FAIL")) {
log.error("[buildUnifiedOrderException][request({}) 发起支付失败]", toJsonString(reqDTO), e);
if (Objects.equals(e.getErrCode(), "PARAM_ERROR")) {
throw invalidParamException(e.getErrCodeDes());
}
throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getErrCodeDes());
}
// 情况二:状态码结果为 FAIL
if (Objects.equals(e.getReturnCode(), "FAIL")) {
throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getReturnMsg());
}
// 情况三:系统异常,这里暂时不打,交给上层的 AbstractPayClient 统一打
return e;
}
static String formatDateV2(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
}
@@ -187,6 +290,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2B(String time) {
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
}
static String formatDateV3(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
}
@@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
}
static String getErrorCode(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCode();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return "CUSTOM_ERROR";
}
return e.getReturnCode();
}
static String getErrorMessage(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCodeDes();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return e.getCustomErrorMsg();
}
return e.getReturnMsg();
}
}

View File

@@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}

View File

@@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
@@ -23,6 +19,7 @@ import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 微信支付【付款码支付】的 PayClient 实现类
@@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
if (expireTime.isAfter(reqDTO.getExpireTime())) {
@@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
}
// 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(expireTime))
.spbillCreateIp(reqDTO.getUserIp())
.authCode(getAuthCode(reqDTO))
.build();
// 执行请求,重试直到失败(过期),或者成功
WxPayException lastWxPayException = null;
for (int i = 1; i < Byte.MAX_VALUE; i++) {
try {
WxPayMicropayResult response = client.micropay(request);
// 支付成功例如说用户输入了密码)
PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder()
.orderExtensionNo(response.getOutTradeNo())
.channelOrderNo(response.getTransactionId())
.channelUserId(response.getOpenid())
.successTime(parseDateV2(response.getTimeEnd()))
.build();
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),
JsonUtils.toJsonString(response))
.setNotify(notify);
// 支付成功例如说1用户输入了密码2
return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), response)
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
} catch (WxPayException ex) {
lastWxPayException = ex;
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
// 1. SYSTEMERROR接口返回错误请立即调用被扫订单结果查询API查询当前订单状态并根据订单的状态决定下一步的操作。
// 2. USERPAYING用户支付中需要输入密码等待 5 秒,然后调用被扫订单结果查询 API查询当前订单的不同状态决定下一步的操作。
@@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
}
// 等待 5 秒,继续下一轮重新发起支付
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
JsonUtils.toJsonString(request), ex.getMessage());
toJsonString(request), ex.getMessage());
ThreadUtil.sleep(5, TimeUnit.SECONDS);
}
}
throw new IllegalStateException("微信 Bar 支付,重试多次失败");
throw lastWxPayException;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return doUnifiedOrderV2(reqDTO);
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
return null;
}
// ========== 各种工具方法 ==========
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {

View File

@@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}

View File

@@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
@@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.productId(reqDTO.getMerchantOrderId())
.totalFee(reqDTO.getPrice()) // 单位分
.productId(reqDTO.getOutTradeNo())
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
@@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
WxPayNativeOrderResult response = client.createOrder(request);
// 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),
response.getCodeUrl());
return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setDescription(reqDTO.getSubject())
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())) // 单位分
.setTimeExpire(formatDateV3(reqDTO.getExpireTime()))
.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()))
.setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求
WxPayNativeOrderResult response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
// 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),
response.getCodeUrl());
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
return null;
return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
*
* 只有公众号或小程序需要该字段
*/
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
private String appId;
/**
* 商户号
*/
@NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
private String mchId;
/**
* API 版本
*/
@NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
private String apiVersion;
// ========== V2 版本的参数 ==========
@@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
/**
* 商户密钥
*/
@NotBlank(message = "商户密钥 不能为空", groups = V2.class)
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
private String mchKey;
/**
* apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
*
* 注意,可通过 {@link #main(String[])} 读取
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
*/
/// private String keyContent;
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
private String keyContent;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
* 注意,可通过 {@link #main(String[])} 读取
* apiclient_key.pem 证书文件的对应字符串
*/
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
private String privateKeyContent;
/**
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
* <p>
* 注意,可通过 {@link #main(String[])} 读取
* apiclient_cert.pem 证书文件的对应的字符串
*/
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
private String privateCertContent;
/**
* apiV3 密钥值
*/
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
private String apiV3Key;
/**

View File

@@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
@@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 微信支付(公众号)的 PayClient 实现类
@@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO))
@@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
WxPayMpOrderResult response = client.createOrder(request);
// 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
JsonUtils.toJsonString(response));
return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
@@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient {
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
// 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
JsonUtils.toJsonString(response));
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// TODO 需要实现
throw new UnsupportedOperationException();
return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
// ========== 各种工具方法 ==========

View File

@@ -1,16 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* 支付框架的错误码枚举
*
* 支付框架,使用 2-002-000-000 段
*
* @author 芋道源码
*/
public interface PayFrameworkErrorCodeConstants {
ErrorCode ORDER_UNIFIED_ERROR = new ErrorCode(2002000000, "发起支付失败,原因:{}");
}

View File

@@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum {
QR_CODE("qr_code"), // 二维码的文字内容
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
BAR_CODE("bar_code"), // 条形码
APP("app"), // 应用【目前暂时用不到】
CUSTOM("custom"), // 自定义:每种支付方式,做个性化处理;例如说,微信公众号支付时,调用 JSAPI 接口
APP("app"), // 应用Android、iOS、微信小程序、微信公众号等需要做自定义处理的
;
/**

View File

@@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的支付状态枚举
*
* @author 遇到源码
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
@@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
private final Integer status;
private final String name;
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
}

View File

@@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
// TODO 芋艿:看看能不能去掉
/**
* 退款通知, 统一的渠道退款状态
*
* @author jason
*/
public enum PayNotifyRefundStatusEnum {
/**
* 支付宝 中 全额退款 trade_status=TRADE_CLOSED 部分退款 trade_status=TRADE_SUCCESS
* 退款成功
*/
SUCCESS,
/**
* 支付宝退款通知没有这个状态
* 退款异常
*/
ABNORMAL;
}

View File

@@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundRespEnum {
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
PROCESSING(3,"退款处理中"),
CLOSED(4, "退款关闭");
private final Integer status;
private final String name;
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundStatusRespEnum {
WAITING(0, "未退款"),
SUCCESS(10, "退款成功"),
FAILURE(20, "退款失败");
private final Integer status;
private final String name;
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
}

View File

@@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
reqDTO.setAmount(123);
reqDTO.setPrice(123);
reqDTO.setSubject("IPhone 13");
reqDTO.setBody("biubiubiu");
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setUserIp("127.0.0.1");
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
return reqDTO;

View File

@@ -1,8 +1,6 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient;
@@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
// 这里,设置可以直接随机整个对象。
Long shopOrderId = System.currentTimeMillis();
PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setAmount(1);
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setPrice(1);
reqDTO.setBody("内容:" + shopOrderId);
reqDTO.setSubject("标题:"+shopOrderId);
String notify="http://niubi.natapp1.cc/api/pay/order/notify";

View File

@@ -0,0 +1,123 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2;
/**
* {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付
*
* @author 芋道源码
*/
@Disabled
public class WxBarPayClientIntegrationTest {
@Test
public void testPayV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起支付
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(String.valueOf(System.currentTimeMillis()))
.body("测试支付-body")
.detail("测试支付-detail")
.totalFee(1) // 单位分
.timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
.spbillCreateIp("127.0.0.1")
.authCode("134298744426278497")
.build();
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayMicropayResult response = client.micropay(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testParseRefundNotifyV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行解析
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>";
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml);
System.out.println(response.getReqInfo());
}
@Test
public void testRefundV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo("1689545667276")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setRefundFee(1)
.setRefundDesc("就是想退了")
.setTotalFee(1);
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundResult response = client.refund(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testRefundV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo("1689506325635")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
.setReason("就是想退了");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundV3Result response = client.refundV3(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
private WxPayConfig buildWxPayConfigV2() {
WxPayConfig config = new WxPayConfig();
config.setAppId("wx62056c0d5e8db250");
config.setMchId("1545083881");
config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
// config.setSignType(WxPayConstants.SignType.MD5);
config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
return config;
}
}

View File

@@ -0,0 +1,83 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3;
/**
* {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付
*
* @author 芋道源码
*/
@Disabled
public class WxNativePayClientIntegrationTest {
@Test
public void testPayV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV3();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起支付
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
.setOutTradeNo(String.valueOf(System.currentTimeMillis()))
.setDescription("测试支付-body")
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分
.setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1"))
.setNotifyUrl("http://127.0.0.1:48080");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testRefundV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV3();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo("1689545729695")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
.setReason("就是想退了");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundV3Result response = client.refundV3(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
private WxPayConfig buildWxPayConfigV3() {
WxPayConfig config = new WxPayConfig();
config.setAppId("wx62056c0d5e8db250");
config.setMchId("1545083881");
config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
// config.setCertSerialNo(serialNo);
config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
return config;
}
}