diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayDirectAppManager.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayDirectAppManager.java index 30ef6a2fc..52a85ea0f 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayDirectAppManager.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayDirectAppManager.java @@ -56,6 +56,16 @@ public class AlipayDirectAppManager extends BaseManager listByChannelMchNoAndAppType(String channelMchNo, String appType) { + return lambdaQuery() + .eq(AlipayDirectApp::getChannelMchNo, channelMchNo) + .eq(AlipayDirectApp::getAppType, appType) + .orderByAsc(AlipayDirectApp::getCreateTime) + .orderByAsc(AlipayDirectApp::getId) + .list(); + } + /// 根据通道商户号查询首个应用(支付/回调,租户内) public Optional findFirstByChannelMchNo(String channelMchNo) { return firstOpt(q -> q diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java index 60877a71f..93ed64e34 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java @@ -112,11 +112,12 @@ public class AlipayDirectAppCapabilityService { /// 支付/回调解析应用:显式配置 > appType 自动推导(须已装载 mchNo,租户内) /// - /// 认证无上下文请用 [#resolveAppNotTenant]。 + /// appType 推导要求该通道商户下该类型应用唯一命中: + /// - 唯一命中:返回该应用 + /// - 该类型存在多个应用:抛 appNotUnique,要求显式配置能力绑 + /// - 该类型无应用:返回 empty,由调用方走最终报错 /// - /// @param channelMchNo 通道商户号 - /// @param capability 支付能力编码 - /// @return 命中的应用;两者均未命中返回 empty,由调用方兜底回退 + /// 认证无上下文请用 [#resolveAppNotTenant]。 public Optional resolveApp(String channelMchNo, String capability) { if (StrUtil.hasBlank(channelMchNo, capability)) { return Optional.empty(); @@ -126,20 +127,22 @@ public class AlipayDirectAppCapabilityService { if (rel.isPresent()) { return alipayDirectAppManager.findById(rel.get().getAlipayDirectAppId()); } - // 2. 未配置则按能力 → appType 自动推导 + // 2. appType 推导:要求该通道商户下该类型应用唯一命中,>1 拒绝猜测 String appType = AlipayDirectAppTypeCode.resolveAppType(capability); if (appType != null) { - return alipayDirectAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType); + List apps = alipayDirectAppManager.listByChannelMchNoAndAppType(channelMchNo, appType); + if (apps.size() > 1) { + // 存在多个同类型应用,请显式配置能力绑定以明确选择 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.alipay.appNotUnique", appType); + } + if (apps.size() == 1) { + return Optional.of(apps.getFirst()); + } } return Optional.empty(); } - /// 认证等无租户上下文时解析应用(忽略租户) - @IgnoreTenant - public Optional resolveAppNotTenant(String channelMchNo, String capability) { - return resolveApp(channelMchNo, capability); - } - /// 查询支付宝直连产品支持的支付能力候选列表(含国际化名称) public List listSupportedCapabilities() { AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct( diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java index 77aebafc7..6dfdc08df 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java @@ -1,11 +1,12 @@ package cn.daxpay.open.channel.alipay.service.direct; -import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppManager; import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectChannelMerchantManager; import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential; import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectApp; import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectAppKeyConfig; import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectChannelMerchant; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,7 +22,7 @@ import java.util.Optional; /// 沙箱标识直接读通道商户固化的 [AlipayDirectChannelMerchant#isSandbox] /// (创建时按当时产品 activeEnv 写入, 不随产品切换改变), 据此选择对应环境的密钥与网关地址。 /// -/// 应用解析优先级:能力关联(显式配置 > appType自动推导) > 通道商户首个应用 > 商户号首个应用(兜底)。 +/// 应用解析优先级:能力关联(显式配置 > appType 唯一推导) > 未命中报错(拒绝首个兜底)。 /// /// 供支付策略([cn.daxpay.open.channel.alipay.strategy.direct.AlipayDirectPayStrategy])组装通道调用凭证。 @Slf4j @@ -29,7 +30,6 @@ import java.util.Optional; @RequiredArgsConstructor public class AlipayDirectConfigAssembler { - private final AlipayDirectAppManager alipayDirectAppManager; private final AlipayDirectChannelMerchantManager alipayDirectChannelMerchantManager; private final AlipayDirectAppKeyConfigService alipayDirectAppKeyConfigService; private final AlipayDirectAppCapabilityService alipayDirectAppCapabilityService; @@ -64,21 +64,16 @@ public class AlipayDirectConfigAssembler { } /// 解析支付使用的应用(须已装载 mchNo,租户内) - /// 优先级:能力关联 > 通道商户首个 > 商户号首个兜底 + /// 优先级:能力关联(显式配置 > appType 唯一推导)> 未命中报错(不再首个兜底) private AlipayDirectApp resolveApp(String mchNo, String channelMchNo, String capability) { - // 1. 能力关联解析(显式配置 > appType自动推导) + // 能力关联解析(显式配置 > appType 唯一推导) Optional resolved = alipayDirectAppCapabilityService.resolveApp(channelMchNo, capability); if (resolved.isPresent()) { return resolved.get(); } - // 2. 兜底:按通道商户号取首个应用 - Optional byChannel = alipayDirectAppManager.findFirstByChannelMchNo(channelMchNo); - if (byChannel.isPresent()) { - return byChannel.get(); - } - // 3. 最终兜底:按商户号取首个应用(兼容单应用旧场景) - // 支付宝: 直连商户应用不存在 - return alipayDirectAppManager.findFirstByMchNo(mchNo) - .orElseThrow(() -> new DataNotExistException("error.channel.alipay.mchAppNotFound")); + // 未命中:不再做通道商户首个/商户号首个兜底,直接报错 + // 支付宝: 未配置该能力对应的应用 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.alipay.appNotConfigured", capability); } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java index c76fddde7..b7bb9860d 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java @@ -54,6 +54,16 @@ public class DouyinDirectAppManager extends BaseManager listByChannelMchNoAndAppType(String channelMchNo, String appType) { + return lambdaQuery() + .eq(DouyinDirectApp::getChannelMchNo, channelMchNo) + .eq(DouyinDirectApp::getAppType, appType) + .orderByAsc(DouyinDirectApp::getCreateTime) + .orderByAsc(DouyinDirectApp::getId) + .list(); + } + /// 按通道商户号与 douyinAppId 查询应用(支付/回调,租户内) public Optional findByChannelMchNoAndDouyinAppId(String channelMchNo, String douyinAppId) { return lambdaQuery() diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java index ffd0e7c61..425ce0e76 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java @@ -109,13 +109,15 @@ public class DouyinDirectAppCapabilityService { capabilityManager.deleteByDouyinDirectAppId(douyinDirectAppId); } - /// 支付/回调解析应用:显式配置 > appType 自动推导 > 通道商户首个兜底(须已装载 mchNo,租户内) + /// 支付/回调解析应用:显式配置 > appType 自动推导(须已装载 mchNo,租户内) + /// + /// appType 推导要求该通道商户下该类型应用唯一命中: + /// - 唯一命中:返回该应用 + /// - 该类型存在多个应用:抛 appNotUnique,要求显式配置能力绑 + /// - 该类型无应用:返回 empty,由调用方走最终报错 + /// 不再"通道商户首个"兜底。 /// /// 认证无上下文请用 [#resolveAppNotTenant]。 - /// - /// @param channelMchNo 通道商户号 - /// @param capability 支付能力编码 - /// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退 public Optional resolveApp(String channelMchNo, String capability) { if (StrUtil.hasBlank(channelMchNo, capability)) { return Optional.empty(); @@ -125,16 +127,21 @@ public class DouyinDirectAppCapabilityService { if (rel.isPresent()) { return douyinDirectAppManager.findById(rel.get().getDouyinDirectAppId()); } - // 2. 未配置则按能力 → appType 自动推导 + // 2. appType 推导:要求该通道商户下该类型应用唯一命中,>1 拒绝猜测 String appType = DouyinAppTypeCode.resolveAppType(capability); if (appType != null) { - Optional byType = douyinDirectAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType); - if (byType.isPresent()) { - return byType; + List apps = douyinDirectAppManager.listByChannelMchNoAndAppType(channelMchNo, appType); + if (apps.size() > 1) { + // 存在多个同类型应用,请显式配置能力绑定以明确选择 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.appNotUnique", appType); + } + if (apps.size() == 1) { + return Optional.of(apps.getFirst()); } } - // 3. 最终兜底:按通道商户号取首个应用 - return douyinDirectAppManager.findFirstByChannelMchNo(channelMchNo); + // 3. 不再首个兜底,返回 empty 由调用方报错 + return Optional.empty(); } /// 认证等无租户上下文时解析应用(忽略租户) @@ -145,8 +152,8 @@ public class DouyinDirectAppCapabilityService { /// H5 silent_auth / JS-SDK 验签用网站应用解析(忽略租户) /// - /// 优先级: channelAppId 显式 > capability 命中且为 web_app > 通道商户首个 web_app。 - /// 勿盲跟 DOUYIN_JSAPI→mini_program 推导。 + /// 优先级: channelAppId 显式 > capability 命中且为 web_app > 该通道商户唯一 web_app。 + /// 不再"首个 web_app"兜底;web_app 不唯一或不存在均直接报错。 @IgnoreTenant public DouyinDirectApp resolveWebAppForH5Auth(String channelMchNo, String capability, String channelAppId) { if (StrUtil.isNotBlank(channelAppId)) { @@ -160,10 +167,19 @@ public class DouyinDirectAppCapabilityService { return byCap.get(); } } - return douyinDirectAppManager.findFirstByChannelMchNoAndAppTypeNotTenant( - channelMchNo, DouyinAppTypeCode.WEB_APP) - .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "error.channel.douyin.webAppNotFound")); + // H5 验签需 web_app:要求该类型唯一命中,>1 抛 notUnique,==0 抛 notConfigured + List webApps = douyinDirectAppManager.listByChannelMchNoAndAppType( + channelMchNo, DouyinAppTypeCode.WEB_APP); + if (webApps.size() > 1) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.appNotUnique", DouyinAppTypeCode.WEB_APP); + } + if (webApps.size() == 1) { + return webApps.getFirst(); + } + // 抖音: 未配置 H5 验签所需的 web_app 类型应用 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.appNotConfigured", DouyinAppTypeCode.WEB_APP); } /// 查询抖音直连产品支持的支付能力候选列表(含国际化名称) diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java index 7506f2469..8ccded3e1 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java @@ -5,6 +5,8 @@ import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManag import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectKeyConfig; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,9 +37,10 @@ public class DouyinDirectConfigAssembler { /// @param capability 支付能力编码(用于选择匹配的应用) /// @return 抖音 SDK 凭证, 字段对齐子应用 DouyinSdkCredential public DouyinSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) { - // 1. 解析支付使用的应用(显式能力关联 > appType 推导 > 通道商户首个兜底) + // 1. 解析支付使用的应用(显式能力关联 > appType 唯一推导;未命中报错,不兜底) DouyinDirectApp app = appCapabilityService.resolveApp(channelMchNo, capability) - .orElseThrow(() -> new DataNotExistException("error.channel.douyin.mchAppNotFound")); + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.appNotConfigured", capability)); // 2. 读取通道商户绑定(获取抖音商户号 dyMchId 作为 mchId) DouyinDirectChannelMerchant merchant = channelMerchantManager.lambdaQuery() diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java index a667e8c4c..5e4916bb5 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java @@ -4,10 +4,9 @@ import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppAuthConfig; import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppAuthConfigService; import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService; -import cn.daxpay.open.payment.auth.AuthRedirectUri; -import cn.daxpay.open.payment.auth.AuthSession; -import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; -import cn.daxpay.open.payment.strategy.auth.AuthContext; +import cn.daxpay.open.payment.auth.core.AuthRedirectUri; +import cn.daxpay.open.payment.auth.core.AuthSession; +import cn.daxpay.open.payment.auth.merchant.AbsProductAuthStrategy; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; @@ -32,7 +31,7 @@ import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class DouyinDirectAuthStrategy extends AbsChannelAuthStrategy { +public class DouyinDirectAuthStrategy extends AbsProductAuthStrategy { private final DouyinH5AuthService douyinH5AuthService; private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService; @@ -63,7 +62,7 @@ public class DouyinDirectAuthStrategy extends AbsChannelAuthStrategy { /// 通过授权 code 换取 openId @Override public AuthResult doAuth(AuthCodeParam param, AuthSession session) { - AuthContext ctx = resolveContext(param, session); + var ctx = resolveContext(param, session); DouyinDirectApp app = douyinDirectAppCapabilityService.resolveWebAppForH5Auth( ctx.channelMchNo(), ctx.capability(), ctx.channelAppId()); DouyinDirectAppAuthConfig authConfig = douyinDirectAppAuthConfigService.findByDouyinDirectAppIdForAuth(app.getId()); diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/direct/WechatDirectAppManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/direct/WechatDirectAppManager.java index b878dd3ac..425e3a1ba 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/direct/WechatDirectAppManager.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/direct/WechatDirectAppManager.java @@ -45,6 +45,16 @@ public class WechatDirectAppManager extends BaseManager listByChannelMchNoAndAppType(String channelMchNo, String appType) { + return lambdaQuery() + .eq(WechatDirectApp::getChannelMchNo, channelMchNo) + .eq(WechatDirectApp::getAppType, appType) + .orderByAsc(WechatDirectApp::getCreateTime) + .orderByAsc(WechatDirectApp::getId) + .list(); + } + /// 按通道商户号与wxAppId查询应用(支付/回调,租户内) public Optional findByChannelMchNoAndWxAppId(String channelMchNo, String wxAppId) { return firstOpt(q -> q diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvAppManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvAppManager.java index dd37466ca..80d70530a 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvAppManager.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvAppManager.java @@ -39,6 +39,15 @@ public class WechatIsvAppManager extends BaseManager listByAppType(String appType) { + return lambdaQuery() + .eq(WechatIsvApp::getAppType, appType) + .orderByAsc(WechatIsvApp::getCreateTime) + .orderByAsc(WechatIsvApp::getId) + .list(); + } + /// 按 wxAppId 查询应用(channelAppId显式指定认证应用时使用, 校验该appId在系统中预配过) public Optional findByWxAppId(String wxAppId) { return firstOpt(q -> q.eq(WechatIsvApp::getWxAppId, wxAppId)); diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectAppCapabilityService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectAppCapabilityService.java index 907b635a1..db3df3fcd 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectAppCapabilityService.java @@ -102,14 +102,15 @@ public class WechatDirectAppCapabilityService { capabilityManager.deleteByWechatDirectAppId(wechatDirectAppId); } - /// 支付/回调解析应用:显式配置 > appType 自动推导 > 通道商户首个兜底 + /// 支付/回调解析应用:显式配置 > appType 自动推导(须已装载 mchNo,租户内) + /// + /// appType 推导要求该通道商户下该类型应用唯一命中: + /// - 唯一命中:返回该应用 + /// - 该类型存在多个应用:抛 appNotUnique,要求显式配置能力绑 + /// - 该类型无应用:返回 empty,由调用方走最终报错 + /// 不再"通道商户首个"兜底。 /// - /// **须已装载 mchNo**([PaymentVerify] 的 initMch / 回调 bindMchNoForCallback),走租户过滤。 /// 认证无上下文请用 [#resolveAppNotTenant]。 - /// - /// @param channelMchNo 通道商户号 - /// @param capability 支付能力编码 - /// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退 public Optional resolveApp(String channelMchNo, String capability) { if (StrUtil.hasBlank(channelMchNo, capability)) { return Optional.empty(); @@ -119,16 +120,21 @@ public class WechatDirectAppCapabilityService { if (rel.isPresent()) { return wechatDirectAppManager.findById(rel.get().getWechatDirectAppId()); } - // 2. 未配置则按能力 → appType 自动推导 + // 2. appType 推导:要求该通道商户下该类型应用唯一命中,>1 拒绝猜测 String appType = WechatAppTypeCode.resolveAppType(capability); if (appType != null) { - Optional byType = wechatDirectAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType); - if (byType.isPresent()) { - return byType; + List apps = wechatDirectAppManager.listByChannelMchNoAndAppType(channelMchNo, appType); + if (apps.size() > 1) { + // 存在多个同类型应用,请显式配置能力绑定以明确选择 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.wechat.appNotUnique", appType); + } + if (apps.size() == 1) { + return Optional.of(apps.getFirst()); } } - // 3. 最终兜底:按通道商户号取首个应用 - return wechatDirectAppManager.findFirstByChannelMchNo(channelMchNo); + // 3. 不再首个兜底,返回 empty + return Optional.empty(); } /// 认证等无租户上下文时解析应用(忽略租户,内部复用 [#resolveApp] 逻辑) diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvAppCapabilityService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvAppCapabilityService.java index 3eaf55d63..ad13c49f4 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvAppCapabilityService.java @@ -95,10 +95,13 @@ public class WechatIsvAppCapabilityService { capabilityManager.deleteByWechatIsvAppId(wechatIsvAppId); } - /// 支付时解析当前支付能力对应的应用:显式配置 > appType自动推导 > 取首个兜底 + /// 支付时解析当前支付能力对应的应用:显式配置 > appType自动推导(要求唯一命中) /// - /// @param capability 支付能力编码 - /// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退 + /// appType 推导要求该类型服务商应用全局唯一命中: + /// - 唯一命中:返回该应用 + /// - 该类型存在多个应用:抛 appNotUnique,要求显式配置能力绑 + /// - 该类型无应用:返回 empty,由调用方走最终报错 + /// 不再"首个服务商应用"兜底。 public Optional resolveApp(String capability) { if (StrUtil.isBlank(capability)) { return Optional.empty(); @@ -108,16 +111,21 @@ public class WechatIsvAppCapabilityService { if (rel.isPresent()) { return wechatIsvAppManager.findById(rel.get().getWechatIsvAppId()); } - // 2. 未配置则按能力 → appType 自动推导 + // 2. appType 推导:要求该类型服务商应用唯一命中,>1 拒绝猜测 String appType = WechatAppTypeCode.resolveAppType(capability); if (appType != null) { - Optional byType = wechatIsvAppManager.findFirstByAppType(appType); - if (byType.isPresent()) { - return byType; + List apps = wechatIsvAppManager.listByAppType(appType); + if (apps.size() > 1) { + // 存在多个同类型服务商应用,请显式配置能力绑定以明确选择 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.wechat.appNotUnique", appType); + } + if (apps.size() == 1) { + return Optional.of(apps.getFirst()); } } - // 3. 最终兜底:取首个服务商应用 - return wechatIsvAppManager.findFirst(); + // 3. 不再首个兜底,返回 empty + return Optional.empty(); } /// 查询微信服务商产品支持的支付能力候选列表(含国际化名称) diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/auth/WechatDirectAuthStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/auth/WechatDirectAuthStrategy.java index 46e09a598..952736bee 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/auth/WechatDirectAuthStrategy.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/auth/WechatDirectAuthStrategy.java @@ -1,11 +1,10 @@ package cn.daxpay.open.channel.wechat.strategy.direct.auth; -import cn.daxpay.open.payment.auth.AuthRedirectUri; -import cn.daxpay.open.payment.auth.AuthSession; +import cn.daxpay.open.payment.auth.core.AuthRedirectUri; +import cn.daxpay.open.payment.auth.core.AuthSession; import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager; import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant; -import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; -import cn.daxpay.open.payment.strategy.auth.AuthContext; +import cn.daxpay.open.payment.auth.merchant.AbsProductAuthStrategy; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; @@ -34,7 +33,7 @@ import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class WechatDirectAuthStrategy extends AbsChannelAuthStrategy { +public class WechatDirectAuthStrategy extends AbsProductAuthStrategy { private final WechatMpAuthService wechatMpAuthService; private final WxAppFacade wxAppFacade; @@ -65,7 +64,7 @@ public class WechatDirectAuthStrategy extends AbsChannelAuthStrategy { /// 应用上下文优先从 session(H5 会话码场景)恢复, 否则取 param(小程序直连场景)。 @Override public AuthResult doAuth(AuthCodeParam param, AuthSession session) { - AuthContext ctx = resolveContext(param, session); + var ctx = resolveContext(param, session); WxAppView app = resolveApp(param.getMchNo(), ctx.channelMchNo(), ctx.capability(), ctx.channelAppId()); WechatAuthResult data = wechatMpAuthService.getTokenAndOpenId( param.getAuthCode(), app.wxAppId(), app.appSecret()); diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/auth/WechatIsvAuthStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/auth/WechatIsvAuthStrategy.java index ea0185eae..18930180d 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/auth/WechatIsvAuthStrategy.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/auth/WechatIsvAuthStrategy.java @@ -3,12 +3,11 @@ package cn.daxpay.open.channel.wechat.strategy.isv.auth; import cn.daxpay.open.channel.wechat.code.WechatAuthAppTypeEnum; import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvChannelMerchantManager; import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvChannelMerchant; -import cn.daxpay.open.payment.auth.AuthRedirectUri; -import cn.daxpay.open.payment.auth.AuthSession; +import cn.daxpay.open.payment.auth.core.AuthRedirectUri; +import cn.daxpay.open.payment.auth.core.AuthSession; import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager; import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant; -import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; -import cn.daxpay.open.payment.strategy.auth.AuthContext; +import cn.daxpay.open.payment.auth.merchant.AbsProductAuthStrategy; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; @@ -41,7 +40,7 @@ import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class WechatIsvAuthStrategy extends AbsChannelAuthStrategy { +public class WechatIsvAuthStrategy extends AbsProductAuthStrategy { private final WechatMpAuthService wechatMpAuthService; private final WxAppFacade wxAppFacade; @@ -68,7 +67,7 @@ public class WechatIsvAuthStrategy extends AbsChannelAuthStrategy { /// 通过授权 code 换取 openId @Override public AuthResult doAuth(AuthCodeParam param, AuthSession session) { - AuthContext ctx = resolveContext(param, session); + var ctx = resolveContext(param, session); ResolvedAuthApp app = resolveAuthApp(param.getMchNo(), ctx.channelMchNo(), ctx.capability(), ctx.channelAppId()); WechatAuthResult data = wechatMpAuthService.getTokenAndOpenId( param.getAuthCode(), app.wxAppId(), app.appSecret());