From f092544c48884b5333f724bf9bd4f4cbde979143 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Fri, 26 Jun 2026 21:03:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor(route):=20=E9=80=9A=E9=81=93=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=9C=BA=E6=99=AF=E6=A8=A1=E5=BC=8F=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BA=E9=80=9A=E9=81=93=E5=95=86=E6=88=B7+=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E8=83=BD=E5=8A=9B=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 场景/基础模式配置以 channelMchNo 替代 product, 解决一商户多通道商户无法唯一定位; PayMethodEnum 加 provider 字段由支付方式直接解析, 移除从 product 反推; PayParam 新增 capability 支持路由回填与直定模式(指定 channelMchNo 跳过路由); 候选数据源改为商户实际开通的 ChannelMerchant, 修复原配置不考虑开通情况; 删除 dead code(PayRouteSceneRouteResolver/PayRouteCapabilityService) 并重建两表 DDL --- _config/sql/update-tables.sql | 76 +++++++ .../route/PayRouteAdminController.java | 30 +-- .../dao/mch/ChannelMerchantManager.java | 9 + .../unipay/param/trade/pay/PayParam.java | 6 + .../basic/PayRouteBasicConfigConvert.java | 2 + .../route/basic/PayRouteBasicConfig.java | 4 +- .../route/scene/PayRouteSceneConfig.java | 8 +- .../route/basic/PayRouteBasicConfigItem.java | 4 +- .../PayRouteSceneCapabilityBatchItem.java | 6 +- .../route/scene/PayRouteSceneConfigItem.java | 6 +- .../basic/PayRouteBasicConfigResult.java | 11 +- .../scene/PayRouteSceneConfigResult.java | 7 +- .../basic/PayRouteBasicConfigService.java | 92 +++++---- .../route/basic/PayRouteBasicMatcher.java | 79 ++++---- .../route/facade/PayRouteConfigService.java | 26 +-- .../service/route/model/RouteHit.java | 10 +- .../runtime/PayRouteProductResolver.java | 26 ++- .../route/runtime/PayRouteService.java | 77 +++++++- .../scene/PayRouteSceneConfigService.java | 185 +++++++----------- .../route/scene/PayRouteSceneMatcher.java | 35 +--- .../scene/PayRouteSceneRouteResolver.java | 63 ------ .../support/PayRouteCapabilityService.java | 57 ------ .../PayRouteStrategyCapabilitySupport.java | 102 ++++++---- .../resources/i18n/en-US/pay/route/error.json | 9 +- .../main/resources/i18n/en-US/validation.json | 6 +- .../resources/i18n/zh-CN/pay/route/error.json | 9 +- .../main/resources/i18n/zh-CN/validation.json | 6 +- .../core/enums/pay/channel/PayMethodEnum.java | 66 ++++--- 28 files changed, 541 insertions(+), 476 deletions(-) delete mode 100644 daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneRouteResolver.java delete mode 100644 daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteCapabilityService.java diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 431035f2e..73ea1306b 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -181,3 +181,79 @@ COMMENT ON COLUMN iam_user_two_factor.deleted IS '逻辑删除标志'; -- 用户ID唯一索引(未删除范围内, 一个用户最多一条绑定记录) CREATE UNIQUE INDEX IF NOT EXISTS uk_iam_user_two_factor_user_id ON iam_user_two_factor (user_id) WHERE deleted = false; + +-- =================================== +-- 支付通道路由: 场景模式配置表 + 基础模式配置表 +-- 重构: 以通道商户号(channel_mch_no)替代支付产品(product)定位,场景模式新增支付能力(capability) +-- =================================== + +DROP TABLE IF EXISTS pay_route_scene_config; +DROP TABLE IF EXISTS pay_route_basic_config; + +-- 支付通道路由场景模式配置(每支付方式一条: 通道商户 + 支付能力) +CREATE TABLE IF NOT EXISTS pay_route_scene_config ( + id bigint NOT NULL, + strategy_id bigint NOT NULL, + provider varchar(32), + channel varchar(32), + method varchar(32) NOT NULL, + channel_mch_no varchar(32) NOT NULL, + capability varchar(32) NOT NULL, + creator bigint, + create_time timestamptz(6), + last_modifier bigint, + last_modified_time timestamptz(6), + version int NOT NULL DEFAULT 0, + deleted boolean NOT NULL DEFAULT false, + CONSTRAINT pay_route_scene_config_pkey PRIMARY KEY (id) +); + +COMMENT ON TABLE pay_route_scene_config IS '支付通道路由场景模式配置'; +COMMENT ON COLUMN pay_route_scene_config.id IS '主键'; +COMMENT ON COLUMN pay_route_scene_config.strategy_id IS '路由策略ID'; +COMMENT ON COLUMN pay_route_scene_config.provider IS '支付渠道(派生自支付方式)'; +COMMENT ON COLUMN pay_route_scene_config.channel IS '通道编码(派生自通道商户绑定的产品)'; +COMMENT ON COLUMN pay_route_scene_config.method IS '支付方式编码'; +COMMENT ON COLUMN pay_route_scene_config.channel_mch_no IS '通道商户号(唯一绑定支付产品)'; +COMMENT ON COLUMN pay_route_scene_config.capability IS '支付能力编码'; +COMMENT ON COLUMN pay_route_scene_config.creator IS '创建人ID'; +COMMENT ON COLUMN pay_route_scene_config.create_time IS '创建时间'; +COMMENT ON COLUMN pay_route_scene_config.last_modifier IS '最后修改人ID'; +COMMENT ON COLUMN pay_route_scene_config.last_modified_time IS '最后修改时间'; +COMMENT ON COLUMN pay_route_scene_config.version IS '版本号'; +COMMENT ON COLUMN pay_route_scene_config.deleted IS '逻辑删除'; + +-- 策略 + 支付方式 唯一索引(未删除范围内) +CREATE UNIQUE INDEX IF NOT EXISTS uk_pay_route_scene_config_method + ON pay_route_scene_config (strategy_id, method) WHERE deleted = false; + +-- 支付通道路由基础模式配置(每支付渠道一条: 通道商户) +CREATE TABLE IF NOT EXISTS pay_route_basic_config ( + id bigint NOT NULL, + strategy_id bigint NOT NULL, + provider varchar(32) NOT NULL, + channel_mch_no varchar(32) NOT NULL, + creator bigint, + create_time timestamptz(6), + last_modifier bigint, + last_modified_time timestamptz(6), + version int NOT NULL DEFAULT 0, + deleted boolean NOT NULL DEFAULT false, + CONSTRAINT pay_route_basic_config_pkey PRIMARY KEY (id) +); + +COMMENT ON TABLE pay_route_basic_config IS '支付通道路由基础模式配置'; +COMMENT ON COLUMN pay_route_basic_config.id IS '主键'; +COMMENT ON COLUMN pay_route_basic_config.strategy_id IS '路由策略ID'; +COMMENT ON COLUMN pay_route_basic_config.provider IS '支付渠道'; +COMMENT ON COLUMN pay_route_basic_config.channel_mch_no IS '通道商户号(唯一绑定支付产品)'; +COMMENT ON COLUMN pay_route_basic_config.creator IS '创建人ID'; +COMMENT ON COLUMN pay_route_basic_config.create_time IS '创建时间'; +COMMENT ON COLUMN pay_route_basic_config.last_modifier IS '最后修改人ID'; +COMMENT ON COLUMN pay_route_basic_config.last_modified_time IS '最后修改时间'; +COMMENT ON COLUMN pay_route_basic_config.version IS '版本号'; +COMMENT ON COLUMN pay_route_basic_config.deleted IS '逻辑删除'; + +-- 策略 + 支付渠道 唯一索引(未删除范围内) +CREATE UNIQUE INDEX IF NOT EXISTS uk_pay_route_basic_config_provider + ON pay_route_basic_config (strategy_id, provider) WHERE deleted = false; diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/route/PayRouteAdminController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/route/PayRouteAdminController.java index a06432d03..60c441238 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/route/PayRouteAdminController.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/route/PayRouteAdminController.java @@ -78,15 +78,15 @@ public class PayRouteAdminController { } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") - @Operation(summary = "通道路由白名单目录下全部产品候选(批量)") - @GetMapping("/scene-config/product-candidates-batch") - public Result>> listSceneProductCandidatesBatch( + @Operation(summary = "通道路由白名单目录下全部通道商户候选(批量)") + @GetMapping("/scene-config/channel-mch-candidates-batch") + public Result>> listSceneChannelMchCandidatesBatch( @NotBlank(message = "{validation.field.appId.notBlank}") String appId) { - return Res.ok(configService.listSceneProductCandidatesBatch(appId)); + return Res.ok(configService.listSceneChannelMchCandidatesBatch(appId)); } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") - @Operation(summary = "按目录项与产品批量返回支付能力候选") + @Operation(summary = "按目录项与通道商户批量返回支付能力候选") @PostMapping("/scene-config/capability-candidates-batch") public Result>> listSceneCapabilityCandidatesBatch( @Valid @RequestBody PayRouteSceneCapabilityBatchParam param) { @@ -94,35 +94,35 @@ public class PayRouteAdminController { } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") - @Operation(summary = "目录项下商户可用支付产品候选") - @GetMapping("/scene-config/product-candidates") - public Result> listSceneProductCandidates( + @Operation(summary = "目录项下商户已开通的通道商户候选") + @GetMapping("/scene-config/channel-mch-candidates") + public Result> listSceneChannelMchCandidates( @NotBlank(message = "{validation.field.appId.notBlank}") String appId, @NotBlank(message = "{validation.field.provider.notBlank}") String provider, @NotBlank(message = "{validation.field.method.notBlank}") String method) { - return Res.ok(configService.listSceneProductCandidatesForMethod(appId, provider, method)); + return Res.ok(configService.listSceneChannelMchCandidatesForMethod(appId, provider, method)); } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") - @Operation(summary = "目录项与产品下支付能力候选") + @Operation(summary = "目录项与通道商户下支付能力候选") @GetMapping("/scene-config/capability-candidates") public Result> listSceneCapabilityCandidates( @NotBlank(message = "{validation.field.appId.notBlank}") String appId, @NotBlank(message = "{validation.field.provider.notBlank}") String provider, @NotBlank(message = "{validation.field.method.notBlank}") String method, - @NotBlank(message = "{validation.field.product.notBlank}") String product) { - return Res.ok(configService.listSceneCapabilityCandidatesForMethod(appId, provider, method, product)); + @NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) { + return Res.ok(configService.listSceneCapabilityCandidatesForMethod(appId, provider, method, channelMchNo)); } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") - @Operation(summary = "推断目录项与产品下唯一支付能力(仅供回显)") + @Operation(summary = "推断目录项与通道商户下唯一支付能力(仅供回显)") @GetMapping("/scene-config/infer-capability") public Result inferSceneCapability( @NotBlank(message = "{validation.field.appId.notBlank}") String appId, @NotBlank(message = "{validation.field.provider.notBlank}") String provider, @NotBlank(message = "{validation.field.method.notBlank}") String method, - @NotBlank(message = "{validation.field.product.notBlank}") String product) { - return Res.ok(configService.inferSceneCapability(appId, provider, method, product)); + @NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) { + return Res.ok(configService.inferSceneCapability(appId, provider, method, channelMchNo)); } @PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View") diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/channel/dao/mch/ChannelMerchantManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/channel/dao/mch/ChannelMerchantManager.java index a90b338ed..59361f40a 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/channel/dao/mch/ChannelMerchantManager.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/channel/dao/mch/ChannelMerchantManager.java @@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; /// # 通道商户管理 /// @@ -51,4 +52,12 @@ public class ChannelMerchantManager extends BaseManager findByMchNoAndChannelMchNo(String mchNo, String channelMchNo){ + return this.lambdaQuery() + .eq(ChannelMerchant::getMchNo, mchNo) + .eq(ChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt(); + } } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/trade/pay/PayParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/trade/pay/PayParam.java index a3b7c2701..ae16331ad 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/trade/pay/PayParam.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/trade/pay/PayParam.java @@ -63,6 +63,12 @@ public class PayParam extends MerchantPaymentCommonParam { @Size(max = 32, message = "{validation.field.method.size}") private String method; + /// 支付能力编码:路由模式由路由引擎回填;直定模式(已传 channelMchNo)时作为输入参与校验 + /// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum + @Schema(description = "支付能力编码") + @Size(max = 32, message = "{validation.field.capability.size}") + private String capability; + // ===== 通道专属参数(策略层校验)===== /// 用户标识 OpenId(微信 jsapi/mini 场景必填) diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/convert/route/basic/PayRouteBasicConfigConvert.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/convert/route/basic/PayRouteBasicConfigConvert.java index a71753e19..780db9370 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/convert/route/basic/PayRouteBasicConfigConvert.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/convert/route/basic/PayRouteBasicConfigConvert.java @@ -13,5 +13,7 @@ public interface PayRouteBasicConfigConvert { PayRouteBasicConfigConvert CONVERT = Mappers.getMapper(PayRouteBasicConfigConvert.class); + /// channelMchants(可选通道商户列表)由 service 按商户开通情况装配,转换时忽略 + @Mapping(target = "channelMchants", ignore = true) PayRouteBasicConfigResult toResult(PayRouteBasicConfig entity); } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/entity/route/basic/PayRouteBasicConfig.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/entity/route/basic/PayRouteBasicConfig.java index 4b4333562..6ceae0366 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/entity/route/basic/PayRouteBasicConfig.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/entity/route/basic/PayRouteBasicConfig.java @@ -26,8 +26,8 @@ public class PayRouteBasicConfig extends MpBaseEntity implements ToResult products = new ArrayList<>(); + /// 该渠道下可选通道商户列表(名称/号码) + @Schema(description = "可选通道商户列表") + private List channelMchants = new ArrayList<>(); } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/result/route/scene/PayRouteSceneConfigResult.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/result/route/scene/PayRouteSceneConfigResult.java index a6e3b6fcb..3e627df45 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/result/route/scene/PayRouteSceneConfigResult.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/result/route/scene/PayRouteSceneConfigResult.java @@ -25,6 +25,9 @@ public class PayRouteSceneConfigResult extends BaseResult { @Schema(description = "支付方式编码") private String method; - @Schema(description = "产品编码") - private String product; + @Schema(description = "通道商户号") + private String channelMchNo; + + @Schema(description = "支付能力编码") + private String capability; } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicConfigService.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicConfigService.java index 9da6fe18c..2a1c504e3 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicConfigService.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicConfigService.java @@ -1,5 +1,7 @@ package cn.daxpay.open.payment.merchant.service.route.basic; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; +import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; import cn.daxpay.open.payment.merchant.dao.route.basic.PayRouteBasicConfigManager; @@ -11,24 +13,23 @@ import cn.daxpay.open.payment.merchant.param.route.basic.PayRouteBasicConfigItem import cn.daxpay.open.payment.merchant.result.route.basic.PayRouteBasicConfigResult; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteConfigProviders; import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; +import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum; -import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; -import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.rest.dto.LabelValue; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /// # 通道路由基础模式配置 /// -/// 按支付渠道配置默认支付产品,保存前校验产品策略能力。 +/// 按支付渠道配置默认通道商户(唯一绑定支付产品),保存前校验通道商户归属与渠道支持。 @Service @RequiredArgsConstructor public class PayRouteBasicConfigService { @@ -36,79 +37,84 @@ public class PayRouteBasicConfigService { private final PayRouteStrategyManager strategyManager; private final PayRouteBasicConfigManager basicConfigManager; private final MchAppInfoManager mchAppInfoManager; + private final ChannelMerchantManager channelMerchantManager; - /// 查询基础模式面板数据(已保存 product + 各渠道可选 products) + /// 查询基础模式面板数据(已保存通道商户号 + 各渠道可选通道商户列表) public List listBasicByAppId(String appId) { + String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId); PayRouteStrategy strategy = requireStrategy(appId); - Map productMap = basicConfigManager.findByStrategyId(strategy.getId()).stream() + Map mchMap = basicConfigManager.findByStrategyId(strategy.getId()).stream() .filter(config -> StrUtil.isNotBlank(config.getProvider())) - .collect(Collectors.toMap(PayRouteBasicConfig::getProvider, PayRouteBasicConfig::getProduct, (a, b) -> a)); + .collect(Collectors.toMap(PayRouteBasicConfig::getProvider, + PayRouteBasicConfig::getChannelMchNo, (a, b) -> a)); return PayRouteConfigProviders.enumsInWhitelistOrder().stream() - .map(provider -> toPanelResult(provider, productMap.get(provider.getCode()))) + .map(provider -> toPanelResult(mchNo, provider, mchMap.get(provider.getCode()))) .toList(); } /// 批量保存基础模式配置(先删后插) @Transactional(rollbackFor = Exception.class) public void saveBasicBatch(PayRouteBasicConfigBatchParam param) { + String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(param.getAppId()); PayRouteStrategy strategy = requireStrategy(param.getAppId()); basicConfigManager.deleteByStrategyId(strategy.getId()); for (PayRouteBasicConfigItem item : param.getItems()) { - if (StrUtil.isBlank(item.getProduct())) { + if (StrUtil.isBlank(item.getChannelMchNo())) { continue; } PayProviderEnum provider = validateBasicPayProviderCode(item.getProvider()); - validateBasicProduct(item.getProduct(), provider); + // 校验通道商户属本商户且启用 + ChannelMerchant mch = channelMerchantManager + .findByMchNoAndChannelMchNo(mchNo, item.getChannelMchNo()) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchNotExist", item.getChannelMchNo())); + if (!Boolean.TRUE.equals(mch.getEnable())) { + // 通道商户[{0}]未启用 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchDisabled", item.getChannelMchNo()); + } + if (!PaymentStrategyFactory.productSupportsProvider(mch.getProduct(), provider)) { + // 支付渠道[{0}]下无可用支付产品 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.basicProductNotAvailable", provider.getCode()); + } + if (!PaymentStrategyFactory.existsByProduct(mch.getProduct(), AbsProductStrategy.class)) { + // 支付产品策略不存在 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.productStrategyMissing"); + } PayRouteBasicConfig config = new PayRouteBasicConfig(); config.setStrategyId(strategy.getId()); config.setProvider(item.getProvider()); - config.setProduct(item.getProduct()); + config.setChannelMchNo(item.getChannelMchNo()); basicConfigManager.save(config); } } - /// 校验产品存在且支持指定支付渠道 - public void validateBasicProduct(String product, PayProviderEnum provider) { - ProductEnum productEnum = ProductEnum.findByCode(product); - if (productEnum == null) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.productInvalid", product); - } - if (!PaymentStrategyFactory.productSupportsProvider(product, provider)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.basicProductNotAvailable", provider.getCode()); - } - if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.productStrategyMissing"); - } - } - /// 校验支付渠道编码合法且在通道路由白名单内 private PayProviderEnum validateBasicPayProviderCode(String providerCode) { PayProviderEnum provider = PayProviderEnum.findByCode(providerCode); if (provider == null || !PayRouteConfigProviders.contains(providerCode)) { + // 支付渠道无效 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.basicProviderInvalid"); } return provider; } - /// 组装单渠道基础模式面板行:已保存 product + 该渠道下可选 products - private PayRouteBasicConfigResult toPanelResult(PayProviderEnum provider, String savedProduct) { - List products = productsForProvider(provider); + /// 组装单渠道基础模式面板行:已保存通道商户号 + 该渠道下可选通道商户列表 + private PayRouteBasicConfigResult toPanelResult(String mchNo, PayProviderEnum provider, String savedChannelMchNo) { + List mchants = channelMerchantManager.findAllByMchNo(mchNo).stream() + .filter(mch -> Boolean.TRUE.equals(mch.getEnable())) + .filter(mch -> PaymentStrategyFactory.productSupportsProvider(mch.getProduct(), provider)) + .map(mch -> new LabelValue( + StrUtil.isNotBlank(mch.getChannelMerchantName()) + ? mch.getChannelMerchantName() : mch.getChannelMchNo(), + mch.getChannelMchNo())) + .toList(); return new PayRouteBasicConfigResult() .setProvider(provider.getCode()) - .setProduct(savedProduct) - .setProducts(products); - } - - /// 从产品枚举中筛出支持指定支付渠道的产品编码 - private List productsForProvider(PayProviderEnum provider) { - return Arrays.stream(ProductEnum.values()) - .map(ProductEnum::getCode) - .filter(product -> PaymentStrategyFactory.productSupportsProvider(product, provider)) - .distinct() - .toList(); + .setChannelMchNo(savedChannelMchNo) + .setChannelMchants(mchants); } /// 按应用号加载路由策略,不存在则抛业务异常 @@ -116,4 +122,4 @@ public class PayRouteBasicConfigService { return strategyManager.findByAppId(appId) .orElseThrow(() -> new DataNotExistException("pay.route.error.routeStrategyNotExist")); } -} \ No newline at end of file +} diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicMatcher.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicMatcher.java index 7d1e40ed7..7a757febc 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicMatcher.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/basic/PayRouteBasicMatcher.java @@ -2,16 +2,17 @@ package cn.daxpay.open.payment.merchant.service.route.basic; import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; import cn.daxpay.open.payment.merchant.entity.route.basic.PayRouteBasicConfig; -import cn.daxpay.open.payment.merchant.service.route.support.PayRouteCapabilityService; import cn.daxpay.open.payment.merchant.service.route.model.RouteHit; import cn.daxpay.open.payment.merchant.service.route.runtime.PayRouteProductResolver; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteI18nHelper; -import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import cn.daxpay.open.payment.old.pay.support.ProductStrategySupport; import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +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.enums.pay.channel.PayProviderEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; -import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -21,75 +22,67 @@ import java.util.Objects; /// # 基础模式通道路由匹配器 /// -/// 按已配置的支付渠道与支付产品,结合产品策略解析通道与支付方式。 +/// 按「支付渠道 → 通道商户」配置,结合支付方式解析通道商户、产品与能力。 +/// 支付渠道由支付方式(PayMethodEnum)自带渠道属性解析,不再从支付产品反推。 @Component @RequiredArgsConstructor public class PayRouteBasicMatcher { private final PayRouteProductResolver productResolver; - private final PayRouteCapabilityService payRouteCapabilityService; /// 基础模式匹配 public RouteHit match(List basicConfigs, PayParam payParam) { - String providerCode = providerFromProduct(payParam); - if (StrUtil.isBlank(providerCode)) { + if (StrUtil.isBlank(payParam.getMethod())) { + // 场景模式下须选择支付方式 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneMethodRequired"); + } + PayMethodEnum methodEnum = PayMethodEnum.findByCode(payParam.getMethod()); + // 支付方式自带渠道属性(OTHER 等无归属时报错) + PayProviderEnum provider = methodEnum.getProvider(); + if (provider == null) { + // 未指定支付产品时,支付渠道不能为空 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.providerRequired"); } - PayProviderEnum provider = PayProviderEnum.findByCode(providerCode); - if (provider == null) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.basicProviderInvalid"); - } - String product = findConfiguredProduct(basicConfigs, providerCode); - if (StrUtil.isBlank(product)) { + String channelMchNo = findConfiguredChannelMchNo(basicConfigs, provider.getCode()); + if (StrUtil.isBlank(channelMchNo)) { + // 支付渠道[{0}]未配置通道商户,请在通道路由基础模式中完成配置 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.basicProductNotConfigured", PayRouteI18nHelper.provider(providerCode)); + "pay.route.error.basicChannelMchNotConfigured", PayRouteI18nHelper.provider(provider.getCode())); } - assertProductConfigured(product, provider); + String product = productResolver.productOfChannelMchNo(channelMchNo); if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { + // 支付产品策略不存在 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.productStrategyMissing"); } - List methodCandidates = payRouteCapabilityService.methodsForProductPayProvider( - product, providerCode); - if (methodCandidates.isEmpty()) { + if (!PaymentStrategyFactory.productSupportsProvider(product, provider)) { + // 支付渠道[{0}]下无可用支付产品 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.methodNotSupportedForProduct", - PayRouteI18nHelper.product(product)); + "pay.route.error.basicProductNotAvailable", PayRouteI18nHelper.provider(provider.getCode())); } - String method = methodCandidates.getFirst(); String channel = productResolver.channelOfProduct(product); - return new RouteHit(channel, method, product, null, null); + // 基础模式不存能力,由 (产品, 支付方式) 派生 + String capability = resolveCapability(product, methodEnum); + return new RouteHit(channel, methodEnum.getCode(), product, channelMchNo, capability, null, null); } - /// 从产品编码推导 Provider(临时兼容,待路由重构后移除) - private static String providerFromProduct(PayParam payParam) { - if (StrUtil.isNotBlank(payParam.getProduct())) { - ProductEnum product = ProductEnum.findByCode(payParam.getProduct()); - if (product != null) { - return product.getChannel(); - } - } - return null; - } - - /// 从基础配置中取指定支付渠道已绑定的产品编码 - private String findConfiguredProduct(List basicConfigs, String providerCode) { + /// 从基础配置中取指定支付渠道已绑定的通道商户号 + private String findConfiguredChannelMchNo(List basicConfigs, String providerCode) { if (basicConfigs == null) { return null; } return basicConfigs.stream() .filter(config -> Objects.equals(config.getProvider(), providerCode)) - .map(PayRouteBasicConfig::getProduct) + .map(PayRouteBasicConfig::getChannelMchNo) .filter(StrUtil::isNotBlank) .findFirst() .orElse(null); } - /// 校验产品策略支持该支付渠道 - private void assertProductConfigured(String product, PayProviderEnum provider) { - if (!PaymentStrategyFactory.productSupportsProvider(product, provider)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.basicProductNotAvailable", PayRouteI18nHelper.provider(provider.getCode())); - } + /// 基础模式支付能力由产品策略声明的(方式→能力)映射取首个 + private String resolveCapability(String product, PayMethodEnum method) { + AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class); + List capabilities = ProductStrategySupport.capabilitiesForMethod(strategy, method); + return capabilities.isEmpty() ? null : capabilities.getFirst().getCode(); } -} \ No newline at end of file +} diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/facade/PayRouteConfigService.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/facade/PayRouteConfigService.java index 4125d5000..815f46bdb 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/facade/PayRouteConfigService.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/facade/PayRouteConfigService.java @@ -77,30 +77,30 @@ public class PayRouteConfigService { sceneConfigService.saveSceneBatch(param); } - /// 通道路由白名单目录下全部产品候选(批量) - public Map> listSceneProductCandidatesBatch(String appId) { - return sceneConfigService.listSceneProductCandidatesBatch(appId); + /// 通道路由白名单目录下全部通道商户候选(批量) + public Map> listSceneChannelMchCandidatesBatch(String appId) { + return sceneConfigService.listSceneChannelMchCandidatesBatch(appId); } - /// 按目录项与产品批量返回支付能力候选 + /// 按目录项与通道商户批量返回支付能力候选 public Map> listSceneCapabilityCandidatesBatch(PayRouteSceneCapabilityBatchParam param) { return sceneConfigService.listSceneCapabilityCandidatesBatch(param); } - /// 目录项下商户可用支付产品候选 - public List listSceneProductCandidatesForMethod(String appId, String provider, String method) { - return sceneConfigService.listSceneProductCandidatesForMethod(appId, provider, method); + /// 目录项下商户已开通的通道商户候选 + public List listSceneChannelMchCandidatesForMethod(String appId, String provider, String method) { + return sceneConfigService.listSceneChannelMchCandidatesForMethod(appId, provider, method); } - /// 目录项与产品下支付能力候选 + /// 目录项与通道商户下支付能力候选 public List listSceneCapabilityCandidatesForMethod( - String appId, String provider, String method, String product) { - return sceneConfigService.listSceneCapabilityCandidatesForMethod(appId, provider, method, product); + String appId, String provider, String method, String channelMchNo) { + return sceneConfigService.listSceneCapabilityCandidatesForMethod(appId, provider, method, channelMchNo); } - /// 推断目录项与产品下唯一支付能力(仅供回显) - public String inferSceneCapability(String appId, String provider, String method, String product) { - return sceneConfigService.inferSceneCapability(appId, provider, method, product); + /// 推断目录项与通道商户下唯一支付能力(仅供回显) + public String inferSceneCapability(String appId, String provider, String method, String channelMchNo) { + return sceneConfigService.inferSceneCapability(appId, provider, method, channelMchNo); } /// 查询基础模式各支付渠道对应产品 diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/model/RouteHit.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/model/RouteHit.java index 60973f1b3..a3dc59a4e 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/model/RouteHit.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/model/RouteHit.java @@ -4,17 +4,21 @@ import cn.daxpay.open.payment.merchant.entity.route.scene.PayRouteSceneConfig; /// # 通道路由命中结果 /// -/// 匹配成功后得到的通道、方式、产品及命中来源 ID,供填充 PayParam。 +/// 匹配成功后得到的通道、方式、产品、通道商户及能力,供填充 PayParam。 /// /// @param channel 支付通道编码 /// @param method 支付方式编码 /// @param product 支付产品编码(可为空,由运行时解析) +/// @param channelMchNo 通道商户号(场景/基础模式命中的核心定位字段) +/// @param capability 支付能力编码(场景模式命中的能力声明;基础模式可为空) /// @param hitRuleId 精细模式命中的规则 ID(预留字段,当前实现恒为 null) /// @param hitConfigId 场景模式命中的场景配置 ID,基础模式为 null -public record RouteHit(String channel, String method, String product, Long hitRuleId, Long hitConfigId) { +public record RouteHit(String channel, String method, String product, String channelMchNo, + String capability, Long hitRuleId, Long hitConfigId) { /// 由场景模式配置构造命中结果 public static RouteHit fromScene(PayRouteSceneConfig config) { - return new RouteHit(config.getChannel(), config.getMethod(), config.getProduct(), null, config.getId()); + return new RouteHit(config.getChannel(), config.getMethod(), null, + config.getChannelMchNo(), config.getCapability(), null, config.getId()); } } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteProductResolver.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteProductResolver.java index 9dcc9974a..865ce2cc5 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteProductResolver.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/runtime/PayRouteProductResolver.java @@ -1,5 +1,7 @@ package cn.daxpay.open.payment.merchant.service.route.runtime; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; +import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport; import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum; import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; @@ -14,11 +16,33 @@ import java.util.Objects; /// # 通道路由产品解析器 /// +/// 通道商户号(channelMchNo)与支付产品一一绑定,是路由重构后的唯一定位锚点; +/// 通道(channel)/支付方式(method)→产品的旧解析逻辑暂时保留以兼容基础模式回退场景。 @Service @RequiredArgsConstructor public class PayRouteProductResolver { private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport; + private final ChannelMerchantManager channelMerchantManager; + + /// 根据通道商户号解析支付产品编码(channelMchNo 唯一绑定 product) + public String productOfChannelMchNo(String channelMchNo) { + if (StrUtil.isBlank(channelMchNo)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchNoRequired"); + } + return channelMerchantManager.lambdaQuery() + .eq(ChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt() + .map(ChannelMerchant::getProduct) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchNotExist", channelMchNo)); + } + + /// 根据通道商户号解析所属通道编码 + public String channelOfChannelMchNo(String channelMchNo) { + return channelOfProduct(productOfChannelMchNo(channelMchNo)); + } /// 根据通道及支付方式解析产品编码 public String resolve(String mchNo, String channel, String method) { @@ -51,4 +75,4 @@ public class PayRouteProductResolver { } return productEnum.getChannel(); } -} \ No newline at end of file +} 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 994bf84dd..ac99b32cd 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 @@ -1,5 +1,6 @@ package cn.daxpay.open.payment.merchant.service.route.runtime; +import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; import cn.daxpay.open.payment.merchant.dao.route.basic.PayRouteBasicConfigManager; import cn.daxpay.open.payment.merchant.dao.route.scene.PayRouteSceneConfigManager; import cn.daxpay.open.payment.merchant.dao.route.strategy.PayRouteStrategyManager; @@ -8,20 +9,29 @@ import cn.daxpay.open.payment.merchant.service.route.scene.PayRouteSceneMatcher; 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; +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.core.exception.BizInfoException; +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; +import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; -import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Objects; + /// # 支付通道路由服务 /// /// 实现 PayRouteFacade,供 NormalPayService 在支付流程中调用。 -/// 已指定 product 则跳过;否则按 appId 加载策略,经基础/场景模式匹配后回填 product。 +/// 三种解析路径: +/// 1. 直定模式:已传 channelMchNo,跳过路由,直接由 channelMchNo 推导产品并校验/派生能力; +/// 2. 兼容跳过:未传 channelMchNo 但已指定 product(重付复用),直接返回; +/// 3. 路由模式:按 appId 加载策略,经基础/场景模式匹配后回填 product/channelMchNo/capability。 /// 调用方需保证 appId 已解析完毕。 @Slf4j @Service @@ -34,9 +44,15 @@ public class PayRouteService implements PayRouteFacade { private final PayRouteProductResolver productResolver; private final PayRouteBasicMatcher basicMatcher; - /// 实付路由解析:已指定 product 则跳过;否则按策略模式匹配并回填 product + /// 实付路由解析:直定模式优先,其次兼容已指定 product,最后按策略模式匹配 @Override public void resolve(PayParam payParam) { + // 直定模式:指定通道商户号,跳过路由 + if (StrUtil.isNotBlank(payParam.getChannelMchNo())) { + resolveDirect(payParam); + return; + } + // 兼容:已指定 product(重付复用),跳过路由 if (StrUtil.isNotBlank(payParam.getProduct())) { return; } @@ -56,6 +72,32 @@ public class PayRouteService implements PayRouteFacade { fillPayParam(payParam, hit); } + /// 直定模式:由通道商户号推导产品,校验或派生支付能力 + 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 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); + } else { + // 已传能力则校验属于该(产品, 支付方式)候选 + validateCapability(product, methodEnum, capability); + } + payParam.setProduct(product); + payParam.setCapability(capability); + } + /// 按应用号从库加载路由数据包(无 Redis 缓存) private PayRouteBundle loadBundle(String appId) { var strategyOpt = strategyManager.findByAppId(appId); @@ -77,11 +119,34 @@ public class PayRouteService implements PayRouteFacade { return PayRouteSceneMatcher.match(bundle.getSceneConfigs(), payParam); } - /// 将命中结果写入 PayParam:仅回填 product(channel 从 ProductEnum 派生,method 由客户端传入) + /// 将命中结果写入 PayParam:回填 product(由通道商户号派生)、通道商户号、支付能力 private void fillPayParam(PayParam payParam, RouteHit hit) { String product = StrUtil.isNotBlank(hit.product()) ? hit.product() - : productResolver.resolve(payParam.getMchNo(), hit.channel(), hit.method()); + : productResolver.productOfChannelMchNo(hit.channelMchNo()); payParam.setProduct(product); + if (StrUtil.isNotBlank(hit.channelMchNo())) { + payParam.setChannelMchNo(hit.channelMchNo()); + } + if (StrUtil.isBlank(payParam.getCapability()) && StrUtil.isNotBlank(hit.capability())) { + payParam.setCapability(hit.capability()); + } + } + + /// 派生支付能力:取产品策略声明的(方式→能力)首个 + 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); + if (!ProductStrategySupport.strategySupportsCapabilityCode(strategy, method.getCode(), capability)) { + // 支付能力[{0}]与产品[{1}]、支付方式[{2}]不匹配 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.sceneCapabilityProductMismatch", capability, product, method.getCode()); + } } } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneConfigService.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneConfigService.java index 914a3ca06..db1af99f5 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneConfigService.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneConfigService.java @@ -1,6 +1,7 @@ package cn.daxpay.open.payment.merchant.service.route.scene; -import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; +import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; import cn.daxpay.open.payment.merchant.dao.route.scene.PayRouteSceneConfigManager; import cn.daxpay.open.payment.merchant.dao.route.strategy.PayRouteStrategyManager; @@ -10,17 +11,12 @@ import cn.daxpay.open.payment.merchant.param.route.scene.PayRouteSceneCapability import cn.daxpay.open.payment.merchant.param.route.scene.PayRouteSceneConfigBatchParam; import cn.daxpay.open.payment.merchant.param.route.scene.PayRouteSceneConfigItem; import cn.daxpay.open.payment.merchant.result.route.scene.PayRouteSceneConfigResult; -import cn.daxpay.open.payment.merchant.service.route.basic.PayRouteBasicConfigService; +import cn.daxpay.open.payment.merchant.service.route.runtime.PayRouteProductResolver; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteConfigProviders; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteI18nHelper; import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport; -import cn.daxpay.open.payment.merchant.service.route.runtime.PayRouteProductResolver; import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService; -import cn.daxpay.open.payment.merchant.service.route.support.PayRouteCapabilityService; -import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum; -import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; import cn.daxpay.open.platform.core.rest.dto.LabelValue; @@ -33,13 +29,12 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /// # 通道路由场景模式配置 /// -/// 按渠道支付方式目录为每个 (provider, method) 绑定支付产品 product; -/// 批量保存要求通道路由白名单内目录项均已配置支付产品,唯一键为 strategy + provider + method。 +/// 配置粒度为「支付方式 → (通道商户, 支付能力)」,每支付方式唯一一行; +/// 通道商户唯一绑定支付产品,通道编码由产品派生。批量保存为全量覆盖。 @Service @RequiredArgsConstructor public class PayRouteSceneConfigService { @@ -49,11 +44,9 @@ public class PayRouteSceneConfigService { private final PayRouteProductResolver productResolver; private final MchAppInfoManager mchAppInfoManager; private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport; - private final PayRouteSceneRouteResolver sceneRouteResolver; - private final PayRouteBasicConfigService basicConfigService; - private final PayRouteCapabilityService payRouteCapabilityService; - private final PayProviderMethodService payProviderMethodService; private final PayRouteMethodValidator payRouteMethodValidator; + private final ChannelMerchantManager channelMerchantManager; + private final PayProviderMethodService payProviderMethodService; /// 查询场景模式配置列表 public List listSceneByAppId(String appId) { @@ -63,61 +56,53 @@ public class PayRouteSceneConfigService { .toList(); } - /// 批量保存场景模式配置(全量覆盖:目录完整、provider+method 唯一、产品能力校验) + /// 批量保存场景模式配置(全量覆盖:method 唯一、通道商户/能力校验) @Transactional(rollbackFor = Exception.class) public void saveSceneBatch(PayRouteSceneConfigBatchParam param) { String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(param.getAppId()); PayRouteStrategy strategy = requireStrategy(param.getAppId()); validateSceneConfigUnique(param.getItems()); - validateSceneProductCapabilityPairing(param.getItems()); + validateSceneChannelMchCapabilityPairing(param.getItems()); sceneConfigManager.deleteByStrategyId(strategy.getId()); List configs = new ArrayList<>(); for (PayRouteSceneConfigItem item : param.getItems()) { - String channel; - String method; - String product; - if (StrUtil.isNotBlank(item.getProvider())) { - if (!PayRouteConfigProviders.contains(item.getProvider())) { - continue; - } - if (StrUtil.isBlank(item.getMethod())) { - // 能力: 支付方式不能为空 - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "error.payment.capability.methodRequiredWithPayProvider"); - } - if (isSceneDirectoryRowEmpty(item)) { - continue; - } - payRouteMethodValidator.validateSceneConfigItem(item.getProvider(), item.getMethod()); - validateSceneMchProduct(mchNo, item.getProduct(), item.getProvider()); - if (!payRouteCapabilityService.productSupportsMethod(item.getProduct(), item.getProvider(), item.getMethod())) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.sceneMethodProductMismatch", - PayRouteI18nHelper.payMethod(item.getMethod()), PayRouteI18nHelper.product(item.getProduct())); - } - payRouteStrategyCapabilitySupport.validateSceneCapability( - item.getProvider(), item.getMethod(), item.getProduct(), item.getCapability()); - var route = sceneRouteResolver.resolve(item.getProduct(), item.getProvider(), item.getMethod()); - channel = route.channel(); - method = route.method(); - product = route.product(); - } else { - if (StrUtil.isBlank(item.getChannel()) || StrUtil.isBlank(item.getMethod())) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.sceneChannelMethodRequired"); - } - payRouteMethodValidator.validateSceneConfigItem(item.getProvider(), item.getMethod()); - channel = item.getChannel(); - method = item.getMethod(); - product = productResolver.resolveAndFill(mchNo, channel, method, item.getProduct()); + if (StrUtil.isBlank(item.getMethod()) || !PayRouteConfigProviders.contains(item.getProvider())) { + continue; } - validateRouteItem(mchNo, channel, method); + if (isSceneRowEmpty(item)) { + continue; + } + payRouteMethodValidator.validateSceneConfigItem(item.getProvider(), item.getMethod()); + // 校验通道商户属本商户且启用 + ChannelMerchant mch = channelMerchantManager + .findByMchNoAndChannelMchNo(mchNo, item.getChannelMchNo()) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchNotExist", item.getChannelMchNo())); + if (!Boolean.TRUE.equals(mch.getEnable())) { + // 通道商户[{0}]未启用 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.channelMchDisabled", item.getChannelMchNo()); + } + String product = mch.getProduct(); + // 校验产品支持该(provider, method) + if (!payRouteStrategyCapabilitySupport.routeProductSupportsMethod( + product, item.getProvider(), item.getMethod())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.route.error.sceneMethodProductMismatch", + PayRouteI18nHelper.payMethod(item.getMethod()), PayRouteI18nHelper.product(product)); + } + // 校验支付能力在候选集合内 + payRouteStrategyCapabilitySupport.validateSceneCapability( + item.getProvider(), item.getMethod(), item.getChannelMchNo(), item.getCapability()); + // 通道编码由产品派生 + String channel = productResolver.channelOfProduct(product); PayRouteSceneConfig config = new PayRouteSceneConfig(); config.setStrategyId(strategy.getId()); - config.setProvider(StrUtil.blankToDefault(item.getProvider(), null)); + config.setProvider(item.getProvider()); config.setChannel(channel); - config.setMethod(method); - config.setProduct(product); + config.setMethod(item.getMethod()); + config.setChannelMchNo(item.getChannelMchNo()); + config.setCapability(item.getCapability()); configs.add(config); } for (PayRouteSceneConfig config : configs) { @@ -125,103 +110,79 @@ public class PayRouteSceneConfigService { } } - - /// 通道路由白名单目录下全部 (provider,method) 的产品候选(批量) - public Map> listSceneProductCandidatesBatch(String appId) { + /// 通道路由白名单目录下全部 (provider,method) 的通道商户候选(批量) + public Map> listSceneChannelMchCandidatesBatch(String appId) { String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId); - return payRouteStrategyCapabilitySupport.listSceneProductCandidatesBatch(mchNo); + return payRouteStrategyCapabilitySupport.listSceneChannelMchCandidatesBatch(mchNo); } - /// 按目录项+产品批量返回支付能力候选 + /// 按目录项+通道商户批量返回支付能力候选 public Map> listSceneCapabilityCandidatesBatch(PayRouteSceneCapabilityBatchParam param) { String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(param.getAppId()); return payRouteStrategyCapabilitySupport.listSceneCapabilityCandidatesBatch(mchNo, param.getItems()); } - /// 按目录项(支付渠道+支付方式)筛选商户已开通且能力匹配的产品候选 - public List listSceneProductCandidatesForMethod(String appId, String provider, String method) { + /// 按目录项(支付渠道+支付方式)筛选商户已开通的通道商户候选 + public List listSceneChannelMchCandidatesForMethod(String appId, String provider, String method) { if (StrUtil.isBlank(method) || !payProviderMethodService.contains(provider, method)) { return List.of(); } String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId); - return payRouteStrategyCapabilitySupport.listSceneProductCandidates(provider, method); + return payRouteStrategyCapabilitySupport.listSceneChannelMchCandidates(mchNo, provider, method); } - /// 按目录项与支付产品筛选支付能力候选(策略 Map ∩ DB,不落库) + /// 按目录项与通道商户筛选支付能力候选(策略 Map ∩ DB,不落库) public List listSceneCapabilityCandidatesForMethod( - String appId, String provider, String method, String product) { - if (StrUtil.isBlank(product)) { + String appId, String provider, String method, String channelMchNo) { + if (StrUtil.isBlank(channelMchNo)) { return List.of(); } - String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId); - return payRouteStrategyCapabilitySupport.listSceneCapabilityCandidates(provider, method, product); + return payRouteStrategyCapabilitySupport.listSceneCapabilityCandidates(provider, method, channelMchNo); } /// 回显推断支付能力(候选唯一时返回编码) - public String inferSceneCapability(String appId, String provider, String method, String product) { - if (StrUtil.isBlank(product)) { + public String inferSceneCapability(String appId, String provider, String method, String channelMchNo) { + if (StrUtil.isBlank(channelMchNo)) { return null; } - String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId); - return payRouteStrategyCapabilitySupport.inferSceneCapability(provider, method, product); + return payRouteStrategyCapabilitySupport.inferSceneCapability(provider, method, channelMchNo); } - /// 校验批量保存项:provider + method 在策略内唯一 + /// 校验批量保存项:支付方式全局唯一 private void validateSceneConfigUnique(List items) { - Set pairKeys = new HashSet<>(); + Set methodKeys = new HashSet<>(); for (PayRouteSceneConfigItem item : items) { - if (StrUtil.isBlank(item.getProvider())) { + if (StrUtil.isBlank(item.getMethod())) { continue; } - String key = item.getProvider() + "|" + StrUtil.blankToDefault(item.getMethod(), ""); - if (!pairKeys.add(key)) { + if (!methodKeys.add(item.getMethod())) { + // 场景模式下同一支付方式存在多条配置 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.duplicatePayProviderMethod", - PayRouteI18nHelper.provider(item.getProvider()), PayRouteI18nHelper.payMethod(item.getMethod())); + "pay.route.error.duplicateSceneMethod", PayRouteI18nHelper.payMethod(item.getMethod())); } } } - /// 校验目录行:支付产品与支付能力须同时为空或同时有值,不可只填其一 - private void validateSceneProductCapabilityPairing(List items) { + /// 校验目录行:通道商户号与支付能力须同时为空或同时有值,不可只填其一 + private void validateSceneChannelMchCapabilityPairing(List items) { for (PayRouteSceneConfigItem item : items) { - if (StrUtil.isBlank(item.getProvider())) { + if (StrUtil.isBlank(item.getMethod())) { continue; } - boolean hasProduct = StrUtil.isNotBlank(item.getProduct()); - boolean hasCapability = StrUtil.isNotBlank(item.getCapability()); - if (hasProduct == hasCapability) { + boolean hasMch = StrUtil.isNotBlank(item.getChannelMchNo()); + boolean hasCap = StrUtil.isNotBlank(item.getCapability()); + if (hasMch == hasCap) { continue; } + // 支付方式[{0}]须同时选择通道商户与支付能力,或同时留空 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.sceneProductCapabilityPair", - PayRouteI18nHelper.provider(item.getProvider()), - PayRouteI18nHelper.payMethod(item.getMethod())); + "pay.route.error.sceneChannelMchCapabilityPair", PayRouteI18nHelper.payMethod(item.getMethod())); } } - /// 目录行未配置(产品与能力均为空) - private boolean isSceneDirectoryRowEmpty(PayRouteSceneConfigItem item) { - return StrUtil.isBlank(item.getProduct()) && StrUtil.isBlank(item.getCapability()); - } - - /// 场景配置所选产品须在商户侧可用且支持该支付渠道 - private void validateSceneMchProduct(String mchNo, String product, String providerCode) { - PayProviderEnum provider = PayProviderEnum.findByCode(providerCode); - if (provider == null) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.basicProviderInvalid"); - } - basicConfigService.validateBasicProduct(product, provider); - } - - /// 校验通道+方式可解析为商户产品且存在对应产品策略 - private void validateRouteItem(String mchNo, String channel, String method) { - PayMethodEnum.findByCode(method); - String product = productResolver.resolve(mchNo, channel, method); - if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.productStrategyMissing"); - } + /// 目录行未配置(通道商户与能力均为空) + private boolean isSceneRowEmpty(PayRouteSceneConfigItem item) { + return StrUtil.isBlank(item.getChannelMchNo()) && StrUtil.isBlank(item.getCapability()); } /// 按应用号加载路由策略,不存在则抛业务异常 diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneMatcher.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneMatcher.java index 7f414d09c..25e29292c 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneMatcher.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneMatcher.java @@ -4,7 +4,6 @@ import cn.daxpay.open.payment.merchant.entity.route.scene.PayRouteSceneConfig; import cn.daxpay.open.payment.merchant.service.route.model.RouteHit; import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam; import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; @@ -15,46 +14,32 @@ import java.util.Objects; /// # 场景模式通道路由匹配器 /// -/// 配置粒度为「支付渠道 + 支付方式 → 产品/通道」;下单须同时传 provider 与 method(含 barcode 消歧)。 +/// 配置粒度为「支付方式 → (通道商户, 支付能力)」,每个支付方式唯一一行; +/// 下单须传 method,按 method 精确命中,命中行的 channelMchNo/capability 即路由结果。 @UtilityClass public class PayRouteSceneMatcher { - /// 场景模式匹配:按 provider + method 精确命中唯一配置行 + /// 场景模式匹配:按 method 精确命中唯一配置行 public RouteHit match(List configs, PayParam payParam) { if (CollUtil.isEmpty(configs)) { return null; } - String providerCode = providerFromProduct(payParam); - if (StrUtil.isBlank(providerCode)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.providerRequiredForScene"); - } if (StrUtil.isBlank(payParam.getMethod())) { - // 能力: 支付方式不能为空 - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "error.payment.capability.methodRequiredWithPayProvider"); + // 场景模式下须选择支付方式 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneMethodRequired"); } - String finalProviderCode = providerCode; + String method = payParam.getMethod(); List candidates = configs.stream() - .filter(config -> Objects.equals(config.getProvider(), finalProviderCode)) - .filter(config -> Objects.equals(config.getMethod(), payParam.getMethod())) + .filter(config -> Objects.equals(config.getMethod(), method)) .toList(); if (candidates.isEmpty()) { return null; } if (candidates.size() > 1) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.duplicateSceneConfig"); + // 场景模式下同一支付方式存在多条配置 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.duplicateSceneMethod", + method); } return RouteHit.fromScene(candidates.getFirst()); } - - /// 从产品编码推导 Provider(临时兼容,待路由重构后移除) - private static String providerFromProduct(PayParam payParam) { - if (StrUtil.isNotBlank(payParam.getProduct())) { - ProductEnum product = ProductEnum.findByCode(payParam.getProduct()); - if (product != null) { - return product.getChannel(); - } - } - return null; - } } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneRouteResolver.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneRouteResolver.java deleted file mode 100644 index 9f678ab6f..000000000 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/scene/PayRouteSceneRouteResolver.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.daxpay.open.payment.merchant.service.route.scene; - -import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; -import cn.daxpay.open.payment.merchant.service.route.support.PayRouteCapabilityService; -import cn.daxpay.open.payment.merchant.service.route.support.PayRouteI18nHelper; -import cn.daxpay.open.payment.merchant.service.route.runtime.PayRouteProductResolver; -import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; -import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum; -import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; -import cn.daxpay.open.platform.core.exception.BizInfoException; -import cn.hutool.core.util.StrUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -/// # 场景模式路由解析(支付产品 + 支付方式 → 通道) -@Service -@RequiredArgsConstructor -public class PayRouteSceneRouteResolver { - - private final PayRouteProductResolver productResolver; - private final PayRouteCapabilityService payRouteCapabilityService; - private final PayRouteMethodValidator payRouteMethodValidator; - - /// 按产品、支付渠道与所选支付方式解析通道 - public SceneRoute resolve(String product, String providerCode, String method) { - if (StrUtil.isBlank(product)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneProductRequired"); - } - if (StrUtil.isBlank(method)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneMethodRequired"); - } - PayProviderEnum provider = PayProviderEnum.findByCode(providerCode); - if (provider == null) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.basicProviderInvalid"); - } - ProductEnum.findByCode(product); - if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.productStrategyMissing"); - } - List methodCandidates = payRouteCapabilityService.methodsForProductPayProvider(product, providerCode); - if (methodCandidates.isEmpty()) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.methodNotSupportedForProduct", - PayRouteI18nHelper.product(product)); - } - if (!methodCandidates.contains(method)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.sceneMethodProductMismatch", - PayRouteI18nHelper.payMethod(method), PayRouteI18nHelper.product(product)); - } - payRouteMethodValidator.validateSceneConfigItem(providerCode, method); - String channel = productResolver.channelOfProduct(product); - return new SceneRoute(channel, method, product); - } - - /// 场景模式解析结果:通道、方式、产品 - public record SceneRoute(String channel, String method, String product) { - } -} \ No newline at end of file diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteCapabilityService.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteCapabilityService.java deleted file mode 100644 index f65ea0ed3..000000000 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/service/route/support/PayRouteCapabilityService.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.daxpay.open.payment.merchant.service.route.support; - -import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; -import cn.daxpay.open.payment.masterdata.constants.product.service.PayProductCapabilityService; -import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService; -import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; -import cn.daxpay.open.payment.old.pay.support.ProductStrategySupport; -import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum; -import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -/// # 通道路由支付能力 -/// -/// 渠道目录 × 支付产品 × 策略能力 求交与校验。 -@Service -@RequiredArgsConstructor -public class PayRouteCapabilityService { - - private final PayProviderMethodService payProviderMethodService; - private final PayProductCapabilityService payProductCapabilityService; - - /// 目录与支付产品已挂载能力求交:指定支付产品在该支付渠道下可用的支付方式 - public List methodsForProductPayProvider(String product, String providerCode) { - PayProviderEnum provider = PayProviderEnum.findByCode(providerCode); - if (provider == null) { - return List.of(); - } - List methodCodes = new ArrayList<>(); - for (PayMethodEnum directoryMethod : payProviderMethodService.listMethodsForBrand(provider)) { - String methodCode = directoryMethod.getCode(); - if (productSupportsMethod(product, providerCode, methodCode)) { - methodCodes.add(methodCode); - } - } - return methodCodes; - } - - /// 支付产品是否支持目录中的 provider + method 组合(策略方式→能力 Map ∧ `pay_md_product_capability`) - public boolean productSupportsMethod(String product, String providerCode, String methodCode) { - if (!payProviderMethodService.contains(providerCode, methodCode)) { - return false; - } - if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { - return false; - } - AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class); - PayMethodEnum method = PayMethodEnum.findByCode(methodCode); - if (!ProductStrategySupport.supportsDirectoryMethod(strategy, method)) { - return false; - } - return payProductCapabilityService.productSupportsMethod(product, methodCode); - } -} 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 026e272c0..570eb9a9d 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 @@ -1,20 +1,20 @@ package cn.daxpay.open.payment.merchant.service.route.support; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; +import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; import cn.daxpay.open.payment.common.util.PaymentStrategyFactory; import cn.daxpay.open.payment.merchant.param.route.scene.PayRouteSceneCapabilityBatchItem; import cn.daxpay.open.payment.masterdata.constants.capability.dao.PayCapabilityManager; import cn.daxpay.open.payment.masterdata.constants.capability.dao.PayProductCapabilityManager; -import cn.daxpay.open.payment.masterdata.constants.provider.dao.PayProviderMethodManager; -import cn.daxpay.open.payment.masterdata.constants.capability.entity.PayCapability; import cn.daxpay.open.payment.masterdata.constants.product.service.PayProductCapabilityService; +import cn.daxpay.open.payment.masterdata.constants.provider.dao.PayProviderMethodManager; import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService; -import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; import cn.daxpay.open.payment.old.pay.support.ProductStrategySupport; +import cn.daxpay.open.payment.strategy.product.AbsProductStrategy; import cn.daxpay.open.platform.common.i18n.util.I18nUtil; import cn.daxpay.open.platform.core.code.CommonErrorCode; 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.enums.pay.channel.ProductEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.model.PayProviderMethodEntry; import cn.daxpay.open.platform.core.rest.dto.LabelValue; @@ -24,15 +24,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -/// # 通道路由:策略方式→能力与 DB 求交 +/// # 通道路由:策略方式→通道商户与能力候选 /// -/// 统一场景模式产品/能力候选及保存校验,数据源为 {@link AbsProductStrategy#methodCapabilityMapping()}。 +/// 统一场景模式通道商户/能力候选及保存校验。 +/// 通道商户候选数据源为商户已开通的 {@link ChannelMerchant}(其 product 须支持对应目录支付方式); +/// 能力候选数据源为 {@link AbsProductStrategy#methodCapabilityMapping()}(由通道商户绑定的产品决定)。 @Service @RequiredArgsConstructor public class PayRouteStrategyCapabilitySupport { @@ -41,23 +42,24 @@ public class PayRouteStrategyCapabilitySupport { private final PayProductCapabilityService payProductCapabilityService; private final PayProductCapabilityManager payProductCapabilityManager; private final PayCapabilityManager payCapabilityManager; + private final ChannelMerchantManager channelMerchantManager; - /// 通道路由白名单目录下全部 (provider,method) 的产品候选(单次请求聚合) - public Map> listSceneProductCandidatesBatch(String mchNo) { + /// 通道路由白名单目录下全部 (provider,method) 的通道商户候选(单次请求聚合) + public Map> listSceneChannelMchCandidatesBatch(String mchNo) { Map> index = new LinkedHashMap<>(); + // 通道商户是商户级数据,目录循环外一次性加载 + List mchants = channelMerchantManager.findAllByMchNo(mchNo); for (PayProviderMethodEntry entry : payProviderMethodService.listDirectoryEntries()) { if (!PayRouteConfigProviders.contains(entry.getProviderCode())) { continue; } - String provider = entry.getProviderCode(); - String method = entry.getMethodCode(); - String key = PayProviderMethodManager.pairKey(provider, method); - index.put(key, listSceneProductCandidates(provider, method)); + String key = PayProviderMethodManager.pairKey(entry.getProviderCode(), entry.getMethodCode()); + index.put(key, filterChannelMchForDirectory(mchants, entry.getProviderCode(), entry.getMethodCode())); } return index; } - /// 按目录项+产品批量返回支付能力候选 + /// 按目录项+通道商户批量返回支付能力候选 public Map> listSceneCapabilityCandidatesBatch( String mchNo, List items) { Map> index = new LinkedHashMap<>(); @@ -65,47 +67,56 @@ public class PayRouteStrategyCapabilitySupport { return index; } for (PayRouteSceneCapabilityBatchItem item : items) { - if (item == null || StrUtil.hasBlank(item.getProvider(), item.getMethod(), item.getProduct())) { + if (item == null || StrUtil.hasBlank(item.getProvider(), item.getMethod(), item.getChannelMchNo())) { continue; } - String key = capabilityBatchKey(item.getProvider(), item.getMethod(), item.getProduct()); - index.put(key, listSceneCapabilityCandidates( - item.getProvider(), item.getMethod(), item.getProduct())); + String key = capabilityBatchKey(item.getProvider(), item.getMethod(), item.getChannelMchNo()); + index.put(key, listSceneCapabilityCandidates(item.getProvider(), item.getMethod(), item.getChannelMchNo())); } return index; } - /// 能力批量候选 Map 的 key:provider|method|product - public static String capabilityBatchKey(String provider, String method, String product) { - return provider + "|" + method + "|" + product; + /// 能力批量候选 Map 的 key:provider|method|channelMchNo + public static String capabilityBatchKey(String provider, String method, String channelMchNo) { + return provider + "|" + method + "|" + channelMchNo; } - /// 目录项下可用支付产品候选 - public List listSceneProductCandidates(String provider, String method) { + /// 目录项下商户已开通且其产品支持该(provider,method)的通道商户候选 + public List listSceneChannelMchCandidates(String mchNo, String provider, String method) { if (!payProviderMethodService.contains(provider, method)) { return List.of(); } + return filterChannelMchForDirectory(channelMerchantManager.findAllByMchNo(mchNo), provider, method); + } + + /// 从商户全部通道商户中筛出启用且其产品支持该(provider,method)的候选 + private List filterChannelMchForDirectory(List mchants, String provider, String method) { PayMethodEnum methodEnum = PayMethodEnum.findByCode(method); List results = new ArrayList<>(); - for (ProductEnum pe : ProductEnum.values()) { - String product = pe.getCode(); - if (!routeProductSupportsMethod(product, methodEnum)) { + for (ChannelMerchant mch : mchants) { + if (!Boolean.TRUE.equals(mch.getEnable())) { continue; } - String label = I18nUtil.getEnumName(pe); - if (results.stream().noneMatch(item -> Objects.equals(item.getValue(), product))) { - results.add(new LabelValue(label, product)); + String product = mch.getProduct(); + if (!routeProductSupportsMethod(product, provider, method)) { + 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; } - /// 指定产品+目录项下支付能力候选(策略 Map ∩ DB ∩ 能力主数据启用) - public List listSceneCapabilityCandidates(String provider, String method, String product) { + /// 指定通道商户+目录项下支付能力候选(策略 Map ∩ DB ∩ 能力主数据启用) + public List listSceneCapabilityCandidates(String provider, String method, String channelMchNo) { if (!payProviderMethodService.contains(provider, method)) { return List.of(); } - if (!PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { + String product = productOfChannelMchNo(channelMchNo); + if (StrUtil.isBlank(product) || !PaymentStrategyFactory.existsByProduct(product, AbsProductStrategy.class)) { return List.of(); } AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class); @@ -117,8 +128,8 @@ public class PayRouteStrategyCapabilitySupport { } /// 候选唯一时返回能力编码(仅供回显) - public String inferSceneCapability(String provider, String method, String product) { - List candidates = listSceneCapabilityCandidates(provider, method, product); + public String inferSceneCapability(String provider, String method, String channelMchNo) { + List candidates = listSceneCapabilityCandidates(provider, method, channelMchNo); if (candidates.size() == 1) { return candidates.getFirst().getValue(); } @@ -126,21 +137,21 @@ public class PayRouteStrategyCapabilitySupport { } /// 校验场景配置项所选能力在候选集合内 - public void validateSceneCapability(String provider, String method, String product, String capability) { + public void validateSceneCapability(String provider, String method, String channelMchNo, String capability) { if (StrUtil.isBlank(capability)) { + // 场景模式下须选择支付能力 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.sceneCapabilityRequired"); } - boolean matched = listSceneCapabilityCandidates(provider, method, product).stream() + boolean matched = listSceneCapabilityCandidates(provider, method, channelMchNo).stream() .anyMatch(item -> Objects.equals(item.getValue(), capability)); if (!matched) { PayCapabilityEnum capabilityEnum = PayCapabilityEnum.findByCode(capability); String capabilityLabel = capabilityEnum != null ? I18nUtil.getEnumName(capabilityEnum) : capability; + // 支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配 throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.route.error.sceneCapabilityProductMismatch", - capabilityLabel, - PayRouteI18nHelper.product(product), - PayRouteI18nHelper.payMethod(method)); + "pay.route.error.sceneCapabilityChannelMchMismatch", + capabilityLabel, channelMchNo, method); } } @@ -164,10 +175,19 @@ public class PayRouteStrategyCapabilitySupport { return routeProductSupportsMethod(product, PayMethodEnum.findByCode(methodCode)); } + /// 通道商户号→产品编码(不存在返回 null) + private String productOfChannelMchNo(String channelMchNo) { + return channelMerchantManager.lambdaQuery() + .eq(ChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt() + .map(ChannelMerchant::getProduct) + .orElse(null); + } + private boolean productCapabilityEnabled(String productCode, String capabilityCode) { if (!payProductCapabilityManager.exists(productCode, capabilityCode)) { return false; } return payCapabilityManager.findByCode(capabilityCode).isPresent(); } -} \ No newline at end of file +} 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 02126f677..ceded6b2e 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 @@ -31,5 +31,12 @@ "routeStrategyNotExist": "Route strategy not found", "routeModeNotExist": "Route mode not found: {0}", "businessSceneNotExist": "Business scene not found: {0}", - "terminalSceneNotExist": "Terminal scene not found: {0}" + "terminalSceneNotExist": "Terminal scene not found: {0}", + "channelMchNoRequired": "Channel merchant no cannot be blank", + "channelMchNotExist": "Channel merchant [{0}] not found", + "channelMchDisabled": "Channel merchant [{0}] is disabled", + "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}]" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 1939c01c1..f2626c062 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -75,7 +75,8 @@ "notNull": "Application ID cannot be null" }, "channelMchNo": { - "size": "Channel merchant no. cannot exceed 32 characters" + "size": "Channel merchant no cannot exceed 32 characters", + "notBlank": "Channel merchant no cannot be blank" }, "subMchId": { "notBlank": "WeChat sub-merchant ID cannot be blank" @@ -181,6 +182,9 @@ "notBlank": "Payment method cannot be blank", "size": "Payment method cannot exceed 32 characters" }, + "capability": { + "size": "Payment capability cannot exceed 32 characters" + }, "provider": { "notBlank": "Pay brand must not be blank", "size": "Pay brand code must not exceed 32 characters" 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 98c3cd6df..37944be16 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 @@ -31,5 +31,12 @@ "routeStrategyNotExist": "路由策略不存在", "routeModeNotExist": "路由模式不存在: {0}", "businessSceneNotExist": "业务场景不存在: {0}", - "terminalSceneNotExist": "终端场景不存在: {0}" + "terminalSceneNotExist": "终端场景不存在: {0}", + "channelMchNoRequired": "通道商户号不能为空", + "channelMchNotExist": "通道商户号[{0}]不存在", + "channelMchDisabled": "通道商户号[{0}]未启用", + "duplicateSceneMethod": "场景模式下同一支付方式[{0}]存在多条配置", + "sceneChannelMchCapabilityPair": "支付方式[{0}]须同时选择通道商户与支付能力,或同时留空", + "basicChannelMchNotConfigured": "支付渠道[{0}]未配置通道商户,请在通道路由基础模式中完成配置", + "sceneCapabilityChannelMchMismatch": "支付能力[{0}]与通道商户[{1}]、支付方式[{2}]不匹配" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index 75088594e..06ef597ed 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -75,7 +75,8 @@ "notNull": "应用ID不可为空" }, "channelMchNo": { - "size": "通道商户号不可超过32位" + "size": "通道商户号不可超过32位", + "notBlank": "通道商户号不可为空" }, "subMchId": { "notBlank": "微信子商户号不可为空" @@ -181,6 +182,9 @@ "notBlank": "支付方式不可为空", "size": "支付方式编码不可超过32位" }, + "capability": { + "size": "支付能力编码不可超过32位" + }, "provider": { "notBlank": "支付渠道不可为空", "size": "支付渠道编码不可超过32位" diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/channel/PayMethodEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/channel/PayMethodEnum.java index bd6c08e0e..0b0db1a7b 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/channel/PayMethodEnum.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/channel/PayMethodEnum.java @@ -11,87 +11,91 @@ import java.util.Objects; /// # 支付方式 /// /// 字典: pay_method;`code` 全局唯一,且仅绑定一个支付渠道。 +/// `provider` 显式声明每个支付方式所属的支付渠道,供通道路由零开销解析(替代旧版从支付产品反推)。 @Getter @RequiredArgsConstructor public enum PayMethodEnum implements I18nSupport { /* 聚合支付 */ /// 聚合扫码支付 - AGGREGATE_PAY_QRCODE("aggregate_pay_qrcode"), + AGGREGATE_PAY_QRCODE("aggregate_pay_qrcode", PayProviderEnum.AGGREGATE_PAY), /// 聚合付款码支付 - AGGREGATE_PAY_BARCODE("aggregate_pay_barcode"), + AGGREGATE_PAY_BARCODE("aggregate_pay_barcode", PayProviderEnum.AGGREGATE_PAY), /* 微信 */ /// 微信小程序收银台 - WECHAT_CASHIER("wechat_cashier"), + WECHAT_CASHIER("wechat_cashier", PayProviderEnum.WECHAT), /// 微信扫码 - WECHAT_QR("wechat_qr"), + WECHAT_QR("wechat_qr", PayProviderEnum.WECHAT), /// 微信jsapi - WECHAT_JSAPI("wechat_jsapi"), + WECHAT_JSAPI("wechat_jsapi", PayProviderEnum.WECHAT), /// 微信小程序 - WECHAT_MINI("wechat_mini"), + WECHAT_MINI("wechat_mini", PayProviderEnum.WECHAT), /// 微信H5 - WECHAT_H5("wechat_h5"), + WECHAT_H5("wechat_h5", PayProviderEnum.WECHAT), /// 微信应用支付 - WECHAT_APP("wechat_app"), + WECHAT_APP("wechat_app", PayProviderEnum.WECHAT), /// 微信付款码 - WECHAT_BARCODE("wechat_barcode"), + WECHAT_BARCODE("wechat_barcode", PayProviderEnum.WECHAT), /* 支付宝 */ /// 支付宝扫码 - ALIPAY_QR("alipay_qr"), + ALIPAY_QR("alipay_qr", PayProviderEnum.ALIPAY), /// 支付宝订单码 - ALIPAY_ORDER_QR("alipay_order_qr"), + ALIPAY_ORDER_QR("alipay_order_qr", PayProviderEnum.ALIPAY), /// 支付宝jsapi - ALIPAY_JSAPI("alipay_jsapi"), + ALIPAY_JSAPI("alipay_jsapi", PayProviderEnum.ALIPAY), /// 支付宝小程序 - ALIPAY_MINI("alipay_mini"), + ALIPAY_MINI("alipay_mini", PayProviderEnum.ALIPAY), /// 支付宝电脑支付 - ALIPAY_PC("alipay_pc"), + ALIPAY_PC("alipay_pc", PayProviderEnum.ALIPAY), /// 支付宝H5 - ALIPAY_H5("alipay_h5"), + ALIPAY_H5("alipay_h5", PayProviderEnum.ALIPAY), /// 支付宝应用支付 - ALIPAY_APP("alipay_app"), + ALIPAY_APP("alipay_app", PayProviderEnum.ALIPAY), /// 支付宝付款码 - ALIPAY_BARCODE("alipay_barcode"), + ALIPAY_BARCODE("alipay_barcode", PayProviderEnum.ALIPAY), /* 银联 */ /// 银联扫码 - UNION_QR("union_qr"), + UNION_QR("union_qr", PayProviderEnum.UNION_PAY), /// 银联jsapi - UNION_JSAPI("union_jsapi"), + UNION_JSAPI("union_jsapi", PayProviderEnum.UNION_PAY), /// 银联H5 - UNION_H5("union_h5"), + UNION_H5("union_h5", PayProviderEnum.UNION_PAY), /// 银联付款码 - UNION_PAY_BARCODE("union_pay_barcode"), + UNION_PAY_BARCODE("union_pay_barcode", PayProviderEnum.UNION_PAY), /* 抖音 */ /// 抖音扫码支付 - DOUYIN_QR("douyin_qr"), + DOUYIN_QR("douyin_qr", PayProviderEnum.DOUYIN), /// 抖音JSAPI支付 - DOUYIN_JSAPI("douyin_jsapi"), + DOUYIN_JSAPI("douyin_jsapi", PayProviderEnum.DOUYIN), /// 抖音H5支付 - DOUYIN_H5("douyin_h5"), + DOUYIN_H5("douyin_h5", PayProviderEnum.DOUYIN), /// 抖音APP支付 - DOUYIN_APP("douyin_app"), + DOUYIN_APP("douyin_app", PayProviderEnum.DOUYIN), /* 卡组 */ /// Visa 网关支付 - VISA_CARD_GATEWAY("visa_card_gateway"), + VISA_CARD_GATEWAY("visa_card_gateway", PayProviderEnum.VISA), /// Visa 刷卡支付 - VISA_CARD_PRESENT("visa_card_present"), + VISA_CARD_PRESENT("visa_card_present", PayProviderEnum.VISA), /// 万事达网关支付 - MASTERCARD_CARD_GATEWAY("mastercard_card_gateway"), + MASTERCARD_CARD_GATEWAY("mastercard_card_gateway", PayProviderEnum.MASTERCARD), /// 万事达刷卡支付 - MASTERCARD_CARD_PRESENT("mastercard_card_present"), + MASTERCARD_CARD_PRESENT("mastercard_card_present", PayProviderEnum.MASTERCARD), - /// 其他支付方式 - OTHER("other"), + /// 其他支付方式(无固定渠道归属) + OTHER("other", null), ; /// 编码 private final String code; + /// 所属支付渠道(OTHER 等无固定归属时为 null) + private final PayProviderEnum provider; + /// 翻译 key 前缀 @Override public String getI18nPrefix() {