init version
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.jojubanking.boot.framework.pay.config;
|
||||
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientFactory;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.PayClientFactoryImpl;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 支付配置类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(PayProperties.class)
|
||||
public class JojuPayAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public PayClientFactory payClientFactory() {
|
||||
return new PayClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jojubanking.boot.framework.pay.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@ConfigurationProperties(prefix = "joju.pay")
|
||||
@Validated
|
||||
@Data
|
||||
public class PayProperties {
|
||||
|
||||
/**
|
||||
* 支付回调地址
|
||||
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
|
||||
*/
|
||||
@NotEmpty(message = "支付回调地址不能为空")
|
||||
@URL(message = "支付回调地址的格式必须是 URL")
|
||||
private String payNotifyUrl;
|
||||
/**
|
||||
* 退款回调地址
|
||||
* 注意点,同 {@link #payNotifyUrl} 属性
|
||||
*/
|
||||
@NotEmpty(message = "退款回调地址不能为空")
|
||||
@URL(message = "退款回调地址的格式必须是 URL")
|
||||
private String refundNotifyUrl;
|
||||
|
||||
|
||||
/**
|
||||
* 支付完成的返回地址
|
||||
*/
|
||||
@URL(message = "支付返回的地址的格式必须是 URL")
|
||||
@NotEmpty(message = "支付返回的地址不能为空")
|
||||
private String payReturnUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client;
|
||||
|
||||
import com.jojubanking.boot.framework.common.exception.ErrorCode;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 将 API 的错误码,转换为通用的错误码
|
||||
*
|
||||
* @see PayCommonResult
|
||||
* @see PayFrameworkErrorCodeConstants
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractPayCodeMapping {
|
||||
|
||||
public final ErrorCode apply(String apiCode, String apiMsg) {
|
||||
if (apiCode == null) {
|
||||
log.error("[apply][API 错误码为空,请排查]");
|
||||
return PayFrameworkErrorCodeConstants.EXCEPTION;
|
||||
}
|
||||
ErrorCode errorCode = this.apply0(apiCode, apiMsg);
|
||||
if (errorCode == null) {
|
||||
log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
|
||||
return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
protected abstract ErrorCode apply0(String apiCode, String apiMsg);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client;
|
||||
|
||||
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
|
||||
/**
|
||||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface PayClient {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
*
|
||||
* @return 渠道编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析支付单的通知结果
|
||||
*
|
||||
* @param data 通知结果
|
||||
* @return 解析结果
|
||||
* @throws Exception 解析失败,抛出异常
|
||||
*/
|
||||
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
|
||||
|
||||
/**
|
||||
* 调用支付渠道,进行退款
|
||||
* @param reqDTO 统一退款请求信息
|
||||
* @return 各支付渠道的统一返回结果
|
||||
*/
|
||||
PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析支付退款通知数据
|
||||
* @param notifyData 支付退款通知请求数据
|
||||
* @return 支付退款通知的Notify DTO
|
||||
*/
|
||||
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
|
||||
|
||||
// TODO @芋艿:后续改成非 default,避免不知道去实现
|
||||
/**
|
||||
* 验证是否渠道通知
|
||||
*
|
||||
* @param notifyData 通知数据
|
||||
* @return 默认是 true
|
||||
*/
|
||||
default boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续改成非 default,避免不知道去实现
|
||||
/**
|
||||
* 判断是否为退款通知
|
||||
*
|
||||
* @param notifyData 通知数据
|
||||
* @return 默认是 false
|
||||
*/
|
||||
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.Validator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 支付客户端的配置,本质是支付渠道的配置
|
||||
* 每个不同的渠道,需要不同的配置,通过子类来定义
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
// @JsonTypeInfo 注解的作用,Jackson 多态
|
||||
// 1. 序列化到时数据库时,增加 @class 属性。
|
||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
||||
public interface PayClientConfig {
|
||||
|
||||
/**
|
||||
* 配置验证参数是
|
||||
*
|
||||
* @param validator 校验对象
|
||||
* @return 配置好的验证参数
|
||||
*/
|
||||
Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator);
|
||||
|
||||
// TODO @aquan:貌似抽象一个 validation group 就好了!
|
||||
/**
|
||||
* 参数校验
|
||||
*
|
||||
* @param validator 校验对象
|
||||
*/
|
||||
default void validate(Validator validator) {
|
||||
Set<ConstraintViolation<PayClientConfig>> violations = verifyParam(validator);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new ConstraintViolationException(violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂接口
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface PayClientFactory {
|
||||
|
||||
/**
|
||||
* 获得支付客户端
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @return 支付客户端
|
||||
*/
|
||||
PayClient getPayClient(Long channelId);
|
||||
|
||||
/**
|
||||
* 创建支付客户端
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @param channelCode 渠道编码
|
||||
* @param config 支付配置
|
||||
*/
|
||||
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
|
||||
Config config);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.jojubanking.boot.framework.common.exception.ErrorCode;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 支付的 CommonResult 拓展类
|
||||
*
|
||||
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class PayCommonResult<T> extends CommonResult<T> {
|
||||
|
||||
/**
|
||||
* API 返回错误码
|
||||
*
|
||||
* 由于第三方的错误码可能是字符串,所以使用 String 类型
|
||||
*/
|
||||
private String apiCode;
|
||||
/**
|
||||
* API 返回提示
|
||||
*/
|
||||
private String apiMsg;
|
||||
|
||||
private PayCommonResult() {
|
||||
}
|
||||
|
||||
public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
|
||||
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
|
||||
PayCommonResult<T> result = new PayCommonResult<T>();
|
||||
result.setApiCode(apiCode);
|
||||
result.setApiMsg(apiMsg);
|
||||
result.setData(data);
|
||||
// 翻译错误码
|
||||
if (codeMapping != null) {
|
||||
ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
|
||||
result.setCode(errorCode.getCode());
|
||||
result.setMsg(errorCode.getMsg());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> PayCommonResult<T> error(Throwable ex) {
|
||||
PayCommonResult<T> result = new PayCommonResult<>();
|
||||
result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
|
||||
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 支付订单,退款订单回调,渠道的统一通知请求数据
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
@Builder
|
||||
public class PayNotifyDataDTO {
|
||||
|
||||
|
||||
/**
|
||||
* HTTP 回调接口的 request body
|
||||
*/
|
||||
private String body;
|
||||
|
||||
|
||||
/**
|
||||
* HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
*/
|
||||
private Map<String,String> params;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付通知 Response DTO
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayOrderNotifyRespDTO {
|
||||
|
||||
/**
|
||||
* 支付订单号(支付模块的)
|
||||
*/
|
||||
private String orderExtensionNo;
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
/**
|
||||
* 支付渠道用户编号
|
||||
*/
|
||||
private String channelUserId;
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 通知的原始数据
|
||||
*
|
||||
* 主要用于持久化,方便后续修复数据,或者排错
|
||||
*/
|
||||
private String data;
|
||||
|
||||
/**
|
||||
* TODO @jason 结合其他的渠道定义成枚举,
|
||||
* alipay
|
||||
* TRADE_CLOSED,未付款交易超时关闭,或支付完成后全额退款。
|
||||
* TRADE_SUCCESS, 交易支付成功
|
||||
* TRADE_FINISHED 交易结束,不可退款。
|
||||
*/
|
||||
private String tradeStatus;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 统一下单 Request DTO
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayOrderUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
// ========== 商户相关字段 ==========
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
@NotEmpty(message = "商户订单编号不能为空")
|
||||
private String merchantOrderId;
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
@NotEmpty(message = "商品标题不能为空")
|
||||
@Length(max = 32, message = "商品标题不能超过 32")
|
||||
private String subject;
|
||||
/**
|
||||
* 商品描述信息
|
||||
*/
|
||||
@NotEmpty(message = "商品描述信息不能为空")
|
||||
@Length(max = 128, message = "商品描述信息长度不能超过128")
|
||||
private String body;
|
||||
/**
|
||||
* 支付结果的 notify 回调地址
|
||||
*/
|
||||
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
/**
|
||||
* 支付结果的 return 回调地址
|
||||
*/
|
||||
@URL(message = "支付结果的 return 回调地址必须是 URL 格式")
|
||||
private String returnUrl;
|
||||
|
||||
// ========== 订单相关字段 ==========
|
||||
|
||||
/**
|
||||
* 支付金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Long amount;
|
||||
|
||||
/**
|
||||
* 支付过期时间
|
||||
*/
|
||||
@NotNull(message = "支付过期时间不能为空")
|
||||
private Date expireTime;
|
||||
|
||||
// ========== 拓展参数 ==========
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
*
|
||||
* 例如说,微信公众号需要传递 openid 参数
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
@Builder
|
||||
public class PayRefundNotifyDTO {
|
||||
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
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 Date refundSuccessTime;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 统一 退款 Request DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class PayRefundUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
|
||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
|
||||
* 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
|
||||
*/
|
||||
private String payTradeNo;
|
||||
|
||||
/**
|
||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
|
||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
|
||||
* 退款请求单号 同一退款请求单号多次请求只退一笔。
|
||||
* 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
|
||||
*/
|
||||
@NotEmpty(message = "退款请求单号")
|
||||
private String merchantRefundId;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
@NotEmpty(message = "退款原因不能为空")
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Long amount;
|
||||
|
||||
/**
|
||||
* 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
|
||||
*/
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.dto;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl;
|
||||
|
||||
import cn.hutool.extra.validation.ValidationUtil;
|
||||
import com.jojubanking.boot.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
|
||||
|
||||
/**
|
||||
* 渠道编号
|
||||
*/
|
||||
private final Long channelId;
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private final String channelCode;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
protected AbstractPayCodeMapping codeMapping;
|
||||
/**
|
||||
* 支付配置
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
|
||||
this.channelId = channelId;
|
||||
this.channelCode = channelCode;
|
||||
this.codeMapping = codeMapping;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][配置({}) 初始化完成]", config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
public final void refresh(Config config) {
|
||||
// 判断是否更新
|
||||
if (config.equals(this.config)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", config);
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected Double calculateAmount(Long amount) {
|
||||
return amount / 100.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
ValidationUtil.validate(reqDTO);
|
||||
// 执行短信发送
|
||||
PayCommonResult<?> result;
|
||||
try {
|
||||
result = doUnifiedOrder(reqDTO);
|
||||
} catch (Throwable ex) {
|
||||
// 打印异常日志
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), ex);
|
||||
// 封装返回
|
||||
return PayCommonResult.error(ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
PayCommonResult<PayRefundUnifiedRespDTO> resp;
|
||||
try {
|
||||
resp = doUnifiedRefund(reqDTO);
|
||||
} catch (Throwable ex) {
|
||||
// 记录异常日志
|
||||
log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex);
|
||||
resp = PayCommonResult.error(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientFactory;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayPcPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXLitePayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXNativePayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXPubPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂实现类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public class PayClientFactoryImpl implements PayClientFactory {
|
||||
|
||||
/**
|
||||
* 支付客户端 Map
|
||||
* key:渠道编号
|
||||
*/
|
||||
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public PayClient getPayClient(Long channelId) {
|
||||
AbstractPayClient<?> client = clients.get(channelId);
|
||||
if (client == null) {
|
||||
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
|
||||
Config config) {
|
||||
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
|
||||
if (client == null) {
|
||||
client = this.createPayClient(channelId, channelCode, config);
|
||||
client.init();
|
||||
clients.put(client.getId(), client);
|
||||
} else {
|
||||
client.refresh(config);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(
|
||||
Long channelId, String channelCode, Config config) {
|
||||
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
|
||||
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
|
||||
// 创建客户端
|
||||
// TODO @芋艿 WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配
|
||||
switch (channelEnum) {
|
||||
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||
case WX_LITE: return (AbstractPayClient<Config>) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付
|
||||
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
|
||||
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
}
|
||||
// 创建失败,错误日志 + 抛出异常
|
||||
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
|
||||
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.jojubanking.boot.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类, 实现支付宝统一的接口。如退款
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
|
||||
protected DefaultAlipayClient client;
|
||||
|
||||
public AbstractAlipayClient(Long channelId, String channelCode,
|
||||
AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
|
||||
super(channelId, channelCode, config, codeMapping);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
|
||||
* //https://opendocs.alipay.com/open/203/105286
|
||||
* @param data 通知结果
|
||||
* @return 解析结果 PayOrderNotifyRespDTO
|
||||
* @throws Exception 解析失败,抛出异常
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
||||
Map<String, String> params = strToMap(data.getBody());
|
||||
|
||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||
.tradeStatus(params.get("trade_status"))
|
||||
.successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.data(data.getBody()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
Map<String, String> params = strToMap(notifyData.getBody());
|
||||
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
||||
.tradeNo(params.get("out_trade_no"))
|
||||
.reqNo(params.get("out_biz_no"))
|
||||
.status(PayNotifyRefundStatusEnum.SUCCESS)
|
||||
.refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.build();
|
||||
return notifyDTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
if (notifyData.getParams().containsKey("refund_fee")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
|
||||
boolean verifyResult = false;
|
||||
try {
|
||||
verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2");
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e);
|
||||
}
|
||||
return verifyResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝统一的退款接口 alipay.trade.refund
|
||||
* @param reqDTO 退款请求 request DTO
|
||||
* @return 退款请求 Response
|
||||
*/
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
|
||||
model.setTradeNo(reqDTO.getChannelOrderNo());
|
||||
model.setOutTradeNo(reqDTO.getPayTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getMerchantRefundId());
|
||||
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
||||
refundRequest.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeRefundResponse response = client.execute(refundRequest);
|
||||
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
|
||||
if (response.isSuccess()) {
|
||||
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
|
||||
//支付宝不返回退款单号,设置为空
|
||||
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
|
||||
respDTO.setChannelRefundId("");
|
||||
return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping);
|
||||
}
|
||||
// 失败。需要抛出异常
|
||||
return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping);
|
||||
} catch (AlipayApiException e) {
|
||||
// TODO 记录异常日志
|
||||
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付宝统一回调参数 str 转 map
|
||||
*
|
||||
* @param s 支付宝支付通知回调参数
|
||||
* @return map 支付宝集合
|
||||
*/
|
||||
public static Map<String, String> strToMap(String s) {
|
||||
// TODO @zxy:这个可以使用 hutool 的 HttpUtil decodeParams 方法么?
|
||||
Map<String, String> stringStringMap = new HashMap<>();
|
||||
// 调整时间格式
|
||||
String s3 = s.replaceAll("%3A", ":");
|
||||
// 获取 map
|
||||
String s4 = s3.replace("+", " ");
|
||||
String[] split = s4.split("&");
|
||||
for (String s1 : split) {
|
||||
String[] split1 = s1.split("=");
|
||||
stringStringMap.put(split1[0], split1[1]);
|
||||
}
|
||||
return stringStringMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Set;
|
||||
|
||||
// TODO TW:参数校验
|
||||
|
||||
/**
|
||||
* 支付宝的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class AlipayPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* 网关地址 - 线上
|
||||
*/
|
||||
public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do";
|
||||
/**
|
||||
* 网关地址 - 沙箱
|
||||
*/
|
||||
public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
|
||||
|
||||
/**
|
||||
* 公钥类型 - 公钥模式
|
||||
*/
|
||||
public static final Integer MODE_PUBLIC_KEY = 1;
|
||||
/**
|
||||
* 公钥类型 - 证书模式
|
||||
*/
|
||||
public static final Integer MODE_CERTIFICATE = 2;
|
||||
|
||||
/**
|
||||
* 签名算法类型 - RSA
|
||||
*/
|
||||
public static final String SIGN_TYPE_DEFAULT = "RSA2";
|
||||
|
||||
/**
|
||||
* 网关地址
|
||||
* 1. {@link #SERVER_URL_PROD}
|
||||
* 2. {@link #SERVER_URL_SANDBOX}
|
||||
*/
|
||||
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String serverUrl;
|
||||
|
||||
/**
|
||||
* 开放平台上创建的应用的 ID
|
||||
*/
|
||||
@NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 签名算法类型,推荐:RSA2
|
||||
* <p>
|
||||
* {@link #SIGN_TYPE_DEFAULT}
|
||||
*/
|
||||
@NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String signType;
|
||||
|
||||
/**
|
||||
* 公钥类型
|
||||
* 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
|
||||
* 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
|
||||
*/
|
||||
@NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private Integer mode;
|
||||
|
||||
// ========== 公钥模式 ==========
|
||||
/**
|
||||
* 商户私钥
|
||||
*/
|
||||
@NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class})
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 支付宝公钥字符串
|
||||
*/
|
||||
@NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class})
|
||||
private String alipayPublicKey;
|
||||
|
||||
// ========== 证书模式 ==========
|
||||
/**
|
||||
* 指定商户公钥应用证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class})
|
||||
private String appCertContent;
|
||||
/**
|
||||
* 指定支付宝公钥证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class})
|
||||
private String alipayPublicCertContent;
|
||||
/**
|
||||
* 指定根证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
|
||||
private String rootCertContent;
|
||||
|
||||
public interface ModePublicKey {
|
||||
}
|
||||
|
||||
public interface ModeCertificate {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
|
||||
return validator.validate(this,
|
||||
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import com.jojubanking.boot.framework.common.exception.ErrorCode;
|
||||
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.jojubanking.boot.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付宝的 PayCodeMapping 实现类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
|
||||
|
||||
@Override
|
||||
protected ErrorCode apply0(String apiCode, String apiMsg) {
|
||||
if (Objects.equals(apiCode, "10000")) {
|
||||
return GlobalErrorCodeConstants.SUCCESS;
|
||||
}
|
||||
// alipay wap api code 返回为null, 暂时定为-9999
|
||||
if (Objects.equals(apiCode, "-9999")) {
|
||||
return GlobalErrorCodeConstants.SUCCESS;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
/**
|
||||
* 支付宝【PC网站支付】的 PayClient 实现类
|
||||
* 文档:https://opendocs.alipay.com/open/270/105898
|
||||
*
|
||||
* @author XGD
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayPcPayClient extends AbstractAlipayClient {
|
||||
|
||||
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config, new AlipayPayCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<AlipayTradePagePayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 构建 AlipayTradePagePayModel 请求
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
// 构建 AlipayTradePagePayRequest
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setBizModel(model);
|
||||
JSONObject bizContent = new JSONObject();
|
||||
// 参数说明可查看: https://opendocs.alipay.com/open/028r8t?scene=22
|
||||
bizContent.put("out_trade_no", reqDTO.getMerchantOrderId());
|
||||
bizContent.put("total_amount", calculateAmount(reqDTO.getAmount()));
|
||||
bizContent.put("subject", reqDTO.getSubject());
|
||||
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
|
||||
// PC扫码支付的方式:支持前置模式和跳转模式。4: 订单码-可定义宽度的嵌入式二维码
|
||||
bizContent.put("qr_pay_mode", "4");
|
||||
// 自定义二维码宽度
|
||||
bizContent.put("qrcode_width", "150");
|
||||
request.setBizContent(bizContent.toJSONString());
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl("");
|
||||
// 执行请求
|
||||
AlipayTradePagePayResponse response;
|
||||
try {
|
||||
response = client.pageExecute(request);
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败]", JsonUtils.toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
||||
}
|
||||
// 响应为表单格式,前端可嵌入响应的页面或关闭当前支付窗口
|
||||
return PayCommonResult.build(StrUtil.blankToDefault(response.getCode(),"10000") ,response.getMsg(), response, codeMapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付宝【扫码支付】的 PayClient 实现类
|
||||
* 文档:https://opendocs.alipay.com/apis/02890k
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayQrPayClient extends AbstractAlipayClient {
|
||||
|
||||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 构建 AlipayTradePrecreateModel 请求
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
|
||||
// TODO TW:userIp + expireTime
|
||||
// 构建 AlipayTradePrecreateRequest
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
// 执行请求
|
||||
AlipayTradePrecreateResponse response;
|
||||
try {
|
||||
response = client.execute(request);
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
||||
}
|
||||
// TODO TW:sub Code 需要测试下各种失败的情况
|
||||
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
||||
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付宝【手机网站】的 PayClient 实现类
|
||||
* 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayWapPayClient extends AbstractAlipayClient {
|
||||
|
||||
|
||||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 构建 AlipayTradeWapPayModel 请求
|
||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
|
||||
model.setProductCode("QUICK_WAP_PAY"); // TODO TW:这里咋整
|
||||
//TODO TW:这里咋整 jason @芋艿 可以去掉吧,
|
||||
// TODO TW 似乎这里不用传sellerId
|
||||
// https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
||||
//model.setSellerId("2088102147948060");
|
||||
model.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(),"yyyy-MM-dd HH:mm:ss"));
|
||||
// TODO TW:userIp
|
||||
// 构建 AlipayTradeWapPayRequest
|
||||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 执行请求
|
||||
AlipayTradeWapPayResponse response;
|
||||
try {
|
||||
response = client.pageExecute(request);
|
||||
} catch (AlipayApiException e) {
|
||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
||||
}
|
||||
|
||||
// TODO TW:sub Code
|
||||
if(response.isSuccess() && Objects.isNull(response.getCode()) && Objects.nonNull(response.getBody())){
|
||||
//成功alipay wap 成功 code 为 null , body 为form 表单
|
||||
return PayCommonResult.build("-9999", "Success", response, codeMapping);
|
||||
}else {
|
||||
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.wx;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.exception.ErrorCode;
|
||||
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.jojubanking.boot.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 微信支付 PayCodeMapping 实现类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class WXCodeMapping extends AbstractPayCodeMapping {
|
||||
|
||||
/**
|
||||
* 错误码 - 成功
|
||||
* 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下
|
||||
*/
|
||||
public static final String CODE_SUCCESS = "SUCCESS";
|
||||
/**
|
||||
* 错误提示 - 成功
|
||||
*/
|
||||
public static final String MESSAGE_SUCCESS = "成功";
|
||||
|
||||
@Override
|
||||
protected ErrorCode apply0(String apiCode, String apiMsg) {
|
||||
if (Objects.equals(apiCode, CODE_SUCCESS)) {
|
||||
return GlobalErrorCodeConstants.SUCCESS;
|
||||
}
|
||||
if (Objects.equals(apiCode, "FAIL")) {
|
||||
if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) {
|
||||
return PayFrameworkErrorCodeConstants.PAY_CONFIG_APP_ID_ERROR;
|
||||
}
|
||||
if (Objects.equals(apiMsg, "签名错误,请检查后再试")
|
||||
|| Objects.equals(apiMsg, "签名错误")) {
|
||||
return PayFrameworkErrorCodeConstants.PAY_CONFIG_SIGN_ERROR;
|
||||
}
|
||||
}
|
||||
if (Objects.equals(apiCode, "PARAM_ERROR")) {
|
||||
if (Objects.equals(apiMsg, "无效的openid")) {
|
||||
return PayFrameworkErrorCodeConstants.PAY_OPENID_ERROR;
|
||||
}
|
||||
}
|
||||
if (Objects.equals(apiCode, "CustomErrorCode")) {
|
||||
if (StrUtil.contains(apiMsg, "必填字段")) {
|
||||
return PayFrameworkErrorCodeConstants.PAY_PARAM_MISSING;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.wx;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.util.io.FileUtils;
|
||||
import com.jojubanking.boot.framework.common.util.object.ObjectUtils;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
|
||||
/**
|
||||
* 微信小程序下支付
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WXLitePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
|
||||
private WxPayService client;
|
||||
|
||||
public WXLitePayClient(Long channelId, WXPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_LITE.getCode(), config, new WXCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
|
||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
||||
// }
|
||||
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());
|
||||
}
|
||||
// 真实客户端
|
||||
this.client = new WxPayServiceImpl();
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
WxPayMpOrderResult response;
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
response = this.unifiedOrderV2(reqDTO);
|
||||
break;
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
|
||||
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
|
||||
response = new WxPayMpOrderResult();
|
||||
BeanUtil.copyProperties(responseV3, response, true);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
|
||||
}
|
||||
return PayCommonResult.build(WXCodeMapping.CODE_SUCCESS, WXCodeMapping.MESSAGE_SUCCESS, response, codeMapping);
|
||||
}
|
||||
|
||||
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.body(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) // v2的时间格式
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.openid(getOpenid(reqDTO))
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.build();
|
||||
// 执行请求
|
||||
return client.createOrder(request);
|
||||
}
|
||||
|
||||
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(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()
|
||||
.intValue())); // 单位分
|
||||
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")); // v3的时间格式
|
||||
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 执行请求
|
||||
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||
}
|
||||
|
||||
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
||||
if (StrUtil.isEmpty(openid)) {
|
||||
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
|
||||
}
|
||||
return openid;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 微信支付回调 分 v2 和v3 的处理方式
|
||||
*
|
||||
* @param data 通知结果
|
||||
* @return 支付回调对象
|
||||
* @throws WxPayException 微信异常类
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
||||
log.info("[parseOrderNotify][微信支付回调data数据:{}]", data.getBody());
|
||||
// 微信支付 v2 回调结果处理
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
return parseOrderNotifyV2(data);
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
return parseOrderNotifyV3(data);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
||||
// 转换结果
|
||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
||||
"支付结果非 SUCCESS");
|
||||
|
||||
return PayOrderNotifyRespDTO
|
||||
.builder()
|
||||
.orderExtensionNo(result.getOutTradeNo())
|
||||
.channelOrderNo(result.getTradeState())
|
||||
.successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO 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(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
//TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
//TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.wx;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.util.io.FileUtils;
|
||||
import com.jojubanking.boot.framework.common.util.object.ObjectUtils;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信 App 支付
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
|
||||
private WxPayService client;
|
||||
|
||||
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||
payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式
|
||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
||||
// }
|
||||
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());
|
||||
}
|
||||
// 真实客户端
|
||||
this.client = new WxPayServiceImpl();
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 这里原生的返回的是支付的 url 所以直接使用string接收
|
||||
// "invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
|
||||
String responseV3;
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
responseV3 = unifiedOrderV2(reqDTO).getCodeUrl();
|
||||
break;
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
responseV3 = this.unifiedOrderV3(reqDTO);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
|
||||
}
|
||||
return PayCommonResult.build(WXCodeMapping.CODE_SUCCESS, WXCodeMapping.MESSAGE_SUCCESS, responseV3, codeMapping);
|
||||
}
|
||||
|
||||
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
//前端
|
||||
String tradeType = reqDTO.getChannelExtras().get("trade_type");
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
|
||||
.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.body(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.productId(tradeType)
|
||||
.build();
|
||||
// 执行请求
|
||||
return client.createOrder(request);
|
||||
}
|
||||
|
||||
private String unifiedOrderV3(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().intValue())); // 单位分
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 执行请求
|
||||
return client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 微信支付回调 分v2 和v3 的处理方式
|
||||
*
|
||||
* @param data 通知结果
|
||||
* @return 支付回调对象
|
||||
* @throws WxPayException 微信异常类
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
||||
log.info("微信支付回调data数据:{}", data.getBody());
|
||||
// 微信支付 v2 回调结果处理
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
return parseOrderNotifyV2(data);
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
return parseOrderNotifyV3(data);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
||||
// 转换结果
|
||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
||||
"支付结果非 SUCCESS");
|
||||
return PayOrderNotifyRespDTO
|
||||
.builder()
|
||||
.orderExtensionNo(result.getOutTradeNo())
|
||||
.channelOrderNo(result.getTradeState())
|
||||
.successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO 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(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.wx;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 微信支付的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class WXPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* API 版本 - V2
|
||||
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
|
||||
*/
|
||||
public static final String API_VERSION_V2 = "v2";
|
||||
/**
|
||||
* API 版本 - V3
|
||||
* https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
|
||||
*/
|
||||
public static final String API_VERSION_V3 = "v3";
|
||||
|
||||
/**
|
||||
* 公众号或者小程序的 appid
|
||||
*/
|
||||
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
|
||||
private String appId;
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
|
||||
private String mchId;
|
||||
/**
|
||||
* API 版本
|
||||
*/
|
||||
@NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
|
||||
private String apiVersion;
|
||||
|
||||
// ========== V2 版本的参数 ==========
|
||||
|
||||
/**
|
||||
* 商户密钥
|
||||
*/
|
||||
@NotBlank(message = "商户密钥 不能为空", groups = V2.class)
|
||||
private String mchKey;
|
||||
/**
|
||||
* apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
||||
* 对应的字符串
|
||||
*
|
||||
* 注意,可通过 {@link #main(String[])} 读取
|
||||
*/
|
||||
/// private String keyContent;
|
||||
|
||||
// ========== V3 版本的参数 ==========
|
||||
/**
|
||||
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
||||
* 对应的字符串
|
||||
* 注意,可通过 {@link #main(String[])} 读取
|
||||
*/
|
||||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
||||
private String privateKeyContent;
|
||||
/**
|
||||
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
||||
* 对应的字符串
|
||||
* <p>
|
||||
* 注意,可通过 {@link #main(String[])} 读取
|
||||
*/
|
||||
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
|
||||
private String privateCertContent;
|
||||
/**
|
||||
* apiV3 密钥值
|
||||
*/
|
||||
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
|
||||
private String apiV3Key;
|
||||
|
||||
/**
|
||||
* 分组校验 v2版本
|
||||
*/
|
||||
public interface V2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组校验 v3版本
|
||||
*/
|
||||
public interface V3 {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
|
||||
return validator.validate(this, this.getApiVersion().equals(API_VERSION_V2) ? V2.class : V3.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws FileNotFoundException {
|
||||
String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
|
||||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
|
||||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
|
||||
System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.wx;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.util.io.FileUtils;
|
||||
import com.jojubanking.boot.framework.common.util.object.ObjectUtils;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
import static com.jojubanking.boot.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
|
||||
import static com.jojubanking.boot.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
|
||||
|
||||
/**
|
||||
* 微信支付(公众号)的 PayClient 实现类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
|
||||
private WxPayService client;
|
||||
|
||||
public WXPubPayClient(Long channelId, WXPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_PUB.getCode(), config, new WXCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
|
||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
||||
// }
|
||||
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());
|
||||
}
|
||||
// 真实客户端
|
||||
this.client = new WxPayServiceImpl();
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
WxPayMpOrderResult response;
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
response = this.unifiedOrderV2(reqDTO);
|
||||
break;
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
|
||||
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
|
||||
response = new WxPayMpOrderResult();
|
||||
BeanUtil.copyProperties(responseV3, response, true);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping);
|
||||
}
|
||||
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
|
||||
}
|
||||
|
||||
|
||||
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.body(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.openid(getOpenid(reqDTO))
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.build();
|
||||
// 执行请求
|
||||
return client.createOrder(request);
|
||||
}
|
||||
|
||||
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(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().intValue())); // 单位分
|
||||
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"));
|
||||
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 执行请求
|
||||
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||
}
|
||||
|
||||
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
||||
if (StrUtil.isEmpty(openid)) {
|
||||
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
|
||||
}
|
||||
return openid;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 微信支付回调 分v2 和v3 的处理方式
|
||||
*
|
||||
* @param data 通知结果
|
||||
* @return 支付回调对象
|
||||
* @throws WxPayException 微信异常类
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
||||
log.info("[parseOrderNotify][微信支付回调data数据: {}]", data.getBody());
|
||||
// 微信支付 v2 回调结果处理
|
||||
switch (config.getApiVersion()) {
|
||||
case WXPayClientConfig.API_VERSION_V2:
|
||||
return parseOrderNotifyV2(data);
|
||||
case WXPayClientConfig.API_VERSION_V3:
|
||||
return parseOrderNotifyV3(data);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
||||
// 转换结果
|
||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
||||
"支付结果非 SUCCESS");
|
||||
return PayOrderNotifyRespDTO
|
||||
.builder()
|
||||
.orderExtensionNo(result.getOutTradeNo())
|
||||
.channelOrderNo(result.getTradeState())
|
||||
.successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO 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(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.jojubanking.boot.framework.pay.core.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付渠道的编码的枚举
|
||||
* 枚举值
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayChannelEnum {
|
||||
|
||||
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
|
||||
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
|
||||
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
|
||||
WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class),
|
||||
|
||||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class);
|
||||
|
||||
/**
|
||||
* 编码
|
||||
* <p>
|
||||
* 参考 https://www.pingxx.com/api/支付渠道属性值.html
|
||||
*/
|
||||
private final String code;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 配置类
|
||||
*/
|
||||
private final Class<? extends PayClientConfig> configClass;
|
||||
|
||||
/**
|
||||
* 微信支付
|
||||
*/
|
||||
public static final String WECHAT = "WECHAT";
|
||||
|
||||
/**
|
||||
* 支付宝支付
|
||||
*/
|
||||
public static final String ALIPAY = "ALIPAY";
|
||||
|
||||
public static PayChannelEnum getByCode(String code) {
|
||||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.jojubanking.boot.framework.pay.core.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 渠道统一的退款返回结果
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayChannelRefundRespEnum {
|
||||
|
||||
SUCCESS(1, "退款成功"),
|
||||
FAILURE(2, "退款失败"),
|
||||
PROCESSING(3,"退款处理中"),
|
||||
CLOSED(4, "退款关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.jojubanking.boot.framework.pay.core.enums;
|
||||
|
||||
import com.jojubanking.boot.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* 支付框架的错误码枚举
|
||||
*
|
||||
* 短信框架,使用 2-002-000-000 段
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface PayFrameworkErrorCodeConstants {
|
||||
|
||||
ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析");
|
||||
|
||||
// ========== 配置相关相关 2002000100 ==========
|
||||
ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确");
|
||||
ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey
|
||||
|
||||
|
||||
// ========== 其它相关 2002000900 开头 ==========
|
||||
ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过
|
||||
ErrorCode PAY_PARAM_MISSING = new ErrorCode(2002000901, "请求参数缺失"); // 例如说,支付少传了金额
|
||||
|
||||
ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.jojubanking.boot.framework.pay.core.enums;
|
||||
|
||||
/**
|
||||
* 退款通知, 统一的渠道退款状态
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public enum PayNotifyRefundStatusEnum {
|
||||
/**
|
||||
* 支付宝 中 全额退款 trade_status=TRADE_CLOSED, 部分退款 trade_status=TRADE_SUCCESS
|
||||
* 退款成功
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* 支付宝退款通知没有这个状态
|
||||
* 退款异常
|
||||
*/
|
||||
ABNORMAL;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.jojubanking.boot.framework.pay.config.JojuPayAutoConfiguration
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.jojubanking.boot.framework.core.client.impl;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.PayClientFactoryImpl;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.wx.WXPubPayClient;
|
||||
import com.jojubanking.boot.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* {@link PayClientFactoryImpl} 的集成测试
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class PayClientFactoryImplTest {
|
||||
|
||||
private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl();
|
||||
|
||||
/**
|
||||
* {@link WXPubPayClient} 的 V2 版本
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_WX_PUB_V2() {
|
||||
// 创建配置
|
||||
WXPayClientConfig config = new WXPayClientConfig();
|
||||
config.setAppId("wx041349c6f39b268b");
|
||||
config.setMchId("1545083881");
|
||||
config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
|
||||
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link WXPubPayClient} 的 V3 版本
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException {
|
||||
// 创建配置
|
||||
WXPayClientConfig config = new WXPayClientConfig();
|
||||
config.setAppId("wx041349c6f39b268b");
|
||||
config.setMchId("1545083881");
|
||||
config.setApiVersion(WXPayClientConfig.API_VERSION_V3);
|
||||
config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
|
||||
config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
|
||||
config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AlipayQrPayClient}
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testCreatePayClient_ALIPAY_QR() {
|
||||
// 创建配置
|
||||
AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
config.setAppId("2021000118634035");
|
||||
config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
|
||||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
|
||||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
reqDTO.setNotifyUrl("http://niubi.natapp1.cc/api/pay/order/notify/alipay-qr/1"); // TODO @tina: 这里改成你的 natapp 回调地址
|
||||
CommonResult<AlipayTradePrecreateResponse> result = (CommonResult<AlipayTradePrecreateResponse>) client.unifiedOrder(reqDTO);
|
||||
System.out.println(JsonUtils.toJsonString(result));
|
||||
System.out.println(result.getData().getQrCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AlipayWapPayClient}
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_ALIPAY_WAP() {
|
||||
// 创建配置
|
||||
AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
config.setAppId("2021000118634035");
|
||||
config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
|
||||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
|
||||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
System.out.println(JsonUtils.toJsonString(result));
|
||||
}
|
||||
|
||||
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
|
||||
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
|
||||
reqDTO.setAmount(123L);
|
||||
reqDTO.setSubject("IPhone 13");
|
||||
reqDTO.setBody("biubiubiu");
|
||||
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setUserIp("127.0.0.1");
|
||||
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
|
||||
return reqDTO;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.jojubanking.boot.framework.pay.core.client.impl.alipay;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.jojubanking.boot.framework.pay.core.client.PayCommonResult;
|
||||
import com.jojubanking.boot.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import com.jojubanking.boot.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.jojubanking.boot.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static com.jojubanking.boot.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
|
||||
|
||||
private final AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
// .setAppId("2021000118634035")
|
||||
// .setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX)
|
||||
// .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT)
|
||||
// // TODO @tina:key 可以随机就好,简洁一点哈。
|
||||
// .setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJ" +
|
||||
// "v890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T" +
|
||||
// "01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH" +
|
||||
// "6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScw" +
|
||||
// "lSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63tr" +
|
||||
// "epo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdk" +
|
||||
// "USmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr" +
|
||||
// "8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w" +
|
||||
// "0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENi" +
|
||||
// "vAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPw" +
|
||||
// "YcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQO" +
|
||||
// "LFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsm" +
|
||||
// "yX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i9" +
|
||||
// "5Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOU" +
|
||||
// "hVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD" +
|
||||
// "/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v1" +
|
||||
// "8p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ" +
|
||||
// "8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4e" +
|
||||
// "N0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6p" +
|
||||
// "bKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erx" +
|
||||
// "TRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=")
|
||||
// .setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0" +
|
||||
// "gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBN" +
|
||||
// "lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" +
|
||||
// "ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
|
||||
|
||||
// TODO @tina:= 前后要有空格哈
|
||||
@InjectMocks
|
||||
AlipayQrPayClient client=new AlipayQrPayClient(10L,config);
|
||||
|
||||
@Mock
|
||||
private DefaultAlipayClient defaultAlipayClient;
|
||||
|
||||
@Test
|
||||
public void testDoInit(){
|
||||
client.doInit();
|
||||
assertNotSame(defaultAlipayClient, ReflectUtil.getFieldValue(client, "defaultAlipayClient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO TW:临时禁用
|
||||
public void create() throws AlipayApiException {
|
||||
// TODO @tina:参数可以尽量随机一点,使用随机方法。这样的好处是,避免对固定参数的依赖,导致可能仅仅满足固定参数的结果
|
||||
// 这里,设置可以直接随机整个对象。
|
||||
Long shopOrderId = System.currentTimeMillis();
|
||||
PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
|
||||
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setAmount(1L);
|
||||
reqDTO.setBody("内容:" + shopOrderId);
|
||||
reqDTO.setSubject("标题:"+shopOrderId);
|
||||
String notify="http://niubi.natapp1.cc/api/pay/order/notify";
|
||||
reqDTO.setNotifyUrl(notify);
|
||||
|
||||
AlipayTradePrecreateResponse response=randomPojo(AlipayTradePrecreateResponse.class,o->o.setQrCode("success"));
|
||||
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request ->{
|
||||
assertEquals(notify,request.getNotifyUrl());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
|
||||
|
||||
PayCommonResult<AlipayTradePrecreateResponse> result = client.doUnifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(response.getCode(), result.getApiCode());
|
||||
assertEquals(response.getMsg(), result.getApiMsg());
|
||||
// TODO @tina:这个断言木有过?
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user