第一次提交
This commit is contained in:
78
fastbee-pay/fastbee-pay-framework/pom.xml
Normal file
78
fastbee-pay/fastbee-pay-framework/pom.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.fastbee.pay</groupId>
|
||||
<artifactId>fastbee-pay</artifactId>
|
||||
<version>3.8.5</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.fastbee.pay.framework</groupId>
|
||||
<artifactId>fastbee-pay-framework</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.projectlombok</groupId>-->
|
||||
<!-- <artifactId>lombok</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<version>2.7.12</version>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>cn.hutool</groupId>-->
|
||||
<!-- <artifactId>hutool-all</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.fastbee</groupId>-->
|
||||
<!-- <artifactId>fastbee-common</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.79.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,80 @@
|
||||
package com.fastbee.pay.framework.client;
|
||||
|
||||
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
public interface PayClient {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
*
|
||||
* @return 渠道编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析 order 回调数据
|
||||
*
|
||||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
* @param body HTTP 回调接口的 request body
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
|
||||
|
||||
/**
|
||||
* 获得支付订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO getOrder(String outTradeNo);
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
/**
|
||||
* 调用支付渠道,进行退款
|
||||
*
|
||||
* @param reqDTO 统一退款请求信息
|
||||
* @return 退款信息
|
||||
*/
|
||||
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析 refund 回调数据
|
||||
*
|
||||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
* @param body HTTP 回调接口的 request body
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
|
||||
|
||||
/**
|
||||
* 获得退款订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @param outRefundNo 外部退款号
|
||||
* @return 退款订单信息
|
||||
*/
|
||||
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.fastbee.pay.framework.client;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
/**
|
||||
* 支付客户端的配置,本质是支付渠道的配置
|
||||
* 每个不同的渠道,需要不同的配置,通过子类来定义
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
// @JsonTypeInfo 注解的作用,Jackson 多态
|
||||
// 1. 序列化到时数据库时,增加 @class 属性。
|
||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
||||
public interface PayClientConfig {
|
||||
|
||||
/**
|
||||
* 参数校验
|
||||
*
|
||||
* @param validator 校验对象
|
||||
*/
|
||||
void validate(Validator validator);
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.fastbee.pay.framework.client;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂接口
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
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,139 @@
|
||||
package com.fastbee.pay.framework.client.dto.order;
|
||||
|
||||
import com.fastbee.pay.framework.client.exception.PayException;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderStatusRespEnum;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 渠道支付订单 Response DTO
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayOrderRespDTO {
|
||||
|
||||
/**
|
||||
* 支付状态
|
||||
* 枚举:{@link PayOrderStatusRespEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
* 对应 PayOrderExtension 的 no 字段
|
||||
*/
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
/**
|
||||
* 支付渠道用户编号
|
||||
*/
|
||||
private String channelUserId;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的同步/异步通知结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
// ========== 主动发起支付时,会返回的字段 ==========
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
* 枚举 {@link PayOrderDisplayModeEnum} 类
|
||||
*/
|
||||
private String displayMode;
|
||||
/**
|
||||
* 展示内容
|
||||
*/
|
||||
private String displayContent;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||
* 如果是系统异常,则会抛出 {@link PayException}
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
public PayOrderRespDTO() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的订单返回
|
||||
*/
|
||||
public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.displayMode = displayMode;
|
||||
respDTO.displayContent = displayContent;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的订单返回
|
||||
*/
|
||||
public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelOrderNo = channelOrderNo;
|
||||
respDTO.channelUserId = channelUserId;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定状态的订单返回,适合支付渠道回调时
|
||||
*/
|
||||
public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = status;
|
||||
respDTO.channelOrderNo = channelOrderNo;
|
||||
respDTO.channelUserId = channelUserId;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
|
||||
*/
|
||||
public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package com.fastbee.pay.framework.client.dto.order;
|
||||
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 统一下单 Request DTO
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayOrderUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
// ========== 商户相关字段 ==========
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
* 对应 PayOrderExtension 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
@NotEmpty(message = "商品标题不能为空")
|
||||
@Length(max = 32, message = "商品标题不能超过 32")
|
||||
private String subject;
|
||||
/**
|
||||
* 商品描述信息
|
||||
*/
|
||||
@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 Integer price;
|
||||
|
||||
/**
|
||||
* 支付过期时间
|
||||
*/
|
||||
@NotNull(message = "支付过期时间不能为空")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
// ========== 拓展参数 ==========
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
* 例如说,微信公众号需要传递 openid 参数
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
* 如果不传递,则每个支付渠道使用默认的方式
|
||||
* 枚举 {@link PayOrderDisplayModeEnum}
|
||||
*/
|
||||
private String displayMode;
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package com.fastbee.pay.framework.client.dto.refund;
|
||||
|
||||
import com.fastbee.pay.framework.client.exception.PayException;
|
||||
import com.fastbee.pay.framework.enums.refund.PayRefundStatusRespEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 渠道退款订单 Response DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayRefundRespDTO {
|
||||
|
||||
/**
|
||||
* 退款状态
|
||||
*
|
||||
* 枚举 {@link PayRefundStatusRespEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部退款号
|
||||
*
|
||||
* 对应 PayRefund 的 no 字段
|
||||
*/
|
||||
private String outRefundNo;
|
||||
|
||||
/**
|
||||
* 渠道退款单号
|
||||
*
|
||||
* 对应 PayRefund.channelRefundNo 字段
|
||||
*/
|
||||
private String channelRefundNo;
|
||||
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的异步通知结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*
|
||||
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||
* 如果是系统异常,则会抛出 {@link PayException}
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
private PayRefundRespDTO() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO waitingOf(String channelRefundNo,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelRefundNo = channelRefundNo;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelRefundNo = channelRefundNo;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【FAILURE】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
|
||||
return failureOf(null, null,
|
||||
outRefundNo, rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【FAILURE】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.fastbee.pay.framework.client.dto.refund;
|
||||
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
* 对应 PayOrderExtension 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 外部退款号
|
||||
* 对应 PayRefund 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "退款请求单号不能为空")
|
||||
private String outRefundNo;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
@NotEmpty(message = "退款原因不能为空")
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 支付金额,单位:分
|
||||
* 目前微信支付在退款的时候,必须传递该字段
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer payPrice;
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款结果的 notify 回调地址
|
||||
*/
|
||||
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.fastbee.pay.framework.client.exception;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 支付系统异常 Exception
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayException extends RuntimeException {
|
||||
|
||||
public PayException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package com.fastbee.pay.framework.client.impl;
|
||||
|
||||
import com.fastbee.common.exception.ServiceException;
|
||||
import com.fastbee.common.utils.ValidationUtils;
|
||||
import com.fastbee.pay.framework.client.PayClient;
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.exception.PayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
|
||||
|
||||
|
||||
/**
|
||||
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
|
||||
|
||||
/**
|
||||
* 渠道编号
|
||||
*/
|
||||
private final Long channelId;
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final String channelCode;
|
||||
/**
|
||||
* 支付配置
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
public AbstractPayClient(Long channelId, String channelCode, Config config) {
|
||||
this.channelId = channelId;
|
||||
this.channelCode = channelCode;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][客户端({}) 初始化完成]", getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
public final void refresh(Config config) {
|
||||
// 判断是否更新
|
||||
if (config.equals(this.config)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
// 执行统一下单
|
||||
PayOrderRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedOrder(reqDTO);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
|
||||
try {
|
||||
return doParseOrderNotify(params, body);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||
getId(), params, body, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO getOrder(String outTradeNo) {
|
||||
try {
|
||||
return doGetOrder(outTradeNo);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
|
||||
getId(), outTradeNo, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
|
||||
throws Throwable;
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
// 执行统一退款
|
||||
PayRefundRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedRefund(reqDTO);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
|
||||
try {
|
||||
return doParseRefundNotify(params, body);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||
getId(), params, body, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
|
||||
try {
|
||||
return doGetRefund(outTradeNo, outRefundNo);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
|
||||
getId(), outTradeNo, outRefundNo, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
|
||||
throws Throwable;
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
private PayException buildPayException(Throwable ex) {
|
||||
if (ex instanceof PayException) {
|
||||
return (PayException) ex;
|
||||
}
|
||||
throw new PayException(ex);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package com.fastbee.pay.framework.client.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.fastbee.pay.framework.client.PayClient;
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import com.fastbee.pay.framework.client.PayClientFactory;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayAppPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayBarPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayPcPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayQrPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayWapPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.mock.MockPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.mock.MockPayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxAppPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxBarPayClient;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxLitePayClient;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxNativePayClient;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxPayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxPubPayClient;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂实现类
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@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));
|
||||
// 创建客户端
|
||||
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 WxAppPayClient(channelId, (WxPayClientConfig) config);
|
||||
case WX_BAR: return (AbstractPayClient<Config>) new WxBarPayClient(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 AlipayAppPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
case ALIPAY_BAR: return (AbstractPayClient<Config>) new AlipayBarPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
// 其它支付
|
||||
case MOCK: return (AbstractPayClient<Config>) new MockPayClient(channelId, (MockPayClientConfig) config);
|
||||
}
|
||||
// 创建失败,错误日志 + 抛出异常
|
||||
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
|
||||
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.AlipayResponse;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel;
|
||||
import com.alipay.api.domain.AlipayTradeQueryModel;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
|
||||
import com.alipay.api.request.AlipayTradeQueryRequest;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
|
||||
import com.alipay.api.response.AlipayTradeQueryResponse;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import com.fastbee.common.utils.object.ObjectUtils;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.impl.AbstractPayClient;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderStatusRespEnum;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
|
||||
protected DefaultAlipayClient client;
|
||||
|
||||
public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
|
||||
*
|
||||
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
|
||||
*/
|
||||
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
|
||||
Assert.isFalse(response.isSuccess());
|
||||
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
|
||||
// 1. 校验回调数据
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
|
||||
StandardCharsets.UTF_8.name(), config.getSignType());
|
||||
|
||||
// 2. 解析订单的状态
|
||||
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
|
||||
Integer status = parseStatus(bodyObj.get("trade_status"));
|
||||
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
|
||||
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
|
||||
status = PayOrderStatusRespEnum.REFUND.getStatus();
|
||||
}
|
||||
Assert.notNull(status, (Supplier<Throwable>) () -> {
|
||||
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
|
||||
});
|
||||
return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
|
||||
bodyObj.get("out_trade_no"), body);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
|
||||
model.setOutTradeNo(outTradeNo);
|
||||
// 1.2 构建 AlipayTradeQueryRequest 请求
|
||||
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeQueryResponse response = client.execute(request);
|
||||
if (!response.isSuccess()) { // 不成功,例如说订单不存在
|
||||
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
outTradeNo, response);
|
||||
}
|
||||
// 2.2 解析订单的状态
|
||||
Integer status = parseStatus(response.getTradeStatus());
|
||||
Assert.notNull(status, (Supplier<Throwable>) () -> {
|
||||
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
|
||||
});
|
||||
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private static Integer parseStatus(String tradeStatus) {
|
||||
return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
|
||||
: ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
|
||||
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
|
||||
}
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
/**
|
||||
* 支付宝统一的退款接口 alipay.trade.refund
|
||||
*
|
||||
* @param reqDTO 退款请求 request DTO
|
||||
* @return 退款请求 Response
|
||||
*/
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getOutRefundNo());
|
||||
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
// 1.2 构建 AlipayTradePayRequest 请求
|
||||
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeRefundResponse response = client.execute(request);
|
||||
if (!response.isSuccess()) {
|
||||
return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
// 2.2 创建返回结果
|
||||
// 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
|
||||
// 另外,支付宝没有退款单号,所以不用设置
|
||||
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
|
||||
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
|
||||
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
|
||||
// ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
|
||||
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
|
||||
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
|
||||
throw new UnsupportedOperationException("支付宝无退款回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
|
||||
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
|
||||
model.setOutTradeNo(outTradeNo);
|
||||
model.setOutRequestNo(outRefundNo);
|
||||
model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
|
||||
// 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
|
||||
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeFastpayRefundQueryResponse response = client.execute(request);
|
||||
if (!response.isSuccess()) {
|
||||
// 明确不存在的情况,应该就是失败,可进行关闭
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
}
|
||||
// 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
|
||||
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||
}
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
|
||||
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||
outRefundNo, response);
|
||||
}
|
||||
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
protected String formatAmount(Integer amount) {
|
||||
return String.valueOf(amount / 100.0);
|
||||
}
|
||||
|
||||
protected String formatTime(LocalDateTime time) {
|
||||
return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
protected LocalDateTime parseTime(String str) {
|
||||
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradeAppPayModel;
|
||||
import com.alipay.api.request.AlipayTradeAppPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeAppPayResponse;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 支付宝【App 支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
|
||||
*
|
||||
* // TODO fastbee:未详细测试,因为手头没 App
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeAppPayModel 请求
|
||||
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody() + "test");
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||
model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示
|
||||
String displayMode = PayOrderDisplayModeEnum.APP.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeAppPayResponse response = client.sdkExecute(request);
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePayModel;
|
||||
import com.alipay.api.request.AlipayTradePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePayResponse;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.fastbee.common.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static com.fastbee.common.exception.ServiceExceptionUtil.exception0;
|
||||
|
||||
|
||||
/**
|
||||
* 支付宝【条码支付】的 PayClient 实现类
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
|
||||
if (StrUtil.isEmpty(authCode)) {
|
||||
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
|
||||
}
|
||||
|
||||
// 1.1 构建 AlipayTradePayModel 请求
|
||||
AlipayTradePayModel model = new AlipayTradePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setScene("bar_code"); // 当面付条码支付场景
|
||||
// ② 个性化的参数
|
||||
model.setAuthCode(authCode);
|
||||
// ③ 支付宝条码支付只有一种展示
|
||||
String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePayRequest 请求
|
||||
AlipayTradePayRequest request = new AlipayTradePayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePayResponse response = client.execute(request);
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
if ("10000".equals(response.getCode())) { // 免密支付
|
||||
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getGmtPayment()),
|
||||
response.getOutTradeNo(), response);
|
||||
}
|
||||
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
|
||||
return PayOrderRespDTO.waitingOf(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
|
||||
import com.fastbee.common.utils.ValidationUtils;
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 支付宝的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Data
|
||||
public class AlipayPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* 公钥类型 - 公钥模式
|
||||
*/
|
||||
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. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
|
||||
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
|
||||
*/
|
||||
@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 void validate(Validator validator) {
|
||||
ValidationUtils.validate(validator, this,
|
||||
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.Method;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付宝【PC 网站】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
|
||||
*
|
||||
* @author XGD
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePagePayModel 请求
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
|
||||
// ② 个性化的参数
|
||||
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
|
||||
model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
|
||||
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
|
||||
String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
|
||||
PayOrderDisplayModeEnum.URL.getMode());
|
||||
|
||||
// 1.2 构建 AlipayTradePagePayRequest 请求
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePagePayResponse response;
|
||||
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
|
||||
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
|
||||
} else {
|
||||
response = client.pageExecute(request, Method.GET.name());
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 支付宝【扫码支付】的 PayClient 实现类
|
||||
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePrecreateModel 请求
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
|
||||
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePrecreateResponse response = client.execute(request);
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.fastbee.pay.framework.client.impl.alipay;
|
||||
|
||||
import cn.hutool.http.Method;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
||||
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 支付宝【Wap 网站】的 PayClient 实现类
|
||||
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeWapPayModel 请求
|
||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝 Wap 支付只有一种展示:URL
|
||||
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradeWapPayRequest 请求
|
||||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
model.setQuitUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.fastbee.pay.framework.client.impl.mock;
|
||||
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.impl.AbstractPayClient;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 模拟支付的 PayClient 实现类
|
||||
* 模拟支付返回结果都是成功,方便大家日常流畅
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class MockPayClient extends AbstractPayClient<MockPayClientConfig> {
|
||||
|
||||
private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS";
|
||||
|
||||
public MockPayClient(Long channelId, MockPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.MOCK.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(),
|
||||
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) {
|
||||
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(),
|
||||
outTradeNo, MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
|
||||
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) {
|
||||
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(),
|
||||
outRefundNo, MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
|
||||
throw new UnsupportedOperationException("模拟支付无退款回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
|
||||
throw new UnsupportedOperationException("模拟支付无支付回调");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.fastbee.pay.framework.client.impl.mock;
|
||||
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
/**
|
||||
* 模拟支付的 PayClientConfig 实现类
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class MockPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* 配置名称
|
||||
*
|
||||
* 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public void validate(Validator validator) {
|
||||
// 模拟支付配置无需校验
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,484 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fastbee.common.utils.file.FileUtils;
|
||||
import com.fastbee.common.utils.object.ObjectUtils;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.client.impl.AbstractPayClient;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderStatusRespEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundQueryRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundQueryV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryResult;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
|
||||
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;
|
||||
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
|
||||
import static com.fastbee.pay.framework.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
|
||||
|
||||
/**
|
||||
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
|
||||
*
|
||||
* @author 遇到源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {
|
||||
|
||||
protected WxPayService client;
|
||||
|
||||
public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 client 客户端
|
||||
*
|
||||
* @param tradeType 交易类型
|
||||
*/
|
||||
protected void doInit(String tradeType) {
|
||||
// 创建 config 配置
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||
payConfig.setTradeType(tradeType);
|
||||
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
|
||||
if (Base64.isBase64(config.getKeyContent())) {
|
||||
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
|
||||
}
|
||||
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||
}
|
||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
||||
}
|
||||
|
||||
// 创建 client 客户端
|
||||
client = new WxPayServiceImpl();
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doUnifiedOrderV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【V2】调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* 【V3】调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws WxPayException;
|
||||
|
||||
/**
|
||||
* 【V2】创建微信下单请求
|
||||
*
|
||||
* @param reqDTO 下信息
|
||||
* @return 下单请求
|
||||
*/
|
||||
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
|
||||
return WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 【V3】创建微信下单请求
|
||||
*
|
||||
* @param reqDTO 下信息
|
||||
* @return 下单请求
|
||||
*/
|
||||
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
request.setDescription(reqDTO.getSubject());
|
||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
|
||||
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doParseOrderNotifyV2(body);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doParseOrderNotifyV3(body);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
|
||||
// 2. 构建结果
|
||||
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
|
||||
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
|
||||
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
response.getOutTradeNo(), body);
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
|
||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||
// 2. 构建结果
|
||||
Integer status = parseStatus(result.getTradeState());
|
||||
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
|
||||
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
|
||||
result.getOutTradeNo(), body);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doGetOrderV2(outTradeNo);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doGetOrderV3(outTradeNo);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||
outTradeNo, e.getXmlString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
|
||||
.outTradeNo(outTradeNo).build();
|
||||
// 执行请求
|
||||
WxPayOrderQueryResult response = client.queryOrder(request);
|
||||
|
||||
// 转换结果
|
||||
Integer status = parseStatus(response.getTradeState());
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
|
||||
.setOutTradeNo(outTradeNo);
|
||||
// 执行请求
|
||||
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
|
||||
|
||||
// 转换结果
|
||||
Integer status = parseStatus(response.getTradeState());
|
||||
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private static Integer parseStatus(String tradeState) {
|
||||
switch (tradeState) {
|
||||
case "NOTPAY":
|
||||
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
|
||||
return PayOrderStatusRespEnum.WAITING.getStatus();
|
||||
case "SUCCESS":
|
||||
return PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||
case "REFUND":
|
||||
return PayOrderStatusRespEnum.REFUND.getStatus();
|
||||
case "CLOSED":
|
||||
case "REVOKED": // 已撤销(刷卡支付独有)
|
||||
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
|
||||
return PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
default:
|
||||
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedRefundV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doUnifiedRefundV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundRequest request = new WxPayRefundRequest()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setRefundFee(reqDTO.getRefundPrice())
|
||||
.setRefundDesc(reqDTO.getReason())
|
||||
.setTotalFee(reqDTO.getPayPrice())
|
||||
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 2.1 执行请求
|
||||
WxPayRefundResult response = client.refundV2(request);
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundV3Request request = new WxPayRefundV3Request()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
|
||||
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
|
||||
.setReason(reqDTO.getReason())
|
||||
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 2.1 执行请求
|
||||
WxPayRefundV3Result response = client.refundV3(request);
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals("SUCCESS", response.getStatus())) {
|
||||
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
if (Objects.equals("PROCESSING", response.getStatus())) {
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doParseRefundNotifyV2(body);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return parseRefundNotifyV3(body);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
|
||||
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
|
||||
// 2. 构建结果
|
||||
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
|
||||
result.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
|
||||
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||
// 2. 构建结果
|
||||
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
|
||||
result.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doGetRefundV2(outTradeNo, outRefundNo);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doGetRefundV3(outTradeNo, outRefundNo);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||
outRefundNo, e.getXmlString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
|
||||
.outTradeNo(outTradeNo)
|
||||
.outRefundNo(outRefundNo)
|
||||
.build();
|
||||
// 2.1 执行请求
|
||||
WxPayRefundQueryResult response = client.refundQuery(request);
|
||||
// 2.2 创建返回结果
|
||||
if (!Objects.equals("SUCCESS", response.getResultCode())) {
|
||||
return PayRefundRespDTO.waitingOf(null,
|
||||
outRefundNo, response);
|
||||
}
|
||||
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
|
||||
record -> record.getOutRefundNo().equals(outRefundNo));
|
||||
if (refund == null) {
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
}
|
||||
switch (refund.getRefundStatus()) {
|
||||
case "SUCCESS":
|
||||
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
|
||||
outRefundNo, response);
|
||||
case "PROCESSING":
|
||||
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
|
||||
outRefundNo, response);
|
||||
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
|
||||
case "FAIL":
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
|
||||
request.setOutRefundNo(outRefundNo);
|
||||
// 2.1 执行请求
|
||||
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
|
||||
// 2.2 创建返回结果
|
||||
switch (response.getStatus()) {
|
||||
case "SUCCESS":
|
||||
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||
outRefundNo, response);
|
||||
case "PROCESSING":
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
outRefundNo, response);
|
||||
case "ABNORMAL": // 退款异常
|
||||
case "CLOSED":
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String formatDateV2(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV2(String time) {
|
||||
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV2B(String time) {
|
||||
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static String formatDateV3(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV3(String time) {
|
||||
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
static String getErrorCode(WxPayException e) {
|
||||
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||
return e.getErrCode();
|
||||
}
|
||||
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||
return "CUSTOM_ERROR";
|
||||
}
|
||||
return e.getReturnCode();
|
||||
}
|
||||
|
||||
static String getErrorMessage(WxPayException e) {
|
||||
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||
return e.getErrCodeDes();
|
||||
}
|
||||
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||
return e.getCustomErrorMsg();
|
||||
}
|
||||
return e.getReturnMsg();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
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.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
|
||||
|
||||
|
||||
/**
|
||||
* 微信支付【App 支付】的 PayClient 实现类
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
|
||||
* // TODO fastbee:未详细测试,因为手头没 App
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxAppPayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxAppPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_APP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.APP);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||
// 执行请求
|
||||
WxPayMpOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||
// 执行请求
|
||||
WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fastbee.common.utils.date.LocalDateTimeUtils;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.fastbee.common.exception.ServiceExceptionUtil.invalidParamException;
|
||||
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
|
||||
|
||||
|
||||
/**
|
||||
* 微信支付【付款码支付】的 PayClient 实现类
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxBarPayClient extends AbstractWxPayClient {
|
||||
|
||||
/**
|
||||
* 微信付款码的过期时间
|
||||
*/
|
||||
private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
|
||||
|
||||
public WxBarPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.MICROPAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
|
||||
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
|
||||
if (expireTime.isAfter(reqDTO.getExpireTime())) {
|
||||
expireTime = reqDTO.getExpireTime();
|
||||
}
|
||||
// 构建 WxPayMicropayRequest 对象
|
||||
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(expireTime))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.authCode(getAuthCode(reqDTO))
|
||||
.build();
|
||||
// 执行请求,重试直到失败(过期),或者成功
|
||||
WxPayException lastWxPayException = null;
|
||||
for (int i = 1; i < Byte.MAX_VALUE; i++) {
|
||||
try {
|
||||
WxPayMicropayResult response = client.micropay(request);
|
||||
// 支付成功,例如说:1)用户输入了密码;2)用户免密支付
|
||||
return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
response.getOutTradeNo(), response).setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
|
||||
} catch (WxPayException ex) {
|
||||
lastWxPayException = ex;
|
||||
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
|
||||
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
|
||||
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||
// 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||
if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
|
||||
throw ex;
|
||||
}
|
||||
// 等待 5 秒,继续下一轮重新发起支付
|
||||
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
|
||||
toJsonString(request), ex.getMessage());
|
||||
ThreadUtil.sleep(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
throw lastWxPayException;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
|
||||
if (StrUtil.isEmpty(authCode)) {
|
||||
throw invalidParamException("支付请求的 authCode 不能为空!");
|
||||
}
|
||||
return authCode;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 微信支付【小程序】的 PayClient 实现类
|
||||
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxLitePayClient extends WxPubPayClient {
|
||||
|
||||
public WxLitePayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
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.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 微信支付【Native 二维码】的 PayClient 实现类
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxNativePayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxNativePayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.NATIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||
// 执行请求
|
||||
WxPayNativeOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||
// 执行请求
|
||||
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.fastbee.common.utils.ValidationUtils;
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 微信支付的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Data
|
||||
public class WxPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* API 版本 - V2
|
||||
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
|
||||
*/
|
||||
public static final String API_VERSION_V2 = "v2";
|
||||
/**
|
||||
* API 版本 - V3
|
||||
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
|
||||
*/
|
||||
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 证书文件的对应字符串【base64 格式】
|
||||
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
|
||||
*/
|
||||
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
|
||||
private String keyContent;
|
||||
|
||||
// ========== V3 版本的参数 ==========
|
||||
/**
|
||||
* apiclient_key.pem 证书文件的对应字符串
|
||||
*/
|
||||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
||||
private String privateKeyContent;
|
||||
/**
|
||||
* apiclient_cert.pem 证书文件的对应的字符串
|
||||
*/
|
||||
@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 void validate(Validator validator) {
|
||||
ValidationUtils.validate(validator, this,
|
||||
API_VERSION_V2.equals(this.getApiVersion()) ? 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,80 @@
|
||||
package com.fastbee.pay.framework.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
|
||||
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
|
||||
import com.fastbee.pay.framework.enums.order.PayOrderDisplayModeEnum;
|
||||
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.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.fastbee.common.exception.ServiceExceptionUtil.invalidParamException;
|
||||
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
|
||||
|
||||
|
||||
/**
|
||||
* 微信支付(公众号)的 PayClient 实现类
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxPubPayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
}
|
||||
|
||||
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.JSAPI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
|
||||
.setOpenid(getOpenid(reqDTO));
|
||||
// 执行请求
|
||||
WxPayMpOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
|
||||
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||
// 执行请求
|
||||
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
||||
if (StrUtil.isEmpty(openid)) {
|
||||
throw invalidParamException("支付请求的 openid 不能为空!");
|
||||
}
|
||||
return openid;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.fastbee.pay.framework.config;
|
||||
|
||||
import com.fastbee.pay.framework.client.PayClientFactory;
|
||||
import com.fastbee.pay.framework.client.impl.PayClientFactoryImpl;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 支付配置类
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class PayAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public PayClientFactory payClientFactory() {
|
||||
return new PayClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.fastbee.pay.framework.enums.channel;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.fastbee.pay.framework.client.PayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.alipay.AlipayPayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.mock.MockPayClientConfig;
|
||||
import com.fastbee.pay.framework.client.impl.weixin.WxPayClientConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付渠道的编码的枚举
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@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),
|
||||
WX_BAR("wx_bar", "微信付款码支付", 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),
|
||||
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
|
||||
|
||||
MOCK("mock", "模拟支付", MockPayClientConfig.class);
|
||||
|
||||
/**
|
||||
* 编码
|
||||
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
|
||||
*/
|
||||
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,29 @@
|
||||
package com.fastbee.pay.framework.enums.order;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付 UI 展示模式
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayOrderDisplayModeEnum {
|
||||
|
||||
URL("url"), // Redirect 跳转链接的方式
|
||||
IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
|
||||
FORM("form"), // HTML 表单提交
|
||||
QR_CODE("qr_code"), // 二维码的文字内容
|
||||
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
|
||||
BAR_CODE("bar_code"), // 条形码
|
||||
APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
|
||||
;
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
*/
|
||||
private final String mode;
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.fastbee.pay.framework.enums.order;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的支付状态枚举
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayOrderStatusRespEnum {
|
||||
|
||||
WAITING(0, "未支付"),
|
||||
SUCCESS(10, "支付成功"),
|
||||
REFUND(20, "已退款"),
|
||||
CLOSED(30, "支付关闭"),
|
||||
;
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 判断是否支付成功
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否已退款
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
public static boolean isRefund(Integer status) {
|
||||
return Objects.equals(status, REFUND.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否支付关闭
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付关闭
|
||||
*/
|
||||
public static boolean isClosed(Integer status) {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.fastbee.pay.framework.enums.refund;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的退款状态枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayRefundStatusRespEnum {
|
||||
|
||||
WAITING(0, "等待退款"),
|
||||
SUCCESS(10, "退款成功"),
|
||||
FAILURE(20, "退款失败");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isFailure(Integer status) {
|
||||
return Objects.equals(status, FAILURE.getStatus());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user