feat 支付流程改为先落库, 后支付

This commit is contained in:
xxm1995
2024-03-21 23:23:21 +08:00
committed by 喵呀
parent 15314197e9
commit 012e08c37a
18 changed files with 456 additions and 49 deletions

View File

@@ -4,6 +4,7 @@ import cn.bootx.platform.common.core.annotation.IgnoreAuth;
import cn.bootx.platform.common.core.exception.BizException;
import cn.bootx.platform.common.core.rest.Res;
import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayTransferService;
import cn.hutool.core.thread.ThreadUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
@@ -27,6 +28,7 @@ import java.util.Objects;
@RequiredArgsConstructor
public class TestController {
private final LockTemplate lockTemplate;
private final AliPayTransferService aliPayTransferService;
@Operation(summary = "锁测试1")
@GetMapping("/lock1")
@@ -54,7 +56,15 @@ public class TestController {
@Operation(summary = "微信回调测试")
@GetMapping(value = {"/wxcs/","wxcs"})
public String wxcs(){
return "ok";
}
@IgnoreAuth
@Operation(summary = "支付宝回调测试")
@GetMapping("/queryAmount")
public String alipay(){
aliPayTransferService.queryAccountAmount();
return "ok";
}

View File

@@ -0,0 +1,40 @@
package cn.bootx.platform.daxpay.service.core.channel.alipay.service;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
import com.alipay.api.domain.AlipayFundAccountQueryModel;
import com.alipay.api.response.AlipayFundAccountQueryResponse;
import com.ijpay.alipay.AliPayApi;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class AliPayTransferService {
private final AliPayConfigService payConfigService;
/**
* 余额查询接口
*/
@SneakyThrows
public void queryAccountAmount() {
AliPayConfig config = payConfigService.getAndCheckConfig();
payConfigService.initConfig(config);
AlipayFundAccountQueryModel model = new AlipayFundAccountQueryModel();
model.setAccountType("ACCTRANS_ACCOUNT");
model.setAlipayUserId(config.getAppId());
AlipayFundAccountQueryResponse response = AliPayApi.accountQueryToResponse(model, null);
System.out.println(response);
}
/**
* 转账接口
*/
@SneakyThrows
public void transfer() {
}
}

View File

@@ -0,0 +1,15 @@
package cn.bootx.platform.daxpay.service.core.order.transfer.convert;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
*
* @author xxm
* @since 2024/3/21
*/
@Mapper
public interface TransferOrderConvert {
TransferOrderConvert CONVERT = Mappers.getMapper(TransferOrderConvert.class);
}

View File

@@ -0,0 +1,18 @@
package cn.bootx.platform.daxpay.service.core.order.transfer.dao;
import cn.bootx.platform.common.mybatisplus.impl.BaseManager;
import cn.bootx.platform.daxpay.service.core.order.transfer.entity.TransferOrder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
/**
*
* @author xxm
* @since 2024/3/21
*/
@Slf4j
@Repository
@RequiredArgsConstructor
public class TransferOrderManager extends BaseManager<TransferOrderMapper, TransferOrder> {
}

View File

@@ -0,0 +1,14 @@
package cn.bootx.platform.daxpay.service.core.order.transfer.dao;
import cn.bootx.platform.daxpay.service.core.order.transfer.entity.TransferOrder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
*
* @author xxm
* @since 2024/3/21
*/
@Mapper
public interface TransferOrderMapper extends BaseMapper<TransferOrder> {
}

View File

@@ -0,0 +1,49 @@
package cn.bootx.platform.daxpay.service.core.order.transfer.entity;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.table.modify.annotation.DbTable;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 转账订单
* @author xxm
* @since 2024/3/21
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@DbTable(comment = "转账订单")
@TableName("pay_transfer_order")
public class TransferOrder extends MpBaseEntity {
/** 业务号 */
private String outTradeNo;
/**
* 支付通道
* @see PayChannelEnum
*/
private String channel;
/** 金额 */
private Integer amount;
/** 状态 */
private String status;
/** 付款方 */
private String payer;
/** 收款方 */
private String payee;
/** 成功时间 */
private LocalDateTime successTime;
}

View File

@@ -0,0 +1,16 @@
package cn.bootx.platform.daxpay.service.core.order.transfer.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 转正订单服务
* @author xxm
* @since 2024/3/21
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TransferOrderService {
}

View File

@@ -0,0 +1,25 @@
package cn.bootx.platform.daxpay.service.core.payment.pay.result;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* 策略和订单
* @author xxm
* @since 2024/3/21
*/
@Getter
@AllArgsConstructor
public class StrategyAndOrder {
/** 支付订单 */
private final PayOrder order;
/** 支付策略集合 */
private final List<AbsPayStrategy> strategies;
}

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.daxpay.service.core.payment.pay.service;
import cn.bootx.platform.common.core.exception.RepetitiveOperationException;
import cn.bootx.platform.common.core.function.CollectorsFunction;
import cn.bootx.platform.daxpay.exception.pay.PayUnsupportedMethodException;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.param.pay.PayParam;
@@ -9,6 +10,8 @@ import cn.bootx.platform.daxpay.result.pay.PayResult;
import cn.bootx.platform.daxpay.service.common.context.PayLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.pay.builder.PayBuilder;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
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.service.core.order.pay.service.PayOrderService;
@@ -19,20 +22,24 @@ import cn.bootx.platform.daxpay.util.PayUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.bootx.platform.daxpay.code.PayStatusEnum.*;
import static cn.bootx.platform.daxpay.code.PayStatusEnum.SUCCESS;
/**
@@ -50,6 +57,8 @@ public class PayService {
private final PayAssistService payAssistService;
private final PayChannelOrderManager channelOrderManager;
private final ClientNoticeService clientNoticeService;
private final LockTemplate lockTemplate;
@@ -62,6 +71,8 @@ public class PayService {
* 注意:
* 组合支付在非第一次支付的时候,只对新传入的异步支付通道进行处理,该通道的价格使用第一次发起的价格,旧的同步支付如果传入后也不做处理,
* 支付单中支付通道列表将会为 旧有的同步支付+新传入的异步支付方式(在具体支付实现中处理)
*
* 订单数据会先进行入库, 才会进行发起支付, 在调用各通道支付之前发生错误, 数据不会入库
*/
@Transactional(rollbackFor = Exception.class)
public PayResult pay(PayParam payParam) {
@@ -81,18 +92,38 @@ public class PayService {
// 初始化上下文
payAssistService.initPayContext(payOrder, payParam);
// 异步支付且非第一次支付
if (Objects.nonNull(payOrder) && payOrder.isAsyncPay()) {
return this.paySyncNotFirst(payParam, payOrder);
// 订单为空, 创建支付订单和通道支付订单
if (Objects.isNull(payOrder)){
// 预支付处理, 无论请求成功还是失败, 各种订单对象都会保存
List<AbsPayStrategy> strategies = SpringUtil.getBean(this.getClass()).prePay(payParam);
// 执行首次支付逻辑
return this.onePay(strategies);
} else {
// 第一次发起支付或同步支付
return this.firstPay(payParam, payOrder);
// 分为同步通道全部支付成功和所有支付都是支付中状态(上次支付失败)
return this.towPay(payParam, payOrder);
}
} finally {
lockTemplate.releaseLock(lock);
}
}
/**
* 组合支付分为同步通道全部支付成功和所有支付都是支付中状态(上次支付失败)
* 单通道支付不需要区分上次支付发起是否成功
*/
private PayResult towPay(PayParam payParam, PayOrder payOrder) {
// 判断是否组合支付且同时包含异步支付
if (payOrder.isCombinationPay()&&payOrder.isAsyncPay()) {
// 同步支付通道全部支付成功, 单独走异步支付支付流程
this.onlyAsyncPay(payParam, payOrder);
// 通道订单全部支付中, 相当于上次支付异常, 重新走支付流程
return this.allPay(payParam, payOrder);
} else {
// 单通道支付走标准支付流程
return this.allPay(payParam, payOrder);
}
}
/**
* 简单下单, 可以视为不支持组合支付的下单接口
*/
@@ -111,14 +142,46 @@ public class PayService {
return this.pay(payParam);
}
/**
* 预支付处理, 无论请求成功还是失败, 各种订单对象都会保存
* 1. 校验参数
* 2. 创建支付订单/通道支付订单/扩展信息
* 3, 返回数据
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public List<AbsPayStrategy> prePay(PayParam payParam){
// 价格检测
PayUtil.validationAmount(payParam.getPayChannels());
// 创建支付订单和扩展记录并返回支付订单对象
PayOrder payOrder = payAssistService.createPayOrder(payParam);
// 获取支付方式,通过工厂生成对应的策略组
List<AbsPayStrategy> strategies = PayStrategyFactory.createAsyncLast(payParam.getPayChannels());
if (CollectionUtil.isEmpty(strategies)) {
throw new PayUnsupportedMethodException();
}
// 初始化支付的参数
for (AbsPayStrategy strategy : strategies) {
strategy.initPayParam(payOrder, payParam);
}
// 执行支付前处理动作
strategies.forEach(AbsPayStrategy::doBeforePayHandler);
// 执行通道支付通道订单的生成
strategies.forEach(AbsPayStrategy::generateChannelOrder);
// 保存通道支付订单
payAssistService.savePayChannelOrder(strategies);
return strategies;
}
/**
* 发起的第一次支付请求(同步/异步)
*/
private PayResult firstPay(PayParam payParam, PayOrder payOrder) {
// 1. 已经发起过支付情况直接返回支付结果
if (Objects.nonNull(payOrder)) {
return PayBuilder.buildPayResultByPayOrder(payOrder);
}
// 2. 价格检测
PayUtil.validationAmount(payParam.getPayChannels());
@@ -127,48 +190,40 @@ public class PayService {
payOrder = payAssistService.createPayOrder(payParam);
// 4. 调用支付方法进行发起支付
this.firstPayHandler(payParam, payOrder);
this.allPay(payParam, payOrder);
// 5. 返回支付结果
return PayBuilder.buildPayResultByPayOrder(payOrder);
}
/**
* 执行第一次发起支付的方法
* 执行首次支付操作
*/
private void firstPayHandler(PayParam payParam, PayOrder payOrder) {
// 1.获取支付方式,通过工厂生成对应的策略组
List<AbsPayStrategy> strategies = PayStrategyFactory.createAsyncLast(payParam.getPayChannels());
if (CollectionUtil.isEmpty(strategies)) {
throw new PayUnsupportedMethodException();
public PayResult onePay(List<AbsPayStrategy> strategies){
PayLocal payInfo = PaymentContextLocal.get().getPayInfo();
// 支付操作
try {
strategies.forEach(AbsPayStrategy::doPayHandler);
} catch (Exception e) {
// 记录错误原因
PayOrderExtra payOrderExtra = payInfo.getPayOrderExtra();
payOrderExtra.setErrorMsg(e.getMessage());
throw e;
}
PayOrder payOrder = payInfo.getPayOrder();
// 2.初始化支付的参数
for (AbsPayStrategy strategy : strategies) {
strategy.initPayParam(payOrder, payParam);
}
// 3.1 执行支付前处理动作
strategies.forEach(AbsPayStrategy::doBeforePayHandler);
// 3.2 执行通道支付单的生成动作
strategies.forEach(AbsPayStrategy::generateChannelOrder);
// 4.1 支付操作
strategies.forEach(AbsPayStrategy::doPayHandler);
// 4.2 支付调用成功操作, 进行扣款、创建记录类类似的操作
// 支付调用成功操作, 进行扣款、创建记录类类似的操作
strategies.forEach(AbsPayStrategy::doSuccessHandler);
// 4.3 保存通道支付订单
payAssistService.savePayChannelOrder(strategies);
// 5.1 如果没有异步支付, 直接进行订单完成处理
if (PayUtil.isNotSync(payParam.getPayChannels())) {
// 如果没有异步支付, 直接进行订单完成处理
if (!payOrder.isAsyncPay()) {
// 修改支付订单状态为成功
payOrder.setStatus(SUCCESS.getCode())
.setPayTime(LocalDateTime.now());
payOrderService.updateById(payOrder);
}
// 5.2 如果异步支付完成, 进行订单完成处理, 同时发送回调消息
// 如果异步支付完成, 进行订单完成处理, 同时发送回调消息
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();
if (asyncPayInfo.isPayComplete()) {
payOrder.setGatewayOrderNo(asyncPayInfo.getGatewayOrderNo())
@@ -179,22 +234,78 @@ public class PayService {
// 如果支付完成 发送通知
if (Objects.equals(payOrder.getStatus(), SUCCESS.getCode())){
PayLocal payInfo = PaymentContextLocal.get().getPayInfo();
clientNoticeService.registerPayNotice(payOrder, payInfo.getPayOrderExtra(), payInfo.getPayChannelOrders());
}
// 组装返回参数
return PayBuilder.buildPayResultByPayOrder(payOrder);
}
/**
* 执行所有的支付方法
*/
private PayResult allPay(PayParam payParam, PayOrder payOrder) {
PayLocal payInfo = PaymentContextLocal.get().getPayInfo();
// 获取支付方式,通过工厂生成对应的策略组
List<AbsPayStrategy> strategies = PayStrategyFactory.createAsyncLast(payParam.getPayChannels());
if (CollectionUtil.isEmpty(strategies)) {
throw new PayUnsupportedMethodException();
}
// 查询通道支付订单
List<PayChannelOrder> orders = channelOrderManager.findAllByPaymentId(payOrder.getId());
Map<String, PayChannelOrder> channelOrderMap = orders.stream()
.collect(Collectors.toMap(PayChannelOrder::getChannel, Function.identity(), CollectorsFunction::retainFirst));
// 初始化支付参数
for (AbsPayStrategy strategy : strategies) {
strategy.initPayParam(payOrder, payParam);
strategy.setChannelOrder(channelOrderMap.get(strategy.getChannel().getCode()));
}
// 执行支付前处理动作
strategies.forEach(AbsPayStrategy::doBeforePayHandler);
// 支付操作
try {
strategies.forEach(AbsPayStrategy::doPayHandler);
} catch (Exception e) {
// 记录错误原因
PayOrderExtra payOrderExtra = payInfo.getPayOrderExtra();
payOrderExtra.setErrorMsg(e.getMessage());
throw e;
}
// 支付调用成功操作, 进行扣款、创建记录类类似的操作
strategies.forEach(AbsPayStrategy::doSuccessHandler);
// 如果没有异步支付, 直接进行订单完成处理
if (PayUtil.isNotSync(payParam.getPayChannels())) {
// 修改支付订单状态为成功
payOrder.setStatus(SUCCESS.getCode())
.setPayTime(LocalDateTime.now());
payOrderService.updateById(payOrder);
}
// 如果异步支付完成, 进行订单完成处理, 同时发送回调消息
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();
if (asyncPayInfo.isPayComplete()) {
payOrder.setGatewayOrderNo(asyncPayInfo.getGatewayOrderNo())
.setStatus(SUCCESS.getCode())
.setPayTime(LocalDateTime.now());
payOrderService.updateById(payOrder);
}
// 如果支付完成 发送通知
if (Objects.equals(payOrder.getStatus(), SUCCESS.getCode())){
clientNoticeService.registerPayNotice(payOrder, payInfo.getPayOrderExtra(), payInfo.getPayChannelOrders());
}
return PayBuilder.buildPayResultByPayOrder(payOrder);
}
/**
* 异步支付执行(非第一次请求), 只执行异步支付策略, 因为同步支付已经支付成功. 报错不影响继续发起支付
*/
private PayResult paySyncNotFirst(PayParam payParam, PayOrder payOrder) {
// 1. 处理支付结束情况(完成/退款/关闭/错误)
List<String> trades = Arrays.asList(SUCCESS.getCode(), CLOSE.getCode(), PARTIAL_REFUND.getCode(),
REFUNDED.getCode(), REFUNDING.getCode(), FAIL.getCode());
if (trades.contains(payOrder.getStatus())) {
return PayBuilder.buildPayResultByPayOrder(payOrder);
}
private PayResult onlyAsyncPay(PayParam payParam, PayOrder payOrder) {
// 2.获取 异步支付通道,通过工厂生成对应的策略组(只包含异步支付的策略, 同步支付已经成功不再继续执行)
PayChannelParam payChannelParam = payAssistService.getAsyncPayParam(payParam, payOrder);

View File

@@ -95,6 +95,7 @@ public class AliPayStrategy extends AbsPayStrategy {
*/
@Override
public void generateChannelOrder() {
super.generateChannelOrder();
}
/**

View File

@@ -98,6 +98,7 @@ public class UnionPayStrategy extends AbsPayStrategy {
*/
@Override
public void generateChannelOrder() {
super.generateChannelOrder();
}
/**

View File

@@ -104,7 +104,9 @@ public class WeChatPayStrategy extends AbsPayStrategy {
* 不使用默认的生成通道支付单方法, 异步支付通道的支付订单自己管理
*/
@Override
public void generateChannelOrder() {}
public void generateChannelOrder() {
super.generateChannelOrder();
}
/**
* 初始化微信支付

View File

@@ -0,0 +1,12 @@
package cn.bootx.platform.daxpay.service.core.payment.transfer.factory;
import lombok.experimental.UtilityClass;
/**
* 转账工具类
* @author xxm
* @since 2024/3/21
*/
@UtilityClass
public class TransferFactory {
}

View File

@@ -0,0 +1,16 @@
package cn.bootx.platform.daxpay.service.core.payment.transfer.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 转账服务
* @author xxm
* @since 2024/3/21
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TransferService {
}

View File

@@ -0,0 +1,49 @@
package cn.bootx.platform.daxpay.service.core.payment.transfer.strategy;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayTransferService;
import cn.bootx.platform.daxpay.service.func.AbsTransferStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 支付宝转账测策略
* @author xxm
* @since 2024/3/21
*/
@Slf4j
@Service
@Scope(SCOPE_PROTOTYPE)
@RequiredArgsConstructor
public class AliPayTransferStrategy extends AbsTransferStrategy {
private final AliPayConfigService payConfigService;
private final AliPayTransferService aliPayTransferService;
private AliPayConfig config;
/**
* 策略标识
*/
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.ALI;
}
/**
* 转账前操作
*/
@Override
public void doBeforeHandler() {
this.config = payConfigService.getAndCheckConfig();
payConfigService.initConfig(this.config);
}
}

View File

@@ -58,8 +58,7 @@ public abstract class AbsPayStrategy implements PayStrategy{
* 支付调起成功的处理方式
*/
public void doSuccessHandler() {
this.channelOrder.setStatus(PayStatusEnum.SUCCESS.getCode())
.setPayTime(LocalDateTime.now());
this.channelOrder.setStatus(PayStatusEnum.SUCCESS.getCode()).setPayTime(LocalDateTime.now());
}
/**

View File

@@ -0,0 +1,23 @@
package cn.bootx.platform.daxpay.service.func;
import cn.bootx.platform.daxpay.service.core.order.transfer.entity.TransferOrder;
import lombok.Getter;
import lombok.Setter;
/**
* 转账抽象策略
* @author xxm
* @since 2024/3/21
*/
@Getter
@Setter
public abstract class AbsTransferStrategy implements PayStrategy{
/** 转账订单 */
private TransferOrder transferOrder;
/**
* 转账前操作
*/
public void doBeforeHandler(){}
}

View File

@@ -40,6 +40,12 @@
<artifactId>daxpay-single-demo</artifactId>
<version>${daxpay.version}</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
<build>