mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 23:01:58 +08:00
feat(develop): 支付调试页新增路由/传值模式切换, 传值模式由能力反推支付方式
- 调试页商户信息支持路由模式(商户+方式+应用动态匹配)与传值模式(通道商户+能力直接决定)切换 - 传值模式 capability 必填, method 由后端从(通道商户, 能力)反推回填, 下游透明 - ProductStrategySupport 新增 methodForCapability 反推; resolveDirect 重构去 inferCapability 派生 - 调试候选接口去 method 参数, 通道商户/能力候选不再依赖支付方式
This commit is contained in:
@@ -4,18 +4,24 @@ import cn.daxpay.open.payment.admin.param.develop.DevelopParam;
|
||||
import cn.daxpay.open.payment.admin.result.develop.DevelopPayResult;
|
||||
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
|
||||
import cn.daxpay.open.payment.admin.service.develop.DevelopTradeService;
|
||||
import cn.daxpay.open.payment.masterdata.constants.provider.result.PayProviderMethodResult;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.dto.LabelValue;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// 交易开发调试(管理)
|
||||
@PermCode(menuCode = "payment:develop:trade")
|
||||
@Tag(name = "交易开发调试服务")
|
||||
@@ -39,4 +45,27 @@ public class DevelopTradeController {
|
||||
public Result<DevelopPayResult> pay(@RequestBody DevelopParam<PayParam> param) {
|
||||
return Res.ok(developTradeService.pay(param));
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
|
||||
@Operation(summary = "已启用渠道支付方式目录")
|
||||
@GetMapping("/method-directory")
|
||||
public Result<List<PayProviderMethodResult>> methodDirectory() {
|
||||
return Res.ok(developTradeService.listMethodDirectory());
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
|
||||
@Operation(summary = "传值模式通道商户候选")
|
||||
@GetMapping("/channel-mch-candidates")
|
||||
public Result<List<LabelValue>> channelMchCandidates(
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo) {
|
||||
return Res.ok(developTradeService.listChannelMchCandidates(mchNo));
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
|
||||
@Operation(summary = "传值模式支付能力候选")
|
||||
@GetMapping("/capability-candidates")
|
||||
public Result<List<LabelValue>> capabilityCandidates(
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) {
|
||||
return Res.ok(developTradeService.listCapabilityCandidates(channelMchNo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,21 @@ import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
|
||||
import cn.daxpay.open.payment.common.context.PaymentScopeManager;
|
||||
import cn.daxpay.open.payment.common.util.ObjectSignStrUtil;
|
||||
import cn.daxpay.open.payment.common.util.PaySignUtil;
|
||||
import cn.daxpay.open.payment.masterdata.constants.provider.result.PayProviderMethodResult;
|
||||
import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService;
|
||||
import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport;
|
||||
import cn.daxpay.open.payment.pay.service.NormalPayService;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.PayResult;
|
||||
import cn.daxpay.open.platform.common.json.util.JsonUtil;
|
||||
import cn.daxpay.open.platform.core.rest.dto.LabelValue;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 交易开发调试服务
|
||||
///
|
||||
/// 复用普通支付核心服务 NormalPayService, 在管理端以指定商户/应用身份真实发起一笔支付,
|
||||
@@ -26,6 +32,8 @@ public class DevelopTradeService {
|
||||
|
||||
private final NormalPayService normalPayService;
|
||||
private final PaymentScopeManager paymentScopeManager;
|
||||
private final PayProviderMethodService payProviderMethodService;
|
||||
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
|
||||
|
||||
/// 生成支付参数签名(与正式签名逻辑一致)
|
||||
public DevelopSignResult sign(DevelopParam<PayParam> param) {
|
||||
@@ -56,4 +64,19 @@ public class DevelopTradeService {
|
||||
.setSignInfo(signInfo)
|
||||
.setPayResult(payResult);
|
||||
}
|
||||
|
||||
/// 已启用渠道支付方式目录(供调试页支付方式下拉)
|
||||
public List<PayProviderMethodResult> listMethodDirectory() {
|
||||
return payProviderMethodService.listDirectoryFlat();
|
||||
}
|
||||
|
||||
/// 传值模式:商户全部启用通道商户候选
|
||||
public List<LabelValue> listChannelMchCandidates(String mchNo) {
|
||||
return payRouteStrategyCapabilitySupport.listDirectChannelMchCandidates(mchNo);
|
||||
}
|
||||
|
||||
/// 传值模式:按通道商户(产品)返回全部启用支付能力候选
|
||||
public List<LabelValue> listCapabilityCandidates(String channelMchNo) {
|
||||
return payRouteStrategyCapabilitySupport.listDirectCapabilityCandidates(channelMchNo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +55,6 @@ public class ProductStrategySupport {
|
||||
return List.copyOf(capabilities);
|
||||
}
|
||||
|
||||
/// 按支付方式编码获取策略声明的支付能力
|
||||
public List<PayCapabilityEnum> capabilitiesForMethodCode(AbsProductStrategy strategy, String methodCode) {
|
||||
if (StrUtil.isBlank(methodCode)) {
|
||||
return List.of();
|
||||
}
|
||||
PayMethodEnum method = PayMethodEnum.findByCode(methodCode);
|
||||
return capabilitiesForMethod(strategy, method);
|
||||
}
|
||||
|
||||
/// 产品是否支持渠道目录中的某一支付方式
|
||||
public boolean supportsDirectoryMethod(AbsProductStrategy strategy, PayMethodEnum method) {
|
||||
@@ -78,7 +70,7 @@ public class ProductStrategySupport {
|
||||
}
|
||||
|
||||
/// 按能力编码判断策略是否在该目录方式下声明支持
|
||||
public boolean strategySupportsCapabilityCode(AbsProductStrategy strategy, String methodCode, String capabilityCode) {
|
||||
public static boolean strategySupportsCapabilityCode(AbsProductStrategy strategy, String methodCode, String capabilityCode) {
|
||||
if (StrUtil.isBlank(methodCode) || StrUtil.isBlank(capabilityCode)) {
|
||||
return false;
|
||||
}
|
||||
@@ -87,8 +79,21 @@ public class ProductStrategySupport {
|
||||
return strategySupportsCapability(strategy, method, capability);
|
||||
}
|
||||
|
||||
/// 产品已声明能力是否覆盖渠道目录中的某一支付方式(兼容旧调用名)
|
||||
public boolean capabilityCoversDirectoryMethod(AbsProductStrategy strategy, PayMethodEnum method) {
|
||||
return supportsDirectoryMethod(strategy, method);
|
||||
/// 反推: 给定策略与能力, 返回所属支付方式(多归属取首个, 无则 null)
|
||||
public static PayMethodEnum methodForCapability(AbsProductStrategy strategy, PayCapabilityEnum capability) {
|
||||
if (capability == null) {
|
||||
return null;
|
||||
}
|
||||
Map<PayMethodEnum, List<PayCapabilityEnum>> mapping = strategy.methodCapabilityMapping();
|
||||
if (CollUtil.isEmpty(mapping)) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<PayMethodEnum, List<PayCapabilityEnum>> entry : mapping.entrySet()) {
|
||||
if (entry.getValue() != null && entry.getValue().contains(capability)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import cn.daxpay.open.payment.merchant.dao.route.scene.PayRouteSceneConfigManage
|
||||
import cn.daxpay.open.payment.merchant.dao.route.strategy.PayRouteStrategyManager;
|
||||
import cn.daxpay.open.payment.merchant.service.route.basic.PayRouteBasicMatcher;
|
||||
import cn.daxpay.open.payment.merchant.service.route.scene.PayRouteSceneMatcher;
|
||||
import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport;
|
||||
import cn.daxpay.open.payment.merchant.service.route.model.RouteHit;
|
||||
import cn.daxpay.open.payment.merchant.entity.route.strategy.PayRouteStrategy;
|
||||
import cn.daxpay.open.payment.merchant.service.route.model.PayRouteBundle;
|
||||
@@ -13,6 +14,7 @@ import cn.daxpay.open.payment.old.pay.support.ProductStrategySupport;
|
||||
import cn.daxpay.open.payment.pay.service.route.PayRouteFacade;
|
||||
import cn.daxpay.open.payment.strategy.product.AbsProductStrategy;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
|
||||
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
@@ -43,6 +45,7 @@ public class PayRouteService implements PayRouteFacade {
|
||||
private final PayRouteBasicConfigManager basicConfigManager;
|
||||
private final PayRouteProductResolver productResolver;
|
||||
private final PayRouteBasicMatcher basicMatcher;
|
||||
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
|
||||
|
||||
/// 实付路由解析:直定模式优先,其次兼容已指定 product,最后按策略模式匹配
|
||||
@Override
|
||||
@@ -72,30 +75,38 @@ public class PayRouteService implements PayRouteFacade {
|
||||
fillPayParam(payParam, hit);
|
||||
}
|
||||
|
||||
/// 直定模式:由通道商户号推导产品,校验或派生支付能力
|
||||
/// 直定模式:capability 必填,由通道商户推导产品;method 未传时由(通道商户, 能力)反推回填
|
||||
private void resolveDirect(PayParam payParam) {
|
||||
if (StrUtil.isBlank(payParam.getMethod())) {
|
||||
// 场景模式下须选择支付方式
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneMethodRequired");
|
||||
}
|
||||
String channelMchNo = payParam.getChannelMchNo();
|
||||
String capability = payParam.getCapability();
|
||||
// 传值模式: 支付能力必填
|
||||
if (StrUtil.isBlank(capability)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.route.error.sceneCapabilityRequired");
|
||||
}
|
||||
String product = productResolver.productOfChannelMchNo(channelMchNo);
|
||||
if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) {
|
||||
// 支付产品策略不存在
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.route.error.productStrategyMissing");
|
||||
}
|
||||
PayMethodEnum methodEnum = PayMethodEnum.findByCode(payParam.getMethod());
|
||||
String capability = payParam.getCapability();
|
||||
if (StrUtil.isBlank(capability)) {
|
||||
// 未传能力则按 (产品, 支付方式) 派生
|
||||
capability = inferCapability(product, methodEnum);
|
||||
String method = payParam.getMethod();
|
||||
if (StrUtil.isBlank(method)) {
|
||||
// 传值模式: 由(通道商户, 支付能力)反推支付方式, 供下游通道策略使用
|
||||
String inferred = payRouteStrategyCapabilitySupport.inferMethodForCapability(channelMchNo, capability);
|
||||
if (inferred == null) {
|
||||
// 支付能力[{0}]与通道商户[{1}]不匹配
|
||||
PayCapabilityEnum capEnum = PayCapabilityEnum.findByCode(capability);
|
||||
String capLabel = capEnum != null ? I18nUtil.getEnumName(capEnum) : capability;
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.route.error.directCapabilityChannelMchMismatch", capLabel, channelMchNo);
|
||||
}
|
||||
payParam.setMethod(inferred);
|
||||
} else {
|
||||
// 已传能力则校验属于该(产品, 支付方式)候选
|
||||
validateCapability(product, methodEnum, capability);
|
||||
// 已传支付方式: 校验(产品, 方式, 能力)一致
|
||||
validateCapability(product, PayMethodEnum.findByCode(method), capability);
|
||||
}
|
||||
payParam.setProduct(product);
|
||||
payParam.setCapability(capability);
|
||||
}
|
||||
|
||||
/// 按应用号从库加载路由数据包(无 Redis 缓存)
|
||||
@@ -133,13 +144,6 @@ public class PayRouteService implements PayRouteFacade {
|
||||
}
|
||||
}
|
||||
|
||||
/// 派生支付能力:取产品策略声明的(方式→能力)首个
|
||||
private String inferCapability(String product, PayMethodEnum method) {
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class);
|
||||
List<PayCapabilityEnum> capabilities = ProductStrategySupport.capabilitiesForMethod(strategy, method);
|
||||
return capabilities.isEmpty() ? null : capabilities.getFirst().getCode();
|
||||
}
|
||||
|
||||
/// 校验指定能力属于该(产品, 支付方式)的策略声明候选
|
||||
private void validateCapability(String product, PayMethodEnum method, String capability) {
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class);
|
||||
|
||||
@@ -91,7 +91,6 @@ public class PayRouteStrategyCapabilitySupport {
|
||||
|
||||
/// 从商户全部通道商户中筛出启用且其产品支持该(provider,method)的候选
|
||||
private List<LabelValue> filterChannelMchForDirectory(List<ChannelMerchant> mchants, String provider, String method) {
|
||||
PayMethodEnum methodEnum = PayMethodEnum.findByCode(method);
|
||||
List<LabelValue> results = new ArrayList<>();
|
||||
for (ChannelMerchant mch : mchants) {
|
||||
if (!Boolean.TRUE.equals(mch.getEnable())) {
|
||||
@@ -136,6 +135,51 @@ public class PayRouteStrategyCapabilitySupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 传值模式: 商户全部启用通道商户候选(不按支付方式过滤)
|
||||
public List<LabelValue> listDirectChannelMchCandidates(String mchNo) {
|
||||
List<ChannelMerchant> mchants = channelMerchantManager.findAllByMchNo(mchNo);
|
||||
List<LabelValue> results = new ArrayList<>();
|
||||
for (ChannelMerchant mch : mchants) {
|
||||
if (!Boolean.TRUE.equals(mch.getEnable())) {
|
||||
continue;
|
||||
}
|
||||
String label = StrUtil.isNotBlank(mch.getChannelMerchantName())
|
||||
? mch.getChannelMerchantName() : mch.getChannelMchNo();
|
||||
if (results.stream().noneMatch(item -> Objects.equals(item.getValue(), mch.getChannelMchNo()))) {
|
||||
results.add(new LabelValue(label, mch.getChannelMchNo()));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 传值模式: 按通道商户(产品)返回全部启用支付能力候选(不按支付方式过滤)
|
||||
public List<LabelValue> listDirectCapabilityCandidates(String channelMchNo) {
|
||||
String product = productOfChannelMchNo(channelMchNo);
|
||||
if (StrUtil.isBlank(product) || !PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) {
|
||||
return List.of();
|
||||
}
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class);
|
||||
return ProductStrategySupport.supportedPayCapabilities(strategy).stream()
|
||||
.filter(capability -> productCapabilityEnabled(product, capability.getCode()))
|
||||
.map(capability -> new LabelValue(I18nUtil.getEnumName(capability), capability.getCode()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 传值模式: 由(通道商户, 支付能力)反推支付方式编码(策略 Map + DB 启用, 无则 null)
|
||||
public String inferMethodForCapability(String channelMchNo, String capabilityCode) {
|
||||
String product = productOfChannelMchNo(channelMchNo);
|
||||
if (StrUtil.isBlank(product) || !PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) {
|
||||
return null;
|
||||
}
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class);
|
||||
PayCapabilityEnum capability = PayCapabilityEnum.findByCode(capabilityCode);
|
||||
if (capability == null || !productCapabilityEnabled(product, capabilityCode)) {
|
||||
return null;
|
||||
}
|
||||
PayMethodEnum method = ProductStrategySupport.methodForCapability(strategy, capability);
|
||||
return method == null ? null : method.getCode();
|
||||
}
|
||||
|
||||
/// 校验场景配置项所选能力在候选集合内
|
||||
public void validateSceneCapability(String provider, String method, String channelMchNo, String capability) {
|
||||
if (StrUtil.isBlank(capability)) {
|
||||
|
||||
@@ -38,5 +38,6 @@
|
||||
"duplicateSceneMethod": "Duplicate scene configuration for pay method [{0}]",
|
||||
"sceneChannelMchCapabilityPair": "For pay method [{0}], select both channel merchant and capability, or leave both empty",
|
||||
"basicChannelMchNotConfigured": "No channel merchant configured for pay brand [{0}]; configure it in basic route mode",
|
||||
"sceneCapabilityChannelMchMismatch": "Capability [{0}] does not match channel merchant [{1}] and method [{2}]"
|
||||
"sceneCapabilityChannelMchMismatch": "Capability [{0}] does not match channel merchant [{1}] and method [{2}]",
|
||||
"directCapabilityChannelMchMismatch": "Capability [{0}] does not match channel merchant [{1}]"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,6 @@
|
||||
"duplicateSceneMethod": "场景模式下同一支付方式[{0}]存在多条配置",
|
||||
"sceneChannelMchCapabilityPair": "支付方式[{0}]须同时选择通道商户与支付能力,或同时留空",
|
||||
"basicChannelMchNotConfigured": "支付渠道[{0}]未配置通道商户,请在通道路由基础模式中完成配置",
|
||||
"sceneCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配"
|
||||
"sceneCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配",
|
||||
"directCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]不匹配"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user