fix(wx): 直连商户支付应用解析禁止回退到平台档应用

根因: WxAppResolveService.resolve() 的 product 参数未参与判断, 直连商户(WECHAT_PAY)未配置商户档应用时自动兜底到平台公众号, 导致 OAuth 拿到平台维度 openId, 与直连商户号不同主体, appid/mchid 不匹配

修复: 按 product 区分, 直连商户第2级(通道能力绑定)与第3级(appType 推导兜底)均不回退平台档, resolveByChannelAppId 直连时商户表优先且不查平台表; 未配置抛 directMchAppNotConfigured。服务商(ISV)平台档兜底保留

新增 i18n key directMchAppNotConfigured(zh-CN/en-US/zh-TW/zh-HK/ja-JP/ko-KR, 东盟回退 zh-CN)
This commit is contained in:
daxpay
2026-07-30 09:11:39 +08:00
parent 2e3685d83c
commit bb5426b54a
7 changed files with 44 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ import cn.daxpay.open.payment.wx.facade.WxAppFacade;
import cn.daxpay.open.payment.wx.facade.WxAppView;
import cn.daxpay.open.payment.wx.facade.WxIsvAppPair;
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.daxpay.open.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.StrUtil;
@@ -58,11 +59,16 @@ public class WxAppResolveService implements WxAppFacade {
}
/// 解析单应用:显式 channelAppId → 通道能力绑 → appType 推导(平台应用唯一命中)
///
/// 直连产品(WECHAT_PAY)禁止回退到平台档应用: OAuth 拿到的 openId 必须与
/// 直连商户号同主体, 使用平台应用会导致 appid 与 mchid 主体不一致。
/// 仅服务商(WECHAT_ISV)及其他产品允许平台档兜底。
@Override
public WxAppView resolve(String mchNo, String channelMchNo, String capability, String channelAppId, String product) {
boolean direct = ProductEnum.WECHAT_PAY.getCode().equals(product);
// 1. 显式 channelAppId
if (StrUtil.isNotBlank(channelAppId)) {
return this.resolveByChannelAppId(mchNo, channelAppId);
return this.resolveByChannelAppId(mchNo, channelAppId, direct);
}
// 2. 通道能力绑定(同能力优先 merchant其次 platform
if (StrUtil.isNotBlank(channelMchNo) && StrUtil.isNotBlank(capability)) {
@@ -71,18 +77,27 @@ public class WxAppResolveService implements WxAppFacade {
if (merchantBind.isPresent()) {
return this.getById(AppScopeEnum.MERCHANT, merchantBind.get().getWxAppRefId());
}
var platformBind = wxChannelAppCapabilityManager.findByChannelMchNoAndCapabilityAndScope(
channelMchNo, capability, AppScopeEnum.PLATFORM.getCode());
if (platformBind.isPresent()) {
return this.getById(AppScopeEnum.PLATFORM, platformBind.get().getWxAppRefId());
// 直连商户不使用平台档应用, 跳过平台能力绑定
if (!direct) {
var platformBind = wxChannelAppCapabilityManager.findByChannelMchNoAndCapabilityAndScope(
channelMchNo, capability, AppScopeEnum.PLATFORM.getCode());
if (platformBind.isPresent()) {
return this.getById(AppScopeEnum.PLATFORM, platformBind.get().getWxAppRefId());
}
}
}
// 3. appType 推导:要求该类型平台应用唯一命中
WxAppView platformFallback = this.resolvePlatformFallback(capability);
if (platformFallback != null) {
return platformFallback;
// 3. appType 推导:要求该类型平台应用唯一命中(仅非直连)
if (!direct) {
WxAppView platformFallback = this.resolvePlatformFallback(capability);
if (platformFallback != null) {
return platformFallback;
}
}
// 微信: 直连商户未配置商户档应用 / 未配置该能力对应的应用
if (direct) {
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.payment.wx.directMchAppNotConfigured", capability);
}
// 微信: 未配置该能力对应的应用
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.payment.wx.appNotConfigured", capability);
}
@@ -136,7 +151,19 @@ public class WxAppResolveService implements WxAppFacade {
}
/// 按 channelAppId 解析单应用
private WxAppView resolveByChannelAppId(String mchNo, String channelAppId) {
///
/// @param direct 是否直连产品(直连时商户表优先且不回退平台表)
private WxAppView resolveByChannelAppId(String mchNo, String channelAppId, boolean direct) {
// 直连商户优先查商户表, 且不回退到平台表
if (direct) {
var mchApp = wxMchAppManager.findByMchNoAndWxAppId(mchNo, channelAppId);
if (mchApp.isPresent()) {
return this.toMerchantView(mchApp.get());
}
// 直连商户未配置商户档应用
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.payment.wx.directMchAppNotConfigured", channelAppId);
}
var platform = wxPlatformAppManager.findByWxAppId(channelAppId);
if (platform.isPresent()) {
return this.toPlatformView(platform.get());

View File

@@ -10,6 +10,7 @@
"appNotBound": "No WeChat app is bound",
"channelMerchantMismatch": "Channel merchant does not exist or does not match the merchant number",
"appNotConfigured": "No application configured for {0}, please bind one on the channel merchant app configuration page",
"directMchAppNotConfigured": "Direct merchant has no merchant-tier app configured for {0}. Please bind one (merchant scope) on the channel merchant app configuration page. Platform-tier apps are not allowed for direct merchants.",
"appNotUnique": "Multiple applications of type {0} exist, please explicitly configure the capability binding to select one",
"mpAuthNotConfigured": "WeChat Official Account auth config is missing, please configure AppId and AppSecret in Third-Party Platform → WeChat Official Account Auth Config"
}

View File

@@ -10,6 +10,7 @@
"appNotBound": "利用可能な微信アプリがバインドされていません",
"channelMerchantMismatch": "チャネル加盟店が存在しないか加盟店番号と一致しません",
"appNotConfigured": "{0} に対応するアプリが未設定です。チャネル加盟店アプリ設定ページでバインドしてください",
"directMchAppNotConfigured": "直連加盟店は {0} に対応する加盟店ティアアプリが未設定です。チャネル加盟店アプリ設定ページで加盟店ティアアプリをバインドしてください(直連加盟店はプラットフォームティアアプリを使用できません)",
"appNotUnique": "タイプ {0} のアプリが複数存在します。能力バインディングを明示的に設定して選択してください",
"mpAuthNotConfigured": "微信公式アカウント認証設定が未入力です。「サードパーティプラットフォーム管理 → 微信公式アカウント認証設定」で AppId と AppSecret を設定してください"
}

View File

@@ -10,6 +10,7 @@
"appNotBound": "사용 가능한 위챗 앱이 바인딩되지 않았습니다",
"channelMerchantMismatch": "채널 가맹점이 없거나 가맹점 번호와 일치하지 않습니다",
"appNotConfigured": "{0}에 해당하는 앱이 구성되지 않았습니다. 채널 가맹점 앱 구성 페이지에서 바인딩하세요",
"directMchAppNotConfigured": "직연 가맹점은 {0}에 해당하는 가맹점 티어 앱이 미구성되었습니다. 채널 가맹점 앱 구성 페이지에서 가맹점 티어 앱을 바인딩하세요(직연 가맹점은 플랫폼 티어 앱 사용 불가)",
"appNotUnique": "{0} 유형의 앱이 여러 개 존재합니다. 명시적으로 역량 바인딩을 구성하여 선택하세요",
"mpAuthNotConfigured": "위챗 공식계정 인증 설정이 누락되었습니다. 「서드파티 플랫폼 관리 → 위챗 공식계정 인증 설정」에서 AppId와 AppSecret을 설정해 주세요"
}

View File

@@ -10,6 +10,7 @@
"appNotBound": "未绑定可用的微信应用",
"channelMerchantMismatch": "通道商户不存在或与商户号不匹配",
"appNotConfigured": "未配置 {0} 对应的应用,请前往通道商户应用配置页绑定",
"directMchAppNotConfigured": "直连商户未配置 {0} 对应的商户档应用,请前往通道商户应用配置页绑定商户档应用(直连商户不可使用平台档应用)",
"appNotUnique": "存在多个 {0} 类型的应用,请显式配置能力绑定以明确选择",
"mpAuthNotConfigured": "微信公众号认证配置未填写,请在「三方平台管理 → 微信公众号认证配置」中配置 AppId 和 AppSecret"
}

View File

@@ -10,6 +10,7 @@
"appNotBound": "未綁定可用的微信應用",
"channelMerchantMismatch": "通道商戶不存在或與商戶號不匹配",
"appNotConfigured": "未設定 {0} 對應的應用,請前往通道商戶應用設定頁綁定",
"directMchAppNotConfigured": "直連商戶未設定 {0} 對應的商戶檔應用,請前往通道商戶應用設定頁綁定商戶檔應用(直連商戶不可使用平台檔應用)",
"appNotUnique": "存在多個 {0} 類型的應用,請顯式設定能力綁定以明確選擇",
"mpAuthNotConfigured": "微信公眾號認證配置未填寫,請在「三方平台管理 → 微信公眾號認證配置」中配置 AppId 和 AppSecret"
}

View File

@@ -10,6 +10,7 @@
"appNotBound": "未綁定可用的微信應用",
"channelMerchantMismatch": "通道商戶不存在或與商戶號不匹配",
"appNotConfigured": "未設定 {0} 對應的應用,請前往通道商戶應用設定頁綁定",
"directMchAppNotConfigured": "直連商戶未設定 {0} 對應的商戶檔應用,請前往通道商戶應用設定頁綁定商戶檔應用(直連商戶不可使用平台檔應用)",
"appNotUnique": "存在多個 {0} 類型的應用,請顯式設定能力綁定以明確選擇",
"mpAuthNotConfigured": "微信公眾號認證配置未填寫,請在「三方平台管理 → 微信公眾號認證配置」中配置 AppId 和 AppSecret"
}