feat SDK编写

This commit is contained in:
bootx
2024-02-02 21:33:46 +08:00
parent dd1d70dcfb
commit e2ab1ea2f0
57 changed files with 1302 additions and 44 deletions

View File

@@ -99,10 +99,11 @@
- [x] 支付和退款同步时, 填充完成时间和网关订单号
- 2024-02-01:
- [x] 支付切换异步方式时, 订单未更换
- [ ] 支付同步时, 支付宝未支付订单无法自动关闭
- [ ] 调整订单页面查询条件
-
- [x] 支付同步时, 支付宝未支付订单无法自动关闭
- [x] 调整订单页面查询条件
- 2024-02-02:
- [ ] 接入SDK编写
- [ ] 接入支付网关的演示项目
- 2.0.1 版本内容
- [ ] 退款操作支持重试
- [ ] 支付流程也改为先落库后支付情况, 避免极端情况导致掉单

View File

@@ -4,7 +4,7 @@
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>
<groupId>cn.bootx.cloud</groupId>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-sdk</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
@@ -13,13 +13,34 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<logback-classic.version>1.2.7</logback-classic.version>
<hutool.version>5.8.25</hutool.version>
<lombok.version>1.18.30</lombok.version>
<junit.version>4.13.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,41 @@
package cn.bootx.platform.daxpay.sdk.code;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
* 支付通道枚举
*
* @author xxm
* @since 2021/7/26
*/
@Getter
@RequiredArgsConstructor
public enum PayChannelEnum {
ALI("ali_pay", "支付宝"),
WECHAT("wechat_pay", "微信支付"),
UNION_PAY("union_pay", "云闪付"),
CASH("cash_pay", "现金支付"),
WALLET("wallet_pay", "钱包支付"),
VOUCHER("voucher_pay", "储值卡支付");
/** 支付通道编码 */
private final String code;
/** 支付通道名称 */
private final String name;
/**
* 根据编码获取枚举
*/
public static PayChannelEnum findByCode(String code){
return Arrays.stream(values())
.filter(o -> Objects.equals(o.getCode(), code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("该枚举不存在"));
}
}

View File

@@ -0,0 +1,41 @@
package cn.bootx.platform.daxpay.sdk.code;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
* 支付状态
* @author xxm
* @since 2023/12/17
*/
@Getter
@RequiredArgsConstructor
public enum PayStatusEnum {
PROGRESS("progress","支付中"),
SUCCESS("success","成功"),
CLOSE("close","支付关闭"),
REFUNDING("refunding","退款中"),
PARTIAL_REFUND("partial_refund","部分退款"),
REFUNDED("refunded","全部退款"),
FAIL("fail","失败");
/** 编码 */
private final String code;
/** 名称 */
private final String name;
/**
* 根据编码获取枚举
*/
public static PayStatusEnum findByCode(String code){
return Arrays.stream(values())
.filter(o -> Objects.equals(o.getCode(), code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("该枚举不存在"));
}
}

View File

@@ -0,0 +1,40 @@
package cn.bootx.platform.daxpay.sdk.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 支付方式
*
* @author xxm
* @since 2021/7/26
*/
@Getter
@AllArgsConstructor
public enum PayWayEnum {
NORMAL("normal", "常规支付"),
WAP("wap", "wap支付"),
APP("app", "应用支付"),
WEB("web", "web支付"),
QRCODE("qrcode", "扫码支付"),
BARCODE("barcode", "付款码"),
JSAPI("jsapi", "公众号/小程序支付");
/** 编码 */
private final String code;
/** 名称 */
private final String name;
/**
* 根据编码获取枚举
*/
public static PayWayEnum findByCode(String code){
return Arrays.stream(values())
.filter(o -> Objects.equals(o.getCode(), code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("该枚举不存在"));
}
}

View File

@@ -0,0 +1,40 @@
package cn.bootx.platform.daxpay.sdk.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 退款状态枚举
* @author xxm
* @since 2023/12/18
*/
@Getter
@AllArgsConstructor
public enum RefundStatusEnum {
/**
* 接口调用成功不代表成功
*/
PROGRESS("progress","退款中"),
SUCCESS("success","成功"),
CLOSE("close","关闭"),
FAIL("fail","失败");
/** 编码 */
private final String code;
/** 名称 */
private final String name;
/**
* 根据编码获取枚举
*/
public static RefundStatusEnum findByCode(String code){
return Arrays.stream(values())
.filter(statusEnum -> Objects.equals(statusEnum.getCode(), code))
.findFirst()
.orElseThrow(() -> new IllegalStateException("该枚举不存在"));
}
}

View File

@@ -0,0 +1,35 @@
package cn.bootx.platform.daxpay.sdk.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 支付签名类型
* @author xxm
* @since 2023/12/24
*/
@Getter
@AllArgsConstructor
public enum SignTypeEnum {
HMAC_SHA256("HMAC_SHA256"),
MD5("MD5"),;
/** 支付方式 */
private final String code;
/**
* 根据编码获取枚举
*/
public static SignTypeEnum findByCode(String code){
return Arrays.stream(values())
.filter(o -> Objects.equals(o.getCode(), code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("该枚举不存在"));
}
}

View File

@@ -0,0 +1,17 @@
package cn.bootx.platform.daxpay.sdk.model;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 支付关闭
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString
public class PayCloseModel extends DaxPayResponseModel {
}

View File

@@ -0,0 +1,41 @@
package cn.bootx.platform.daxpay.sdk.model;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayStatusEnum;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 支付相应对象
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString
public class PayOrderModel extends DaxPayResponseModel {
/** 支付ID */
private Long paymentId;
/** 是否是异步支付 */
private boolean asyncPay;
/**
* 异步支付通道
* @see PayChannelEnum
*/
private String asyncChannel;
/** 支付参数体(通常用于发起异步支付的参数) */
private String payBody;
/**
* 支付状态
* @see PayStatusEnum
*/
private String status;
}

View File

@@ -0,0 +1,26 @@
package cn.bootx.platform.daxpay.sdk.model;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
/**
* 支付退款通道明细数据
* @author xxm
* @since 2024/1/17
*/
public class RefundChannelOrderModel extends DaxPayResponseModel {
/** 通道 */
private String channel;
/** 通道支付单id */
private Long payChannelId;
/** 异步支付方式 */
private boolean async;
/** 订单金额" */
private Integer orderAmount;
/** 退款金额 */
private Integer amount;
}

View File

@@ -0,0 +1,24 @@
package cn.bootx.platform.daxpay.sdk.model;
import cn.bootx.platform.daxpay.sdk.code.RefundStatusEnum;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
/**
* 退款响应参数
* @author xxm
* @since 2023/12/18
*/
public class RefundModel extends DaxPayResponseModel {
/** 退款ID */
private Long refundId;
/** 退款订单号, 如果请求时未传, 默认为退款ID */
private String refundNo;
/**
* 退款状态
* @see RefundStatusEnum
*/
private String status;
}

View File

@@ -0,0 +1,54 @@
package cn.bootx.platform.daxpay.sdk.model;
import cn.bootx.platform.daxpay.sdk.code.RefundStatusEnum;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 退款订单数据
* @author xxm
* @since 2024/1/16
*/
public class RefundOrderResult extends DaxPayResponseModel {
/** 支付号 */
private Long paymentId;
/** 关联的业务id */
private String businessNo;
/** 退款号 */
private String refundNo;
/** 标题 */
private String title;
/** 退款金额 */
private BigDecimal amount;
/** 剩余可退 */
private BigDecimal refundableBalance;
/**
* 异步支付通道发给网关的退款号, 用与将记录关联起来
*/
private String gatewayOrderNo;
/** 通道退款订单 */
private List<RefundChannelOrderModel> channels;
/** 退款终端ip */
private String clientIp;
/** 退款时间 */
private LocalDateTime refundTime;
/**
* 退款状态
* @see RefundStatusEnum
*/
private String status;
}

View File

@@ -0,0 +1,29 @@
package cn.bootx.platform.daxpay.sdk.net;
import cn.bootx.platform.daxpay.sdk.code.SignTypeEnum;
import lombok.Builder;
import lombok.Getter;
/**
* 支付配置
* @author xxm
* @since 2024/2/2
*/
@Getter
@Builder
public class DaxPayConfig {
/** 服务地址 */
private String serviceUrl;
/** 签名方式 */
@Builder.Default
private SignTypeEnum signType = SignTypeEnum.MD5;
/** 签名秘钥 */
private String signSecret;
/** 请求超时时间 */
@Builder.Default
private int reqTimeout = 30000;
}

View File

@@ -0,0 +1,54 @@
package cn.bootx.platform.daxpay.sdk.net;
import cn.bootx.platform.daxpay.sdk.code.SignTypeEnum;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.bootx.platform.daxpay.sdk.util.PaySignUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.experimental.UtilityClass;
import java.util.Objects;
/**
* 支付发起工具包
* @author xxm
* @since 2024/2/2
*/
@UtilityClass
public class DaxPayKit {
private DaxPayConfig config;
public void initConfig(DaxPayConfig config){
DaxPayKit.config = config;
}
/**
* 支付请求执行类
* @param request 请求参数
* @return DaxPayResult 响应类
* @param <T> 业务对象
*/
public <T extends DaxPayResponseModel> DaxPayResult<T> execute(DaxPayRequest<T> request, boolean sign){
// 判断是是否进行签名
if (sign) {
if (Objects.equals(SignTypeEnum.MD5, config.getSignType())){
String md5Sign = PaySignUtil.md5Sign(request, config.getSignSecret());
request.setSign(md5Sign);
} else {
String hmacSha256Sign = PaySignUtil.hmacSha256Sign(request, config.getSignSecret());
request.setSign(hmacSha256Sign);
}
}
String data = JSONUtil.toJsonStr(request);
String path = config.getServiceUrl() + request.path();
HttpResponse execute = HttpUtil.createPost(path)
.body(data, ContentType.JSON.getValue())
.timeout(config.getReqTimeout())
.execute();
String body = execute.body();
return request.toModel(body);
}
}

View File

@@ -0,0 +1,52 @@
package cn.bootx.platform.daxpay.sdk.net;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import lombok.Getter;
import lombok.Setter;
/**
* 请求接口
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
public abstract class DaxPayRequest<T extends DaxPayResponseModel> {
/** 方法请求路径 */
public abstract String path();
/**
* 将请求返回结果反序列化为实体类
*/
public abstract DaxPayResult<T> toModel(String json);
/** 客户端ip */
private String clientIp;
/** 商户扩展参数,回调时会原样返回 */
private String attach;
/** 是否不进行同步通知的跳转 */
private boolean notReturn;
/** 同步通知URL */
private String returnUrl;
/** 是否不启用异步通知 */
private boolean notNotify;
/** 异步通知地址 */
private String notifyUrl;
/** 签名 */
private String sign;
/** API版本号 */
private String version = "1.0";
/** 请求时间,传输时间戳 */
private Long reqTime;
}

View File

@@ -0,0 +1,16 @@
package cn.bootx.platform.daxpay.sdk.net;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
*
* @author xxm
* @since 2024/2/2
*/
@Setter
@Getter
@ToString
public abstract class DaxPayResponseModel {
}

View File

@@ -0,0 +1,9 @@
package cn.bootx.platform.daxpay.sdk.param;
/**
* 通道支付参数标识接口
* @author xxm
* @since 2023/12/17
*/
public interface ChannelParam {
}

View File

@@ -0,0 +1,19 @@
package cn.bootx.platform.daxpay.sdk.param.channel;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import lombok.Getter;
import lombok.Setter;
/**
* 支付宝支付参数
* @author xxm
* @since 2021/2/27
*/
@Getter
@Setter
public class AliPayParam implements ChannelParam {
/** 授权码(主动扫描用户的付款码) */
private String authCode;
}

View File

@@ -0,0 +1,20 @@
package cn.bootx.platform.daxpay.sdk.param.channel;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import lombok.Getter;
import lombok.Setter;
/**
* 储值卡支付参数
*
* @author xxm
* @since 2022/3/14
*/
@Getter
@Setter
public class VoucherPayParam implements ChannelParam {
/** 储值卡号 */
private String cardNo;
}

View File

@@ -0,0 +1,23 @@
package cn.bootx.platform.daxpay.sdk.param.channel;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import lombok.Getter;
import lombok.Setter;
/**
* 钱包支付参数
*
* @author xxm
* @since 2020/12/8
*/
@Getter
@Setter
public class WalletPayParam implements ChannelParam {
/** 钱包ID */
private Long walletId;
/** 用户ID */
private String userId;
}

View File

@@ -0,0 +1,22 @@
package cn.bootx.platform.daxpay.sdk.param.channel;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import lombok.Getter;
import lombok.Setter;
/**
* 微信支付参数
* @author xxm
* @since 2021/6/21
*/
@Getter
@Setter
public class WeChatPayParam implements ChannelParam {
/** 微信openId */
private String openId;
/** 授权码(主动扫描用户的付款码) */
private String authCode;
}

View File

@@ -0,0 +1,46 @@
package cn.bootx.platform.daxpay.sdk.param.pay;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import cn.bootx.platform.daxpay.sdk.param.channel.AliPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.VoucherPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WalletPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
import lombok.Getter;
import lombok.Setter;
/**
* 同意下单支付方式参数
*
* @author xxm
* @since 2020/12/8
*/
@Getter
@Setter
public class PayChannelParam {
/**
* 支付通道编码
* @see PayChannelEnum#getCode()
*/
private String channel;
/**
* 支付方式编码
* @see PayWayEnum#getCode()
*/
private String way;
/** 支付金额 */
private Integer amount;
/**
* 附加支付参数, 传输json格式字符串
* @see AliPayParam
* @see WeChatPayParam
* @see VoucherPayParam
* @see WalletPayParam
*/
private ChannelParam channelParam;
}

View File

@@ -0,0 +1,41 @@
package cn.bootx.platform.daxpay.sdk.param.pay;
import cn.bootx.platform.daxpay.sdk.model.PayCloseModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayRequest;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付关闭参数
* @author xxm
* @since 2023/12/17
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class PayCloseParam extends DaxPayRequest<PayCloseModel> {
/** 支付单ID */
private Long paymentId;
/** 业务号 */
private String businessNo;
/**
* 方法请求路径
*/
@Override
public String path() {
return "/unipay/close";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<PayCloseModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<PayCloseModel>>() {}, false);
}
}

View File

@@ -0,0 +1,57 @@
package cn.bootx.platform.daxpay.sdk.param.pay;
import cn.bootx.platform.daxpay.sdk.model.PayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayRequest;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
/**
* 支付参数
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString
public class PayParam extends DaxPayRequest<PayOrderModel> {
/** 业务号 */
private String businessNo;
/** 支付标题 */
private String title;
/** 支付描述 */
private String description;
/** 过期时间, 多次传输以第一次为准 */
private Long expiredTime;
/** 用户付款中途退出返回商户网站的地址(部分支付场景中可用) */
private String quitUrl;
/** 支付通道信息参数 */
private List<PayChannelParam> payChannels;
/**
* 方法请求路径
*/
@Override
public String path() {
return "/unipay/pay";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<PayOrderModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<PayOrderModel>>() {}, false);
}
}

View File

@@ -0,0 +1,82 @@
package cn.bootx.platform.daxpay.sdk.param.pay;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.model.PayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayRequest;
import cn.bootx.platform.daxpay.sdk.param.ChannelParam;
import cn.bootx.platform.daxpay.sdk.param.channel.AliPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.VoucherPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WalletPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 简单支付请求参数
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString
public class SimplePayParam extends DaxPayRequest<PayOrderModel> {
/** 业务号 */
private String businessNo;
/** 支付标题 */
private String title;
/** 支付描述 */
private String description;
/** 过期时间 */
private Long expiredTime;
/** 用户付款中途退出返回商户网站的地址(部分支付场景中可用) */
private String quitUrl;
/**
* @see PayChannelEnum#getCode()
*/
private String channel;
/**
* 支付方式编码
* @see PayWayEnum#getCode()
*/
private String payWay;
/** 支付金额 */
private Integer amount;
/**
* 附加支付参数
* @see AliPayParam
* @see WeChatPayParam
* @see VoucherPayParam
* @see WalletPayParam
*/
private ChannelParam channelParam;
/**
* 请求路径
*/
@Override
public String path() {
return "/unipay/simplePay";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<PayOrderModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<PayOrderModel>>() {}, false);
}
}

View File

@@ -0,0 +1,29 @@
package cn.bootx.platform.daxpay.sdk.param.refund;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 分通道退款参数
* @author xxm
* @since 2023/12/18
*/
@Getter
@Setter
@ToString
public class RefundChannelParam {
/**
* 支付通道编码
* @see PayChannelEnum#getCode()
*/
private String channel;
/**
* 退款金额
*/
private Integer amount;
}

View File

@@ -0,0 +1,61 @@
package cn.bootx.platform.daxpay.sdk.param.refund;
import cn.bootx.platform.daxpay.sdk.model.RefundModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayRequest;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.*;
import java.util.List;
/**
* 退款参数,适用于组合支付的订单退款操作中,
* @author xxm
* @since 2023/12/18
*/
@Getter
@Setter
@ToString
public class RefundParam extends DaxPayRequest<RefundModel> {
/** 支付单ID */
private Long paymentId;
/** 业务号 */
private String businessNo;
/**
* 部分退款需要传输支付通道参数参数
*/
private boolean refundAll;
/**
* 退款号可以为空, 但不可以重复, 如果退款号为空, 则系统会自动生成退款号, 与退款ID一致
*/
private String refundNo;
/**
* 部分退款时必传
*/
private List<RefundChannelParam> refundChannels;
/** 退款原因 */
private String reason;
/**
* 方法请求路径
*/
@Override
public String path() {
return "/unipay/refund";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<RefundModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<RefundModel>>() {}, false);
}
}

View File

@@ -0,0 +1,61 @@
package cn.bootx.platform.daxpay.sdk.param.refund;
import cn.bootx.platform.daxpay.sdk.model.RefundModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayRequest;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 简单退款参数,只可以用于非组合的支付订单
* @author xxm
* @since 2023/12/18
*/
@Getter
@Setter
@ToString
public class SimpleRefundParam extends DaxPayRequest<RefundModel> {
/**
* 支付单ID, 优先级高于业务号
*/
private Long paymentId;
/** 业务号 */
private String businessNo;
/**
* 退款号可以为空, 但不可以重复, 如果退款号为空, 则系统会自动生成退款号, 与退款ID一致
*/
private String refundNo;
/**
* 是否全部退款, 部分退款需要传输refundModes参数
*/
private boolean refundAll;
/** 退款金额 */
private Integer amount;
/** 退款原因 */
private String reason;
/**
* 方法请求路径
*/
@Override
public String path() {
return "/unipay/simpleRefund";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<RefundModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<RefundModel>>() {}, false);
}
}

View File

@@ -0,0 +1,29 @@
package cn.bootx.platform.daxpay.sdk.response;
import cn.bootx.platform.daxpay.sdk.net.DaxPayResponseModel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
*
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString
public class DaxPayResult<T extends DaxPayResponseModel> {
/** 提示信息 */
private String msg = "success";
/** 响应码 */
private int code = 0;
/** 响应体 */
private T data;
/** 数据签名 */
private String sign;
}

View File

@@ -0,0 +1,185 @@
package cn.bootx.platform.daxpay.sdk.util;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONUtil;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* 如果需要进行签名,
* 1. 参数名ASCII码从小到大排序字典序
* 2. 如果参数的值为空不参与签名
* 3. 参数名不区分大小写
* 4. 嵌套对象转换成先转换成MAP再序列化为字符串
* 5. 支持两层嵌套, 更多层级嵌套未测试, 可能会导致不可预知的问题
*/
@UtilityClass
public class PaySignUtil {
/**
* 将参数转换为map对象. 使用ChatGPT生成
* 1. 参数名ASCII码从小到大排序字典序
* 2. 如果参数的值为空不参与签名;
* 3. 参数名不区分大小写;
*/
public Map<String, String> toMap(Object object) {
Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
toMap(object, map);
return map;
}
/**
* 将参数转换为map对象. 使用ChatGPT生成, 仅局限于对请求支付相关参数进行签名
*/
@SneakyThrows
private void toMap(Object object, Map<String, String> map) {
Class<?> clazz = object.getClass();
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
Object fieldValue = field.get(object);
if (fieldValue != null) {
// 基础类型及包装类 和 字符串类型
if (ClassUtil.isBasicType(field.getType())|| field.getType().equals(String.class)) {
String fieldValueString = String.valueOf(fieldValue);
map.put(fieldName, fieldValueString);
}
// 集合类型
else if (Collection.class.isAssignableFrom(field.getType())) {
Collection<?> collection = (Collection<?>) fieldValue;
if (!collection.isEmpty()) {
List<Map<String, String>> maps = collection.stream()
.filter(Objects::nonNull)
.map(item -> {
Map<String, String> nestedMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
toMap(item, nestedMap);
return nestedMap;
})
.collect(Collectors.toList());
map.put(fieldName, JSONUtil.toJsonStr(maps));
}
// 其他类型
} else {
Map<String, String> nestedMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
toMap(fieldValue, nestedMap);
String nestedJson = JSONUtil.toJsonStr(map);
map.put(fieldName, nestedJson);
}
}
}
clazz = clazz.getSuperclass();
}
}
/**
* 把所有元素排序, 并拼接成字符, 用于签名
*/
public static String createLinkString(Map<String, String> params) {
String connStr = "&";
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder content = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
// 拼接时,不包括最后一个&字符
if (i == keys.size() - 1) {
content.append(key)
.append("=")
.append(value);
} else {
content.append(key)
.append("=")
.append(value)
.append(connStr);
}
}
return content.toString();
}
/**
* 生成16进制 MD5 字符串
*
* @param data 数据
* @return MD5 字符串
*/
public String md5(String data) {
return SecureUtil.md5(data);
}
/**
* 生成16进制的 sha256 字符串
*
* @param data 数据
* @param signKey 密钥
* @return sha256 字符串
*/
public String hmacSha256(String data, String signKey) {
return SecureUtil.hmac(HmacAlgorithm.HmacSHA256, signKey).digestHex(data);
}
/**
* 生成待签名字符串
* @param object 待签名对象
* @param signKey 签名Key
* @return 待签名字符串
*/
public String signString(Object object, String signKey){
// 签名
Map<String, String> map = toMap(object);
// 生成签名前先去除sign参数
map.remove("sign");
// 创建待签名字符串
String data = createLinkString(map);
// 将签名key追加到字符串最后
return data + "&key=" + signKey;
}
/**
* md5方式进行签名
*
* @return 签名值
*/
public String md5Sign(Object object, String signKey){
String data = signString(object, signKey);
return md5(data);
}
/**
* hmacSha256方式进行签名
*
* @return 签名值
*/
public String hmacSha256Sign(Object object, String signKey){
String data = signString(object, signKey);
return hmacSha256(data, signKey);
}
/**
* MD5签名验证
*/
public boolean verifyMd5Sign(Object object, String signKey, String sign){
String md5Sign = md5Sign(object, signKey);
return md5Sign.equals(sign);
}
/**
* hmacSha256签名验证
*/
public boolean verifyHmacSha256Sign(Object object, String signKey, String sign){
String hmacSha256Sign = hmacSha256Sign(object, signKey);
return hmacSha256Sign.equals(sign);
}
}

View File

@@ -0,0 +1,39 @@
package cn.bootx.platform.daxpay.sdk;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.model.PayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayConfig;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import cn.bootx.platform.daxpay.sdk.param.pay.SimplePayParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
/**
*
* @author xxm
* @since 2024/2/2
*/
public class SimplePayOrderTest {
public static void main(String[] args) {
DaxPayConfig config = DaxPayConfig.builder()
.serviceUrl("http://127.0.0.1:9000")
.signSecret("123456")
.build();
DaxPayKit.initConfig(config);
// 简单支付参数
SimplePayParam param = new SimplePayParam();
param.setBusinessNo("1");
param.setAmount(1);
param.setTitle("测试接口支付");
param.setChannel(PayChannelEnum.ALI.getCode());
param.setPayWay(PayWayEnum.QRCODE.getCode());
param.setClientIp("127.0.0.1");
param.setNotNotify(true);
param.setNotReturn(true);
DaxPayResult<PayOrderModel> execute = DaxPayKit.execute(param, true);
System.out.println(execute);
}
}

View File

@@ -5,5 +5,5 @@ package cn.bootx.platform.daxpay.param;
* @author xxm
* @since 2023/12/17
*/
public interface IChannelParam {
public interface ChannelParam {
}

View File

@@ -1,6 +1,6 @@
package cn.bootx.platform.daxpay.param.channel;
import cn.bootx.platform.daxpay.param.IChannelParam;
import cn.bootx.platform.daxpay.param.ChannelParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -10,7 +10,7 @@ import lombok.Data;
*/
@Data
@Schema(title = "支付宝支付参数")
public class AliPayParam implements IChannelParam {
public class AliPayParam implements ChannelParam {
@Schema(description = "授权码(主动扫描用户的付款码)")
private String authCode;

View File

@@ -1,6 +1,6 @@
package cn.bootx.platform.daxpay.param.channel;
import cn.bootx.platform.daxpay.param.IChannelParam;
import cn.bootx.platform.daxpay.param.ChannelParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -12,7 +12,7 @@ import lombok.Data;
*/
@Data
@Schema(title = "储值卡支付参数")
public class VoucherPayParam implements IChannelParam {
public class VoucherPayParam implements ChannelParam {
@Schema(description = "储值卡号")
private String cardNo;

View File

@@ -1,6 +1,6 @@
package cn.bootx.platform.daxpay.param.channel;
import cn.bootx.platform.daxpay.param.IChannelParam;
import cn.bootx.platform.daxpay.param.ChannelParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -12,7 +12,7 @@ import lombok.Data;
*/
@Data
@Schema(title = "钱包支付参数")
public class WalletPayParam implements IChannelParam {
public class WalletPayParam implements ChannelParam {
@Schema(description = "钱包ID")
private Long walletId;

View File

@@ -1,6 +1,6 @@
package cn.bootx.platform.daxpay.param.channel;
import cn.bootx.platform.daxpay.param.IChannelParam;
import cn.bootx.platform.daxpay.param.ChannelParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -10,7 +10,7 @@ import lombok.Data;
*/
@Data
@Schema(title = "微信支付参数")
public class WeChatPayParam implements IChannelParam {
public class WeChatPayParam implements ChannelParam {
@Schema(description = "微信openId")
private String openId;

View File

@@ -48,5 +48,5 @@ public class PayChannelParam {
* @see WalletPayParam
*/
@Schema(description = "附加支付参数")
private String channelExtra;
private String channelParam;
}

View File

@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
public abstract class PayCommonParam {
/** 客户端ip */
@NotBlank(message = "客户端ip不可为空")
// @NotBlank(message = "客户端ip不可为空")
@Schema(description = "客户端ip")
private String clientIp;
@@ -48,12 +48,12 @@ public abstract class PayCommonParam {
/** API版本号 */
@Schema(description = "API版本号")
@NotBlank(message = "API版本号必填")
// @NotBlank(message = "API版本号必填")
private String version = "1.0.0";
/** 请求时间,时间戳转时间 */
@Schema(description = "请求时间,传输时间戳")
@NotNull(message = "请求时间必填")
// @NotNull(message = "请求时间必填")
@JsonDeserialize(using = TimestampToLocalDateTimeDeserializer.class)
private LocalDateTime reqTime;

View File

@@ -50,7 +50,7 @@ public class SimplePayParam extends PayCommonParam{
*/
@Schema(description = "支付通道编码")
@NotBlank(message = "支付通道编码不可为空")
private String payChannel;
private String channel;
/**
* @see PayWayEnum#getCode()
@@ -70,8 +70,8 @@ public class SimplePayParam extends PayCommonParam{
* @see VoucherPayParam
* @see WalletPayParam
*/
@Schema(description = "附加支付参数")
private String channelExtra;
@Schema(description = "附加通道支付参数")
private String channelParam;
}

View File

@@ -14,6 +14,6 @@ import org.slf4j.MDC;
@Schema(title = "支付通用返回参数")
public class CommonResult {
@Schema(description = "请求ID")
private String reqId = MDC.get(CommonCode.TRACE_ID);
@Schema(description = "追踪ID")
private String traceId = MDC.get(CommonCode.TRACE_ID);
}

View File

@@ -29,7 +29,7 @@ public class PayResult extends CommonResult {
* @see PayChannelEnum#ASYNC_TYPE_CODE
*/
@Schema(description = "异步支付通道")
private String asyncPayChannel;
private String asyncChannel;
/** 支付参数体(通常用于发起异步支付的参数) */

View File

@@ -22,4 +22,7 @@ public class RefundResult extends CommonResult {
@Schema(description = "退款订单号, 如果请求时未传, 则默认使用退款ID")
private String refundNo;
@Schema(description = "退款状态")
private String status;
}

View File

@@ -153,7 +153,7 @@ public class PaySignUtil {
// 创建待签名字符串
String data = createLinkString(map);
// 将签名key追加到字符串最后
return "&key=" + signKey;
return data + "&key=" + signKey;
}
/**

View File

@@ -46,7 +46,7 @@ public class VoucherPayService {
VoucherPayParam voucherPayParam;
try {
// 储值卡参数验证
String extraParamsJson = payChannelParam.getChannelExtra();
String extraParamsJson = payChannelParam.getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
voucherPayParam = JSONUtil.toBean(extraParamsJson, VoucherPayParam.class);
} else {

View File

@@ -32,7 +32,7 @@ public class PayNoticeResult {
* @see PayChannelEnum#ASYNC_TYPE_CODE
*/
@Schema(description = "异步支付通道")
private String asyncPayChannel;
private String asyncChannel;
@Schema(description = "支付金额")
private Integer amount;

View File

@@ -97,7 +97,7 @@ public class PayBuilder {
.setPayWay(channelParam.getWay())
.setAmount(channelParam.getAmount())
.setRefundableBalance(channelParam.getAmount())
.setChannelExtra(channelParam.getChannelExtra());
.setChannelExtra(channelParam.getChannelParam());
}
/**
@@ -126,7 +126,7 @@ public class PayBuilder {
paymentResult = new PayResult();
paymentResult.setPaymentId(payOrder.getId());
paymentResult.setAsyncPay(payOrder.isAsyncPay());
paymentResult.setAsyncPayChannel(payOrder.getAsyncChannel());
paymentResult.setAsyncChannel(payOrder.getAsyncChannel());
paymentResult.setStatus(payOrder.getStatus());
// 设置异步支付参数

View File

@@ -68,7 +68,7 @@ public class PayChannelOrderService {
.setAmount(payChannelParam.getAmount())
.setRefundableBalance(payChannelParam.getAmount())
.setPayTime(LocalDateTime.now())
.setChannelExtra(payChannelParam.getChannelExtra())
.setChannelExtra(payChannelParam.getChannelParam())
.setStatus(payStatus.getCode());
channelOrderManager.deleteByPaymentIdAndAsync(payOrder.getId());
channelOrderManager.save(payChannelOrder);
@@ -77,7 +77,7 @@ public class PayChannelOrderService {
payOrderChannelOpt.get()
.setPayWay(payChannelParam.getWay())
.setPayTime(LocalDateTime.now())
.setChannelExtra(payChannelParam.getChannelExtra())
.setChannelExtra(payChannelParam.getChannelParam())
.setStatus(payStatus.getCode());
channelOrderManager.updateById(payOrderChannelOpt.get());
}

View File

@@ -85,6 +85,7 @@ public class PayAssistService {
asyncPayInfo.setExpiredTime(payParam.getExpiredTime());
return;
}
// 读取本地时间
LocalDateTime paymentExpiredTime = PayUtil.getPaymentExpiredTime(platform.getOrderTimeout());
asyncPayInfo.setExpiredTime(paymentExpiredTime);
}

View File

@@ -102,10 +102,10 @@ public class PayService {
// 组装支付参数
PayParam payParam = new PayParam();
PayChannelParam payChannelParam = new PayChannelParam();
payChannelParam.setChannel(simplePayParam.getPayChannel());
payChannelParam.setChannel(simplePayParam.getChannel());
payChannelParam.setWay(simplePayParam.getPayWay());
payChannelParam.setAmount(simplePayParam.getAmount());
payChannelParam.setChannelExtra(simplePayParam.getChannelExtra());
payChannelParam.setChannelParam(simplePayParam.getChannelParam());
BeanUtil.copyProperties(simplePayParam,payParam, CopyOptions.create().ignoreNullValue());
payParam.setPayChannels(Collections.singletonList(payChannelParam));
// 复用支付下单接口

View File

@@ -52,7 +52,7 @@ public class AliPayStrategy extends AbsPayStrategy {
public void doBeforePayHandler() {
try {
// 支付宝参数验证
String extraParamsJson = this.getPayChannelParam().getChannelExtra();
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
this.aliPayParam = JSONUtil.toBean(extraParamsJson, AliPayParam.class);
}

View File

@@ -54,7 +54,7 @@ public class WalletPayStrategy extends AbsPayStrategy {
WalletPayParam walletPayParam = new WalletPayParam();
try {
// 钱包参数验证
String extraParamsJson = this.getPayChannelParam().getChannelExtra();
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
walletPayParam = JSONUtil.toBean(extraParamsJson, WalletPayParam.class);
}

View File

@@ -55,7 +55,7 @@ public class WeChatPayStrategy extends AbsPayStrategy {
public void doBeforePayHandler() {
try {
// 微信参数验证
String extraParamsJson = this.getPayChannelParam().getChannelExtra();
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
this.weChatPayParam = JSONUtil.toBean(extraParamsJson, WeChatPayParam.class);
}

View File

@@ -42,7 +42,7 @@ public class PayCallbackRecord extends MpCreateEntity implements EntityBaseFunct
* @see PayChannelEnum#getCode()
*/
@DbColumn(comment = "支付通道")
private String payChannel;
private String channel;
/**
* 回调类型

View File

@@ -30,7 +30,7 @@ public class PayCallbackRecordDto extends BaseDto {
* @see PayChannelEnum#getCode()
*/
@Schema(description = "支付通道")
private String payChannel;
private String channel;
/**
* 回调类型

View File

@@ -109,7 +109,7 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
.orElse(null);
PayCallbackRecord payNotifyRecord = new PayCallbackRecord()
.setPayChannel(this.getChannel().getCode())
.setChannel(this.getChannel().getCode())
.setNotifyInfo(JSONUtil.toJsonStr(callbackInfo.getCallbackParam()))
.setOrderId(callbackInfo.getOrderId())
.setGatewayOrderNo(callbackInfo.getGatewayOrderNo())

View File

@@ -30,13 +30,12 @@ public class PayCallbackRecordQuery extends QueryOrder {
* @see PayChannelEnum#getCode()
*/
@Schema(description = "支付通道")
private String payChannel;
private String channel;
/**
* 回调处理状态
* @see PayCallbackStatusEnum
* @see RefundCallbackStatusEnum
*/
@Schema(description = "回调处理状态")
private String status;

View File

@@ -40,7 +40,7 @@ class PaymentSignServiceTest {
p1.setWay("wx_app");
AliPayParam aliPayParam = new AliPayParam();
aliPayParam.setAuthCode("6688");
p1.setChannelExtra(JSONUtil.toJsonStr(aliPayParam));
p1.setChannelParam(JSONUtil.toJsonStr(aliPayParam));
PayChannelParam p2 = new PayChannelParam();
p2.setAmount(100);
@@ -49,7 +49,7 @@ class PaymentSignServiceTest {
WeChatPayParam weChatPayParam = new WeChatPayParam();
weChatPayParam.setOpenId("w2qsz2xawe3gbhyyff28fs01fd");
weChatPayParam.setAuthCode("8866");
p2.setChannelExtra(JSONUtil.toJsonStr(weChatPayParam));
p2.setChannelParam(JSONUtil.toJsonStr(weChatPayParam));
List<PayChannelParam> payWays = Arrays.asList(p1, p2);
payParam.setPayChannels(payWays);