refactor(gateway): 对齐网关业务单与普通支付容器并收敛无用字段

预下单 orderNo 改用 ORD 号段,补齐 extraParam/relationOrderNo 与 Result 字段;
支付回写后 reload 容器避免 payBody 丢失;终态 failed 校验对齐 Normal。
网关不支持线下被扫,删除 terminalNo/barCode 及相关 API 与 DDL 列。
This commit is contained in:
DaxPay Dev
2026-07-14 19:13:59 +08:00
parent 2b2d030165
commit abe0a243ea
9 changed files with 199 additions and 85 deletions

View File

@@ -145,6 +145,14 @@ COMMENT ON COLUMN "public"."pay_gateway_order"."pay_body" IS '支付参数体(
ALTER TABLE "public"."pay_gateway_order" ADD COLUMN IF NOT EXISTS "pay_body_type" varchar(32);
COMMENT ON COLUMN "public"."pay_gateway_order"."pay_body_type" IS '支付参数体类型';
-- 网关容器对齐普通容器: 通道附加参数
ALTER TABLE "public"."pay_gateway_order" ADD COLUMN IF NOT EXISTS "extra_param" varchar(2048);
COMMENT ON COLUMN "public"."pay_gateway_order"."extra_param" IS '通道附加参数';
-- 网关不支持线下终端/被扫: 移除无用列(重构期可破坏性变更)
ALTER TABLE "public"."pay_gateway_order" DROP COLUMN IF EXISTS "terminal_no";
ALTER TABLE "public"."pay_gateway_order" DROP COLUMN IF EXISTS "bar_code";
-- 实际上送串索引(回调反查)
CREATE INDEX IF NOT EXISTS "idx_pay_trade_relation_order_no" ON "public"."pay_trade" ("relation_order_no");
CREATE INDEX IF NOT EXISTS "idx_pay_normal_order_order_no" ON "public"."pay_normal_order" ("order_no");

View File

@@ -5,11 +5,15 @@ import cn.daxpay.open.payment.trade.order.result.GatewayPayOrderResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/// # 网关支付业务单转换
/// # 网关支付业务单转换(管理)
///
/// 仅做容器(GatewayPayOrder)到列表 Result 的映射;
/// 详情场景的资金凭证(PayTrade)字段由 Service 层手动补充, 与 [NormalPayOrderConvert] 一致
@Mapper
public interface GatewayPayOrderConvert {
GatewayPayOrderConvert CONVERT = Mappers.getMapper(GatewayPayOrderConvert.class);
/// GatewayPayOrder → Result (列表用, 不含资金凭证字段)
GatewayPayOrderResult toResult(GatewayPayOrder entity);
}

View File

@@ -1,6 +1,7 @@
package cn.daxpay.open.payment.trade.order.entity;
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
import cn.daxpay.open.payment.merchant.enums.CashierSceneEnum;
import cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum;
import cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum;
import cn.daxpay.open.payment.unipay.param.trade.pay.GoodsDetail;
@@ -20,23 +21,24 @@ import java.util.List;
/// # 网关支付业务单容器
///
/// 业务容器统一落在 trade.order与 NormalPayOrder 同包)。
/// 聚合扫码/收银台预下单场景: 创建时不知具体通道, 仅承载收款意图
/// 业务容器统一落在 trade.order[NormalPayOrder] 同包,纯持久化无编排 service)。
/// 聚合扫码/收银台预下单场景: 创建时不知具体通道, 仅承载收款意图bizOrderNo / 标题 / 回调 等);
/// 用户真正支付时再创建 pay_trade(trade_type=gateway) 并回填 channel/product/method。
/// 网关 orderNo 为预下单 URL 号, 与资金 tradeNo 身份分离
/// 网关 orderNo 为预下单 URL 号(号段 order()=ORD…, 与资金 tradeNoPAY…身份分离;
/// 冗余存储金额/支付/时间线字段, 便于后台查询无需 JOIN pay_trade。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName(value = "pay_gateway_order", autoResultMap = true)
public class GatewayPayOrder extends MchBaseEntity {
/// 平台网关业务单号(URL 用, 预下单即生成, 可无 trade)
/// 平台业务单号(容器身份,与 tradeNo 独立生成;预下单即生成可无 trade;普通通道默认作为上送号 / URL 落地号)
private String orderNo;
/// 商户业务单号
private String bizOrderNo;
/// 网关类型
/// 网关支付类型(预下单写入)
/// @see GatewayPayTypeEnum
private String gatewayType;
@@ -50,25 +52,29 @@ public class GatewayPayOrder extends MchBaseEntity {
/// @see GatewayOrderStatusEnum
private String status;
/// 异步通知地址
/// 异步通知地址(出站商户通知用)
private String notifyUrl;
/// 同步跳转地址
private String returnUrl;
/// 商户附加参数
/// 商户附加参数(回调原样返回)
private String attach;
/// 过期时间
/// 业务单过期时间(超时关单权威,不落 pay_trade
private OffsetDateTime expiredTime;
/// 金额(最小货币单位)
// ===== 金额(冗余,方便查询;预下单即确定,支付前可无 Trade=====
/// 业务单金额(最小货币单位)
private Long amount;
/// 币种
/// @see CurrencyEnum
private String currency;
// ===== 支付信息(支付时回填,查询过滤用)=====
/// 支付通道编码(冗余自 product → ProductEnum#getChannel对应 ChannelEnum非 PayProviderEnum
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum
private String channel;
@@ -81,44 +87,41 @@ public class GatewayPayOrder extends MchBaseEntity {
/// @see cn.daxpay.open.platform.core.enums.unipay.PayLimitPayEnum
private String limitPay;
/// 支付产品
/// 支付产品编码
/// @see ProductEnum
private String product;
/// 支付能力(路由回填)
private String capability;
/// 通道商户号(路由回填)
private String channelMchNo;
/// 通道应用 AppId本笔交易实际使用的微信/通道侧 AppId 快照;解析后写入,关退同步复用)
private String channelAppId;
// ===== 支付请求参数(支付时写入)=====
// ===== 支付请求参数(支付时写入,审计保留;网关无被扫,不承载 barCode=====
/// 微信 openidjsapi/app/miniapp
private String openid;
/// 付款码(被扫支付
private String barCode;
/// 收银场景 wechat_pay/alipay/union_pay
/// 收银场景(环境识别,支付时回填;与聚合/收银台配置共用同一字典
/// @see CashierSceneEnum
private String scene;
/// 最后发起设备 mobile/pc
/// 最后发起设备mobile/pc,支付时回填;与 H5 端 window.__DEVICE__ 一致)
private String device;
// ===== 时间线(冗余,查询展示用)=====
/// 支付成功时间
private OffsetDateTime payTime;
/// 关闭时间
private OffsetDateTime closeTime;
/// 下单客户端 IP关单/同步/退款透传通道的单一事实源,兼审计排查
private String clientIp;
// ===== 通道路由(同步时用于解析通道应用凭证;支付路由后回填)=====
/// 终端设备编码
private String terminalNo;
/// 通道商户号(路由回填)
private String channelMchNo;
/// 支付能力编码(路由回填)
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum
private String capability;
/// 通道应用 AppId本笔交易实际使用的微信/通道侧 AppId 快照;解析后写入,关退同步复用)
private String channelAppId;
// ===== 通道回执(支付成功/同步后写入)=====
@@ -126,7 +129,7 @@ public class GatewayPayOrder extends MchBaseEntity {
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum
private String provider;
/// 付款用户 ID(支付宝 buyer_user_id 等
/// 付款用户标识(支付宝 user_id、微信 openid 等,非通道 AppId
private String buyerId;
/// 通道方记录的支付产品
@@ -141,29 +144,37 @@ public class GatewayPayOrder extends MchBaseEntity {
/// 活动类型
private String promotionType;
/// 支付参数体(已拉起缓存,仅落容器)
/// 支付参数体(如微信 prepay_id 组装串,非空表示已拉起支付,免重复请求通道;仅落容器)
private String payBody;
/// 支付参数体类型
/// 支付参数体类型jsapi/sdk/app
private String payBodyType;
// ===== 通道关联订单号(部分通道专用)=====
// ===== 关联订单号 =====
/// 透传订单号(三方通道产生的透传订单号)
private String transOrderNo;
/// 实际上送通道的商户订单号(展示冗余;反查权威在 pay_trade.relation_order_no
/// 普通通道与 orderNo 一致;特殊通道为变形号
private String relationOrderNo;
// ===== 请求信息(关单/同步/退款透传依赖 + 审计)=====
/// 通道附加参数
private String extraParam;
/// 应用号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appId;
/// 商品明细
/// 订单商品明细列表jsonb 存储)
@TableField(typeHandler = JacksonTypeHandler.class)
private List<GoodsDetail> goodsDetail;
/// 下单客户端 IP关单/同步/退款透传通道的单一事实源,兼审计排查
private String clientIp;
/// 错误信息
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String errorMsg;

View File

@@ -8,6 +8,8 @@ import lombok.experimental.Accessors;
import java.time.OffsetDateTime;
/// # 网关支付业务单查询参数(管理)
///
/// 查询维度与 [NormalPayOrderQuery] 对齐, 另含网关类型 gatewayType / 平台单号 orderNo。
@Data
@Accessors(chain = true)
@Schema(title = "网关支付业务单查询参数")
@@ -29,10 +31,16 @@ public class GatewayPayOrderQuery {
@Schema(description = "商户业务单号")
private String bizOrderNo;
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
@Schema(description = "订单标题")
private String title;
/// @see cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
@Schema(description = "网关类型")
private String gatewayType;
/// @see cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
@Schema(description = "业务状态")
private String status;
@@ -41,6 +49,12 @@ public class GatewayPayOrderQuery {
@Schema(description = "支付通道")
private String channel;
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
@Schema(description = "支付方式")
private String method;
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
@Schema(description = "支付产品")
private String product;
@@ -52,4 +66,12 @@ public class GatewayPayOrderQuery {
@QueryParam(type = QueryParam.CompareTypeEnum.LE, fieldName = "create_time")
@Schema(description = "创建时间-结束")
private OffsetDateTime createTimeEnd;
@QueryParam(type = QueryParam.CompareTypeEnum.GE, fieldName = "amount")
@Schema(description = "金额下限(分)")
private Long amountMin;
@QueryParam(type = QueryParam.CompareTypeEnum.LE, fieldName = "amount")
@Schema(description = "金额上限(分)")
private Long amountMax;
}

View File

@@ -9,12 +9,18 @@ import lombok.experimental.Accessors;
import java.time.OffsetDateTime;
/// # 网关支付业务单(管理)
///
/// 列表场景仅填充容器(GatewayPayOrder)字段;
/// 详情场景额外填充资金凭证(PayTrade)字段: tradeNo / outOrderNo / fundStatus 等。
/// 字段契约与 [NormalPayOrderResult] 对齐, 另含网关独有 gatewayType / scene / device。
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "网关支付业务单")
public class GatewayPayOrderResult extends BaseResult {
// ===== 容器(业务单)字段 =====
@Schema(description = "商户号")
private String mchNo;
@@ -27,6 +33,7 @@ public class GatewayPayOrderResult extends BaseResult {
@Schema(description = "商户业务单号")
private String bizOrderNo;
/// @see cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum
@Schema(description = "网关类型")
private String gatewayType;
@@ -36,39 +43,10 @@ public class GatewayPayOrderResult extends BaseResult {
@Schema(description = "描述")
private String description;
/// @see cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum
@Schema(description = "业务状态")
private String status;
@Schema(description = "金额(分)")
private Long amount;
@Schema(description = "币种")
private String currency;
@Schema(description = "过期时间")
private OffsetDateTime expiredTime;
@Schema(description = "支付通道")
private String channel;
@Schema(description = "支付方式")
private String method;
@Schema(description = "支付产品")
private String product;
@Schema(description = "收银场景")
private String scene;
@Schema(description = "设备")
private String device;
@Schema(description = "支付成功时间")
private OffsetDateTime payTime;
@Schema(description = "关闭时间")
private OffsetDateTime closeTime;
@Schema(description = "异步通知地址")
private String notifyUrl;
@@ -78,19 +56,103 @@ public class GatewayPayOrderResult extends BaseResult {
@Schema(description = "商户附加参数")
private String attach;
// ===== Trade 联表 =====
@Schema(description = "过期时间")
private OffsetDateTime expiredTime;
@Schema(description = "金额(分)")
private Long amount;
@Schema(description = "币种")
private String currency;
@Schema(description = "支付通道")
private String channel;
@Schema(description = "支付方式")
private String method;
@Schema(description = "支付产品")
private String product;
@Schema(description = "限制支付类型")
private String limitPay;
/// @see cn.daxpay.open.payment.merchant.enums.CashierSceneEnum
@Schema(description = "收银场景(环境识别)")
private String scene;
@Schema(description = "设备(mobile/pc)")
private String device;
@Schema(description = "支付成功时间")
private OffsetDateTime payTime;
@Schema(description = "关闭时间")
private OffsetDateTime closeTime;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "支付能力编码")
private String capability;
@Schema(description = "通道应用AppId")
private String channelAppId;
@Schema(description = "客户端IP")
private String clientIp;
@Schema(description = "通道附加参数")
private String extraParam;
// ===== 资金凭证(PayTrade)联表字段, 仅详情时填充 =====
@Schema(description = "资金交易号")
private String tradeNo;
@Schema(description = "通道订单号")
private String outOrderNo;
/// @see cn.daxpay.open.payment.trade.enums.PayFundStatusEnum
@Schema(description = "资金状态")
private String fundStatus;
@Schema(description = "可退金额")
@Schema(description = "可退金额(分)")
private Long refundableBalance;
@Schema(description = "支付参数体")
private String payBody;
@Schema(description = "支付参数体类型")
private String payBodyType;
@Schema(description = "付款用户ID")
private String buyerId;
@Schema(description = "微信openid")
private String openid;
@Schema(description = "支付渠道(厂商)")
private String provider;
@Schema(description = "通道方记录的支付产品")
private String tradeProduct;
@Schema(description = "通道方记录的交易方式")
private String tradeWay;
@Schema(description = "银行卡类型")
private String bankType;
@Schema(description = "活动类型")
private String promotionType;
@Schema(description = "透传订单号")
private String transOrderNo;
@Schema(description = "实际上送通道的商户订单号")
private String relationOrderNo;
@Schema(description = "错误信息")
private String errorMsg;
}

View File

@@ -16,8 +16,9 @@ public class AggregateQrPayParam {
@Size(max = 64, message = "{validation.field.orderNo.size}")
private String orderNo;
/// 收银场景(环境识别,与聚合/收银台配置共用 [CashierSceneEnum]
/// @see CashierSceneEnum
@Schema(description = "收银场景 wechat_pay/alipay/union_pay")
@Schema(description = "收银场景(CashierSceneEnum: wechat_pay/alipay/union_pay/douyin/browser)")
@NotBlank(message = "{validation.field.scene.notBlank}")
@Size(max = 32, message = "{validation.field.scene.size}")
private String scene;
@@ -26,7 +27,7 @@ public class AggregateQrPayParam {
@Size(max = 128, message = "{validation.field.openId.size}")
private String openId;
@Schema(description = "设备 mobile/pc")
@Schema(description = "设备(mobile/pc, 与 H5 __DEVICE__ 一致)")
@Size(max = 16, message = "{validation.field.device.size}")
private String device;

View File

@@ -3,9 +3,7 @@ package cn.daxpay.open.payment.unipay.gateway.param;
import cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum;
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
import cn.daxpay.open.payment.unipay.param.trade.pay.GoodsDetail;
import cn.daxpay.open.payment.unipay.param.trade.pay.TerminalInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -57,13 +55,14 @@ public class GatewayPrePayParam extends MerchantPaymentCommonParam {
@Size(max = 512, message = "{validation.field.attach.size}")
private String attach;
/// 支付扩展参数JSON 格式,通道特有的长尾参数;与 [NormalPayParam#extraParam] 对齐)
@Schema(description = "支付扩展参数")
@Size(max = 2048, message = "{validation.field.extraParam.size}")
private String extraParam;
@Schema(description = "过期时间")
private OffsetDateTime expiredTime;
@Valid
@Schema(description = "终端信息")
private TerminalInfo terminal;
@Schema(description = "商品明细")
private List<GoodsDetail> goodsDetail;
}

View File

@@ -88,19 +88,20 @@ public class GatewayPayAssistService {
if (Objects.equals(status, GatewayOrderStatusEnum.PAID.getCode())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
}
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode())
.contains(status)) {
// failed 与 closed/expired 同为终态, 不允许再返回原 URL 假装可付
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode(),
GatewayOrderStatusEnum.FAILED.getCode()).contains(status)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.failedOrClosed");
}
return this.buildPrePayResult(order);
}
OffsetDateTime expiredTime = payAssistService.getExpiredTime(param.getExpiredTime());
String terminalNo = param.getTerminal() != null ? param.getTerminal().getTerminalNo() : null;
GatewayPayOrder order = new GatewayPayOrder();
order.setAppId(param.getAppId());
order.setOrderNo(TradeNoGenerateUtil.pay());
// 容器业务单号用 order() 号段(ORD…), 与资金 tradeNo 的 pay() 号段(PAY…)身份分离
order.setOrderNo(TradeNoGenerateUtil.order());
order.setBizOrderNo(param.getBizOrderNo());
order.setGatewayType(typeEnum.getCode());
order.setTitle(param.getTitle());
@@ -109,11 +110,11 @@ public class GatewayPayAssistService {
order.setNotifyUrl(param.getNotifyUrl());
order.setReturnUrl(param.getReturnUrl());
order.setAttach(param.getAttach());
order.setExtraParam(param.getExtraParam());
order.setExpiredTime(expiredTime);
order.setAmount(param.getAmount());
order.setCurrency(CurrencyEnum.CNY.getCode());
order.setClientIp(param.getClientIp());
order.setTerminalNo(terminalNo);
order.setGoodsDetail(param.getGoodsDetail());
gatewayPayOrderManager.save(order);
@@ -140,8 +141,9 @@ public class GatewayPayAssistService {
if (Objects.equals(status, GatewayOrderStatusEnum.PAID.getCode())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
}
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode())
.contains(status)) {
// 与 NormalPayOrder 终态校验对齐: failed / closed / expired 均不可继续支付
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode(),
GatewayOrderStatusEnum.FAILED.getCode()).contains(status)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.failedOrClosed");
}
if (Objects.nonNull(order.getExpiredTime())

View File

@@ -157,8 +157,9 @@ public class GatewayPayHandleService {
order.setLimitPay(payParam.getLimitPay() != null
? String.join(",", payParam.getLimitPay()) : null);
order.setOpenid(payParam.getOpenId());
order.setBarCode(payParam.getAuthCode());
order.setProduct(payParam.getProduct());
// 与 Normal 对齐: 容器冗余 relationOrderNo, 供 ContainerFieldResolver / 管理展示
order.setRelationOrderNo(order.getOrderNo());
gatewayPayOrderManager.updateById(order);
return trade;
}
@@ -170,9 +171,11 @@ public class GatewayPayHandleService {
trade.setPayTime(result.getFinishTime());
}
trade.setOutOrderNo(result.getOutOrderNo());
// 回执与 payBody 写容器, 由 payAfterHandel 统一处理
// 回执与 payBody 写容器, 由 payAfterHandel 统一处理(内部 reload 容器)
payUniHandleService.payAfterHandel(trade, result);
return this.buildResult(order, trade);
// 必须 reload: payAfterHandel 写的是另一份实体, 入参 order 无 payBody
GatewayPayOrder latest = gatewayPayOrderManager.findById(order.getId()).orElse(order);
return this.buildResult(latest, trade);
}
private void fillRouteOnOrder(GatewayPayOrder order, NormalPayParam payParam, String scene, String device) {
@@ -211,6 +214,8 @@ public class GatewayPayHandleService {
payParam.setNotifyUrl(order.getNotifyUrl());
payParam.setReturnUrl(order.getReturnUrl());
payParam.setAttach(order.getAttach());
// 预下单写入的通道扩展参数, 与 Normal 容器透传一致
payParam.setExtraParam(order.getExtraParam());
payParam.setExpiredTime(order.getExpiredTime());
payParam.setClientIp(StrUtil.blankToDefault(clientIp, order.getClientIp()));
payParam.setGoodsDetail(order.getGoodsDetail());