-
-
Notifications
You must be signed in to change notification settings - Fork 9k
添加直接获取配置的方法,解决多商户管理场景下的 ThreadLocal 限制 #3863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param appId 微信应用 id | ||
| * @return 对应的配置对象,如果不存在则返回 null | ||
| */ | ||
| WxPayConfig getConfig(String mchId, String appId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
| } |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()); | ||
| // ... 执行退款操作 |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
文档示例中的使用模式存在不一致。在场景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; |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.