mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 22:45:58 +08:00
fix(wechat): 服务商密钥缺失fail-fast校验 + 子商户应用解析只读显式配置
This commit is contained in:
@@ -27,7 +27,7 @@ import java.util.Optional;
|
||||
/// - sub_appid ← [WechatIsvMchApp.wxAppId] (子商户应用AppId, 可选; 未配置留空)
|
||||
///
|
||||
/// 服务商应用解析(委托 [WechatIsvAppCapabilityService.resolveApp]): 显式配置 > appType自动推导 > 首个兜底。
|
||||
/// 子商户应用解析(委托 [WechatIsvMchAppCapabilityService.resolveApp]): 同优先级, 未命中返回 empty(sub_appid 留空)。
|
||||
/// 子商户应用解析(委托 [WechatIsvMchAppCapabilityService.resolveApp]): 仅显式配置, 未配置返回 empty(sub_appid 留空)。
|
||||
///
|
||||
/// 供支付策略([cn.daxpay.open.channel.wechat.strategy.isv.*])组装通道调用凭证。
|
||||
@Slf4j
|
||||
@@ -47,8 +47,8 @@ public class WechatIsvConfigAssembler {
|
||||
/// @param capability 支付能力编码(用于解析应用)
|
||||
/// @return 微信 SDK 凭证(服务商模式, subMchId/subAppId 已填充)
|
||||
public WechatSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
|
||||
// 服务商密钥(全局唯一, 含 sp_mchid 与证书)
|
||||
WechatIsvKeyConfig keyConfig = wechatIsvKeyConfigService.findByProduct(ProductEnum.WECHAT_ISV.getCode());
|
||||
// 服务商密钥(全局唯一, 含 sp_mchid 与证书; 缺失或关键字段为空时 fail-fast)
|
||||
WechatIsvKeyConfig keyConfig = wechatIsvKeyConfigService.getByProductForPay(ProductEnum.WECHAT_ISV.getCode());
|
||||
// 特约商户绑定(取 sub_mchid)
|
||||
WechatIsvChannelMerchant channelMerchant = wechatIsvChannelMerchantManager.findByChannelMchNo(channelMchNo)
|
||||
// 微信: 通道商户配置不存在
|
||||
|
||||
@@ -4,6 +4,8 @@ import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvKeyConfigParam;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -34,6 +36,21 @@ public class WechatIsvKeyConfigService {
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 支付场景查询服务商密钥(必填校验, 不创建记录)
|
||||
///
|
||||
/// 与 [findByProduct] 的 upsert 语义不同, 此方法只读不写:
|
||||
/// 记录不存在或关键字段(wxMchId/apiKeyV3/privateKey/certSerialNo)任一为空时 fail-fast,
|
||||
/// 避免空凭证下发到子应用后子应用才发现问题。
|
||||
public WechatIsvKeyConfig getByProductForPay(String product) {
|
||||
WechatIsvKeyConfig config = wechatIsvKeyConfigManager.findByProduct(product)
|
||||
.orElseThrow(() -> new BizInfoException("error.channel.wechat.isvKeyNotConfigured"));
|
||||
if (StrUtil.hasBlank(config.getWxMchId(), config.getApiKeyV3(),
|
||||
config.getPrivateKey(), config.getCertSerialNo())) {
|
||||
throw new BizInfoException("error.channel.wechat.isvKeyNotConfigured");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 保存服务商密钥配置
|
||||
/// 注意: 微信服务商模式不支持沙箱环境
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package cn.daxpay.open.channel.wechat.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.code.WechatAppTypeCode;
|
||||
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppCapabilityManager;
|
||||
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppManager;
|
||||
@@ -37,8 +36,8 @@ import java.util.stream.Collectors;
|
||||
/// 由调用方(未来的微信服务商支付策略/配置组装器)回退到全局服务商应用配置
|
||||
/// ([cn.daxpay.open.channel.wechat.service.isv.WechatIsvAppCapabilityService])。
|
||||
///
|
||||
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底,
|
||||
/// 三者均未命中返回 empty(语义为"使用全局服务商配置")。
|
||||
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:仅读取显式配置, 未配置返回 empty
|
||||
/// (语义为"该能力未在子商户维度配置, sub_appid 留空, 走纯服务商应用模式")。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -109,33 +108,22 @@ public class WechatIsvMchAppCapabilityService {
|
||||
capabilityManager.deleteByWechatIsvMchAppId(wechatIsvMchAppId);
|
||||
}
|
||||
|
||||
/// 支付时解析当前支付能力对应的子商户应用:显式配置 > appType自动推导 > 通道商户首个兜底
|
||||
/// 支付时解析当前支付能力对应的子商户应用(仅读取显式配置)
|
||||
///
|
||||
/// 三者均未命中返回 empty,语义为"该能力未在子商户维度配置,请回退到全局服务商应用配置"。
|
||||
/// 调用方(未来的微信服务商配置组装器)应在此基础上回退 [WechatIsvAppCapabilityService.resolveApp]。
|
||||
/// 子商户应用(sub_appid)为可选项, 不做 appType 推导与首个兜底, 避免取到不相关的子应用
|
||||
/// 导致 openid 体系不匹配。未配置返回 empty, 由调用方留空 sub_appid, 走纯服务商应用模式。
|
||||
///
|
||||
/// @param channelMchNo 通道商户号(服务商特约商户)
|
||||
/// @param capability 支付能力编码
|
||||
/// @return 命中的子商户应用;未命中返回 empty(应回退全局服务商配置)
|
||||
/// @param capability 支付能力编码
|
||||
/// @return 命中的子商户应用;未配置返回 empty(sub_appid 留空)
|
||||
public Optional<WechatIsvMchApp> resolveApp(String channelMchNo, String capability) {
|
||||
if (StrUtil.hasBlank(channelMchNo, capability)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 1. 显式配置优先
|
||||
var rel = capabilityManager.findOne(channelMchNo, capability);
|
||||
if (rel.isPresent()) {
|
||||
return wechatIsvMchAppManager.findById(rel.get().getWechatIsvMchAppId());
|
||||
}
|
||||
// 2. 未配置则按能力 → appType 自动推导
|
||||
String appType = WechatAppTypeCode.resolveAppType(capability);
|
||||
if (appType != null) {
|
||||
Optional<WechatIsvMchApp> byType = wechatIsvMchAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType);
|
||||
if (byType.isPresent()) {
|
||||
return byType;
|
||||
}
|
||||
}
|
||||
// 3. 最终兜底:按通道商户号取首个子商户应用;仍未命中返回 empty(回退全局)
|
||||
return wechatIsvMchAppManager.findFirstByChannelMchNo(channelMchNo);
|
||||
// 子商户应用(sub_appid)为可选项, 仅读取显式配置, 未配置则返回 empty
|
||||
// (sub_appid 留空时走纯服务商应用 sp_appid + sub_mchid 模式)
|
||||
return capabilityManager.findOne(channelMchNo, capability)
|
||||
.flatMap(rel -> wechatIsvMchAppManager.findById(rel.getWechatIsvMchAppId()));
|
||||
}
|
||||
|
||||
/// 查询微信服务商产品支持的支付能力候选列表(含国际化名称)
|
||||
|
||||
@@ -49,5 +49,6 @@
|
||||
"unsupportedPayMethod": "Unsupported WeChat payment method: {0}",
|
||||
"accessTokenLockFailed": "Failed to get AccessToken: cannot acquire distributed lock",
|
||||
"refreshAccessTokenFailed": "Failed to refresh AccessToken: {0}",
|
||||
"refreshAccessTokenEmpty": "Failed to refresh AccessToken: returned token is empty"
|
||||
"refreshAccessTokenEmpty": "Failed to refresh AccessToken: returned token is empty",
|
||||
"isvKeyNotConfigured": "WeChat ISV key is not configured or incomplete, please configure the ISV key first"
|
||||
}
|
||||
@@ -49,5 +49,6 @@
|
||||
"unsupportedPayMethod": "暂不支持的微信支付方式: {0}",
|
||||
"accessTokenLockFailed": "获取AccessToken失败:无法获取分布式锁",
|
||||
"refreshAccessTokenFailed": "刷新AccessToken失败: {0}",
|
||||
"refreshAccessTokenEmpty": "刷新AccessToken失败:返回的Token为空"
|
||||
"refreshAccessTokenEmpty": "刷新AccessToken失败:返回的Token为空",
|
||||
"isvKeyNotConfigured": "微信服务商密钥未配置或不完整,请先完成服务商密钥配置"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user