mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 06:46:04 +08:00
feat(gateway): 预下单结果回传实际类型并校验类型不可中途变更, 落地页改H5/小程序双链接, 下线linkForm维度
This commit is contained in:
@@ -76,11 +76,7 @@ DROP TABLE IF EXISTS "public"."pay_gateway_aggregate_config";
|
||||
DROP TABLE IF EXISTS "public"."pay_gateway_code_client_env";
|
||||
DROP TABLE IF EXISTS "public"."pay_gateway_code_config";
|
||||
|
||||
-- 4. 聚合订单表加 link_form 字段(聚合小程序链接前缀分流)
|
||||
ALTER TABLE "public"."pay_gateway_order" ADD COLUMN IF NOT EXISTS "link_form" varchar(16) COLLATE "pg_catalog"."default" DEFAULT 'h5';
|
||||
COMMENT ON COLUMN "public"."pay_gateway_order"."link_form" IS '链接形态: h5/mini(聚合小程序扫码), 缺省 h5';
|
||||
|
||||
-- 5. 菜单: 删除旧的码牌配置 + 聚合配置菜单, 新增统一网关支付配置菜单
|
||||
-- 4. 菜单: 删除旧的码牌配置 + 聚合配置菜单, 新增统一网关支付配置菜单
|
||||
DELETE FROM "public"."iam_perm_menu" WHERE menu_code IN ('merchant:gateway-aggregate', 'merchant:gateway-code');
|
||||
|
||||
-- admin 端(4040121 原聚合位 → 统一配置)
|
||||
|
||||
@@ -44,9 +44,6 @@ public class GatewayPayOrder extends MchBaseEntity {
|
||||
/// @see GatewayPayTypeEnum
|
||||
private String gatewayType;
|
||||
|
||||
/// 链接形态: h5/mini(聚合小程序扫码前缀分流, 缺省 h5; 收银台不使用)
|
||||
private String linkForm;
|
||||
|
||||
/// 订单来源(业务入口权威;预下单即写入,与 gatewayType 对应 cashier/aggress_pay)
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum
|
||||
private String source;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class GatewayPayAssistService {
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
/// 预下单: 仅创建容器, 返回落地页 URL
|
||||
/// 预下单: 仅创建容器, 返回 H5 与小程序映射链接
|
||||
public GatewayPrePayResult prePay(GatewayPrePayParam param) {
|
||||
if (StrUtil.isBlank(param.getClientIp())) {
|
||||
param.setClientIp(WebServletUtil.getClientIp());
|
||||
@@ -85,23 +85,20 @@ public class GatewayPayAssistService {
|
||||
merchantContextLoader.initMch(param.getMchNo());
|
||||
|
||||
return lockExecutor.execute(
|
||||
"payment:gateway:pre:" + param.getAppId() + ":" + param.getBizOrderNo()
|
||||
+ ":" + StrUtil.blankToDefault(param.getLinkForm(), "h5"),
|
||||
"payment:gateway:pre:" + param.getAppId() + ":" + param.getBizOrderNo(),
|
||||
() -> self.doPrePay(param, typeEnum),
|
||||
() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.processing")
|
||||
);
|
||||
}
|
||||
|
||||
/// 预下单事务体: 幂等校验 → 新建容器订单 → 返回落地页 URL
|
||||
/// 预下单事务体: 幂等校验 → 新建容器订单 → 返回 H5 与小程序映射链接
|
||||
///
|
||||
/// - 已有未终态单则直接返回原 URL
|
||||
/// - 已有未终态单则直接返回原 H5 与小程序映射链接
|
||||
/// - PAID/failed/closed/expired 视为终态拒绝重入
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GatewayPrePayResult doPrePay(GatewayPrePayParam param, GatewayPayTypeEnum typeEnum) {
|
||||
// 幂等: 已有未终态单则返回原 URL(形态绑定: linkForm 不匹配视为不同订单)
|
||||
String reqLinkForm = StrUtil.blankToDefault(param.getLinkForm(), "h5");
|
||||
var existing = gatewayPayOrderManager.findByBizOrderNo(param.getBizOrderNo(), param.getAppId())
|
||||
.filter(o -> Objects.equals(reqLinkForm, StrUtil.blankToDefault(o.getLinkForm(), "h5")));
|
||||
// 幂等: 已有未终态单则返回原双链接
|
||||
var existing = gatewayPayOrderManager.findByBizOrderNo(param.getBizOrderNo(), param.getAppId());
|
||||
if (existing.isPresent()) {
|
||||
GatewayPayOrder order = existing.get();
|
||||
String status = order.getStatus();
|
||||
@@ -109,12 +106,18 @@ public class GatewayPayAssistService {
|
||||
// 支付: 已经支付成功, 请勿重新支付
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
|
||||
}
|
||||
// failed 与 closed/expired 同为终态, 不允许再返回原 URL 假装可付
|
||||
// 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.equals(order.getGatewayType(), typeEnum.getCode())) {
|
||||
// 网关: 该订单号已使用, 订单类型不可中途变更, 请更换订单号
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.gateway.bizOrderTypeImmutable");
|
||||
}
|
||||
return this.buildPrePayResult(order);
|
||||
}
|
||||
|
||||
@@ -129,8 +132,6 @@ public class GatewayPayAssistService {
|
||||
order.setOrderNo(TradeNoGenerateUtil.order());
|
||||
order.setBizOrderNo(param.getBizOrderNo());
|
||||
order.setGatewayType(typeEnum.getCode());
|
||||
// 链接形态: 聚合小程序(mini)用 /am/ 前缀, 缺省 h5
|
||||
order.setLinkForm(reqLinkForm);
|
||||
// 来源: 容器权威, 预下单即写入(trade 创建时再冗余拷贝)
|
||||
order.setSource(resolveSourceByGatewayType(typeEnum));
|
||||
order.setTitle(param.getTitle());
|
||||
@@ -187,8 +188,11 @@ public class GatewayPayAssistService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成落地页 URL
|
||||
public String buildGatewayUrl(GatewayPayOrder order) {
|
||||
/// 构建预下单返回结果(同时返回 H5 链接 + 小程序映射链接)
|
||||
///
|
||||
/// - cashier: h5Url = /cashier/{orderNo}, miniUrl = /cm/{orderNo}
|
||||
/// - aggregate: h5Url = /aggregate/{orderNo}, miniUrl = /am/{orderNo}
|
||||
public GatewayPrePayResult buildPrePayResult(GatewayPayOrder order) {
|
||||
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
|
||||
String gatewayBase = urlConfig.getPaymentGatewayBaseUrl();
|
||||
if (StrUtil.isBlank(gatewayBase)) {
|
||||
@@ -196,23 +200,18 @@ public class GatewayPayAssistService {
|
||||
throw new OperationFailException(CommonCode.FAIL_CODE, "error.common.gatewayUrlNotConfigured");
|
||||
}
|
||||
gatewayBase = StrUtil.removeSuffix(gatewayBase, "/");
|
||||
if (Objects.equals(order.getGatewayType(), GatewayPayTypeEnum.AGGREGATE.getCode())) {
|
||||
// 小程序形态用 /am/ 前缀(扫码拉起小程序), H5 用 /aggregate/
|
||||
if ("mini".equals(order.getLinkForm())) {
|
||||
return gatewayBase + "/am/" + order.getOrderNo();
|
||||
}
|
||||
return gatewayBase + "/aggregate/" + order.getOrderNo();
|
||||
}
|
||||
return gatewayBase + "/cashier/" + order.getOrderNo();
|
||||
}
|
||||
|
||||
/// 构建预下单返回结果(含落地页 URL)
|
||||
public GatewayPrePayResult buildPrePayResult(GatewayPayOrder order) {
|
||||
boolean isAggregate = Objects.equals(order.getGatewayType(), GatewayPayTypeEnum.AGGREGATE.getCode());
|
||||
String h5Url = gatewayBase + (isAggregate ? "/aggregate/" : "/cashier/") + order.getOrderNo();
|
||||
// 小程序映射链接按网关业务类型分流, 靠各平台「普通链接二维码」规则拉起小程序
|
||||
String miniUrl = gatewayBase + (isAggregate ? "/am/" : "/cm/") + order.getOrderNo();
|
||||
return new GatewayPrePayResult()
|
||||
.setOrderNo(order.getOrderNo())
|
||||
.setBizOrderNo(order.getBizOrderNo())
|
||||
.setStatus(order.getStatus())
|
||||
.setUrl(this.buildGatewayUrl(order))
|
||||
// 回传实际生效类型(幂等命中时为已有订单的类型, 与链接前缀一致)
|
||||
.setGatewayType(order.getGatewayType())
|
||||
.setH5Url(h5Url)
|
||||
.setMiniUrl(miniUrl)
|
||||
.setExpiredTime(order.getExpiredTime());
|
||||
}
|
||||
|
||||
|
||||
@@ -43,11 +43,6 @@ public class GatewayPrePayParam extends MerchantPaymentCommonParam {
|
||||
@Size(max = 32, message = "{validation.field.gatewayPayType.size}")
|
||||
private String gatewayPayType;
|
||||
|
||||
/// 链接形态: h5/mini(聚合小程序扫码前缀分流, 缺省 h5; 仅 aggregate 类型有效)
|
||||
@Schema(description = "链接形态: h5/mini(聚合小程序, 缺省 h5)")
|
||||
@Size(max = 16, message = "{validation.field.linkForm.size}")
|
||||
private String linkForm;
|
||||
|
||||
@Schema(description = "异步通知地址")
|
||||
@Size(max = 256, message = "{validation.field.notifyUrl.size}")
|
||||
private String notifyUrl;
|
||||
|
||||
@@ -21,8 +21,17 @@ public class GatewayPrePayResult {
|
||||
@Schema(description = "业务状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "网关落地页 URL")
|
||||
private String url;
|
||||
/// 网关支付类型(实际生效类型; 幂等命中时为已有订单的类型)
|
||||
@Schema(description = "网关支付类型")
|
||||
private String gatewayType;
|
||||
|
||||
/// H5 落地页 URL(cashier → /cashier/, aggregate → /aggregate/)
|
||||
@Schema(description = "H5支付链接")
|
||||
private String h5Url;
|
||||
|
||||
/// 小程序映射 URL(cashier → /cm/, aggregate → /am/ 前缀, 靠各平台「普通链接二维码」规则拉起小程序)
|
||||
@Schema(description = "小程序映射链接")
|
||||
private String miniUrl;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private OffsetDateTime expiredTime;
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "Client environment code cannot be blank",
|
||||
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
|
||||
"codeRuntimeMismatch": "QR program type does not match runtime",
|
||||
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
|
||||
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
|
||||
"bizOrderTypeImmutable": "This order no. is already in use and its type cannot be changed midway, please use a new one"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "Refund processing, do not repeat",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "Kode lingkungan klien tidak boleh kosong",
|
||||
"codeClientEnvNotConfigured": "Metode pembayaran belum dikonfigurasi untuk lingkungan dan bentuk ini",
|
||||
"codeRuntimeMismatch": "Jenis landing kode QR tidak cocok dengan runtime",
|
||||
"codeClientEnvChannelMchRequired": "Pada mode DIRECT, nomor merchant saluran baris yang diisi wajib"
|
||||
"codeClientEnvChannelMchRequired": "Pada mode DIRECT, nomor merchant saluran baris yang diisi wajib",
|
||||
"bizOrderTypeImmutable": "Nomor pesanan sudah digunakan dan jenis pesanan tidak dapat diubah, silakan ganti nomor pesanan"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "Proses pengembalian dana, jangan ulangi",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "クライアント環境コードは必須です",
|
||||
"codeClientEnvNotConfigured": "この環境と形態には支払方法が設定されていません",
|
||||
"codeRuntimeMismatch": "QRのプログラム種別とランタイムが一致しません",
|
||||
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です"
|
||||
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です",
|
||||
"bizOrderTypeImmutable": "この注文番号は使用済みで、注文タイプは途中変更できません。新しい注文番号をご使用ください"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "返金処理、繰り返さない",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "클라이언트 환경 코드는 비울 수 없습니다",
|
||||
"codeClientEnvNotConfigured": "해당 환경과 형태에 결제 수단이 구성되지 않았습니다",
|
||||
"codeRuntimeMismatch": "코드 프로그램 유형과 런타임이 일치하지 않습니다",
|
||||
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다"
|
||||
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다",
|
||||
"bizOrderTypeImmutable": "이 주문번호는 이미 사용되었으며 주문 유형은 중간에 변경할 수 없습니다. 주문번호를 변경해 주세요"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "환불 처리 중입니다. 반복하지 마세요.",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "Kod persekitaran klien tidak boleh kosong",
|
||||
"codeClientEnvNotConfigured": "Kaedah pembayaran belum dikonfigurasi untuk persekitaran dan bentuk ini",
|
||||
"codeRuntimeMismatch": "Jenis pendaratan kod QR tidak sepadan dengan runtime",
|
||||
"codeClientEnvChannelMchRequired": "Dalam mod DIRECT, nombor peniaga saluran baris yang diisi wajib"
|
||||
"codeClientEnvChannelMchRequired": "Dalam mod DIRECT, nombor peniaga saluran baris yang diisi wajib",
|
||||
"bizOrderTypeImmutable": "Nombor pesanan telah digunakan dan jenis pesanan tidak boleh ditukar, sila tukar nombor pesanan"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "Pemprosesan bayaran balik, jangan ulangi",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "รหัสสภาพแวดล้อมไคลเอนต์ต้องไม่ว่าง",
|
||||
"codeClientEnvNotConfigured": "สภาพแวดล้อมและรูปแบบนี้ยังไม่ได้กำหนดวิธีการชำระเงิน",
|
||||
"codeRuntimeMismatch": "ประเภทการลงจอดของรหัส QR ไม่ตรงกับรันไทม์",
|
||||
"codeClientEnvChannelMchRequired": "โหมด DIRECT: ต้องระบุหมายเลขร้านค้าช่องทางของแถวที่กรอก"
|
||||
"codeClientEnvChannelMchRequired": "โหมด DIRECT: ต้องระบุหมายเลขร้านค้าช่องทางของแถวที่กรอก",
|
||||
"bizOrderTypeImmutable": "หมายเลขคำสั่งซื้อถูกใช้แล้ว และประเภทคำสั่งซื้อไม่สามารถเปลี่ยนแปลงได้ กรุณาเปลี่ยนหมายเลขคำสั่งซื้อ"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "ดำเนินการคืนเงินอย่าทำซ้ำ",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "Mã môi trường máy khách không được để trống",
|
||||
"codeClientEnvNotConfigured": "Môi trường và hình thức này chưa cấu hình phương thức thanh toán",
|
||||
"codeRuntimeMismatch": "Loại landing mã QR không khớp với runtime",
|
||||
"codeClientEnvChannelMchRequired": "Chế độ DIRECT: bắt buộc có số thương nhân kênh cho mỗi dòng đã điền"
|
||||
"codeClientEnvChannelMchRequired": "Chế độ DIRECT: bắt buộc có số thương nhân kênh cho mỗi dòng đã điền",
|
||||
"bizOrderTypeImmutable": "Số đơn hàng đã được sử dụng và loại đơn hàng không thể thay đổi, vui lòng đổi số đơn hàng"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "Xử lý hoàn tiền, không lặp lại",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "客户端环境编码不能为空",
|
||||
"codeClientEnvNotConfigured": "该客户端环境与支付形态未配置支付方式",
|
||||
"codeRuntimeMismatch": "码牌落地类型与运行形态不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填",
|
||||
"bizOrderTypeImmutable": "该订单号已使用,订单类型不可中途变更,请更换订单号"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款处理中,请勿重复操作",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "用戶端環境編碼不能為空",
|
||||
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
|
||||
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
|
||||
"bizOrderTypeImmutable": "該訂單號已使用,訂單類型不可中途變更,請更換訂單號"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款處理中,請勿重複操作",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"codeClientEnvBlank": "用戶端環境編碼不能為空",
|
||||
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
|
||||
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
|
||||
"bizOrderTypeImmutable": "該訂單號已使用,訂單類型不可中途變更,請更換訂單號"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款處理中,請勿重複操作",
|
||||
|
||||
Reference in New Issue
Block a user