feat 签名算法支持嵌套参数优化, 新增支付演示模块

This commit is contained in:
bootx
2024-02-09 00:32:16 +08:00
parent 9aeeee0ac0
commit a1efe54da4
41 changed files with 589 additions and 121 deletions

View File

@@ -1,21 +1,16 @@
# CHANGELOG
## [v1.0.1] 支付之路: 多商户适配
- 新增: 钱包支持多商户和多应用
- 新增: 储值卡支持多商户和多应
- 新增: 网关同步记录进行保存
- 新增: 钱包支持设置开通时的默认金额
- 新增: 储值卡多卡支付演示
- 优化: 结算台支持多商户
- 优化: 重构支付消息通知结构, 使后期支持多种消息中间件
- 优化: 拆分网关同步相关代码
- 优化: 保存各渠道的支付单
## [v1.0.0] 支付之路
- 支持单渠道支付、聚合支付、组合支付、退款功能
- 单渠道支付:支持支付宝、微信、现金、钱包、储值卡等多种支付方式
- 聚合支付:支持微信或支付宝使用同一个码
- 组合支付:支持多种同步支付和一个异步支付(微信、支付宝)进行组合支付
- 支持退款:部分对款、全部退款等方式
- 储值卡:支持单卡支付、多卡支付,退款时支持退款到原储值卡中,也支持将余额退到同一个卡上
- 支付宝支持web支付、wap支付、扫码支付、付款码支付、APP支付
- 微信wap支付、扫码支付、付款码支付、APP支付、公众号/小程序支付
## [v2.0.0]
- 支持请求参数签名和验签机制已经支持SHA256和
- 去除调用时的用户概念,作为独立的支付网关使
- 支持支付功能、已对支付宝、微信进行支持
- 支持支付订单超时自动进行关闭
- 支持支付订单关闭功能,关闭
- 支持支付退款功能,可以进行全部退款或部分退款
- 支持支付同步功能,通过同步接口可以获取第三方支付网关的状态
- 支持支付和退款订单的修复功能,根据取第三方支付网关订单的状态,对订单进行修正,如支付同步、退款同步、消息回调等可触发
- 部分支付对账功能,已经实现支付宝和微信对账单下载解析和保存的功能
- 支持对各支付通道进行管理包括是否启用、显示Logo图等
- 支持对支付网关对外暴露的接口进行管理,支持启停用、是否验签、是否消息通知等功能
- 组合支付已预先进行支持,支持一个异步支付+多个同步支付通道组合进行收单支付
- 提供Java版本SDK简化业务系统对支付网关的调用
- 对回调记录、同步记录、修复记录、关闭记录、进行报错

View File

@@ -1,10 +1,15 @@
2.0.1 里程碑
- 支持各类回调通知
- 现金、钱包、储值卡功能进行重构
-
2.0.0 重构进度
- 已经完成的
- [x] 参数签名和验签机制
- [x] 开放接口供第三方调用
- [x] 将零散的上下文对象进行抽取为统一的上下文对象
- [x] 拆分原有的策略类,实现粒度更细
- [x] 去除用户概念,作为独立的支付网关使用, 不与其他系统产生耦合性
- [x] 去除用户概念,作为独立的支付网关使用,不与其他系统产生耦合性
- 2023-12-31:
- [x] 支付关闭相关逻辑
- [x] 各支付通道补充相关未实现的逻辑
@@ -55,7 +60,7 @@
- 2024-01-13:
- [x] (前端) 微信/支付宝支付通道配置
- [x] 支付通道支持停用
- [x] 请求支付网关时区退款号以R开头, 用于与支付ID的区分
- [x] ~~请求支付网关时区退款号以R开头, 用于与支付ID的区分~~
- [x] (前端) 平台配置
- 2024-01-15:
- [x] 优化支付相关订单和记录的查询条件

View File

@@ -0,0 +1,43 @@
<?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>cn.bootx.platform</groupId>
<artifactId>dax-pay</artifactId>
<version>2.0.0</version>
</parent>
<artifactId>daxpay-single-demo</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>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Bootx Platform 基础API -->
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>service-baseapi</artifactId>
<version>${bootx-platform.version}</version>
</dependency>
<!-- 支付SDK 结算台 -->
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-sdk</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,12 @@
package cn.bootx.platform.daxpay.demo;
import org.springframework.context.annotation.ComponentScan;
/**
* 支付演示模块
* @author xxm
* @since 2024/2/8
*/
@ComponentScan
public class DaxPaySingleDemoApp {
}

View File

@@ -0,0 +1,28 @@
package cn.bootx.platform.daxpay.demo.config;
import cn.bootx.platform.daxpay.sdk.code.SignTypeEnum;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 演示模块配置类
* @author xxm
* @since 2024/2/8
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "dax-pay.demo")
public class DaxPayDemoProperties {
/** 服务地址 */
private String serverUrl;
/** 签名方式 */
private SignTypeEnum signType = SignTypeEnum.MD5;
/** 签名秘钥 */
private String signSecret;
/** 请求超时时间 */
private int reqTimeout = 30000;
}

View File

@@ -0,0 +1,37 @@
package cn.bootx.platform.daxpay.demo.config;
import cn.bootx.platform.daxpay.sdk.net.DaxPayConfig;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* 初始化
* @author xxm
* @since 2024/2/8
*/
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(DaxPayDemoProperties.class)
public class DaxPayKitConfiguration {
private final DaxPayDemoProperties daxPayDemoProperties;
/**
* 初始化
*/
@EventListener(ApplicationStartedEvent.class)
// @EventListener(webstarteve.class)
public void initDaxPayKit(){
DaxPayConfig config = DaxPayConfig.builder()
.serviceUrl(daxPayDemoProperties.getServerUrl())
.signType(daxPayDemoProperties.getSignType())
.signSecret(daxPayDemoProperties.getSignSecret())
.reqTimeout(daxPayDemoProperties.getReqTimeout())
.build();
DaxPayKit.initConfig(config);
}
}

View File

@@ -0,0 +1,39 @@
package cn.bootx.platform.daxpay.demo.controller;
import cn.bootx.platform.common.core.rest.Res;
import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.common.core.util.ValidationUtil;
import cn.bootx.platform.daxpay.demo.param.CashierSimplePayParam;
import cn.bootx.platform.daxpay.demo.result.PayOrderResult;
import cn.bootx.platform.daxpay.demo.service.DaxPayCashierService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 结算台演示
* @author xxm
* @since 2024/2/8
*/
@Tag(name = "结算台演示")
@RestController
@RequestMapping("/demo/cashier")
@RequiredArgsConstructor
public class DaxPayCashierController {
private final DaxPayCashierService cashierService;
@Operation(summary = "创建支付订单并发起")
@PostMapping("/simplePayCashier")
public ResResult<PayOrderResult> simplePayCashier(@RequestBody CashierSimplePayParam param){
ValidationUtil.validateParam(param);
return Res.ok(cashierService.simplePayCashier(param));
}
@Operation(summary = "查询支付订单")
@GetMapping("/queryPayOrder")
public ResResult<Boolean> queryPayOrder(Long paymentId){
return Res.ok(cashierService.queryPayOrder(paymentId));
}
}

View File

@@ -0,0 +1,47 @@
package cn.bootx.platform.daxpay.demo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 结算台简单支付参数(单通道支付)
*
* @author xxm
* @since 2022/2/23
*/
@Data
@Accessors(chain = true)
@Schema(title = "结算台简单支付参数(单通道支付)")
public class CashierSimplePayParam {
@Schema(description = "业务号")
@NotNull
private String businessNo;
@Schema(description = "标题")
@NotNull
private String title;
@Schema(description = "金额")
@NotNull
private BigDecimal amount;
@Schema(description = "openId(微信支付时使用)")
private String openId;
@Schema(description = "支付渠道")
@NotNull
private String channel;
@Schema(description = "支付方式")
@NotNull
private String payWay;
@Schema(description = "付款码")
private String authCode;
}

View File

@@ -0,0 +1,31 @@
package cn.bootx.platform.daxpay.demo.result;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import lombok.Getter;
import lombok.Setter;
/**
* 发起支付后响应对象
* @author xxm
* @since 2024/2/8
*/
@Getter
@Setter
public class PayOrderResult {
/** 支付ID */
private Long paymentId;
/** 是否是异步支付 */
private boolean asyncPay;
/**
* 异步支付通道
* @see PayChannelEnum
*/
private String asyncChannel;
/** 支付参数体(通常用于发起异步支付的参数) */
private String payBody;
}

View File

@@ -0,0 +1,128 @@
package cn.bootx.platform.daxpay.demo.service;
import cn.bootx.platform.common.core.exception.BizException;
import cn.bootx.platform.common.spring.util.WebServletUtil;
import cn.bootx.platform.daxpay.demo.param.CashierSimplePayParam;
import cn.bootx.platform.daxpay.demo.result.PayOrderResult;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayStatusEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.model.pay.PayOrderModel;
import cn.bootx.platform.daxpay.sdk.model.pay.QueryPayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import cn.bootx.platform.daxpay.sdk.param.channel.AliPayParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
import cn.bootx.platform.daxpay.sdk.param.pay.QueryPayOrderParam;
import cn.bootx.platform.daxpay.sdk.param.pay.SimplePayParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 支付结算台服务类
* @author xxm
* @since 2024/2/8
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DaxPayCashierService {
/**
* 结算台简单支付, 创建支付订单并拉起支付
*/
public PayOrderResult simplePayCashier(CashierSimplePayParam param){
// 将参数转换为简单支付参数
SimplePayParam simplePayParam = new SimplePayParam();
simplePayParam.setBusinessNo(param.getBusinessNo());
int amount = param.getAmount()
.multiply(BigDecimal.valueOf(100))
.intValue();
simplePayParam.setTitle(param.getTitle());
simplePayParam.setAmount(amount);
simplePayParam.setChannel(param.getChannel());
simplePayParam.setPayWay(param.getPayWay());
// 支付宝通道
if (Objects.equals(PayChannelEnum.ALI.getCode(), param.getChannel())){
// 付款码支付
if (Objects.equals(PayWayEnum.BARCODE.getCode(), param.getPayWay())){
AliPayParam aliPayParam = new AliPayParam();
aliPayParam.setAuthCode(param.getAuthCode());
simplePayParam.setChannelParam(aliPayParam);
}
}
// 微信通道
if (Objects.equals(PayChannelEnum.WECHAT.getCode(), param.getChannel())){
WeChatPayParam wechatPayParam = new WeChatPayParam();
// 付款码支付
if (Objects.equals(PayWayEnum.BARCODE.getCode(), param.getPayWay())){
wechatPayParam.setAuthCode(param.getAuthCode());
simplePayParam.setChannelParam(wechatPayParam);
}
// 微信jsapi 方式支付
if (Objects.equals(PayWayEnum.JSAPI.getCode(), param.getPayWay())){
wechatPayParam.setOpenId(param.getOpenId());
simplePayParam.setChannelParam(wechatPayParam);
}
}
String ip = Optional.ofNullable(WebServletUtil.getRequest())
.map(ServletUtil::getClientIP)
.orElse("127.0.0.1");
simplePayParam.setClientIp(ip);
// 异步回调地址
simplePayParam.setNotifyUrl("http://localhost:9000");
// 同步回调地址
simplePayParam.setReturnUrl("http://localhost:9000");
// 发起支付
DaxPayResult<PayOrderModel> execute = DaxPayKit.execute(simplePayParam);
// 判断是否支付成功
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
// 判断是否发起支付成功
PayOrderModel payOrderModel = execute.getData();
// 状态 支付中或支付完成返回
List<String> list = Arrays.asList(PayStatusEnum.PROGRESS.getCode(), PayStatusEnum.SUCCESS.getCode());
if (list.contains(payOrderModel.getStatus())){
PayOrderResult payOrderResult = new PayOrderResult();
BeanUtil.copyProperties(payOrderModel, payOrderResult);
return payOrderResult;
} else {
throw new BizException("订单状态异常,无法进行支付");
}
}
/**
* 订单查询接口
*/
public boolean queryPayOrder(Long paymentId){
QueryPayOrderParam queryPayOrderParam = new QueryPayOrderParam();
queryPayOrderParam.setPaymentId(paymentId);
DaxPayResult<QueryPayOrderModel> execute = DaxPayKit.execute(queryPayOrderParam);
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
QueryPayOrderModel data = execute.getData();
String status = data.getStatus();
if (Objects.equals(status, PayStatusEnum.PROGRESS.getCode())){
return false;
}
else if (Objects.equals(status, PayStatusEnum.SUCCESS.getCode())){
return true;
} else {
throw new BizException("订单状态不是支付中或支付完成,请检查");
}
}
}

View File

@@ -0,0 +1 @@
cn.bootx.platform.daxpay.demo.DaxPaySingleDemoApp

View File

@@ -51,23 +51,34 @@
</properties>
<dependencies>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- 日志框架 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
<scope>provided</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- 注解标识 -->
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>13.0</version>
<scope>provided</scope>
</dependency>
<!-- 测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@@ -8,7 +8,7 @@ import lombok.Setter;
import lombok.ToString;
/**
* 支付相应对象
* 发起支付后响应对象
* @author xxm
* @since 2024/2/2
*/

View File

@@ -8,6 +8,7 @@ import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
@@ -16,12 +17,14 @@ import java.util.Objects;
* @author xxm
* @since 2024/2/2
*/
@Slf4j
@UtilityClass
public class DaxPayKit {
private DaxPayConfig config;
public void initConfig(DaxPayConfig config){
log.debug("DaxPayKit初始化...");
DaxPayKit.config = config;
}
@@ -54,12 +57,14 @@ public class DaxPayKit {
}
}
String data = JSONUtil.toJsonStr(request);
log.debug("请求参数:{}", data);
String path = config.getServiceUrl() + request.path();
HttpResponse execute = HttpUtil.createPost(path)
.body(data, ContentType.JSON.getValue())
.timeout(config.getReqTimeout())
.execute();
String body = execute.body();
log.debug("响应参数:{}", body);
return request.toModel(body);
}
}

View File

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

View File

@@ -0,0 +1,9 @@
package cn.bootx.platform.daxpay.sdk.param;
/**
* 标示字段可以转换为MAP进行排序
* @author xxm
* @since 2024/2/8
*/
public interface SortMapParam {
}

View File

@@ -15,7 +15,7 @@ import lombok.EqualsAndHashCode;
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class QueryPayParam extends DaxPayRequest<QueryPayOrderModel> {
public class QueryPayOrderParam extends DaxPayRequest<QueryPayOrderModel> {
/** 支付ID */
private Long paymentId;

View File

@@ -15,6 +15,7 @@ import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
/**
* 简单支付请求参数
@@ -27,9 +28,11 @@ import lombok.ToString;
public class SimplePayParam extends DaxPayRequest<PayOrderModel> {
/** 业务号 */
@NotNull
private String businessNo;
/** 支付标题 */
@NotNull
private String title;
/** 支付描述 */
@@ -42,17 +45,21 @@ public class SimplePayParam extends DaxPayRequest<PayOrderModel> {
private String quitUrl;
/**
* 支付通道
* @see PayChannelEnum#getCode()
*/
@NotNull
private String channel;
/**
* 支付方式编码
* @see PayWayEnum#getCode()
*/
@NotNull
private String payWay;
/** 支付金额 */
@NotNull
private Integer amount;
/**

View File

@@ -15,7 +15,7 @@ import lombok.EqualsAndHashCode;
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class QueryRefundParam extends DaxPayRequest<QueryRefundOrderModel> {
public class QueryRefundOrderParam extends DaxPayRequest<QueryRefundOrderModel> {
/** 退款ID */
private Long refundId;

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.daxpay.sdk.util;
import cn.bootx.platform.daxpay.sdk.param.SortMapParam;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONUtil;
@@ -94,9 +95,8 @@ public class PaySignUtil {
}
}
/**
* 把所有元素排序, 并拼接成字符, 用于签名
* 把所有元素排序, 并拼接成字符, 用于签名, 同时会过滤掉 " 和 \ 字符
*/
public static String createLinkString(Map<String, String> params) {
String connStr = "&";
@@ -118,10 +118,12 @@ public class PaySignUtil {
.append(connStr);
}
}
return content.toString();
String s = content.toString();
s = StrUtil.replace(s,"\\","");
s = StrUtil.replace(s,"\"","");
return s;
}
/**
* 生成16进制 MD5 字符串
*

View File

@@ -3,7 +3,7 @@ package cn.bootx.platform.daxpay.sdk.query;
import cn.bootx.platform.daxpay.sdk.model.pay.QueryPayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayConfig;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import cn.bootx.platform.daxpay.sdk.param.pay.QueryPayParam;
import cn.bootx.platform.daxpay.sdk.param.pay.QueryPayOrderParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import org.junit.Before;
import org.junit.Test;
@@ -27,7 +27,7 @@ public class QueryPayOrderTest {
@Test
public void testPay() {
QueryPayParam param = new QueryPayParam();
QueryPayOrderParam param = new QueryPayOrderParam();
param.setBusinessNo("2");

View File

@@ -3,7 +3,7 @@ package cn.bootx.platform.daxpay.sdk.query;
import cn.bootx.platform.daxpay.sdk.model.refund.QueryRefundOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayConfig;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import cn.bootx.platform.daxpay.sdk.param.refund.QueryRefundParam;
import cn.bootx.platform.daxpay.sdk.param.refund.QueryRefundOrderParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import org.junit.Before;
import org.junit.Test;
@@ -27,7 +27,7 @@ public class QueryRefundOrderTest {
@Test
public void testPay() {
QueryRefundParam param = new QueryRefundParam();
QueryRefundOrderParam param = new QueryRefundOrderParam();
param.setRefundId(1755263825769361408L);

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.daxpay.sdk.util;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
import cn.bootx.platform.daxpay.sdk.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.sdk.param.pay.PayParam;
import lombok.extern.slf4j.Slf4j;
@@ -43,7 +44,42 @@ public class PayParamSignTest {
Map<String, String> map = PaySignUtil.toMap(param);
log.info("转换为有序MAP后的内容: {}",map);
String data = PaySignUtil.createLinkString(map);
log.info("将MAP拼接字符串: {}",data);
log.info("将MAP拼接字符串, 并过滤掉特殊字符: {}",data);
String sign = "123456";
data += "&sign="+sign;
data = data.toUpperCase();
log.info("添加秘钥并转换为大写的字符串: {}",data);
log.info("MD5: {}",PaySignUtil.md5(data));
log.info("HmacSHA256: {}",PaySignUtil.hmacSha256(data,sign));
}
/**
* 多层嵌套对象签名测试
*/
@Test
public void sign2(){
PayParam param = new PayParam();
param.setClientIp("127.0.0.1");
param.setBusinessNo("P0002");
param.setTitle("测试接口支付");
PayChannelParam payChannelParam = new PayChannelParam();
payChannelParam.setChannel(PayChannelEnum.WECHAT.getCode());
payChannelParam.setWay(PayWayEnum.QRCODE.getCode());
payChannelParam.setAmount(1);
WeChatPayParam weChatPayParam = new WeChatPayParam();
weChatPayParam.setOpenId("6688812");
weChatPayParam.setAuthCode("123456");
payChannelParam.setChannelParam(weChatPayParam);
List<PayChannelParam> payChannels = Collections.singletonList(payChannelParam);
param.setPayChannels(payChannels);
Map<String, String> map = PaySignUtil.toMap(param);
log.info("转换为有序MAP后的内容: {}",map);
String data = PaySignUtil.createLinkString(map).replaceAll("\\\"","").replaceAll("\"","");
log.info("将MAP拼接字符串, 并过滤掉特殊字符: {}",data);
String sign = "123456";
data += "&sign="+sign;
data = data.toUpperCase();

View File

@@ -16,8 +16,9 @@
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-service</artifactId>
<version>${dax.version}</version>
<version>${daxpay.version}</version>
</dependency>
<!-- 安全认证 -->
<dependency>
<groupId>cn.bootx.platform</groupId>

View File

@@ -11,6 +11,7 @@ import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Map;
/**
* 同意下单支付方式参数
@@ -48,5 +49,5 @@ public class PayChannelParam {
* @see WalletPayParam
*/
@Schema(description = "附加支付参数")
private String channelParam;
private Map<String,Object> channelParam;
}

View File

@@ -16,6 +16,7 @@ import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 简单下单参数
@@ -71,7 +72,7 @@ public class SimplePayParam extends PayCommonParam{
* @see WalletPayParam
*/
@Schema(description = "附加通道支付参数")
private String channelParam;
private Map<String, Object> channelParam;
}

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.daxpay.util;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONUtil;
@@ -99,7 +100,7 @@ public class PaySignUtil {
/**
* 把所有元素排序, 并拼接成字符, 用于签名
* 把所有元素排序, 并拼接成字符, 用于签名, 同时会过滤掉 " 和 \ 字符
*/
public static String createLinkString(Map<String, String> params) {
String connStr = "&";
@@ -121,7 +122,10 @@ public class PaySignUtil {
.append(connStr);
}
}
return content.toString();
String s = content.toString();
s = StrUtil.replace(s,"\\","");
s = StrUtil.replace(s,"\"","");
return s;
}

View File

@@ -15,7 +15,7 @@
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-service</artifactId>
<version>${dax.version}</version>
<version>${daxpay.version}</version>
</dependency>
</dependencies>

View File

@@ -70,7 +70,7 @@
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-core</artifactId>
<version>${dax.version}</version>
<version>${daxpay.version}</version>
</dependency>
<!-- 支付宝支付 -->
@@ -109,6 +109,7 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- 分布式锁 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>

View File

@@ -1,5 +1,8 @@
package cn.bootx.platform.daxpay.service.core.channel.voucher.service;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.channel.VoucherPayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.service.code.VoucherCode;
import cn.bootx.platform.daxpay.service.core.channel.voucher.dao.VoucherLogManager;
import cn.bootx.platform.daxpay.service.core.channel.voucher.dao.VoucherManager;
@@ -9,17 +12,17 @@ import cn.bootx.platform.daxpay.service.core.channel.voucher.entity.VoucherLog;
import cn.bootx.platform.daxpay.service.core.channel.voucher.entity.VoucherPayOrder;
import cn.bootx.platform.daxpay.service.core.channel.voucher.entity.VoucherRecord;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.channel.VoucherPayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
/**
* 储值卡支付
*
@@ -46,9 +49,10 @@ public class VoucherPayService {
VoucherPayParam voucherPayParam;
try {
// 储值卡参数验证
String extraParamsJson = payChannelParam.getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
voucherPayParam = JSONUtil.toBean(extraParamsJson, VoucherPayParam.class);
Map<String, Object> channelParam = payChannelParam.getChannelParam();
if (CollUtil.isNotEmpty(channelParam)) {
voucherPayParam = BeanUtil.toBean(channelParam, VoucherPayParam.class);
} else {
throw new PayFailureException("储值卡支付参数错误");
}

View File

@@ -2,6 +2,9 @@ package cn.bootx.platform.daxpay.service.core.order.pay.builder;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.param.pay.PayParam;
import cn.bootx.platform.daxpay.result.pay.PayResult;
import cn.bootx.platform.daxpay.service.common.context.AsyncPayLocal;
import cn.bootx.platform.daxpay.service.common.context.NoticeLocal;
import cn.bootx.platform.daxpay.service.common.context.PlatformLocal;
@@ -9,20 +12,15 @@ import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrderExtra;
import cn.bootx.platform.daxpay.entity.RefundableInfo;
import cn.bootx.platform.daxpay.param.pay.PayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.result.pay.PayResult;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.experimental.UtilityClass;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 支付对象构建器
@@ -91,28 +89,19 @@ public class PayBuilder {
* 构建订单关联通道信息
*/
public PayChannelOrder buildPayChannelOrder(PayChannelParam channelParam) {
return new PayChannelOrder()
PayChannelOrder payChannelOrder = new PayChannelOrder()
.setAsync(PayChannelEnum.ASYNC_TYPE_CODE.contains(channelParam.getChannel()))
.setChannel(channelParam.getChannel())
.setPayWay(channelParam.getWay())
.setAmount(channelParam.getAmount())
.setRefundableBalance(channelParam.getAmount())
.setChannelExtra(channelParam.getChannelParam());
}
.setRefundableBalance(channelParam.getAmount());
/**
* 构建支付订单的可退款信息列表
*/
@Deprecated
private List<RefundableInfo> buildRefundableInfo(List<PayChannelParam> payChannelParamList) {
if (CollectionUtil.isEmpty(payChannelParamList)) {
return Collections.emptyList();
Map<String, Object> cp = channelParam.getChannelParam();
if (CollUtil.isNotEmpty(cp)){
payChannelOrder.setChannelExtra(JSONUtil.toJsonStr(cp));
}
return payChannelParamList.stream()
.map(o-> new RefundableInfo()
.setChannel(o.getChannel())
.setAmount(o.getAmount()))
.collect(Collectors.toList());
return payChannelOrder;
}

View File

@@ -11,6 +11,8 @@ import cn.bootx.platform.daxpay.service.core.order.pay.convert.PayOrderConvert;
import cn.bootx.platform.daxpay.service.dto.order.pay.PayChannelOrderDto;
import cn.bootx.table.modify.annotation.DbColumn;
import cn.bootx.table.modify.annotation.DbTable;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -66,6 +68,7 @@ public class PayChannelOrder extends MpCreateEntity implements EntityBaseFunctio
* @see WalletPayParam
*/
@DbColumn(comment = "附加支付参数")
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelExtra;
/**

View File

@@ -12,6 +12,7 @@ import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundChannelOrder;
import cn.bootx.platform.daxpay.service.dto.order.pay.PayChannelOrderDto;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -19,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -57,6 +59,12 @@ public class PayChannelOrderService {
PayStatusEnum payStatus = asyncPayInfo.isPayComplete() ? PayStatusEnum.SUCCESS : PayStatusEnum.PROGRESS;
// 判断新发起的
Optional<PayChannelOrder> payOrderChannelOpt = channelOrderManager.findByPaymentIdAndChannel(payOrder.getId(), payChannelParam.getChannel());
// 扩展信息处理
Map<String, Object> channelParam = payChannelParam.getChannelParam();
String channelParamStr = null;
if (Objects.nonNull(channelParam)){
channelParamStr = JSONUtil.toJsonStr(channelParam);
}
if (!payOrderChannelOpt.isPresent()){
PayChannelOrder payChannelOrder = new PayChannelOrder();
// 替换原有的的支付通道信息
@@ -68,7 +76,7 @@ public class PayChannelOrderService {
.setAmount(payChannelParam.getAmount())
.setRefundableBalance(payChannelParam.getAmount())
.setPayTime(LocalDateTime.now())
.setChannelExtra(payChannelParam.getChannelParam())
.setChannelExtra(channelParamStr)
.setStatus(payStatus.getCode());
channelOrderManager.deleteByPaymentIdAndAsync(payOrder.getId());
channelOrderManager.save(payChannelOrder);
@@ -77,7 +85,7 @@ public class PayChannelOrderService {
payOrderChannelOpt.get()
.setPayWay(payChannelParam.getWay())
.setPayTime(LocalDateTime.now())
.setChannelExtra(payChannelParam.getChannelParam())
.setChannelExtra(channelParamStr)
.setStatus(payStatus.getCode());
channelOrderManager.updateById(payOrderChannelOpt.get());
}

View File

@@ -10,13 +10,15 @@ import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfig
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayService;
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayChannelOrderService;
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.Map;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
@@ -52,9 +54,9 @@ public class AliPayStrategy extends AbsPayStrategy {
public void doBeforePayHandler() {
try {
// 支付宝参数验证
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
this.aliPayParam = JSONUtil.toBean(extraParamsJson, AliPayParam.class);
Map<String, Object> channelParam = this.getPayChannelParam().getChannelParam();
if (CollUtil.isNotEmpty(channelParam)) {
this.aliPayParam = BeanUtil.toBean(channelParam, AliPayParam.class);
}
else {
this.aliPayParam = new AliPayParam();

View File

@@ -1,23 +1,24 @@
package cn.bootx.platform.daxpay.service.core.payment.pay.strategy;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.code.WalletCode;
import cn.bootx.platform.daxpay.service.core.channel.wallet.entity.Wallet;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayOrderService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletQueryService;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.exception.waller.WalletBannedException;
import cn.bootx.platform.daxpay.exception.waller.WalletLackOfBalanceException;
import cn.bootx.platform.daxpay.service.code.WalletCode;
import cn.bootx.platform.daxpay.service.core.channel.wallet.entity.Wallet;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayOrderService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletQueryService;
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
import cn.bootx.platform.daxpay.service.param.channel.wallet.WalletPayParam;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Objects;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
@@ -54,9 +55,9 @@ public class WalletPayStrategy extends AbsPayStrategy {
WalletPayParam walletPayParam = new WalletPayParam();
try {
// 钱包参数验证
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
walletPayParam = JSONUtil.toBean(extraParamsJson, WalletPayParam.class);
Map<String, Object> channelParam = this.getPayChannelParam().getChannelParam();
if (CollUtil.isNotEmpty(channelParam)) {
walletPayParam = BeanUtil.toBean(channelParam, WalletPayParam.class);
}
} catch (JSONException e) {
throw new PayFailureException("支付参数错误");

View File

@@ -2,7 +2,6 @@ package cn.bootx.platform.daxpay.service.core.payment.pay.strategy;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayAmountAbnormalException;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayConfigService;
@@ -10,13 +9,14 @@ import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPaySer
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayChannelOrderService;
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
import cn.bootx.platform.daxpay.service.param.channel.wechat.WeChatPayParam;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.Map;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
@@ -53,18 +53,12 @@ public class WeChatPayStrategy extends AbsPayStrategy {
*/
@Override
public void doBeforePayHandler() {
try {
// 微信参数验证
String extraParamsJson = this.getPayChannelParam().getChannelParam();
if (StrUtil.isNotBlank(extraParamsJson)) {
this.weChatPayParam = JSONUtil.toBean(extraParamsJson, WeChatPayParam.class);
}
else {
this.weChatPayParam = new WeChatPayParam();
}
}
catch (JSONException e) {
throw new PayFailureException("支付参数错误");
// 微信参数验证
Map<String, Object> channelParam = this.getPayChannelParam().getChannelParam();
if (CollUtil.isNotEmpty(channelParam)) {
this.weChatPayParam = BeanUtil.toBean(channelParam, WeChatPayParam.class);
} else {
this.weChatPayParam = new WeChatPayParam();
}
// 检查金额

View File

@@ -54,6 +54,10 @@ public class PayExpiredTimeTask {
paySyncParam.setPaymentId(paymentId);
paySyncService.sync(paySyncParam);
} catch (Exception e) {
// 如果是未查询到取消支付订单, 则删除这个任务
if (Objects.equals("未查询到支付订单", e.getMessage())){
repository.removeKeys(expiredKey);
}
log.error("超时取消任务 异常", e);
} finally {
lockTemplate.releaseLock(lock);

View File

@@ -1,17 +1,18 @@
package cn.bootx.platform.daxpay.core.payment.common.service;
package cn.bootx.platform.daxpay.core.util;
import cn.bootx.platform.daxpay.param.channel.AliPayParam;
import cn.bootx.platform.daxpay.param.channel.WeChatPayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.param.pay.PayParam;
import cn.bootx.platform.daxpay.util.PaySignUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.bean.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 支付签名服务
@@ -19,7 +20,7 @@ import java.util.List;
* @since 2023/12/24
*/
@Slf4j
class PaymentSignServiceTest {
class PayParamSignTest {
@Test
void toMap() {
@@ -40,7 +41,7 @@ class PaymentSignServiceTest {
p1.setWay("wx_app");
AliPayParam aliPayParam = new AliPayParam();
aliPayParam.setAuthCode("6688");
p1.setChannelParam(JSONUtil.toJsonStr(aliPayParam));
p1.setChannelParam(BeanUtil.beanToMap(aliPayParam,false,true));
PayChannelParam p2 = new PayChannelParam();
p2.setAmount(100);
@@ -49,17 +50,21 @@ class PaymentSignServiceTest {
WeChatPayParam weChatPayParam = new WeChatPayParam();
weChatPayParam.setOpenId("w2qsz2xawe3gbhyyff28fs01fd");
weChatPayParam.setAuthCode("8866");
p2.setChannelParam(JSONUtil.toJsonStr(weChatPayParam));
p2.setChannelParam(BeanUtil.beanToMap(aliPayParam,false,true));
List<PayChannelParam> payWays = Arrays.asList(p1, p2);
payParam.setPayChannels(payWays);
// 签名
String sign = "123456";
String md5Sign = PaySignUtil.md5Sign(payParam, sign);
String hmacSha256Sign = PaySignUtil.hmacSha256Sign(payParam, sign);
log.info("MD5: {}",md5Sign);
log.info("HmacSHA256: {}", hmacSha256Sign);
Map<String, String> map = PaySignUtil.toMap(payParam);
log.info("转换为有序MAP后的内容: {}",map);
String data = PaySignUtil.createLinkString(map);
log.info("将MAP拼接字符串, 并过滤掉特殊字符: {}",data);
String sign = "123456";
data += "&sign="+sign;
data = data.toUpperCase();
log.info("添加秘钥并转换为大写的字符串: {}",data);
log.info("MD5: {}",PaySignUtil.md5(data));
log.info("HmacSHA256: {}",PaySignUtil.hmacSha256(data,sign));
}
}

View File

@@ -16,13 +16,13 @@
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-gateway</artifactId>
<version>${dax.version}</version>
<version>${daxpay.version}</version>
</dependency>
<!-- 管理平台 -->
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-admin</artifactId>
<version>${dax.version}</version>
<version>${daxpay.version}</version>
</dependency>
<!-- 定时任务 -->
<dependency>
@@ -35,7 +35,11 @@
<artifactId>common-starter-code-gen</artifactId>
</dependency>
<!-- 支付演示模块 -->
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>daxpay-single-demo</artifactId>
<version>${daxpay.version}</version>
</dependency>
</dependencies>

View File

@@ -72,6 +72,7 @@ bootx:
base-packages:
"[支付网关API]": cn.bootx.platform.daxpay.gateway
"[支付管理接口]": cn.bootx.platform.daxpay.admin
"[支付演示模块]": cn.bootx.platform.daxpay.demo
"[BootxPlatform接口]":
- cn.bootx.platform.common
- cn.bootx.platform.starter
@@ -150,3 +151,11 @@ dromara:
enableStorage: true
# 文件存储路径
storage-path: D:/data/files/
# 支付系统配置
dax-pay:
# 演示模块
demo:
# 网关地址
server-url: http://localhost:9000
# 签名秘钥
sign-secret: 123456

View File

@@ -20,6 +20,7 @@
<modules>
<module>daxpay-single</module>
<module>daxpay-single-sdk</module>
<module>daxpay-single-demo</module>
</modules>
<properties>
@@ -29,7 +30,7 @@
<!-- 二方库版本 -->
<bootx-platform.version>1.3.6.1</bootx-platform.version>
<dax.version>2.0.0</dax.version>
<daxpay.version>2.0.0</daxpay.version>
<!-- 三方库 -->
<slf4j.version>1.7.30</slf4j.version>
<redisson.version>3.16.8</redisson.version>