-
-
Notifications
You must be signed in to change notification settings - Fork 9k
支持一个商户号配置多个小程序appId #3849
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?
支持一个商户号配置多个小程序appId #3849
Conversation
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
🤖 Augment PR Summary总结:本 PR 为 wx-pay 多配置场景补充“仅按商户号(mchId)切换配置”的能力,以支持同一商户号绑定多个小程序 appId 的业务。 主要变更:
技术要点:切换仍基于 🤖 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.
|
|
||
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (String key : this.configMap.keySet()) { |
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 mchId 商户标识 | ||
| * @return 切换是否成功 boolean | ||
| */ | ||
| boolean switchover(String mchId); |
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.
| boolean success = payService.switchover(testMchId, testAppId1); | ||
| System.out.println("切换结果: " + success); | ||
| System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey()); | ||
| assert success : "切换应该成功"; |
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
这个 Pull Request 为 weixin-java-pay 模块添加了支持一个商户号配置多个小程序 appId 的功能。该功能解决了当前只能通过 mchId + appId 组合切换配置的限制,新增了仅使用商户号进行切换的 API。
Changes:
- 在
WxPayService接口中新增了两个重载方法:switchover(String mchId)和switchoverTo(String mchId) - 在
BaseWxPayServiceImpl中实现了这两个方法,使用精确匹配和前缀匹配策略查找配置 - 添加了完整的单元测试覆盖新功能的各种场景
- 提供了详细的使用文档,说明配置方式、使用场景和注意事项
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java | 新增两个仅使用商户号切换配置的接口方法 |
| weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java | 实现新增的两个方法,使用精确匹配和前缀匹配逻辑查找配置 |
| weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java | 添加全面的单元测试,覆盖新功能的各种使用场景 |
| weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java | 提供手动测试类,用于验证功能的正确性 |
| weixin-java-pay/MULTI_APPID_USAGE.md | 新增详细的使用文档,包含配置示例、使用场景和最佳实践 |
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (String key : this.configMap.keySet()) { | ||
| if (key.startsWith(prefix)) { | ||
| WxPayConfigHolder.set(key); | ||
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key); | ||
| return true; | ||
| } |
Copilot
AI
Jan 14, 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.
当同一商户号配置了多个 appId 时,前缀匹配会返回 configMap.keySet() 中遇到的第一个匹配项。由于 HashMap 的迭代顺序不确定,这会导致非确定性行为,每次调用可能返回不同的配置。这种不可预测性可能在生产环境中引起问题。建议在文档或方法注释中明确说明这一行为,或者考虑使用确定的排序方式(如字典序)来选择配置,以保证行为的可预测性。
| **注意**:当使用仅商户号切换时,会按照以下逻辑查找配置: | ||
| 1. 先尝试精确匹配商户号(针对只配置商户号、没有 appId 的情况) | ||
| 2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置) | ||
| 3. 如果有多个匹配项,返回找到的第一个 |
Copilot
AI
Jan 14, 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.
文档在描述前缀匹配逻辑时(第 79 行)提到"如果有多个匹配项,返回找到的第一个",但没有明确说明"第一个"的定义标准。由于 HashMap 的迭代顺序不确定,这种表述可能会给用户造成误导,让他们认为返回的配置是可预测的。建议在此处明确说明返回的配置是不确定的,或者明确指出应该避免这种使用场景,推荐使用精确匹配。
| 3. 如果有多个匹配项,返回找到的第一个 | |
| 3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId) |
| /** | ||
| * 仅根据商户号进行切换. | ||
| * 适用于一个商户号对应多个appId的场景,切换时会匹配第一个符合该商户号的配置. | ||
| * | ||
| * @param mchId 商户标识 | ||
| * @return 切换是否成功 boolean | ||
| */ | ||
| boolean switchover(String mchId); |
Copilot
AI
Jan 14, 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.
方法的 Javadoc 注释中提到"切换时会匹配第一个符合该商户号的配置",但没有说明在找不到配置时的行为。建议补充说明:当找不到匹配的配置时,方法返回 false。另外,"第一个"的定义不够明确,建议说明这是基于 HashMap 的迭代顺序,因此是不确定的。
| private String testMchId = "1234567890"; | ||
| private String testAppId1 = "wx1111111111111111"; | ||
| private String testAppId2 = "wx2222222222222222"; | ||
| private String testAppId3 = "wx3333333333333333"; |
Copilot
AI
Jan 14, 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.
测试类中的字段 testMchId、testAppId1、testAppId2、testAppId3 应该声明为 final,因为它们在 @BeforeMethod 中被赋值后不再改变。使用 final 修饰符可以提高代码的不可变性和线程安全性。
| private String testMchId = "1234567890"; | |
| private String testAppId1 = "wx1111111111111111"; | |
| private String testAppId2 = "wx2222222222222222"; | |
| private String testAppId3 = "wx3333333333333333"; | |
| private final String testMchId = "1234567890"; | |
| private final String testAppId1 = "wx1111111111111111"; | |
| private final String testAppId2 = "wx2222222222222222"; | |
| private final String testAppId3 = "wx3333333333333333"; |
| public class MultiAppIdSwitchoverTest { | ||
|
|
||
| private WxPayService payService; | ||
| private String testMchId = "1234567890"; | ||
| private String testAppId1 = "wx1111111111111111"; | ||
| private String testAppId2 = "wx2222222222222222"; | ||
| private String testAppId3 = "wx3333333333333333"; | ||
|
|
||
| @BeforeMethod | ||
| public void setup() { | ||
| payService = new WxPayServiceImpl(); | ||
|
|
||
| // 配置同一个商户号,三个不同的appId | ||
| WxPayConfig config1 = new WxPayConfig(); | ||
| config1.setMchId(testMchId); | ||
| config1.setAppId(testAppId1); | ||
| config1.setMchKey("test_key_1"); | ||
|
|
||
| WxPayConfig config2 = new WxPayConfig(); | ||
| config2.setMchId(testMchId); | ||
| config2.setAppId(testAppId2); | ||
| config2.setMchKey("test_key_2"); | ||
|
|
||
| WxPayConfig config3 = new WxPayConfig(); | ||
| config3.setMchId(testMchId); | ||
| config3.setAppId(testAppId3); | ||
| config3.setMchKey("test_key_3"); | ||
|
|
||
| Map<String, WxPayConfig> configMap = new HashMap<>(); | ||
| configMap.put(testMchId + "_" + testAppId1, config1); | ||
| configMap.put(testMchId + "_" + testAppId2, config2); | ||
| configMap.put(testMchId + "_" + testAppId3, config3); | ||
|
|
||
| payService.setMultiConfig(configMap); | ||
| } | ||
|
|
||
| /** | ||
| * 测试使用 mchId + appId 精确切换(原有功能,确保向后兼容) | ||
| */ | ||
| @Test | ||
| public void testSwitchoverWithMchIdAndAppId() { | ||
| // 切换到第一个配置 | ||
| boolean success = payService.switchover(testMchId, testAppId1); | ||
| assertTrue(success); | ||
| assertEquals(payService.getConfig().getAppId(), testAppId1); | ||
| assertEquals(payService.getConfig().getMchKey(), "test_key_1"); | ||
|
|
||
| // 切换到第二个配置 | ||
| success = payService.switchover(testMchId, testAppId2); | ||
| assertTrue(success); | ||
| assertEquals(payService.getConfig().getAppId(), testAppId2); | ||
| assertEquals(payService.getConfig().getMchKey(), "test_key_2"); | ||
|
|
||
| // 切换到第三个配置 | ||
| success = payService.switchover(testMchId, testAppId3); | ||
| assertTrue(success); | ||
| assertEquals(payService.getConfig().getAppId(), testAppId3); | ||
| assertEquals(payService.getConfig().getMchKey(), "test_key_3"); | ||
| } | ||
|
|
||
| /** | ||
| * 测试仅使用 mchId 切换(新功能) | ||
| * 应该能够成功切换到该商户号的某个配置 | ||
| */ | ||
| @Test | ||
| public void testSwitchoverWithMchIdOnly() { | ||
| // 仅使用商户号切换,应该能够成功切换到该商户号的某个配置 | ||
| boolean success = payService.switchover(testMchId); | ||
| assertTrue(success, "应该能够通过mchId切换配置"); | ||
|
|
||
| // 验证配置确实是该商户号的配置之一 | ||
| WxPayConfig currentConfig = payService.getConfig(); | ||
| assertNotNull(currentConfig); | ||
| assertEquals(currentConfig.getMchId(), testMchId); | ||
|
|
||
| // appId应该是三个中的一个 | ||
| String currentAppId = currentConfig.getAppId(); | ||
| assertTrue( | ||
| testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId), | ||
| "当前appId应该是配置的appId之一" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 测试 switchoverTo 方法(带链式调用,使用 mchId + appId) | ||
| */ | ||
| @Test | ||
| public void testSwitchoverToWithMchIdAndAppId() { | ||
| WxPayService result = payService.switchoverTo(testMchId, testAppId2); | ||
| assertNotNull(result); | ||
| assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用"); | ||
| assertEquals(payService.getConfig().getAppId(), testAppId2); | ||
| } | ||
|
|
||
| /** | ||
| * 测试 switchoverTo 方法(带链式调用,仅使用 mchId) | ||
| */ | ||
| @Test | ||
| public void testSwitchoverToWithMchIdOnly() { | ||
| WxPayService result = payService.switchoverTo(testMchId); | ||
| assertNotNull(result); | ||
| assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用"); | ||
| assertEquals(payService.getConfig().getMchId(), testMchId); | ||
| } | ||
|
|
||
| /** | ||
| * 测试切换到不存在的商户号 | ||
| */ | ||
| @Test | ||
| public void testSwitchoverToNonexistentMchId() { | ||
| boolean success = payService.switchover("nonexistent_mch_id"); | ||
| assertFalse(success, "切换到不存在的商户号应该失败"); | ||
| } | ||
|
|
||
| /** | ||
| * 测试 switchoverTo 切换到不存在的商户号(应该抛出异常) | ||
| */ | ||
| @Test(expectedExceptions = WxRuntimeException.class) | ||
| public void testSwitchoverToNonexistentMchIdThrowsException() { | ||
| payService.switchoverTo("nonexistent_mch_id"); | ||
| } | ||
|
|
||
| /** | ||
| * 测试切换到不存在的 mchId + appId 组合 | ||
| */ | ||
| @Test | ||
| public void testSwitchoverToNonexistentAppId() { | ||
| boolean success = payService.switchover(testMchId, "wx9999999999999999"); | ||
| assertFalse(success, "切换到不存在的appId应该失败"); | ||
| } | ||
|
|
||
| /** | ||
| * 测试添加配置后能够正常切换 | ||
| */ | ||
| @Test | ||
| public void testAddConfigAndSwitchover() { | ||
| String newAppId = "wx4444444444444444"; | ||
|
|
||
| // 动态添加一个新的配置 | ||
| WxPayConfig newConfig = new WxPayConfig(); | ||
| newConfig.setMchId(testMchId); | ||
| newConfig.setAppId(newAppId); | ||
| newConfig.setMchKey("test_key_4"); | ||
|
|
||
| payService.addConfig(testMchId, newAppId, newConfig); | ||
|
|
||
| // 切换到新添加的配置 | ||
| boolean success = payService.switchover(testMchId, newAppId); | ||
| assertTrue(success); | ||
| assertEquals(payService.getConfig().getAppId(), newAppId); | ||
| assertEquals(payService.getConfig().getMchKey(), "test_key_4"); | ||
|
|
||
| // 使用仅mchId切换也应该能够找到配置 | ||
| success = payService.switchover(testMchId); | ||
| assertTrue(success); | ||
| assertEquals(payService.getConfig().getMchId(), testMchId); | ||
| } | ||
|
|
||
| /** | ||
| * 测试移除配置后切换 | ||
| */ | ||
| @Test | ||
| public void testRemoveConfigAndSwitchover() { | ||
| // 移除一个配置 | ||
| payService.removeConfig(testMchId, testAppId1); | ||
|
|
||
| // 切换到已移除的配置应该失败 | ||
| boolean success = payService.switchover(testMchId, testAppId1); | ||
| assertFalse(success); | ||
|
|
||
| // 但仍然能够切换到其他配置 | ||
| success = payService.switchover(testMchId, testAppId2); | ||
| assertTrue(success); | ||
|
|
||
| // 使用仅mchId切换应该仍然有效(因为还有其他appId的配置) | ||
| success = payService.switchover(testMchId); | ||
| assertTrue(success); | ||
| } | ||
|
|
||
| /** | ||
| * 测试单个配置的场景(确保向后兼容) | ||
| */ | ||
| @Test | ||
| public void testSingleConfig() { | ||
| WxPayService singlePayService = new WxPayServiceImpl(); | ||
| WxPayConfig singleConfig = new WxPayConfig(); | ||
| singleConfig.setMchId("single_mch_id"); | ||
| singleConfig.setAppId("single_app_id"); | ||
| singleConfig.setMchKey("single_key"); | ||
|
|
||
| singlePayService.setConfig(singleConfig); | ||
|
|
||
| // 直接获取配置应该成功 | ||
| assertEquals(singlePayService.getConfig().getMchId(), "single_mch_id"); | ||
| assertEquals(singlePayService.getConfig().getAppId(), "single_app_id"); | ||
|
|
||
| // 使用精确匹配切换 | ||
| boolean success = singlePayService.switchover("single_mch_id", "single_app_id"); | ||
| assertTrue(success); | ||
|
|
||
| // 使用仅mchId切换 | ||
| success = singlePayService.switchover("single_mch_id"); | ||
| assertTrue(success); | ||
| } | ||
| } |
Copilot
AI
Jan 14, 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.
测试用例缺少对边界情况的覆盖。建议增加以下测试场景:
- 调用
switchover(null)或switchover("")时的行为验证 - 调用
switchoverTo(null)或switchoverTo("")时的异常处理验证 - 测试商户号存在包含关系的场景(如同时配置 "123" 和 "1234"),验证前缀匹配是否会错误匹配
这些测试对于确保 API 的健壮性和正确性非常重要。
|
|
||
| ## 解决方案 | ||
|
|
||
| 从 4.8.1 版本开始,WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。 |
Copilot
AI
Jan 14, 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.8.1 版本开始支持,但需要确认这个版本号是否正确。建议验证当前项目的版本号,并确保文档中的版本号与实际发布版本一致。如果这是待发布的版本,建议在 PR 合并前再次确认版本号。
| 从 4.8.1 版本开始,WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。 | |
| WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。 |
| /** | ||
| * 仅根据商户号进行切换. | ||
| * 适用于一个商户号对应多个appId的场景,切换时会匹配第一个符合该商户号的配置. | ||
| * | ||
| * @param mchId 商户标识 | ||
| * @return 切换成功 ,则返回当前对象,方便链式调用,否则抛出异常 | ||
| */ | ||
| WxPayService switchoverTo(String mchId); |
Copilot
AI
Jan 14, 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.
方法的 Javadoc 注释不够完整。建议补充以下信息:
- 当找不到匹配配置时,会抛出 WxRuntimeException 异常
- "第一个"的定义不明确,建议说明这是基于 HashMap 的迭代顺序
- 建议添加 @throws 标签说明可能抛出的异常类型和情况
| public boolean switchover(String mchId) { | ||
| // 先尝试精确匹配(针对只有mchId没有appId的配置) | ||
| if (this.configMap.containsKey(mchId)) { | ||
| WxPayConfigHolder.set(mchId); | ||
| return true; | ||
| } | ||
|
|
||
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (String key : this.configMap.keySet()) { | ||
| if (key.startsWith(prefix)) { | ||
| WxPayConfigHolder.set(key); | ||
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key); | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| log.error("无法找到对应mchId=【{}】的商户号配置信息,请核实!", mchId); | ||
| return false; | ||
| } |
Copilot
AI
Jan 14, 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.
新增的 switchover(String mchId) 方法存在输入参数校验缺失的问题。当 mchId 参数为 null 或空字符串时,会导致 NullPointerException 或产生无意义的前缀匹配(如 "_")。建议在方法开始处添加参数校验,例如使用 StringUtils.isBlank(mchId) 进行检查,如果为空则直接返回 false 或抛出适当的异常。
| public WxPayService switchoverTo(String mchId) { | ||
| // 先尝试精确匹配(针对只有mchId没有appId的配置) | ||
| if (this.configMap.containsKey(mchId)) { | ||
| WxPayConfigHolder.set(mchId); | ||
| return this; | ||
| } | ||
|
|
||
| // 尝试前缀匹配(查找以 mchId_ 开头的配置) | ||
| String prefix = mchId + "_"; | ||
| for (String key : this.configMap.keySet()) { | ||
| if (key.startsWith(prefix)) { | ||
| WxPayConfigHolder.set(key); | ||
| log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key); | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】的商户号配置信息,请核实!", mchId)); | ||
| } |
Copilot
AI
Jan 14, 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.
新增的 switchoverTo(String mchId) 方法同样缺少输入参数校验。当 mchId 参数为 null 或空字符串时会导致运行时错误。建议在方法开始处添加参数校验,如果参数无效则抛出明确的异常信息。
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.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
问题
当前 wx-pay 使用
mchId + "_" + appId作为配置唯一标识,切换时必须同时指定商户号和appId。一个商户号绑定多个小程序时,无法仅通过商户号切换配置。变更
API 扩展
在
WxPayService中新增仅使用商户号切换的重载方法:实现逻辑
在
BaseWxPayServiceImpl中实现智能查找:mchId_*使用示例
向后兼容
文档
新增
weixin-java-pay/MULTI_APPID_USAGE.md,包含配置示例、使用场景和最佳实践。Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.