mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 15:35:38 +08:00
refactor(route): 通道路由场景模式重构为通道商户+支付能力定位
场景/基础模式配置以 channelMchNo 替代 product, 解决一商户多通道商户无法唯一定位; PayMethodEnum 加 provider 字段由支付方式直接解析, 移除从 product 反推; PayParam 新增 capability 支持路由回填与直定模式(指定 channelMchNo 跳过路由); 候选数据源改为商户实际开通的 ChannelMerchant, 修复原配置不考虑开通情况; 删除 dead code(PayRouteSceneRouteResolver/PayRouteCapabilityService) 并重建两表 DDL
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<Map<String, List<LabelValue>>> listSceneProductCandidatesBatch(
|
||||
@Operation(summary = "通道路由白名单目录下全部通道商户候选(批量)")
|
||||
@GetMapping("/scene-config/channel-mch-candidates-batch")
|
||||
public Result<Map<String, List<LabelValue>>> 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<Map<String, List<LabelValue>>> 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<List<LabelValue>> listSceneProductCandidates(
|
||||
@Operation(summary = "目录项下商户已开通的通道商户候选")
|
||||
@GetMapping("/scene-config/channel-mch-candidates")
|
||||
public Result<List<LabelValue>> 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<List<LabelValue>> 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<String> 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")
|
||||
|
||||
@@ -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<ChannelMerchantMapper, C
|
||||
.eq(ChannelMerchant::getMchNo, mchNo)
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 根据商户号与通道商户号查询唯一通道商户(不存在返回 empty)
|
||||
public Optional<ChannelMerchant> findByMchNoAndChannelMchNo(String mchNo, String channelMchNo){
|
||||
return this.lambdaQuery()
|
||||
.eq(ChannelMerchant::getMchNo, mchNo)
|
||||
.eq(ChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 场景必填)
|
||||
|
||||
@@ -13,5 +13,7 @@ public interface PayRouteBasicConfigConvert {
|
||||
|
||||
PayRouteBasicConfigConvert CONVERT = Mappers.getMapper(PayRouteBasicConfigConvert.class);
|
||||
|
||||
/// channelMchants(可选通道商户列表)由 service 按商户开通情况装配,转换时忽略
|
||||
@Mapping(target = "channelMchants", ignore = true)
|
||||
PayRouteBasicConfigResult toResult(PayRouteBasicConfig entity);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public class PayRouteBasicConfig extends MpBaseEntity implements ToResult<PayRou
|
||||
/// @see PayProviderEnum
|
||||
private String provider;
|
||||
|
||||
/// 支付产品编码
|
||||
private String product;
|
||||
/// 通道商户号(唯一绑定一个支付产品,替代旧版 product 字段)
|
||||
private String channelMchNo;
|
||||
|
||||
@Override
|
||||
public PayRouteBasicConfigResult toResult() {
|
||||
|
||||
@@ -31,8 +31,12 @@ public class PayRouteSceneConfig extends MpBaseEntity implements ToResult<PayRou
|
||||
/// 支付方式编码
|
||||
private String method;
|
||||
|
||||
/// 产品编码
|
||||
private String product;
|
||||
/// 通道商户号(唯一绑定一个支付产品,替代旧版 product 字段)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 支付能力编码(商户为该支付方式+通道商户选定的能力)
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum
|
||||
private String capability;
|
||||
|
||||
@Override
|
||||
public PayRouteSceneConfigResult toResult() {
|
||||
|
||||
@@ -16,6 +16,6 @@ public class PayRouteBasicConfigItem {
|
||||
@Schema(description = "支付渠道: wechat/alipay/union_pay")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "支付产品编码,空表示清除该场景配置")
|
||||
private String product;
|
||||
@Schema(description = "通道商户号,空表示清除该渠道配置")
|
||||
private String channelMchNo;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class PayRouteSceneCapabilityBatchItem {
|
||||
@Schema(description = "支付方式")
|
||||
private String method;
|
||||
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
@Schema(description = "支付产品")
|
||||
private String product;
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}")
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
}
|
||||
@@ -13,10 +13,10 @@ public class PayRouteSceneConfigItem {
|
||||
@Schema(description = "支付渠道")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "支付产品")
|
||||
private String product;
|
||||
@Schema(description = "通道商户号(场景模式定位通道商户,由其推导支付产品)")
|
||||
private String channelMchNo;
|
||||
|
||||
@Schema(description = "支付能力(场景保存校验用,不落库)")
|
||||
@Schema(description = "支付能力")
|
||||
private String capability;
|
||||
|
||||
@Schema(description = "支付通道")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.payment.merchant.result.route.basic;
|
||||
|
||||
import cn.daxpay.open.platform.core.result.BaseResult;
|
||||
import cn.daxpay.open.platform.core.rest.dto.LabelValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -21,10 +22,10 @@ public class PayRouteBasicConfigResult extends BaseResult {
|
||||
@Schema(description = "支付渠道: wechat/alipay/union_pay")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "支付产品编码")
|
||||
private String product;
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
/// 该渠道下可选支付产品编码列表
|
||||
@Schema(description = "可选支付产品编码")
|
||||
private List<String> products = new ArrayList<>();
|
||||
/// 该渠道下可选通道商户列表(名称/号码)
|
||||
@Schema(description = "可选通道商户列表")
|
||||
private List<LabelValue> channelMchants = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<PayRouteBasicConfigResult> listBasicByAppId(String appId) {
|
||||
String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId);
|
||||
PayRouteStrategy strategy = requireStrategy(appId);
|
||||
Map<String, String> productMap = basicConfigManager.findByStrategyId(strategy.getId()).stream()
|
||||
Map<String, String> 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<String> products = productsForProvider(provider);
|
||||
/// 组装单渠道基础模式面板行:已保存通道商户号 + 该渠道下可选通道商户列表
|
||||
private PayRouteBasicConfigResult toPanelResult(String mchNo, PayProviderEnum provider, String savedChannelMchNo) {
|
||||
List<LabelValue> 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<String> 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PayRouteBasicConfig> 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<String> 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<PayRouteBasicConfig> basicConfigs, String providerCode) {
|
||||
/// 从基础配置中取指定支付渠道已绑定的通道商户号
|
||||
private String findConfiguredChannelMchNo(List<PayRouteBasicConfig> 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<PayCapabilityEnum> capabilities = ProductStrategySupport.capabilitiesForMethod(strategy, method);
|
||||
return capabilities.isEmpty() ? null : capabilities.getFirst().getCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,30 +77,30 @@ public class PayRouteConfigService {
|
||||
sceneConfigService.saveSceneBatch(param);
|
||||
}
|
||||
|
||||
/// 通道路由白名单目录下全部产品候选(批量)
|
||||
public Map<String, List<LabelValue>> listSceneProductCandidatesBatch(String appId) {
|
||||
return sceneConfigService.listSceneProductCandidatesBatch(appId);
|
||||
/// 通道路由白名单目录下全部通道商户候选(批量)
|
||||
public Map<String, List<LabelValue>> listSceneChannelMchCandidatesBatch(String appId) {
|
||||
return sceneConfigService.listSceneChannelMchCandidatesBatch(appId);
|
||||
}
|
||||
|
||||
/// 按目录项与产品批量返回支付能力候选
|
||||
/// 按目录项与通道商户批量返回支付能力候选
|
||||
public Map<String, List<LabelValue>> listSceneCapabilityCandidatesBatch(PayRouteSceneCapabilityBatchParam param) {
|
||||
return sceneConfigService.listSceneCapabilityCandidatesBatch(param);
|
||||
}
|
||||
|
||||
/// 目录项下商户可用支付产品候选
|
||||
public List<LabelValue> listSceneProductCandidatesForMethod(String appId, String provider, String method) {
|
||||
return sceneConfigService.listSceneProductCandidatesForMethod(appId, provider, method);
|
||||
/// 目录项下商户已开通的通道商户候选
|
||||
public List<LabelValue> listSceneChannelMchCandidatesForMethod(String appId, String provider, String method) {
|
||||
return sceneConfigService.listSceneChannelMchCandidatesForMethod(appId, provider, method);
|
||||
}
|
||||
|
||||
/// 目录项与产品下支付能力候选
|
||||
/// 目录项与通道商户下支付能力候选
|
||||
public List<LabelValue> 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);
|
||||
}
|
||||
|
||||
/// 查询基础模式各支付渠道对应产品
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PayCapabilityEnum> capabilities = ProductStrategySupport.capabilitiesForMethod(strategy, method);
|
||||
return capabilities.isEmpty() ? null : capabilities.getFirst().getCode();
|
||||
}
|
||||
|
||||
/// 校验指定能力属于该(产品, 支付方式)的策略声明候选
|
||||
private void validateCapability(String product, PayMethodEnum method, String capability) {
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(product, AbsProductStrategy.class);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PayRouteSceneConfigResult> 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<PayRouteSceneConfig> 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<String, List<LabelValue>> listSceneProductCandidatesBatch(String appId) {
|
||||
/// 通道路由白名单目录下全部 (provider,method) 的通道商户候选(批量)
|
||||
public Map<String, List<LabelValue>> listSceneChannelMchCandidatesBatch(String appId) {
|
||||
String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(appId);
|
||||
return payRouteStrategyCapabilitySupport.listSceneProductCandidatesBatch(mchNo);
|
||||
return payRouteStrategyCapabilitySupport.listSceneChannelMchCandidatesBatch(mchNo);
|
||||
}
|
||||
|
||||
/// 按目录项+产品批量返回支付能力候选
|
||||
/// 按目录项+通道商户批量返回支付能力候选
|
||||
public Map<String, List<LabelValue>> listSceneCapabilityCandidatesBatch(PayRouteSceneCapabilityBatchParam param) {
|
||||
String mchNo = mchAppInfoManager.requireMchNoByAppIdNotTenant(param.getAppId());
|
||||
return payRouteStrategyCapabilitySupport.listSceneCapabilityCandidatesBatch(mchNo, param.getItems());
|
||||
}
|
||||
|
||||
/// 按目录项(支付渠道+支付方式)筛选商户已开通且能力匹配的产品候选
|
||||
public List<LabelValue> listSceneProductCandidatesForMethod(String appId, String provider, String method) {
|
||||
/// 按目录项(支付渠道+支付方式)筛选商户已开通的通道商户候选
|
||||
public List<LabelValue> 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<LabelValue> 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<PayRouteSceneConfigItem> items) {
|
||||
Set<String> pairKeys = new HashSet<>();
|
||||
Set<String> 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<PayRouteSceneConfigItem> items) {
|
||||
/// 校验目录行:通道商户号与支付能力须同时为空或同时有值,不可只填其一
|
||||
private void validateSceneChannelMchCapabilityPairing(List<PayRouteSceneConfigItem> 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());
|
||||
}
|
||||
|
||||
/// 按应用号加载路由策略,不存在则抛业务异常
|
||||
|
||||
@@ -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<PayRouteSceneConfig> 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<PayRouteSceneConfig> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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) {
|
||||
}
|
||||
}
|
||||
@@ -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<String> methodsForProductPayProvider(String product, String providerCode) {
|
||||
PayProviderEnum provider = PayProviderEnum.findByCode(providerCode);
|
||||
if (provider == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, List<LabelValue>> listSceneProductCandidatesBatch(String mchNo) {
|
||||
/// 通道路由白名单目录下全部 (provider,method) 的通道商户候选(单次请求聚合)
|
||||
public Map<String, List<LabelValue>> listSceneChannelMchCandidatesBatch(String mchNo) {
|
||||
Map<String, List<LabelValue>> index = new LinkedHashMap<>();
|
||||
// 通道商户是商户级数据,目录循环外一次性加载
|
||||
List<ChannelMerchant> 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<String, List<LabelValue>> listSceneCapabilityCandidatesBatch(
|
||||
String mchNo, List<PayRouteSceneCapabilityBatchItem> items) {
|
||||
Map<String, List<LabelValue>> 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<LabelValue> listSceneProductCandidates(String provider, String method) {
|
||||
/// 目录项下商户已开通且其产品支持该(provider,method)的通道商户候选
|
||||
public List<LabelValue> 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<LabelValue> filterChannelMchForDirectory(List<ChannelMerchant> mchants, String provider, String method) {
|
||||
PayMethodEnum methodEnum = PayMethodEnum.findByCode(method);
|
||||
List<LabelValue> 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<LabelValue> listSceneCapabilityCandidates(String provider, String method, String product) {
|
||||
/// 指定通道商户+目录项下支付能力候选(策略 Map ∩ DB ∩ 能力主数据启用)
|
||||
public List<LabelValue> 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<LabelValue> candidates = listSceneCapabilityCandidates(provider, method, product);
|
||||
public String inferSceneCapability(String provider, String method, String channelMchNo) {
|
||||
List<LabelValue> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}]"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}]不匹配"
|
||||
}
|
||||
|
||||
@@ -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位"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user