diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopTradeController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopTradeController.java index 8dc522eb2..ada0bb317 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopTradeController.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopTradeController.java @@ -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 pay(@RequestBody DevelopParam param) { return Res.ok(developTradeService.pay(param)); } + + @PermCode(code = "view", nameCn = "查看", nameEn = "View") + @Operation(summary = "已启用渠道支付方式目录") + @GetMapping("/method-directory") + public Result> methodDirectory() { + return Res.ok(developTradeService.listMethodDirectory()); + } + + @PermCode(code = "view", nameCn = "查看", nameEn = "View") + @Operation(summary = "传值模式通道商户候选") + @GetMapping("/channel-mch-candidates") + public Result> 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> capabilityCandidates( + @NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) { + return Res.ok(developTradeService.listCapabilityCandidates(channelMchNo)); + } } diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopTradeService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopTradeService.java index b6f86dc5a..d8c6b9fce 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopTradeService.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopTradeService.java @@ -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 param) { @@ -56,4 +64,19 @@ public class DevelopTradeService { .setSignInfo(signInfo) .setPayResult(payResult); } + + /// 已启用渠道支付方式目录(供调试页支付方式下拉) + public List listMethodDirectory() { + return payProviderMethodService.listDirectoryFlat(); + } + + /// 传值模式:商户全部启用通道商户候选 + public List listChannelMchCandidates(String mchNo) { + return payRouteStrategyCapabilitySupport.listDirectChannelMchCandidates(mchNo); + } + + /// 传值模式:按通道商户(产品)返回全部启用支付能力候选 + public List listCapabilityCandidates(String channelMchNo) { + return payRouteStrategyCapabilitySupport.listDirectCapabilityCandidates(channelMchNo); + } } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/old/pay/support/ProductStrategySupport.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/old/pay/support/ProductStrategySupport.java index c00a97c4d..70f77847b 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/old/pay/support/ProductStrategySupport.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/old/pay/support/ProductStrategySupport.java @@ -55,14 +55,6 @@ public class ProductStrategySupport { return List.copyOf(capabilities); } - /// 按支付方式编码获取策略声明的支付能力 - public List 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> mapping = strategy.methodCapabilityMapping(); + if (CollUtil.isEmpty(mapping)) { + return null; + } + for (Map.Entry> entry : mapping.entrySet()) { + if (entry.getValue() != null && entry.getValue().contains(capability)) { + return entry.getKey(); + } + } + return null; } + } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteService.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteService.java index ac99b32cd..318320026 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteService.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteService.java @@ -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 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); diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteStrategyCapabilitySupport.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteStrategyCapabilitySupport.java index 570eb9a9d..2c134f54b 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteStrategyCapabilitySupport.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteStrategyCapabilitySupport.java @@ -91,7 +91,6 @@ public class PayRouteStrategyCapabilitySupport { /// 从商户全部通道商户中筛出启用且其产品支持该(provider,method)的候选 private List filterChannelMchForDirectory(List mchants, String provider, String method) { - PayMethodEnum methodEnum = PayMethodEnum.findByCode(method); List results = new ArrayList<>(); for (ChannelMerchant mch : mchants) { if (!Boolean.TRUE.equals(mch.getEnable())) { @@ -136,6 +135,51 @@ public class PayRouteStrategyCapabilitySupport { return null; } + /// 传值模式: 商户全部启用通道商户候选(不按支付方式过滤) + public List listDirectChannelMchCandidates(String mchNo) { + List mchants = channelMerchantManager.findAllByMchNo(mchNo); + List 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 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)) { diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/route/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/route/error.json index ceded6b2e..f9feda313 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/route/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/route/error.json @@ -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}]" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/route/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/route/error.json index 37944be16..f043445a8 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/route/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/route/error.json @@ -38,5 +38,6 @@ "duplicateSceneMethod": "场景模式下同一支付方式[{0}]存在多条配置", "sceneChannelMchCapabilityPair": "支付方式[{0}]须同时选择通道商户与支付能力,或同时留空", "basicChannelMchNotConfigured": "支付渠道[{0}]未配置通道商户,请在通道路由基础模式中完成配置", - "sceneCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配" + "sceneCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配", + "directCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]不匹配" }