refactor(route): 清理通道路由死代码与冗余字段, 移除重付兼容分支

- RouteHit 精简为 (product, channelMchNo, capability), 删除 channel/method 等死字段
- PayRouteProductResolver 删除 resolve/resolveAndFill 等零调用死方法, 仅保留 productOfChannelMchNo
- 移除 inferSceneCapability 全链路(Controller/Facade/Service/Support)
- PayRouteStrategy 移除 provider 字段(改由支付方式解析)
- PayRouteSceneConfig 移除 provider/channel 冗余字段(由 method 派生), saveSceneBatch 改为从 method 派生 provider
- 移除 PayRouteService.resolve 兼容分支与 NormalPayService 重付复用分支
- update-tables.sql 同步清理 provider/channel 列
This commit is contained in:
DaxPay Dev
2026-06-27 11:29:02 +08:00
parent 64c1e7fb60
commit ca34c043a8
18 changed files with 28 additions and 412 deletions

View File

@@ -1,259 +0,0 @@
-- ===================================
-- 云音箱设备域: 厂商配置表 + 设备台账表
-- ===================================
-- 设备厂商配置
CREATE TABLE IF NOT EXISTS device_vendor_config (
id bigint NOT NULL,
device_type varchar(32) NOT NULL,
vendor_code varchar(64) NOT NULL,
config_name varchar(128) NOT NULL,
app_id varchar(128),
app_secret varchar(512),
enable boolean NOT NULL DEFAULT true,
ext_param jsonb,
remark varchar(512),
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 device_vendor_config_pkey PRIMARY KEY (id)
);
COMMENT ON TABLE device_vendor_config IS '设备厂商配置';
COMMENT ON COLUMN device_vendor_config.id IS '主键';
COMMENT ON COLUMN device_vendor_config.device_type IS '设备类型';
COMMENT ON COLUMN device_vendor_config.vendor_code IS '厂商代码';
COMMENT ON COLUMN device_vendor_config.config_name IS '配置名称';
COMMENT ON COLUMN device_vendor_config.app_id IS '应用ID';
COMMENT ON COLUMN device_vendor_config.app_secret IS '应用密钥';
COMMENT ON COLUMN device_vendor_config.enable IS '是否启用';
COMMENT ON COLUMN device_vendor_config.ext_param IS '扩展参数';
COMMENT ON COLUMN device_vendor_config.remark IS '备注';
COMMENT ON COLUMN device_vendor_config.creator IS '创建人ID';
COMMENT ON COLUMN device_vendor_config.create_time IS '创建时间';
COMMENT ON COLUMN device_vendor_config.last_modifier IS '最后修改人ID';
COMMENT ON COLUMN device_vendor_config.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN device_vendor_config.version IS '版本号';
COMMENT ON COLUMN device_vendor_config.deleted IS '逻辑删除';
-- 设备类型+厂商+配置名称唯一索引(未删除范围内)
CREATE UNIQUE INDEX IF NOT EXISTS uk_device_vendor_config_name
ON device_vendor_config (device_type, vendor_code, config_name) WHERE deleted = false;
-- 厂商查询索引
CREATE INDEX IF NOT EXISTS idx_device_vendor_config_vendor
ON device_vendor_config (device_type, vendor_code, deleted);
-- 云音箱设备
CREATE TABLE IF NOT EXISTS device_speaker (
id bigint NOT NULL,
mch_no varchar(32),
vendor_code varchar(64),
vendor_config_id bigint,
device_sn varchar(64) NOT NULL,
imei varchar(32),
shop_id varchar(64),
device_name varchar(128),
status varchar(16) NOT NULL DEFAULT 'unbound',
bind_time timestamptz(6),
last_online_time timestamptz(6),
remark varchar(512),
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 device_speaker_pkey PRIMARY KEY (id)
);
COMMENT ON TABLE device_speaker IS '云音箱设备';
COMMENT ON COLUMN device_speaker.id IS '主键';
COMMENT ON COLUMN device_speaker.mch_no IS '商户号';
COMMENT ON COLUMN device_speaker.device_sn IS '设备序列号';
COMMENT ON COLUMN device_speaker.imei IS 'IMEI';
COMMENT ON COLUMN device_speaker.shop_id IS '厂商门店ID';
COMMENT ON COLUMN device_speaker.device_name IS '设备名称';
COMMENT ON COLUMN device_speaker.status IS '设备状态';
COMMENT ON COLUMN device_speaker.bind_time IS '绑定时间';
COMMENT ON COLUMN device_speaker.last_online_time IS '最后在线时间';
COMMENT ON COLUMN device_speaker.remark IS '备注';
COMMENT ON COLUMN device_speaker.creator IS '创建人ID';
COMMENT ON COLUMN device_speaker.create_time IS '创建时间';
COMMENT ON COLUMN device_speaker.last_modifier IS '最后修改人ID';
COMMENT ON COLUMN device_speaker.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN device_speaker.version IS '版本号';
COMMENT ON COLUMN device_speaker.deleted IS '逻辑删除';
COMMENT ON COLUMN device_speaker.vendor_code IS '厂商代码';
COMMENT ON COLUMN device_speaker.vendor_config_id IS '厂商配置ID';
-- 设备序列号唯一索引(未删除范围内)
CREATE UNIQUE INDEX IF NOT EXISTS uk_device_speaker_sn ON device_speaker (device_sn) WHERE deleted = false;
-- 商户号查询索引
CREATE INDEX IF NOT EXISTS idx_device_speaker_mch_no ON device_speaker (mch_no, deleted);
-- 云打印设备
CREATE TABLE IF NOT EXISTS device_printer (
id bigint NOT NULL,
mch_no varchar(32),
vendor_code varchar(64),
vendor_config_id bigint,
device_sn varchar(64) NOT NULL,
imei varchar(32),
shop_id varchar(64),
device_name varchar(128),
status varchar(16) NOT NULL DEFAULT 'unbound',
bind_time timestamptz(6),
last_online_time timestamptz(6),
remark varchar(512),
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 device_printer_pkey PRIMARY KEY (id)
);
COMMENT ON TABLE device_printer IS '云打印设备';
COMMENT ON COLUMN device_printer.id IS '主键';
COMMENT ON COLUMN device_printer.mch_no IS '商户号';
COMMENT ON COLUMN device_printer.device_sn IS '设备序列号';
COMMENT ON COLUMN device_printer.imei IS 'IMEI';
COMMENT ON COLUMN device_printer.shop_id IS '厂商门店ID';
COMMENT ON COLUMN device_printer.device_name IS '设备名称';
COMMENT ON COLUMN device_printer.status IS '设备状态';
COMMENT ON COLUMN device_printer.bind_time IS '绑定时间';
COMMENT ON COLUMN device_printer.last_online_time IS '最后在线时间';
COMMENT ON COLUMN device_printer.remark IS '备注';
COMMENT ON COLUMN device_printer.creator IS '创建人ID';
COMMENT ON COLUMN device_printer.create_time IS '创建时间';
COMMENT ON COLUMN device_printer.last_modifier IS '最后修改人ID';
COMMENT ON COLUMN device_printer.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN device_printer.version IS '版本号';
COMMENT ON COLUMN device_printer.deleted IS '逻辑删除';
COMMENT ON COLUMN device_printer.vendor_code IS '厂商代码';
COMMENT ON COLUMN device_printer.vendor_config_id IS '厂商配置ID';
-- 设备序列号唯一索引(未删除范围内)
CREATE UNIQUE INDEX IF NOT EXISTS uk_device_printer_sn ON device_printer (device_sn) WHERE deleted = false;
-- 商户号查询索引
CREATE INDEX IF NOT EXISTS idx_device_printer_mch_no ON device_printer (mch_no, deleted);
-- ===================================
-- 双因素认证: 用户 TOTP 绑定表
-- ===================================
-- 用户双因素认证绑定记录(一对一, 记录存在即代表该用户已启用 TOTP 双因素认证)
-- 移除最后验证时间字段(仅展示用途, 已废弃)
ALTER TABLE iam_user_two_factor DROP COLUMN IF EXISTS last_verify_time;
CREATE TABLE IF NOT EXISTS iam_user_two_factor (
id bigint NOT NULL,
user_id bigint NOT NULL,
secret varchar(512) NOT NULL,
backup_codes jsonb,
backup_codes_remaining int NOT NULL DEFAULT 0,
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 iam_user_two_factor_pkey PRIMARY KEY (id)
);
COMMENT ON TABLE iam_user_two_factor IS '用户双因素认证绑定记录';
COMMENT ON COLUMN iam_user_two_factor.id IS '主键';
COMMENT ON COLUMN iam_user_two_factor.user_id IS '用户ID';
COMMENT ON COLUMN iam_user_two_factor.secret IS 'TOTP 密钥';
COMMENT ON COLUMN iam_user_two_factor.backup_codes IS '备用验证码';
COMMENT ON COLUMN iam_user_two_factor.backup_codes_remaining IS '剩余可用备用验证码数量';
COMMENT ON COLUMN iam_user_two_factor.creator IS '创建人ID';
COMMENT ON COLUMN iam_user_two_factor.create_time IS '创建时间';
COMMENT ON COLUMN iam_user_two_factor.last_modifier IS '最后修改人ID';
COMMENT ON COLUMN iam_user_two_factor.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN iam_user_two_factor.version IS '版本号(乐观锁)';
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;

View File

@@ -114,17 +114,6 @@ public class PayRouteAdminController {
return Res.ok(configService.listSceneCapabilityCandidatesForMethod(appId, provider, method, channelMchNo));
}
@PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View")
@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.channelMchNo.notBlank}") String channelMchNo) {
return Res.ok(configService.inferSceneCapability(appId, provider, method, channelMchNo));
}
@PermCode(code = "view", nameCn = "通道路由查看", nameEn = "Pay Route View")
@Operation(summary = "查询基础模式配置列表")
@GetMapping("/basic-config/list-by-app-id")

View File

@@ -56,12 +56,7 @@ public class NormalPayService {
public PayResult payHandle(PayParam payParam, PayTrade trade) {
// 解析应用号:空则取商户默认应用
payAssistService.resolveApp(payParam);
// 重付场景:已有订单且 param 未传产品时,复用首次产品,不重复路由
if (Objects.nonNull(trade) && StrUtil.isBlank(payParam.getProduct())
&& StrUtil.isNotBlank(trade.getProduct())) {
payParam.setProduct(trade.getProduct());
}
// 路由解析:已指定 product 则跳过,否则按 appId+method 策略匹配
// 路由解析:直定模式(已传 channelMchNo)直接解析,否则按 appId+method 策略匹配
payRouteFacade.resolve(payParam);
var payStrategy = PaymentStrategyFactory.createByProduct(payParam.getProduct(), AbsPayStrategy.class);
payStrategy.setPayParam(payParam);

View File

@@ -8,7 +8,7 @@ import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
/// 实现在 merchant 模块PayRouteService按应用路由策略匹配并回填产品。
public interface PayRouteFacade {
/// 解析支付路由:已指定 product 则跳过,否则按策略模式匹配并回填 product
/// 解析支付路由:直定模式(已传 channelMchNo)直接解析,否则按策略模式匹配并回填。
/// 调用方需保证 appId 已解析完毕。
void resolve(PayParam payParam);
}

View File

@@ -4,7 +4,6 @@ import cn.daxpay.open.payment.merchant.convert.route.scene.PayRouteSceneConfigCo
import cn.daxpay.open.payment.merchant.result.route.scene.PayRouteSceneConfigResult;
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -21,13 +20,6 @@ public class PayRouteSceneConfig extends MpBaseEntity implements ToResult<PayRou
/// 路由策略ID
private Long strategyId;
/// 支付渠道, 空表示通用方式qrcode/barcode 等)
/// @see PayProviderEnum
private String provider;
/// 通道编码
private String channel;
/// 支付方式编码
private String method;

View File

@@ -29,10 +29,6 @@ public class PayRouteStrategy extends MchBaseEntity implements ToResult<PayRoute
/// @see PayRouteModeEnum
private String mode;
/// 支付渠道(微信/支付宝/银联)
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum
private String provider;
/// 是否启用
private boolean enable;

View File

@@ -10,18 +10,12 @@ import lombok.experimental.Accessors;
@Schema(title = "场景模式配置项")
public class PayRouteSceneConfigItem {
@Schema(description = "支付渠道")
private String provider;
@Schema(description = "通道商户号(场景模式定位通道商户,由其推导支付产品)")
private String channelMchNo;
@Schema(description = "支付能力")
private String capability;
@Schema(description = "支付通道")
private String channel;
@Schema(description = "支付方式")
private String method;
}

View File

@@ -24,9 +24,6 @@ public class PayRouteStrategyParam {
@Schema(description = "路由模式basic/scene")
private String mode;
@Schema(description = "支付渠道")
private String provider;
@Schema(description = "是否启用")
private Boolean enable;

View File

@@ -16,12 +16,6 @@ public class PayRouteSceneConfigResult extends BaseResult {
@Schema(description = "路由策略ID")
private String strategyId;
@Schema(description = "支付渠道")
private String provider;
@Schema(description = "通道编码")
private String channel;
@Schema(description = "支付方式编码")
private String method;

View File

@@ -23,9 +23,6 @@ public class PayRouteStrategyResult extends BaseResult {
@Schema(description = "路由模式")
private String mode;
@Schema(description = "支付渠道")
private String provider;
@Schema(description = "是否启用")
private boolean enable;

View File

@@ -60,10 +60,9 @@ public class PayRouteBasicMatcher {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"pay.route.error.basicProductNotAvailable", PayRouteI18nHelper.provider(provider.getCode()));
}
String channel = productResolver.channelOfProduct(product);
// 基础模式不存能力,由 (产品, 支付方式) 派生
String capability = resolveCapability(product, methodEnum);
return new RouteHit(channel, methodEnum.getCode(), product, channelMchNo, capability, null, null);
return new RouteHit(product, channelMchNo, capability);
}
/// 从基础配置中取指定支付渠道已绑定的通道商户号

View File

@@ -98,11 +98,6 @@ public class PayRouteConfigService {
return sceneConfigService.listSceneCapabilityCandidatesForMethod(appId, provider, method, channelMchNo);
}
/// 推断目录项与通道商户下唯一支付能力(仅供回显)
public String inferSceneCapability(String appId, String provider, String method, String channelMchNo) {
return sceneConfigService.inferSceneCapability(appId, provider, method, channelMchNo);
}
/// 查询基础模式各支付渠道对应产品
public List<PayRouteBasicConfigResult> listBasicByAppId(String appId) {
return basicConfigService.listBasicByAppId(appId);

View File

@@ -1,24 +1,16 @@
package cn.daxpay.open.payment.merchant.service.route.model;
import cn.daxpay.open.payment.merchant.entity.route.scene.PayRouteSceneConfig;
/// # 通道路由命中结果
///
/// 匹配成功后得到的通道、方式、产品、通道商户及能力,供填充 PayParam。
/// 匹配成功后得到的支付产品、通道商户及能力,供填充 PayParam。
///
/// @param channel 支付通道编码
/// @param method 支付方式编码
/// @param product 支付产品编码(可为空,由运行时解析)
/// @param product 支付产品编码(由通道商户号派生)
/// @param channelMchNo 通道商户号(场景/基础模式命中的核心定位字段)
/// @param capability 支付能力编码(场景模式命中的能力声明;基础模式可为空)
/// @param hitRuleId 精细模式命中的规则 ID预留字段当前实现恒为 null
/// @param hitConfigId 场景模式命中的场景配置 ID基础模式为 null
public record RouteHit(String channel, String method, String product, String channelMchNo,
String capability, Long hitRuleId, Long hitConfigId) {
/// @param capability 支付能力编码(场景模式命中的能力声明;基础模式可为空)
public record RouteHit(String product, String channelMchNo, String capability) {
/// 由场景模式配置构造命中结果
public static RouteHit fromScene(PayRouteSceneConfig config) {
return new RouteHit(config.getChannel(), config.getMethod(), null,
config.getChannelMchNo(), config.getCapability(), null, config.getId());
/// 由场景模式配置构造命中结果product 留空,由运行时按通道商户号派生)
public static RouteHit fromScene(String channelMchNo, String capability) {
return new RouteHit(null, channelMchNo, capability);
}
}

View File

@@ -2,27 +2,19 @@ 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;
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.Service;
import java.util.Arrays;
import java.util.Objects;
/// # 通道路由产品解析器
///
/// 通道商户号(channelMchNo)与支付产品一一绑定,是路由重构后的唯一定位锚点
/// 通道(channel)/支付方式(method)→产品的旧解析逻辑暂时保留以兼容基础模式回退场景。
/// 通道商户号(channelMchNo)与支付产品一一绑定,是路由的唯一定位锚点
@Service
@RequiredArgsConstructor
public class PayRouteProductResolver {
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
private final ChannelMerchantManager channelMerchantManager;
/// 根据通道商户号解析支付产品编码(channelMchNo 唯一绑定 product)
@@ -38,41 +30,4 @@ public class PayRouteProductResolver {
.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) {
return Arrays.stream(ProductEnum.values())
.filter(pe -> Objects.equals(pe.getChannel(), channel))
.map(ProductEnum::getCode)
.filter(product -> payRouteStrategyCapabilitySupport.routeProductSupportsMethod(
product, PayMethodEnum.findByCode(method)))
.findFirst()
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"pay.route.error.productNotResolved", channel, method));
}
/// 填充并校验产品字段
public String resolveAndFill(String mchNo, String channel, String method, String product) {
String resolved = resolve(mchNo, channel, method);
if (StrUtil.isNotBlank(product) && !Objects.equals(product, resolved)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"pay.route.error.productMismatch", product, resolved);
}
return resolved;
}
/// 根据产品编码填充通道
public String channelOfProduct(String product) {
ProductEnum productEnum = ProductEnum.findByCode(product);
if (productEnum == null) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"pay.route.error.productInvalid", product);
}
return productEnum.getChannel();
}
}

View File

@@ -30,10 +30,9 @@ import java.util.Objects;
/// # 支付通道路由服务
///
/// 实现 PayRouteFacade供 NormalPayService 在支付流程中调用。
/// 种解析路径:
/// 1. 直定模式:已传 channelMchNo跳过路由直接由 channelMchNo 推导产品并校验/派生能力;
/// 2. 兼容跳过:未传 channelMchNo 但已指定 product(重付复用),直接返回;
/// 3. 路由模式:按 appId 加载策略,经基础/场景模式匹配后回填 product/channelMchNo/capability。
/// 种解析路径:
/// 1. 直定模式:已传 channelMchNo跳过路由由 channelMchNo 推导产品capability 必填method 未传时由能力反推。
/// 2. 路由模式:按 appId 加载策略,经基础/场景模式匹配后回填 product/channelMchNo/capability。
/// 调用方需保证 appId 已解析完毕。
@Slf4j
@Service
@@ -47,7 +46,7 @@ public class PayRouteService implements PayRouteFacade {
private final PayRouteBasicMatcher basicMatcher;
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
/// 实付路由解析:直定模式优先,其次兼容已指定 product最后按策略模式匹配
/// 实付路由解析:直定模式优先,否则按策略模式匹配
@Override
public void resolve(PayParam payParam) {
// 直定模式:指定通道商户号,跳过路由
@@ -55,10 +54,6 @@ public class PayRouteService implements PayRouteFacade {
resolveDirect(payParam);
return;
}
// 兼容:已指定 product(重付复用),跳过路由
if (StrUtil.isNotBlank(payParam.getProduct())) {
return;
}
String appId = payParam.getAppId();
var bundle = loadBundle(appId);
if (bundle == null || bundle.getStrategy() == null) {

View File

@@ -11,12 +11,13 @@ 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.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.masterdata.constants.provider.service.PayProviderMethodService;
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;
@@ -41,7 +42,6 @@ public class PayRouteSceneConfigService {
private final PayRouteStrategyManager strategyManager;
private final PayRouteSceneConfigManager sceneConfigManager;
private final PayRouteProductResolver productResolver;
private final MchAppInfoManager mchAppInfoManager;
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
private final PayRouteMethodValidator payRouteMethodValidator;
@@ -66,13 +66,19 @@ public class PayRouteSceneConfigService {
sceneConfigManager.deleteByStrategyId(strategy.getId());
List<PayRouteSceneConfig> configs = new ArrayList<>();
for (PayRouteSceneConfigItem item : param.getItems()) {
if (StrUtil.isBlank(item.getMethod()) || !PayRouteConfigProviders.contains(item.getProvider())) {
if (StrUtil.isBlank(item.getMethod())) {
continue;
}
// 支付渠道由支付方式派生(OTHER 等无渠道归属或非白名单则跳过)
PayProviderEnum provider = PayMethodEnum.findByCode(item.getMethod()).getProvider();
if (provider == null || !PayRouteConfigProviders.contains(provider.getCode())) {
continue;
}
String providerCode = provider.getCode();
if (isSceneRowEmpty(item)) {
continue;
}
payRouteMethodValidator.validateSceneConfigItem(item.getProvider(), item.getMethod());
payRouteMethodValidator.validateSceneConfigItem(providerCode, item.getMethod());
// 校验通道商户属本商户且启用
ChannelMerchant mch = channelMerchantManager
.findByMchNoAndChannelMchNo(mchNo, item.getChannelMchNo())
@@ -86,20 +92,16 @@ public class PayRouteSceneConfigService {
String product = mch.getProduct();
// 校验产品支持该(provider, method)
if (!payRouteStrategyCapabilitySupport.routeProductSupportsMethod(
product, item.getProvider(), item.getMethod())) {
product, providerCode, 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);
providerCode, item.getMethod(), item.getChannelMchNo(), item.getCapability());
PayRouteSceneConfig config = new PayRouteSceneConfig();
config.setStrategyId(strategy.getId());
config.setProvider(item.getProvider());
config.setChannel(channel);
config.setMethod(item.getMethod());
config.setChannelMchNo(item.getChannelMchNo());
config.setCapability(item.getCapability());
@@ -140,14 +142,6 @@ public class PayRouteSceneConfigService {
return payRouteStrategyCapabilitySupport.listSceneCapabilityCandidates(provider, method, channelMchNo);
}
/// 回显推断支付能力(候选唯一时返回编码)
public String inferSceneCapability(String appId, String provider, String method, String channelMchNo) {
if (StrUtil.isBlank(channelMchNo)) {
return null;
}
return payRouteStrategyCapabilitySupport.inferSceneCapability(provider, method, channelMchNo);
}
/// 校验批量保存项:支付方式全局唯一
private void validateSceneConfigUnique(List<PayRouteSceneConfigItem> items) {
Set<String> methodKeys = new HashSet<>();

View File

@@ -40,6 +40,6 @@ public class PayRouteSceneMatcher {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.duplicateSceneMethod",
method);
}
return RouteHit.fromScene(candidates.getFirst());
return RouteHit.fromScene(candidates.getFirst().getChannelMchNo(), candidates.getFirst().getCapability());
}
}

View File

@@ -126,15 +126,6 @@ public class PayRouteStrategyCapabilitySupport {
.toList();
}
/// 候选唯一时返回能力编码(仅供回显)
public String inferSceneCapability(String provider, String method, String channelMchNo) {
List<LabelValue> candidates = listSceneCapabilityCandidates(provider, method, channelMchNo);
if (candidates.size() == 1) {
return candidates.getFirst().getValue();
}
return null;
}
/// 传值模式: 商户全部启用通道商户候选(不按支付方式过滤)
public List<LabelValue> listDirectChannelMchCandidates(String mchNo) {
List<ChannelMerchant> mchants = channelMerchantManager.findAllByMchNo(mchNo);