From d2d08b4cc6c4044a48374a799203d92fca09ffa4 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Mon, 20 Jul 2026 23:00:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20=E8=81=9A=E5=90=88=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E5=89=8D=E7=BD=AE=E6=8F=90=E7=A4=BA=20+=20=E6=94=B6?= =?UTF-8?q?=E9=93=B6=E5=8F=B0=E9=94=81=E5=AE=9A=E9=A1=B9=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=20+=20DIRECT=20=E5=B9=82=E7=AD=89=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 聚合 meta 进入时检测订单已锁定且当前环境 method 不匹配直接抛 channelLocked; 收银台 items 返回 locked 标记供前端禁用切换; 修复 DIRECT 模式 item.method 为空时 handle 锁定校验误判切换导致幂等返回失效 --- .../pay/gateway/AggregatePayService.java | 10 ++++++++ .../pay/gateway/CashierPayService.java | 24 ++++++++++++++++++- .../pay/gateway/GatewayPayHandleService.java | 8 ++++++- .../gateway/CashierItemPublicResult.java | 4 ++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/AggregatePayService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/AggregatePayService.java index 30fcbdeca..f80abe2c4 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/AggregatePayService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/AggregatePayService.java @@ -7,6 +7,7 @@ import cn.daxpay.open.payment.merchant.enums.ClientEnvEnum; import cn.daxpay.open.payment.merchant.enums.ClientRuntimeEnum; import cn.daxpay.open.payment.merchant.service.gateway.ClientEnvPayResolveService; import cn.daxpay.open.payment.strategy.risk.PayRiskChecker; +import cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum; import cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum; import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; import cn.daxpay.open.payment.unipay.param.gateway.AggregateQrPayParam; @@ -52,6 +53,15 @@ public class AggregatePayService { ClientRuntimeEnum runtime = ClientRuntimeEnum.ofOrDefault(runtimeCode); var resolved = clientEnvPayResolveService.resolveRequired(order.getAppId(), clientEnv, runtime); + // 订单已发起支付(支付中)时, 支付方式已锁定到容器: 当前环境解析出的 method 与锁定值不一致视为换端, 提前拒绝 + // 与 GatewayPayHandleService#handle 的 method 比较语义一致(聚合 product 传 null, 实际只比 method) + if (Objects.equals(order.getStatus(), GatewayOrderStatusEnum.PAYING.getCode()) + && StrUtil.isNotBlank(order.getMethod()) + && !Objects.equals(order.getMethod(), resolved.method())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.channelLocked"); + } + GatewayAggregateConfig config = aggregateConfigManager.findByAppId(order.getAppId()) .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.aggregateConfigMissing")); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierPayService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierPayService.java index 4c3727da2..b76e64332 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierPayService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierPayService.java @@ -7,6 +7,7 @@ import cn.daxpay.open.payment.merchant.enums.CashierItemResolveModeEnum; import cn.daxpay.open.payment.merchant.enums.ClientEnvEnum; import cn.daxpay.open.payment.merchant.enums.GatewayCashierTypeEnum; import cn.daxpay.open.payment.strategy.risk.PayRiskChecker; +import cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum; import cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum; import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; import cn.daxpay.open.payment.unipay.param.gateway.CashierPayParam; @@ -62,12 +63,33 @@ public class CashierPayService { GatewayCashierTypeEnum typeEnum = GatewayCashierTypeEnum.findByCode(cashierType); String bucketClientEnv = normalizeClientEnvForBucket(typeEnum, clientEnv); ClientEnvEnum envEnum = StrUtil.isBlank(bucketClientEnv) ? null : ClientEnvEnum.findByCode(bucketClientEnv); + // 订单已发起支付(支付中)且 method 有值时, 支付方式已锁定, 需标记匹配的支付项供前端禁用切换 + boolean orderLocked = Objects.equals(order.getStatus(), GatewayOrderStatusEnum.PAYING.getCode()) + && StrUtil.isNotBlank(order.getMethod()); return gatewayCashierItemManager.listByAppAndBucket(order.getAppId(), typeEnum.getCode(), bucketClientEnv) .stream() - .map(item -> this.toPublicResult(item, envEnum)) + .map(item -> this.toPublicResult(item, envEnum) + // 命中锁定的支付项置 locked=true, 前端据此自动选中并禁用其他项 + .setLocked(orderLocked && this.isLockedItem(item, order))) .toList(); } + /// 判断收银台支付项是否为订单已锁定的支付方式 + /// + /// - METHOD 模式: 比较支付方式编码 + /// - DIRECT 模式: 比较通道商户号 + 支付能力(均为路由回填到容器的权威值) + private boolean isLockedItem(GatewayCashierItem item, GatewayPayOrder order) { + CashierItemResolveModeEnum resolveMode = CashierItemResolveModeEnum.findByCode(item.getResolveMode()); + if (resolveMode == CashierItemResolveModeEnum.METHOD) { + return Objects.equals(item.getMethod(), order.getMethod()); + } + if (resolveMode == CashierItemResolveModeEnum.DIRECT) { + return Objects.equals(item.getChannelMchNo(), order.getChannelMchNo()) + && Objects.equals(item.getCapability(), order.getCapability()); + } + return false; + } + /// 收银台发起支付 public NormalPayResult pay(CashierPayParam param) { GatewayPayOrder order = gatewayPayAssistService.getOrderAndCheck(param.getOrderNo()); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java index 27a18c840..a7efa7ef7 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java @@ -108,8 +108,14 @@ public class GatewayPayHandleService { // 通道/方式不一致则拒绝换端(product/method 在容器上) // 聚合支付 product 传 null(由路由解析填充) 是合法设计, 用容器已持久化值补位避免误判切换支付方式 String effectiveProduct = StrUtil.blankToDefault(product, current.getProduct()); + // 收银台 DIRECT 模式 item.method 可能为空(由路由反推), 容器上的 method 是首次支付时路由反推后的值; + // 这里先反推对齐, 避免与容器已持久化值口径不一致而误判切换支付方式 + String effectiveMethod = method; + if (StrUtil.isBlank(effectiveMethod) && StrUtil.isNotBlank(channelMchNo)) { + effectiveMethod = payRouteService.inferMethodForCapability(channelMchNo, capability); + } if (!Objects.equals(current.getProduct(), effectiveProduct) - || !Objects.equals(current.getMethod(), method)) { + || !Objects.equals(current.getMethod(), effectiveMethod)) { throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.channelLocked"); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/CashierItemPublicResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/CashierItemPublicResult.java index e08219d35..96b248715 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/CashierItemPublicResult.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/CashierItemPublicResult.java @@ -30,4 +30,8 @@ public class CashierItemPublicResult { @Schema(description = "是否需要先走 OAuth 获取 openId(前端据此跳授权)") private Boolean needOpenId; + + // 是否为订单已锁定的支付项(订单支付中且与已锁定的支付方式匹配) + @Schema(description = "是否为订单已锁定的支付项") + private Boolean locked; }