mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 06:46:04 +08:00
feat(gateway): 聚合锁定前置提示 + 收银台锁定项标记 + DIRECT 幂等修复
聚合 meta 进入时检测订单已锁定且当前环境 method 不匹配直接抛 channelLocked; 收银台 items 返回 locked 标记供前端禁用切换; 修复 DIRECT 模式 item.method 为空时 handle 锁定校验误判切换导致幂等返回失效
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -30,4 +30,8 @@ public class CashierItemPublicResult {
|
||||
|
||||
@Schema(description = "是否需要先走 OAuth 获取 openId(前端据此跳授权)")
|
||||
private Boolean needOpenId;
|
||||
|
||||
// 是否为订单已锁定的支付项(订单支付中且与已锁定的支付方式匹配)
|
||||
@Schema(description = "是否为订单已锁定的支付项")
|
||||
private Boolean locked;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user