refactor: 优化 PayParam 参数设计与时间格式

This commit is contained in:
DaxPay Dev
2026-06-16 21:07:08 +08:00
parent a0ff848d7b
commit b8dc17d83b
9 changed files with 237 additions and 105 deletions

View File

@@ -1,5 +1,7 @@
package org.dromara.daxpay.payment.common.bo;
import org.dromara.daxpay.platform.core.enums.pay.channel.CurrencyEnum;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
@@ -14,7 +16,7 @@ import java.util.Objects;
/// @param currency ISO 4217 币种代码,默认 CNY
public record MoneyAmount(Long amount, String currency) {
public static final String DEFAULT_CURRENCY = "CNY";
public static final String DEFAULT_CURRENCY = CurrencyEnum.CNY.getCode();
public MoneyAmount {
currency = Objects.requireNonNullElse(currency, DEFAULT_CURRENCY);

View File

@@ -6,6 +6,9 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.dromara.daxpay.payment.common.enums.NormalOrderStatusEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.CurrencyEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.PayMethodEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.ProductEnum;
import java.time.OffsetDateTime;
@@ -13,6 +16,7 @@ import java.time.OffsetDateTime;
///
/// 普通支付场景的容器承载商户业务单信息bizOrderNo / 商品标题 / 回调地址 等)
/// 与 pay_trade 一对一关联trade_type = normal
/// 冗余存储金额/支付/时间线字段,便于后台查询无需 JOIN pay_trade
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@@ -43,4 +47,45 @@ public class PayNormalOrder extends MchAppBaseEntity {
/// 业务单过期时间
private OffsetDateTime expiredTime;
// ===== 金额(冗余自 PayTrade方便查询=====
/// 业务单金额(最小货币单位)
private Long amount;
/// 币种
/// @see CurrencyEnum
private String currency;
// ===== 支付信息(冗余,查询过滤用)=====
/// 支付通道
private String channel;
/// 支付方式
/// @see PayMethodEnum
private String method;
/// 支付产品编码
/// @see ProductEnum
private String product;
// ===== 时间线(冗余,查询展示用)=====
/// 支付成功时间
private OffsetDateTime payTime;
/// 关闭时间
private OffsetDateTime closeTime;
// ===== 请求信息(低频,审计排查用)=====
/// 通道附加参数
private String extraParam;
/// 客户端 IP
private String clientIp;
/// 终端设备编码
private String terminalNo;
}

View File

@@ -1,5 +1,7 @@
package org.dromara.daxpay.payment.pay.service;
import org.dromara.daxpay.platform.core.enums.pay.channel.CurrencyEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.ProductEnum;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.util.DateTimeUtil;
@@ -8,6 +10,7 @@ import org.dromara.daxpay.payment.common.enums.NormalOrderStatusEnum;
import org.dromara.daxpay.payment.common.enums.PayFundStatusEnum;
import org.dromara.daxpay.payment.common.enums.PayTradeTypeEnum;
import org.dromara.daxpay.payment.common.util.PayUtil;
import cn.hutool.core.util.StrUtil;
import org.dromara.daxpay.payment.pay.convert.PayTradeConvert;
import org.dromara.daxpay.payment.pay.order.dao.PayNormalOrderManager;
import org.dromara.daxpay.payment.pay.order.entity.PayNormalOrder;
@@ -35,11 +38,22 @@ public class PayAssistService {
private final PayNormalOrderManager payNormalOrderManager;
private final PayTradeManager payTradeManager;
/// 创建支付订单(容器 + 资金交易 + 通道信息
/// 创建支付订单(容器 + 资金交易)
@Transactional(rollbackFor = Exception.class)
public PayTrade createOrder(PayParam payParam) {
OffsetDateTime expiredTime = this.getExpiredTime(payParam.getExpiredTime());
// 创建容器 PayNormalOrder
Long amount = Long.valueOf(PayUtil.convertCentAmount(payParam.getAmount()));
// 从产品编码派生通道编码
String channel = payParam.getProduct();
if (StrUtil.isNotBlank(channel)) {
ProductEnum productEnum = ProductEnum.findByCode(payParam.getProduct());
if (productEnum != null) {
channel = productEnum.getChannel();
}
}
// 终端信息
String terminalNo = payParam.getTerminal() != null ? payParam.getTerminal().getTerminalNo() : null;
// 创建容器 PayNormalOrder含冗余字段方便查询
PayNormalOrder normalOrder = new PayNormalOrder();
normalOrder.setBizOrderNo(payParam.getBizOrderNo());
normalOrder.setTitle(payParam.getTitle());
@@ -49,25 +63,30 @@ public class PayAssistService {
normalOrder.setReturnUrl(payParam.getReturnUrl());
normalOrder.setAttach(payParam.getAttach());
normalOrder.setExpiredTime(expiredTime);
normalOrder.setAmount(amount);
normalOrder.setCurrency(CurrencyEnum.CNY.getCode());
normalOrder.setChannel(channel);
normalOrder.setMethod(payParam.getMethod());
normalOrder.setProduct(payParam.getProduct());
normalOrder.setExtraParam(payParam.getExtraParam());
normalOrder.setTerminalNo(terminalNo);
payNormalOrderManager.save(normalOrder);
// 创建资金交易 PayTrade
Long amount = Long.valueOf(PayUtil.convertCentAmount(payParam.getAmount()));
PayTrade trade = new PayTrade();
trade.setTradeNo(TradeNoGenerateUtil.pay());
trade.setTradeType(PayTradeTypeEnum.NORMAL.getCode());
trade.setContainerId(normalOrder.getId());
trade.setProduct(payParam.getProduct());
trade.setChannel(payParam.getChannel());
trade.setChannel(channel);
trade.setMethod(payParam.getMethod());
trade.setOtherMethod(payParam.getOtherMethod());
trade.setLimitPay(payParam.getLimitPay());
trade.setProvider(payParam.getProvider());
trade.setLimitPay(payParam.getLimitPay() != null
? String.join(",", payParam.getLimitPay()) : null);
trade.setAmount(amount);
trade.setCurrency("CNY");
trade.setCurrency(CurrencyEnum.CNY.getCode());
trade.setRefundableBalance(amount);
trade.setStatus(PayFundStatusEnum.PROGRESS.getCode());
trade.setExpiredTime(expiredTime);
trade.setSource(payParam.getSource());
trade.setSource(org.dromara.daxpay.platform.core.enums.pay.trade.TradeSourceEnum.MCH_API.getCode());
trade.setBarCode(payParam.getAuthCode());
trade.setOpenid(payParam.getOpenId());
payTradeManager.save(trade);

View File

@@ -25,32 +25,37 @@ public class PayUniHandleService {
private final PayTradeManager payTradeManager;
private final PayNormalOrderManager payNormalOrderManager;
/// 支付成功后续处理
/// 支付成功后续处理(同步冗余时间线到容器)
public void paySuccess(PayTrade trade) {
PayNormalOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId())
.orElse(null);
if (normalOrder != null) {
normalOrder.setStatus(NormalOrderStatusEnum.PAID.getCode());
normalOrder.setPayTime(trade.getPayTime());
payNormalOrderManager.updateById(normalOrder);
}
payTradeManager.updateById(trade);
}
/// 支付失败处理
/// 支付失败处理(同步冗余时间线到容器)
public void payFail(PayTrade trade, PayNormalOrder normalOrder, String errMsg) {
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
trade.setStatus(PayFundStatusEnum.FAIL.getCode());
trade.setErrorMsg(errMsg);
trade.setCloseTime(OffsetDateTime.now(ZoneOffset.UTC));
trade.setCloseTime(now);
normalOrder.setStatus(NormalOrderStatusEnum.CLOSED.getCode());
normalOrder.setCloseTime(now);
payTradeManager.updateById(trade);
payNormalOrderManager.updateById(normalOrder);
}
/// 支付关闭处理
/// 支付关闭处理(同步冗余时间线到容器)
public void payClose(PayTrade trade, PayNormalOrder normalOrder) {
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
trade.setStatus(PayFundStatusEnum.CLOSE.getCode());
trade.setCloseTime(OffsetDateTime.now(ZoneOffset.UTC));
trade.setCloseTime(now);
normalOrder.setStatus(NormalOrderStatusEnum.CLOSED.getCode());
normalOrder.setCloseTime(now);
payTradeManager.updateById(trade);
payNormalOrderManager.updateById(normalOrder);
}

View File

@@ -49,7 +49,6 @@ public class UniTradeController {
// 订单: 该商户没有此接口的权限
throw new OperationFailException(CommonCode.FAIL_CODE, "error.payment.order.merchantNoApiPermission");
}
payParam.setSource(TradeSourceEnum.MCH_API.getCode());
return DaxRes.ok(payService.pay(payParam));
}

View File

@@ -1,7 +1,6 @@
package org.dromara.daxpay.payment.unipay.param;
import org.dromara.daxpay.platform.core.validation.IpAddress;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
@@ -34,10 +33,10 @@ public abstract class PaymentCommonParam {
@Size(max = 1024, message = "{validation.field.sign.size}")
private String sign;
/// 请求时间 格式yyyy-MM-dd HH:mm:ss
@Schema(description = "请求时间, 格式yyyy-MM-dd HH:mm:ss")
/// 请求时间(北京时间,格式 yyyy-MM-dd HH:mm:ss
@Schema(description = "请求时间(北京时间,yyyy-MM-dd HH:mm:ss)")
@NotNull(message = "{validation.field.reqTime.notNull}")
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private OffsetDateTime reqTime;
}

View File

@@ -1,28 +1,28 @@
package org.dromara.daxpay.payment.unipay.param.trade.pay;
import org.dromara.daxpay.platform.core.enums.pay.channel.ChannelEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.PayProviderEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.ProductEnum;
import org.dromara.daxpay.platform.core.enums.unipay.PayLimitPayEnum;
import org.dromara.daxpay.platform.core.enums.pay.channel.PayMethodEnum;
import org.dromara.daxpay.payment.unipay.param.MerchantPaymentCommonParam;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.List;
/// # 统一下单参数
/// # 统一下单参数(国内普通支付)
///
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(title = "支付参数")
public class PayParam extends MerchantPaymentCommonParam {
// ===== 业务核心 =====
/// 商户订单号
@Schema(description = "商户订单号")
@NotBlank(message = "{validation.field.bizOrderNo.notBlank}")
@@ -40,109 +40,80 @@ public class PayParam extends MerchantPaymentCommonParam {
@Size(max = 50, message = "{validation.field.description.size}")
private String description;
/// 是否开启分账
@Schema(description = "是否开启分账")
private Boolean allocation;
/// 自动分账
@Schema(description = "自动分账")
private Boolean autoAllocation;
/// 过期时间
@Schema(description = "过期时间(UTC)")
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private OffsetDateTime expiredTime;
/// 支付渠道(微信/支付宝/银联), 路由未指定 product 时必填
/// @see PayProviderEnum
@Schema(description = "支付渠道")
@Size(max = 32, message = "{validation.field.provider.size}")
private String provider;
/// 支付产品编码, 为空时由通道路由引擎自动选择
/// @see ProductEnum
@Schema(description = "支付产品编码")
@Size(max = 32, message = "{validation.field.product.size}")
private String product;
/// 支付通道编码
/// @see ChannelEnum#getCode()
@Schema(description = "支付通道编码")
@Size(max = 32, message = "{validation.field.channel.size}")
private String channel;
/// 支付方式编码, product为空时可由路由引擎回填
/// @see PayMethodEnum
@Schema(description = "支付方式编码")
@Size(max = 32, message = "{validation.field.method.size}")
private String method;
/// 其他支付方式, 只有在 支付方式编码(method) 为 其他支付(other)时才会生效
/// 用于处理各种通道各自定义的支付方式
@Size(max = 128, message = "{validation.field.otherMethod.size}")
@Schema(description = "其他支付方式")
private String otherMethod;
/// 限制用户支付类型, 目前支持限制信用卡
/// @see PayLimitPayEnum
@Schema(description = "限制用户支付类型")
@Size(max = 128, message = "{validation.field.limitPay.size}")
private String limitPay;
/// 支付金额
/// 支付金额(元)
@Schema(description = "支付金额")
@NotNull(message = "{validation.field.amount.notNull}")
@DecimalMin(value = "0.01", message = "{validation.field.amount.decimalMin}")
@Digits(integer = 8, fraction = 2, message = "{validation.field.amount.digits}")
private BigDecimal amount;
/// opAppId
@Size(max = 128, message = "{validation.field.opAppId.size}")
@Schema(description = "opAppId")
private String opAppId;
// ===== 支付路由 =====
/// 用户标识OpenId
/// 支付产品编码,为空时由通道路由引擎自动选择
/// @see ProductEnum
@Schema(description = "支付产品编码")
@Size(max = 32, message = "{validation.field.product.size}")
private String product;
/// 支付方式编码
/// @see PayMethodEnum
@Schema(description = "支付方式编码")
@NotBlank(message = "{validation.field.method.notBlank}")
@Size(max = 32, message = "{validation.field.method.size}")
private String method;
// ===== 通道专属参数(策略层校验)=====
/// 用户标识 OpenId微信 jsapi/mini 场景必填)
@Size(max = 128, message = "{validation.field.openId.size}")
@Schema(description = "用户标识OpenId")
private String openId;
/// 付款码
/// 付款码(被扫支付必填)
@Size(max = 128, message = "{validation.field.authCode.size}")
@Schema(description = "付款码")
private String authCode;
/// 终端设备编码
@Size(max = 128, message = "{validation.field.terminalNo.size}")
@Schema(description = "终端设备编码")
private String terminalNo;
/// 限制支付类型列表,如限制信用卡
/// @see org.dromara.daxpay.platform.core.enums.unipay.PayLimitPayEnum
@Schema(description = "限制支付类型")
@Size(max = 10, message = "{validation.field.limitPay.size}")
private List<String> limitPay;
/// 支付扩展参数
/// 支付扩展参数JSON 格式,通道特有的长尾参数)
@Schema(description = "支付扩展参数")
@Size(max = 2048, message = "{validation.field.extraParam.size}")
private String extraParam;
/// 商户扩展参数,回调时会原样返回
@Schema(description = "商户扩展参数")
@Size(max = 500, message = "{validation.field.attach.size}")
private String attach;
/// 订单来源
@Schema(description = "订单来源", hidden = true)
@Null(message = "{validation.field.orderSource.mustBeNull}")
private String source;
/// 同步跳转地址, 支付完毕后用户浏览器返回到该地址, 不传输跳转到默认地址
@Schema(description = "同步通知URL")
@Size(max = 200, message = "{validation.field.returnUrl.size}")
private String returnUrl;
/// 用户付款中途退出返回商户网站的地址(部分支付场景中可用)
@Schema(description = "退出地址")
@Size(max = 200, message = "{validation.field.quitUrl.size}")
private String quitUrl;
// ===== 通知回调 =====
/// 异步通知地址
@Schema(description = "异步通知地址")
@Size(max = 200, message = "{validation.field.notifyUrl.size}")
private String notifyUrl;
/// 同步跳转地址,支付完毕后用户浏览器返回到该地址
@Schema(description = "同步通知URL")
@Size(max = 200, message = "{validation.field.returnUrl.size}")
private String returnUrl;
/// 商户扩展参数,回调时会原样返回
@Schema(description = "商户扩展参数")
@Size(max = 500, message = "{validation.field.attach.size}")
private String attach;
// ===== 时间 =====
/// 过期时间(北京时间,格式 yyyy-MM-dd HH:mm:ss空则默认 30 分钟)
@Schema(description = "过期时间(北京时间yyyy-MM-dd HH:mm:ss)")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private OffsetDateTime expiredTime;
// ===== 终端信息(线下场景选填)=====
/// 终端信息(线下 POS/收银台场景)
@Valid
@Schema(description = "终端信息")
private TerminalInfo terminal;
}

View File

@@ -0,0 +1,46 @@
package org.dromara.daxpay.payment.unipay.param.trade.pay;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
/// # 终端信息
///
/// 线下支付场景POS/收银台/自助终端)的设备信息
@Data
@Schema(title = "终端信息")
public class TerminalInfo {
/// 终端设备号
@Size(max = 64, message = "{validation.field.terminalNo.size}")
@Schema(description = "终端设备号")
private String terminalNo;
/// 门店编号
@Size(max = 64, message = "{validation.field.storeNo.size}")
@Schema(description = "门店编号")
private String storeNo;
/// 操作员号
@Size(max = 64, message = "{validation.field.operatorId.size}")
@Schema(description = "操作员号")
private String operatorId;
/// 设备名称
@Size(max = 128, message = "{validation.field.deviceName.size}")
@Schema(description = "设备名称")
private String deviceName;
/// 设备 IP 地址
@Size(max = 64, message = "{validation.field.deviceIp.size}")
@Schema(description = "设备IP地址")
private String deviceIp;
/// 经度
@Schema(description = "经度")
private Double longitude;
/// 纬度
@Schema(description = "纬度")
private Double latitude;
}

View File

@@ -0,0 +1,46 @@
package org.dromara.daxpay.platform.core.enums.pay.channel;
import org.dromara.daxpay.platform.core.exception.DataNotExistException;
import org.dromara.daxpay.platform.core.i18n.I18nSupport;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/// # 币种枚举
///
/// ISO 4217 三位字母币种代码
/// 字典: currency
@Getter
@RequiredArgsConstructor
public enum CurrencyEnum implements I18nSupport {
/// 人民币
CNY("cny"),
/// 美元
USD("usd"),
/// 日元
JPY("jpy"),
/// 欧元
EUR("eur"),
/// 港元
HKD("hkd"),
/// 英镑
GBP("gbp"),
;
private final String code;
@Override
public String getI18nPrefix() {
return "enum.currency";
}
/// 根据编码获取枚举
public static CurrencyEnum findByCode(String code) {
return Arrays.stream(values())
.filter(e -> e.getCode().equals(code))
.findFirst()
.orElseThrow(() -> new DataNotExistException("不存在的币种: " + code));
}
}