Conversation
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
🤖 Augment PR SummarySummary: 本 PR 为微信支付多商户场景补充“直接按参数取配置”的能力,绕开基于 ThreadLocal 的上下文限制。 Changes:
Why: 解决异步/线程池下 ThreadLocal 丢失导致无法可靠获取多商户配置的问题,并在保持原有切换接口可用的前提下提供更直接的访问方式。 🤖 Was this summary useful? React with 👍 or 👎 |
There was a problem hiding this comment.
Pull request overview
本 PR 为 WxPayService 添加了两个直接获取配置的新方法,解决了多商户管理场景下 ThreadLocal 的限制问题。新增方法不依赖线程上下文,适合在异步、线程池等环境中使用。
Changes:
- 在
WxPayService接口中新增getConfig(String mchId, String appId)和getConfig(String mchId)两个重载方法 - 在
BaseWxPayServiceImpl中实现这两个方法,直接从ConcurrentHashMap读取配置 - 添加 8 个测试用例全面覆盖新功能的各种场景(正常、边界、异常情况)
- 更新
MULTI_APPID_USAGE.md文档,补充使用场景和最佳实践说明
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java |
在接口中添加两个新的 getConfig 重载方法,包含完整的 Javadoc 文档 |
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java |
实现新增的两个 getConfig 方法,包含参数验证、精确匹配和前缀匹配逻辑 |
weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java |
新增 8 个测试方法,覆盖新功能的各种使用场景和边界条件 |
weixin-java-pay/MULTI_APPID_USAGE.md |
更新文档,新增使用方式说明、场景示例和方法对比表 |
| public WxPayConfig getConfig(String mchId) { | ||
| if (StringUtils.isBlank(mchId)) { | ||
| log.warn("商户号mchId不能为空"); | ||
| return null; | ||
| } | ||
|
|
||
| // 先尝试精确匹配(针对只有mchId没有appId的配置) | ||
| if (this.configMap.containsKey(mchId)) { | ||
| return this.configMap.get(mchId); | ||
| } | ||
|
|
||
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) { | ||
| if (entry.getKey().startsWith(prefix)) { | ||
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey()); | ||
| return entry.getValue(); | ||
| } | ||
| } | ||
|
|
||
| log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
getConfig(String mchId) 方法的行为存在不确定性问题。文档中提到"当存在多个匹配项时返回的配置是不可预测的",但实际实现总是返回第一个匹配的配置(取决于 HashMap 的遍历顺序)。
这种不确定性可能导致:
- 不同环境下返回不同的配置
- 相同代码在不同时间执行可能得到不同结果
- 难以调试和定位问题
建议:
- 在文档中更明确地说明这是基于内部存储顺序的,不应依赖
- 或者考虑使用确定性的排序(如字典序)来返回最小的 key 对应的配置
- 在日志中输出实际返回的配置 key,帮助调试
| // 或者在不确定 appId 的情况下 | ||
| public void processRefund(String mchId, String outTradeNo) { | ||
| // 获取该商户号的任意一个配置 | ||
| WxPayConfig config = payService.getConfig(mchId); | ||
|
|
||
| if (config == null) { | ||
| log.error("找不到商户配置:mchId={}", mchId); | ||
| return; | ||
| } | ||
|
|
||
| // 先切换到该配置,然后进行退款 | ||
| payService.switchover(mchId, config.getAppId()); | ||
| // ... 执行退款操作 |
There was a problem hiding this comment.
文档示例中的使用模式存在不一致。在场景4的第二个例子(processRefund方法)中,先使用 getConfig(mchId) 获取配置,然后仍然调用 switchover 方法切换配置。
这种模式的问题:
- 既然已经获取到了配置,理论上应该可以直接使用,不需要再次切换
- 违背了新方法"不依赖 ThreadLocal"的设计初衷
- 可能会让用户困惑:到底应该直接使用 config 还是需要先切换
建议改进示例:
- 如果用户需要调用 payService 的其他方法(如 refund),应该直接使用 switchover 方法
- 或者更清楚地说明:getConfig 方法仅用于读取配置信息,如果需要执行支付操作,仍需要调用 switchover 设置 ThreadLocal
更好的示例可能是:
// 方式1:仅读取配置信息
WxPayConfig config = payService.getConfig(mchId);
String apiV3Key = config.getApiV3Key();
// 方式2:需要执行支付操作时,直接使用 switchover
payService.switchover(mchId);
payService.refund(refundRequest);| // 或者在不确定 appId 的情况下 | |
| public void processRefund(String mchId, String outTradeNo) { | |
| // 获取该商户号的任意一个配置 | |
| WxPayConfig config = payService.getConfig(mchId); | |
| if (config == null) { | |
| log.error("找不到商户配置:mchId={}", mchId); | |
| return; | |
| } | |
| // 先切换到该配置,然后进行退款 | |
| payService.switchover(mchId, config.getAppId()); | |
| // ... 执行退款操作 | |
| // 或者在不确定 appId 的情况下,仅通过商户号发起退款 | |
| public void processRefund(String mchId, String outTradeNo) { | |
| // 直接根据商户号切换(内部会选择该商户号下的一个配置) | |
| if (!payService.switchover(mchId)) { | |
| log.error("商户配置切换失败:mchId={}", mchId); | |
| return; | |
| } | |
| // 在完成上下文切换后,执行退款操作 | |
| // ... 执行退款操作,如: | |
| // WxPayRefundRequest request = new WxPayRefundRequest(); | |
| // request.setOutTradeNo(outTradeNo); | |
| // payService.refund(request); |
| for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) { | ||
| if (entry.getKey().startsWith(prefix)) { | ||
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey()); | ||
| return entry.getValue(); | ||
| } | ||
| } | ||
|
|
||
| log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId); | ||
| return null; |
There was a problem hiding this comment.
getConfig(String mchId) 方法在处理大量配置时可能存在性能问题。当前实现使用 for 循环遍历所有 configMap 条目来查找前缀匹配的配置,时间复杂度为 O(n)。
建议优化:
- 可以考虑使用 Stream API 的 findFirst() 来提前结束遍历
- 或者维护一个从 mchId 到 configKey 的索引映射来实现 O(1) 查找
例如使用 Stream:
return this.configMap.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(prefix))
.findFirst()
.map(entry -> {
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
return entry.getValue();
})
.orElseGet(() -> {
log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
return null;
});| for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) { | |
| if (entry.getKey().startsWith(prefix)) { | |
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey()); | |
| return entry.getValue(); | |
| } | |
| } | |
| log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId); | |
| return null; | |
| return this.configMap.entrySet().stream() | |
| .filter(entry -> entry.getKey().startsWith(prefix)) | |
| .findFirst() | |
| .map(entry -> { | |
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey()); | |
| return entry.getValue(); | |
| }) | |
| .orElseGet(() -> { | |
| log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId); | |
| return null; | |
| }); |
当前
WxPayConfigHolder基于ThreadLocal实现,在异步、线程池等场景下配置会丢失,对多商户管理不友好。新增方法
WxPayConfig getConfig(String mchId, String appId)- 直接精确获取配置WxPayConfig getConfig(String mchId)- 根据商户号获取配置两个新方法均不依赖
ThreadLocal,可在任意上下文中使用。使用示例
变更内容
WxPayService中添加两个方法重载BaseWxPayServiceImpl中实现,直接从ConcurrentHashMap读取MultiAppIdSwitchoverTest中添加 8 个测试用例覆盖新功能MULTI_APPID_USAGE.md,补充使用场景和最佳实践所有原有方法和行为保持不变,完全向后兼容。
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.