feat(cashier): 实现收银台支付功能

- 新增收银台支付参数和结果对象
- 实现收银台聚合支付配置查询
- 添加收银台支付逻辑,支持多种支付方式
- 优化支付服务,增加重复支付检查
- 调整支付同步服务,增加待支付状态检查
This commit is contained in:
DaxPay
2024-11-29 16:44:27 +08:00
parent ae0e954f51
commit 250a50c87a
14 changed files with 174 additions and 52 deletions

View File

@@ -9,13 +9,13 @@
- [ ] 网关配套移动端开发
- [ ] 同步回调页
- [x] 微信通道添加单独的认证跳转地址, 处理它的特殊情况
- [ ] 支付订单新增待支付状态, 此时不需要
- [x] 支付订单新增待支付状态
## 3.0.0.beta3
- [ ] 收银台台功能
- [ ] 收银台配置
- [ ] 分类配置
- [ ] 明细配置
- [ ] 聚合支付配置
- [x] 收银台配置
- [x] 分类配置
- [x] 明细配置
- [x] 聚合支付配置
- [ ] 支持通过订单信息生成多种类型的收银台链接,
- [ ] pc收银台
- [ ] 扫码支付

View File

@@ -22,10 +22,10 @@ import java.util.Objects;
@AllArgsConstructor
public enum CheckoutCallTypeEnum {
SCAN("scan", "扫码支付"),
BAR_CODE("barCode", "条码支付"),
QR_CODE("qr_code", "扫码支付"),
BAR_CODE("bar_code", "条码支付"),
LINK("link", "跳转链接"),
MINI_APP("miniApp", "小程序支付"),
MINI_APP("mini_app", "小程序支付"),
AGGREGATE("aggregate", "聚合支付"),
APP("app", "APP支付"),
;

View File

@@ -0,0 +1,27 @@
package org.dromara.daxpay.core.param.checkout;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 收银台聚合支付参数
* @author xxm
* @since 2024/11/26
*/
@Data
@Accessors(chain = true)
@Schema(title = "收银台支付参数")
public class CheckoutAggregatePayParam {
@Schema(description = "要支付的订单号")
private String orderNo;
@Schema(description = "聚合支付类型")
private String aggregateType;
@Schema(description = "唯一标识")
private String openId;
}

View File

@@ -1,4 +1,4 @@
package org.dromara.daxpay.core.param.cashier;
package org.dromara.daxpay.core.param.checkout;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;

View File

@@ -1,4 +1,4 @@
package org.dromara.daxpay.core.param.cashier;
package org.dromara.daxpay.core.param.checkout;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -5,14 +5,14 @@ import lombok.Data;
import lombok.experimental.Accessors;
/**
* 收银台聚合支付配置
* 收银台聚合支付配置和订单信息
* @author xxm
* @since 2024/11/27
*/
@Data
@Accessors(chain = true)
@Schema(title = "收银台聚合支付配置")
public class CheckoutAggregateResult {
public class CheckoutAggregateOrderAndConfigResult {
/** 订单信息 */
@Schema(description = "订单信息")

View File

@@ -0,0 +1,27 @@
package org.dromara.daxpay.core.result.checkout;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.daxpay.core.enums.PayStatusEnum;
/**
* 收银台支付结果
* @author xxm
* @since 2024/11/29
*/
@Data
@Accessors(chain = true)
@Schema(title = "收银台支付结果")
public class CheckoutPayResult {
/** 链接 */
private String url;
/**
* 支付状态
* @see PayStatusEnum
*/
@Schema(description = "支付结果")
private String payStatus;
}

View File

@@ -5,10 +5,10 @@ import cn.bootx.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dromara.daxpay.core.param.cashier.CheckoutParam;
import org.dromara.daxpay.core.param.cashier.CheckoutPayParam;
import org.dromara.daxpay.core.param.checkout.CheckoutParam;
import org.dromara.daxpay.core.param.checkout.CheckoutPayParam;
import org.dromara.daxpay.core.result.DaxResult;
import org.dromara.daxpay.core.result.checkout.CheckoutAggregateResult;
import org.dromara.daxpay.core.result.checkout.CheckoutAggregateOrderAndConfigResult;
import org.dromara.daxpay.core.result.checkout.CheckoutOrderAndConfigResult;
import org.dromara.daxpay.core.result.checkout.CheckoutUrlResult;
import org.dromara.daxpay.core.util.DaxRes;
@@ -45,7 +45,7 @@ public class CheckoutController {
@Operation(summary = "获取聚合支付配置")
@GetMapping("/getAggregateConfig")
public Result<CheckoutAggregateResult> getAggregateConfig(String orderNo, String checkoutType){
public Result<CheckoutAggregateOrderAndConfigResult> getAggregateConfig(String orderNo, String checkoutType){
return Res.ok(checkoutQueryService.getAggregateConfig(orderNo, checkoutType));
}

View File

@@ -12,7 +12,7 @@ import org.dromara.daxpay.core.enums.PayRefundStatusEnum;
import org.dromara.daxpay.core.enums.PayStatusEnum;
import org.dromara.daxpay.core.exception.AmountExceedLimitException;
import org.dromara.daxpay.core.exception.TradeStatusErrorException;
import org.dromara.daxpay.core.param.cashier.CheckoutParam;
import org.dromara.daxpay.core.param.checkout.CheckoutParam;
import org.dromara.daxpay.core.util.PayUtil;
import org.dromara.daxpay.core.util.TradeNoGenerateUtil;
import org.dromara.daxpay.service.code.DaxPayCode;

View File

@@ -100,8 +100,8 @@ public class CheckoutQueryService {
/**
* 收银台聚合支付配置
*/
public CheckoutAggregateResult getAggregateConfig(String orderNo, String aggregateType){
var checkoutInfoResult = new CheckoutAggregateResult();
public CheckoutAggregateOrderAndConfigResult getAggregateConfig(String orderNo, String aggregateType){
var checkoutInfoResult = new CheckoutAggregateOrderAndConfigResult();
// 订单信息
PayOrder payOrder = checkoutAssistService.getOrderAndCheck(orderNo);
CheckoutOrderResult order = new CheckoutOrderResult()

View File

@@ -1,25 +1,34 @@
package org.dromara.daxpay.service.service.cashier;
import cn.bootx.platform.common.spring.util.WebServletUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.core.enums.CheckoutCallTypeEnum;
import org.dromara.daxpay.core.enums.CheckoutTypeEnum;
import org.dromara.daxpay.core.enums.PayStatusEnum;
import org.dromara.daxpay.core.exception.TradeProcessingException;
import org.dromara.daxpay.core.exception.UnsupportedAbilityException;
import org.dromara.daxpay.core.param.cashier.CheckoutParam;
import org.dromara.daxpay.core.param.cashier.CheckoutPayParam;
import org.dromara.daxpay.core.result.checkout.CheckoutOrderAndConfigResult;
import org.dromara.daxpay.core.param.checkout.CheckoutAggregatePayParam;
import org.dromara.daxpay.core.param.checkout.CheckoutParam;
import org.dromara.daxpay.core.param.checkout.CheckoutPayParam;
import org.dromara.daxpay.core.param.trade.pay.PayParam;
import org.dromara.daxpay.core.result.checkout.CheckoutPayResult;
import org.dromara.daxpay.core.result.checkout.CheckoutUrlResult;
import org.dromara.daxpay.core.result.trade.pay.PayResult;
import org.dromara.daxpay.service.dao.config.checkout.CheckoutItemConfigManager;
import org.dromara.daxpay.service.entity.config.PlatformConfig;
import org.dromara.daxpay.service.entity.config.checkout.CheckoutItemConfig;
import org.dromara.daxpay.service.entity.order.pay.PayOrder;
import org.dromara.daxpay.service.service.assist.PaymentAssistService;
import org.dromara.daxpay.service.service.config.PlatformConfigService;
import org.dromara.daxpay.service.service.trade.pay.PayService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
/**
@@ -35,8 +44,8 @@ public class CheckoutService {
private final LockTemplate lockTemplate;
private final PlatformConfigService platformConfigService;
private final PaymentAssistService paymentAssistService;
private final CheckoutQueryService checkoutQueryService;
private final CheckoutItemConfigManager checkoutItemConfigManager;
private final PayService payService;
/**
* 生成收银台链接
@@ -99,30 +108,66 @@ public class CheckoutService {
/**
* 支付调用
*/
public void pay(CheckoutPayParam param){
public CheckoutPayResult pay(CheckoutPayParam param){
// 订单信息
PayOrder order = checkoutAssistService.getOrderAndCheck(param.getOrderNo());
paymentAssistService.initMchApp(order.getAppId());
PayOrder payOrder = checkoutAssistService.getOrderAndCheck(param.getOrderNo());
paymentAssistService.initMchApp(payOrder.getAppId());
// 获取配置项
var itemConfig = checkoutItemConfigManager.findByIdAndAppId(param.getItemId(),order.getAppId())
var itemConfig = checkoutItemConfigManager.findByIdAndAppId(param.getItemId(),payOrder.getAppId())
.orElseThrow(() -> new TradeProcessingException("支付配置项不存在"));
// 判断支付调用类型
CheckoutCallTypeEnum callTypeEnum = CheckoutCallTypeEnum.findBuyCode(itemConfig.getCallType());
switch (callTypeEnum) {
case SCAN -> {
// 根据通道和支付方式返回扫码链接
}
case BAR_CODE -> {
// 调用支付逻辑直接进行支付
}
case LINK -> {
// 返回支付链接
case QR_CODE, LINK, BAR_CODE -> {
return this.checkoutPay(param, payOrder);
}
case AGGREGATE -> {
// 直接返回手机端的聚合收银台链接, 如何判断
PlatformConfig config = platformConfigService.getConfig();
// 直接返回手机端的聚合收银台链接
String url = StrUtil.format("{}/aggregate/{}", config.getGatewayPcUrl(), payOrder.getOrderNo());
return new CheckoutPayResult().setUrl(url).setPayStatus(PayStatusEnum.WAIT.getCode());
}
default -> throw new UnsupportedAbilityException("不支持的支付调用类型");
}
}
/**
* 处理参数使用通用支付接口调起支付
*/
private CheckoutPayResult checkoutPay(CheckoutPayParam param, PayOrder payOrder){
// 查询配置
CheckoutItemConfig itemConfig = checkoutItemConfigManager.findByIdAndAppId(param.getItemId(),payOrder.getAppId())
.orElseThrow(() -> new TradeProcessingException("支付配置项不存在"));
paymentAssistService.initMchApp(payOrder.getAppId());
// 构建支付参数
String clientIP = JakartaServletUtil.getClientIP(WebServletUtil.getRequest());
PayParam payParam = new PayParam();
payParam.setChannel(itemConfig.getChannel());
payParam.setMethod(itemConfig.getPayMethod());
payParam.setAppId(itemConfig.getAppId());
payParam.setClientIp(clientIP);
payParam.setReqTime(LocalDateTime.now());
// 获取策略
// AbsChannelCashierStrategy cashierStrategy = PaymentStrategyFactory.create(itemConfig.getChannel(), AbsChannelCashierStrategy.class);
// 进行参数预处理
// cashierStrategy.handlePayParam(param, payParam);
// 发起支付
PayResult payResult = payService.pay(payParam, payOrder);
return new CheckoutPayResult()
.setUrl(payResult.getPayBody())
.setPayStatus(payResult.getStatus());
}
/**
* 聚合支付
*/
public PayResult aggregatePay(CheckoutAggregatePayParam param){
// 订单信息
PayOrder payOrder = checkoutAssistService.getOrderAndCheck(param.getOrderNo());
return new PayResult();
}
}

View File

@@ -72,6 +72,28 @@ public class PayService {
}
}
/**
* 支付入口, 内部调用时使用
*/
public PayResult pay(PayParam payParam, PayOrder payOrder){
// 获取商户订单号
String bizOrderNo = payOrder.getBizOrderNo();
// 加锁
LockInfo lock = lockTemplate.lock("payment:pay:" + bizOrderNo,10000,200);
if (Objects.isNull(lock)){
log.warn("正在支付中,请勿重复支付");
throw new TradeProcessingException("正在支付中,请勿重复支付");
}
try {
return this.repeatPay(payParam,payOrder);
} catch (Exception e) {
log.error("支付异常",e);
throw e;
} finally {
lockTemplate.releaseLock(lock);
}
}
/**
* 首次支付 无事务
* 拆分为多阶段1. 保存订单记录信息 2 调起支付 3. 支付成功后操作

View File

@@ -2,12 +2,13 @@ package org.dromara.daxpay.service.service.trade.pay;
import cn.bootx.platform.core.exception.RepetitiveOperationException;
import cn.bootx.platform.core.util.DateTimeUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.core.enums.PayStatusEnum;
import org.dromara.daxpay.core.enums.TradeTypeEnum;
import org.dromara.daxpay.core.exception.OperationFailException;
import org.dromara.daxpay.core.exception.PayFailureException;
import org.dromara.daxpay.core.exception.SystemUnknownErrorException;
import org.dromara.daxpay.core.exception.TradeNotExistException;
import org.dromara.daxpay.core.exception.*;
import org.dromara.daxpay.core.param.trade.pay.PaySyncParam;
import org.dromara.daxpay.core.result.trade.pay.PaySyncResult;
import org.dromara.daxpay.service.bo.sync.PaySyncResultBo;
@@ -21,10 +22,6 @@ import org.dromara.daxpay.service.service.record.sync.TradeSyncRecordService;
import org.dromara.daxpay.service.strategy.AbsPayCloseStrategy;
import org.dromara.daxpay.service.strategy.AbsSyncPayOrderStrategy;
import org.dromara.daxpay.service.util.PaymentStrategyFactory;
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;
@@ -71,6 +68,11 @@ public class PaySyncService {
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PaySyncResult syncPayOrder(PayOrder payOrder) {
// 待支付状态不允许同步
if (Objects.equals(payOrder.getStatus(), WAIT.getCode())){
throw new TradeStatusErrorException("订单未开始支付, 请重新确认支付状态");
}
// 加锁
LockInfo lock = lockTemplate.lock("sync:pay" + payOrder.getId(),10000,200);
if (Objects.isNull(lock)){

View File

@@ -1,9 +1,14 @@
package org.dromara.daxpay.service.service.trade.refund;
import cn.bootx.platform.starter.redis.delay.service.DelayJobService;
import cn.bootx.platform.core.exception.DataNotExistException;
import cn.bootx.platform.core.util.BigDecimalUtil;
import cn.bootx.platform.core.util.ValidationUtil;
import cn.bootx.platform.starter.redis.delay.service.DelayJobService;
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.dromara.daxpay.core.enums.PayRefundStatusEnum;
import org.dromara.daxpay.core.enums.RefundStatusEnum;
import org.dromara.daxpay.core.exception.TradeNotExistException;
@@ -22,11 +27,6 @@ import org.dromara.daxpay.service.service.order.pay.PayOrderQueryService;
import org.dromara.daxpay.service.service.record.flow.TradeFlowRecordService;
import org.dromara.daxpay.service.strategy.AbsRefundStrategy;
import org.dromara.daxpay.service.util.PaymentStrategyFactory;
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.Transactional;
@@ -91,11 +91,10 @@ public class RefundService {
* 首次退款
*/
private RefundResult firstRefund(RefundParam param) {
// 获取支付订单
PayOrder payOrder = payOrderQueryService.findByBizOrOrderNo(param.getOrderNo(), param.getBizOrderNo(), param.getAppId())
.orElseThrow(() -> new DataNotExistException("支付订单不存在"));
// 检查退款参数
// 检查退款参数和支付订单
refundAssistService.checkAndParam(param, payOrder);
// 通过退款参数获取退款策略
AbsRefundStrategy refundStrategy = PaymentStrategyFactory.create(payOrder.getChannel(), AbsRefundStrategy.class);