(cashfeatier): 添加收银台支付功能

- 新增收银台类型枚举和相关参数类
- 实现收银台订单创建和支付逻辑
- 添加收银台配置管理和查询功能- 优化支付辅助服务,增加订单状态检查和限额验证
This commit is contained in:
DaxPay
2024-11-26 20:26:54 +08:00
parent eaa936c9c7
commit c842447d2f
16 changed files with 605 additions and 26 deletions

View File

@@ -0,0 +1,48 @@
package org.dromara.daxpay.service.controller.unipay;
import cn.bootx.platform.core.rest.Res;
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.result.DaxResult;
import org.dromara.daxpay.core.result.cashier.CheckoutUrlResult;
import org.dromara.daxpay.core.util.DaxRes;
import org.dromara.daxpay.service.common.anno.PaymentVerify;
import org.dromara.daxpay.service.service.cashier.CheckoutService;
import org.springframework.web.bind.annotation.*;
/**
* 收银台服务
* @author xxm
* @since 2024/11/26
*/
@Tag(name = "收银台服务")
@RestController
@RequestMapping("/unipay/checkout")
@RequiredArgsConstructor
public class CheckoutController {
private final CheckoutService checkoutService;
@PaymentVerify
@Operation(summary = "创建一个收银台链接")
@PostMapping("/creat")
public DaxResult<CheckoutUrlResult> creat(@RequestBody CheckoutParam checkoutParam){
return DaxRes.ok(checkoutService.creat(checkoutParam));
}
@Operation(summary = "获取收银台订单信息")
@GetMapping("/info")
public Result<Void> getInfo(){
return Res.ok();
}
@Operation(summary = "发起支付")
@PostMapping("/pay")
public Result<Void> pay(@RequestBody CheckoutPayParam checkoutParam){
return Res.ok();
}
}

View File

@@ -0,0 +1,18 @@
package org.dromara.daxpay.service.dao.config.checkout;
import cn.bootx.platform.common.mybatisplus.impl.BaseManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.service.entity.config.checkout.CheckoutItemConfig;
import org.springframework.stereotype.Repository;
/**
* 收银台配置项
* @author xxm
* @since 2024/11/26
*/
@Slf4j
@Repository
@RequiredArgsConstructor
public class CheckoutItemConfigManager extends BaseManager<CheckoutItemConfigMapper, CheckoutItemConfig> {
}

View File

@@ -0,0 +1,14 @@
package org.dromara.daxpay.service.dao.config.checkout;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.daxpay.service.entity.config.checkout.CheckoutItemConfig;
/**
* 收银台配置项
* @author xxm
* @since 2024/11/26
*/
@Mapper
public interface CheckoutItemConfigMapper extends MPJBaseMapper<CheckoutItemConfig> {
}

View File

@@ -1,9 +1,13 @@
package org.dromara.daxpay.service.entity.config.checkout;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.dromara.daxpay.service.common.entity.MchAppBaseEntity;
import org.dromara.daxpay.service.convert.config.CheckoutGroupConfigConvert;
import org.dromara.daxpay.service.param.config.checkout.CheckoutGroupConfigParam;
import org.dromara.daxpay.service.result.config.checkout.CheckoutGroupConfigResult;
/**
* 收银台类目配置
@@ -13,7 +17,7 @@ import org.dromara.daxpay.service.common.entity.MchAppBaseEntity;
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class CheckoutGroupConfig extends MchAppBaseEntity {
public class CheckoutGroupConfig extends MchAppBaseEntity implements ToResult<CheckoutGroupConfigResult> {
/** 类型 web/h5/小程序 */
private String type;
@@ -23,4 +27,16 @@ public class CheckoutGroupConfig extends MchAppBaseEntity {
/** 排序 */
private Double sort;
public static CheckoutGroupConfig init(CheckoutGroupConfigParam param){
return CheckoutGroupConfigConvert.CONVERT.toEntity(param);
}
/**
* 转换
*/
@Override
public CheckoutGroupConfigResult toResult() {
return CheckoutGroupConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -1,11 +1,15 @@
package org.dromara.daxpay.service.entity.config.checkout;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.dromara.daxpay.core.enums.ChannelEnum;
import org.dromara.daxpay.core.enums.PayMethodEnum;
import org.dromara.daxpay.service.common.entity.MchAppBaseEntity;
import org.dromara.daxpay.service.convert.config.CheckoutItemConfigConvert;
import org.dromara.daxpay.service.param.config.checkout.CheckoutItemConfigParam;
import org.dromara.daxpay.service.result.config.checkout.CheckoutItemConfigResult;
/**
* 收银台配置项
@@ -15,7 +19,7 @@ import org.dromara.daxpay.service.common.entity.MchAppBaseEntity;
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class CheckoutItemConfig extends MchAppBaseEntity {
public class CheckoutItemConfig extends MchAppBaseEntity implements ToResult<CheckoutItemConfigResult> {
/** 类目配置Id */
private Long classifyId;
@@ -38,4 +42,28 @@ public class CheckoutItemConfig extends MchAppBaseEntity {
/** 是否开启分账 */
private boolean allocation;
/**
* 类型
* 1. 扫码支付
* 2. 条码支付
* 3. 跳转链接
* 4. 小程序支付
* 5. 聚合支付
*/
private String type;
/**
* 构造
*/
public static CheckoutItemConfig init(CheckoutItemConfigParam param) {
return CheckoutItemConfigConvert.CONVERT.toEntity(param);
}
/**
* 转换
*/
@Override
public CheckoutItemConfigResult toResult() {
return CheckoutItemConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -14,7 +14,7 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支付订单
* 支付订单显示对象, 不与 PayOrderResult重名
* @author xxm
* @since 2021/2/25
*/

View File

@@ -0,0 +1,151 @@
package org.dromara.daxpay.service.service.cashier;
import cn.bootx.platform.core.exception.ValidationFailedException;
import cn.bootx.platform.core.util.BigDecimalUtil;
import cn.bootx.platform.core.util.DateTimeUtil;
import cn.bootx.platform.starter.redis.delay.service.DelayJobService;
import cn.hutool.core.bean.BeanUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.core.enums.PayAllocStatusEnum;
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.util.PayUtil;
import org.dromara.daxpay.core.util.TradeNoGenerateUtil;
import org.dromara.daxpay.service.code.DaxPayCode;
import org.dromara.daxpay.service.common.context.MchAppLocal;
import org.dromara.daxpay.service.common.local.PaymentContextLocal;
import org.dromara.daxpay.service.dao.order.pay.PayOrderManager;
import org.dromara.daxpay.service.entity.order.pay.PayOrder;
import org.dromara.daxpay.service.service.order.pay.PayOrderQueryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
* 收银台支撑服务
* @author xxm
* @since 2024/11/26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CheckoutAssistService {
private final DelayJobService delayJobService;
private final PayOrderManager payOrderManager;
private final PayOrderQueryService payOrderQueryService;
/**
* 校验支付状态,支付成功则返回,支付失败则抛出对应的异常
*/
public PayOrder getOrderAndCheck(String orderNo) {
PayOrder payOrder = payOrderQueryService.findByOrderNo(orderNo).orElse(null);
return getOrderAndCheck(payOrder);
}
/**
* 校验支付状态,支付成功则返回,支付失败则抛出对应的异常
*/
public PayOrder getOrderAndCheck(CheckoutParam param) {
// 根据订单查询支付记录
PayOrder payOrder = payOrderQueryService.findByBizOrderNo(param.getBizOrderNo(), param.getAppId()).orElse(null);
return getOrderAndCheck(payOrder);
}/**
* 校验支付状态,支付成功则返回,支付失败则抛出对应的异常
*/
public PayOrder getOrderAndCheck(PayOrder payOrder) {
if (Objects.nonNull(payOrder)) {
// 已经支付状态
if (PayStatusEnum.SUCCESS.getCode()
.equals(payOrder.getStatus())) {
throw new TradeStatusErrorException("已经支付成功,请勿重新支付");
}
// 支付失败类型状态
List<String> tradesStatus = List.of(
PayStatusEnum.FAIL.getCode(),
PayStatusEnum.CLOSE.getCode(),
PayStatusEnum.CANCEL.getCode());
if (tradesStatus.contains(payOrder.getStatus())) {
throw new TradeStatusErrorException("支付失败或已经被关闭");
}
// 退款类型状态
if (Objects.equals(payOrder.getRefundStatus(), PayRefundStatusEnum.REFUNDING.getCode())) {
throw new TradeStatusErrorException("该订单处于退款状态");
}
// 其他状态直接抛出兜底异常
throw new TradeStatusErrorException("订单不是待支付状态,请重新确认订单状态");
}
return null;
}
/**
* 检验订单是否超过限额
*/
public void validationLimitAmount(CheckoutParam checkoutParam) {
MchAppLocal mchAppInfo = PaymentContextLocal.get()
.getMchAppInfo();
// 总额校验
if (BigDecimalUtil.isGreaterThan(checkoutParam.getAmount(),mchAppInfo.getLimitAmount())) {
throw new AmountExceedLimitException("支付金额超过限额");
}
}
/**
* 校验订单超时时间是否正常
*/
public void validationExpiredTime(CheckoutParam payParam) {
LocalDateTime expiredTime = this.getExpiredTime(payParam);
if (Objects.nonNull(expiredTime) && DateTimeUtil.lt(expiredTime,LocalDateTime.now())) {
throw new ValidationFailedException("支付超时时间设置有误, 请检查!");
}
}
/**
* 创建支付订单并保存, 返回支付订单
*/
@Transactional(rollbackFor = Exception.class)
public PayOrder createPayOrder(CheckoutParam checkoutParam) {
// 订单超时时间
LocalDateTime expiredTime = this.getExpiredTime(checkoutParam);
// 构建支付订单对象
PayOrder order = new PayOrder();
BeanUtil.copyProperties(checkoutParam, order);
order.setOrderNo(TradeNoGenerateUtil.pay())
.setStatus(PayStatusEnum.WAIT.getCode())
.setRefundStatus(PayRefundStatusEnum.NO_REFUND.getCode())
.setExpiredTime(expiredTime)
.setRefundableBalance(checkoutParam.getAmount());
// 如果支持分账, 设置分账状态为待分账
if (order.getAllocation()) {
order.setAllocStatus(PayAllocStatusEnum.WAITING.getCode());
}
payOrderManager.save(order);
// 注册支付超时任务
delayJobService.registerByTransaction(order.getId(), DaxPayCode.Event.MERCHANT_PAY_TIMEOUT, order.getExpiredTime());
return order;
}
/**
* 获取支付订单超时时间
*/
private LocalDateTime getExpiredTime(CheckoutParam payParam) {
MchAppLocal mchAppLocal = PaymentContextLocal.get().getMchAppInfo();
// 支付参数传入
if (Objects.nonNull(payParam.getExpiredTime())) {
return payParam.getExpiredTime();
}
// 根据商户应用配置计算出时间
return PayUtil.getPaymentExpiredTime(mchAppLocal.getOrderTimeout());
}
}

View File

@@ -0,0 +1,105 @@
package org.dromara.daxpay.service.service.cashier;
import cn.hutool.core.util.StrUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.core.enums.CheckoutTypeEnum;
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.result.cashier.CheckoutUrlResult;
import org.dromara.daxpay.service.entity.config.PlatformConfig;
import org.dromara.daxpay.service.entity.order.pay.PayOrder;
import org.dromara.daxpay.service.service.assist.PaymentAssistService;
import org.dromara.daxpay.service.service.config.CheckoutConfigService;
import org.dromara.daxpay.service.service.config.PlatformConfigService;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 收银台服务
* @author xxm
* @since 2024/11/26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CheckoutService {
private final CheckoutAssistService payAssistService;
private final LockTemplate lockTemplate;
private final PlatformConfigService platformConfigService;
private final PaymentAssistService paymentAssistService;
private final CheckoutConfigService checkoutConfigService;
/**
* 生成收银台链接
*/
public CheckoutUrlResult creat(CheckoutParam checkoutParam){
// 校验支付限额
payAssistService.validationLimitAmount(checkoutParam);
// 校验超时时间, 不可早于当前
payAssistService.validationExpiredTime(checkoutParam);
// 获取商户订单号
String bizOrderNo = checkoutParam.getBizOrderNo();
// 加锁
LockInfo lock = lockTemplate.lock("payment:pay:" + bizOrderNo,10000,200);
if (Objects.isNull(lock)){
log.warn("正在发起调起收银台中,请勿重复操作");
throw new TradeProcessingException("正在发起调起收银台中,请勿重复操作");
}
try {
// 查询并检查订单
PayOrder payOrder = payAssistService.getOrderAndCheck(checkoutParam);
// 订单已经存在直接返回链接, 不存在创建订单后返回链接
if (Objects.isNull(payOrder)){
// 执行支付前的保存动作, 保存支付订单和扩展记录
payOrder = payAssistService.createPayOrder(checkoutParam);
String checkoutUrl = this.getCheckoutUrl(payOrder.getOrderNo(), checkoutParam.getCheckoutType());
return new CheckoutUrlResult().setUrl(checkoutUrl);
} else {
// 直接返回收银台链接
String checkoutUrl = this.getCheckoutUrl(payOrder.getOrderNo(), checkoutParam.getCheckoutType());
return new CheckoutUrlResult().setUrl(checkoutUrl);
}
} finally {
lockTemplate.releaseLock(lock);
}
}
/**
* 获取收银台链接
*/
public String getCheckoutUrl(String code, String checkoutType){
CheckoutTypeEnum checkoutTypeEnum = CheckoutTypeEnum.findBuyCode(checkoutType);
PlatformConfig config = platformConfigService.getConfig();
switch (checkoutTypeEnum) {
case H5 -> {
return StrUtil.format("{}/checkout/{}",config.getGatewayMobileUrl(), code);
}
case PC -> {
return StrUtil.format("{}/checkout/{}",config.getGatewayPcUrl(), code);
}
case MINI_APP -> {
throw new UnsupportedAbilityException("暂不支持小程序收银台");
}
default -> throw new UnsupportedAbilityException("不支持的收银台类型");
}
}
/**
* 获取收银台相关信息, 不需要签名和鉴权
*/
public void info(String orderNo){
// 订单信息
PayOrder orderAndCheck = payAssistService.getOrderAndCheck(orderNo);
// 配置信息
paymentAssistService.initMchApp(orderAndCheck.getAppId());
// 获取相关配置
}
}

View File

@@ -0,0 +1,26 @@
package org.dromara.daxpay.service.service.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.service.dao.config.checkout.CheckoutConfigManager;
import org.dromara.daxpay.service.dao.config.checkout.CheckoutGroupConfigManager;
import org.dromara.daxpay.service.dao.config.checkout.CheckoutItemConfigManager;
import org.springframework.stereotype.Service;
/**
* 收银台配置
* @author xxm
* @since 2024/11/26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CheckoutConfigService {
private final CheckoutConfigManager checkoutConfigManager;
private final CheckoutGroupConfigManager checkoutGroupConfigManager;
private final CheckoutItemConfigManager checkoutItemConfigManager;
}

View File

@@ -85,6 +85,7 @@ public class PayAssistService {
.setReqTime(payParam.getReqTime())
.setChannel(payParam.getChannel())
.setMethod(payParam.getMethod())
.setStatus(PayStatusEnum.PROGRESS.getCode())
.setExtraParam(payParam.getExtraParam());
if (!order.getAllocation()) {
order.setAllocStatus(null);