diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java index 0c35b0da1..6869ebc95 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java @@ -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) // 微信: 通道商户配置不存在 diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvKeyConfigService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvKeyConfigService.java index e993ed76a..dda7c96a0 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvKeyConfigService.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvKeyConfigService.java @@ -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) diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvMchAppCapabilityService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvMchAppCapabilityService.java index e463a7886..b99adf0e6 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvMchAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvMchAppCapabilityService.java @@ -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 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 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())); } /// 查询微信服务商产品支持的支付能力候选列表(含国际化名称) diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/wechat.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/wechat.json index 892dc701b..ae70bcc64 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/wechat.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/wechat.json @@ -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" } \ No newline at end of file diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/wechat.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/wechat.json index f679d8420..12ef273d3 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/wechat.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/wechat.json @@ -49,5 +49,6 @@ "unsupportedPayMethod": "暂不支持的微信支付方式: {0}", "accessTokenLockFailed": "获取AccessToken失败:无法获取分布式锁", "refreshAccessTokenFailed": "刷新AccessToken失败: {0}", - "refreshAccessTokenEmpty": "刷新AccessToken失败:返回的Token为空" + "refreshAccessTokenEmpty": "刷新AccessToken失败:返回的Token为空", + "isvKeyNotConfigured": "微信服务商密钥未配置或不完整,请先完成服务商密钥配置" }