From 8512374940a5390f56d2e93fd6885fe001f86fee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 03:48:53 +0000 Subject: [PATCH 1/2] Initial plan From 742e757f1d7d8f30e2877178fd1d2258e15985b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 03:54:42 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=85=8D=E7=BD=AE=E7=9A=84=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=A4=9A=E5=95=86=E6=88=B7=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- weixin-java-pay/MULTI_APPID_USAGE.md | 109 +++++++++++++++++- .../wxpay/service/WxPayService.java | 22 ++++ .../service/impl/BaseWxPayServiceImpl.java | 39 +++++++ .../impl/MultiAppIdSwitchoverTest.java | 107 +++++++++++++++++ 4 files changed, 271 insertions(+), 6 deletions(-) diff --git a/weixin-java-pay/MULTI_APPID_USAGE.md b/weixin-java-pay/MULTI_APPID_USAGE.md index e4a7d0b9e..cc71fa7e7 100644 --- a/weixin-java-pay/MULTI_APPID_USAGE.md +++ b/weixin-java-pay/MULTI_APPID_USAGE.md @@ -53,7 +53,50 @@ configMap.put(mchId + "_" + config3.getAppId(), config3); payService.setMultiConfig(configMap); ``` -### 2. 切换配置的方式 +### 2. 获取配置的方式 + +#### 方式一:直接获取配置(推荐,新功能) + +直接通过商户号和 appId 获取配置,**不依赖 ThreadLocal**,适用于多商户管理场景: + +```java +// 精确获取指定商户号和 appId 的配置 +WxPayConfig config1 = payService.getConfig("1234567890", "wx1111111111111111"); + +// 仅使用商户号获取配置(会返回该商户号的任意一个配置) +WxPayConfig config = payService.getConfig("1234567890"); + +// 使用获取的配置进行支付操作 +if (config != null) { + String appId = config.getAppId(); + String mchKey = config.getMchKey(); + // ... 使用配置信息 +} +``` + +**优势**: +- 不依赖 ThreadLocal,可以在任何上下文中使用 +- 适合在异步场景、线程池等环境中使用 +- 线程安全,不会因为线程切换导致配置丢失 +- 可以同时获取多个不同的配置 + +#### 方式二:切换配置后使用(原有方式) + +通过切换配置,然后调用 `getConfig()` 获取当前配置: + +```java +// 精确切换到指定的配置 +payService.switchover("1234567890", "wx1111111111111111"); +WxPayConfig config = payService.getConfig(); // 获取当前切换的配置 + +// 仅使用商户号切换 +payService.switchover("1234567890"); +config = payService.getConfig(); // 获取切换后的配置 +``` + +**注意**:此方式依赖 ThreadLocal,需要注意线程上下文的问题。 + +### 3. 切换配置的方式 #### 方式一:精确切换(原有方式,向后兼容) @@ -92,7 +135,7 @@ WxPayUnifiedOrderResult result = payService .unifiedOrder(request); ``` -### 3. 动态添加配置 +### 4. 动态添加配置 ```java // 运行时动态添加新的 appId 配置 @@ -107,7 +150,7 @@ payService.addConfig("1234567890", "wx4444444444444444", newConfig); payService.switchover("1234567890", "wx4444444444444444"); ``` -### 4. 移除配置 +### 5. 移除配置 ```java // 移除特定的 appId 配置 @@ -174,24 +217,78 @@ WxPayRefundRequest refundRequest = new WxPayRefundRequest(); WxPayRefundResult refundResult = payService.refund(refundRequest); ``` +### 场景4:多商户管理(推荐使用直接获取配置) + +```java +// 在多商户管理系统中,可以直接获取指定商户的配置 +// 这种方式不依赖 ThreadLocal,适合异步场景和线程池环境 + +public void processMerchantOrder(String mchId, String appId, Order order) { + // 直接获取配置,无需切换 + WxPayConfig config = payService.getConfig(mchId, appId); + + if (config == null) { + log.error("找不到商户配置:mchId={}, appId={}", mchId, appId); + return; + } + + // 使用配置信息 + String merchantKey = config.getMchKey(); + String apiV3Key = config.getApiV3Key(); + + // ... 处理订单逻辑 +} + +// 或者在不确定 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()); + // ... 执行退款操作 +} +``` + +## 新增方法对比 + +| 方法 | 说明 | 是否依赖 ThreadLocal | 适用场景 | +|-----|------|---------------------|---------| +| `getConfig()` | 获取当前配置 | 是 | 单线程同步场景 | +| `getConfig(String mchId, String appId)` | 直接获取指定配置 | **否** | 多商户管理、异步场景、线程池 | +| `getConfig(String mchId)` | 根据商户号获取配置 | **否** | 不确定 appId 的场景 | +| `switchover(String mchId, String appId)` | 精确切换配置 | 是 | 需要切换上下文的场景 | +| `switchover(String mchId)` | 根据商户号切换 | 是 | 不关心 appId 的切换场景 | + ## 注意事项 1. **向后兼容**:所有原有的使用方式继续有效,不需要修改现有代码。 2. **配置隔离**:每个 `mchId + appId` 组合都是独立的配置,修改一个配置不会影响其他配置。 -3. **线程安全**:配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的。 +3. **线程安全**: + - 配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的 + - 直接获取配置方法(`getConfig(mchId, appId)`)不依赖 ThreadLocal,可以在任何上下文中安全使用 4. **自动切换**:在处理支付回调时,SDK 会自动根据回调中的 `mchId` 和 `appId` 切换到正确的配置。 5. **推荐实践**: - - 如果知道具体的 appId,建议使用精确切换方式,避免歧义 - - 如果使用仅商户号切换,确保该商户号下至少有一个可用的配置 + - 如果知道具体的 appId,建议使用精确切换或获取方式,避免歧义 + - 在多商户管理、异步场景、线程池等环境中,建议使用 `getConfig(mchId, appId)` 直接获取配置 + - 如果使用仅商户号切换或获取,确保该商户号下至少有一个可用的配置 ## 相关 API | 方法 | 参数 | 返回值 | 说明 | |-----|------|--------|------| +| `getConfig()` | 无 | WxPayConfig | 获取当前配置(依赖 ThreadLocal) | +| `getConfig(String mchId, String appId)` | 商户号, appId | WxPayConfig | 直接获取指定配置(不依赖 ThreadLocal) | +| `getConfig(String mchId)` | 商户号 | WxPayConfig | 根据商户号获取配置(不依赖 ThreadLocal) | | `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置 | | `switchover(String mchId)` | 商户号 | boolean | 仅使用商户号切换 | | `switchoverTo(String mchId, String appId)` | 商户号, appId | WxPayService | 精确切换,支持链式调用 | diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index 2db2987d1..81aa6bdfc 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java @@ -785,11 +785,33 @@ default WxPayService switchoverTo(String mchId) { /** * 获取配置. + * 在多商户配置场景下,会根据 WxPayConfigHolder 中的值获取对应的配置. * * @return the config */ WxPayConfig getConfig(); + /** + * 根据商户号和 appId 直接获取配置. + * 此方法不依赖 ThreadLocal,可以在任何上下文中使用,适用于多商户管理场景. + * + * @param mchId 商户号 + * @param appId 微信应用 id + * @return 对应的配置对象,如果不存在则返回 null + */ + WxPayConfig getConfig(String mchId, String appId); + + /** + * 根据商户号直接获取配置. + * 此方法不依赖 ThreadLocal,可以在任何上下文中使用. + * 适用于一个商户号对应多个 appId 的场景,会返回该商户号的任意一个配置. + * 注意:当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式. + * + * @param mchId 商户号 + * @return 对应的配置对象,如果不存在则返回 null + */ + WxPayConfig getConfig(String mchId); + /** * 设置配置对象. * diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java index 4b51c498d..c213abb71 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java @@ -154,6 +154,45 @@ public WxPayConfig getConfig() { return this.configMap.get(WxPayConfigHolder.get()); } + @Override + public WxPayConfig getConfig(String mchId, String appId) { + if (StringUtils.isBlank(mchId)) { + log.warn("商户号mchId不能为空"); + return null; + } + if (StringUtils.isBlank(appId)) { + log.warn("应用ID appId不能为空"); + return null; + } + String configKey = this.getConfigKey(mchId, appId); + return this.configMap.get(configKey); + } + + @Override + 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 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; + } + @Override public void setConfig(WxPayConfig config) { final String defaultKey = this.getConfigKey(config.getMchId(), config.getAppId()); diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java index c1c1460fe..9549bc72b 100644 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java @@ -52,6 +52,113 @@ public void setup() { payService.setMultiConfig(configMap); } + /** + * 测试直接通过 mchId 和 appId 获取配置(新功能) + */ + @Test + public void testGetConfigWithMchIdAndAppId() { + // 测试获取第一个配置 + WxPayConfig config1 = payService.getConfig(testMchId, testAppId1); + assertNotNull(config1, "应该能够获取到配置"); + assertEquals(config1.getMchId(), testMchId); + assertEquals(config1.getAppId(), testAppId1); + assertEquals(config1.getMchKey(), "test_key_1"); + + // 测试获取第二个配置 + WxPayConfig config2 = payService.getConfig(testMchId, testAppId2); + assertNotNull(config2); + assertEquals(config2.getAppId(), testAppId2); + assertEquals(config2.getMchKey(), "test_key_2"); + + // 测试获取第三个配置 + WxPayConfig config3 = payService.getConfig(testMchId, testAppId3); + assertNotNull(config3); + assertEquals(config3.getAppId(), testAppId3); + assertEquals(config3.getMchKey(), "test_key_3"); + } + + /** + * 测试直接通过 mchId 获取配置(新功能) + */ + @Test + public void testGetConfigWithMchIdOnly() { + WxPayConfig config = payService.getConfig(testMchId); + assertNotNull(config, "应该能够通过mchId获取配置"); + assertEquals(config.getMchId(), testMchId); + + // appId应该是三个中的一个 + String currentAppId = config.getAppId(); + assertTrue( + testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId), + "获取的配置的appId应该是配置的appId之一" + ); + } + + /** + * 测试 getConfig 方法不依赖 ThreadLocal + * 在不切换配置的情况下也能直接获取 + */ + @Test + public void testGetConfigWithoutSwitchover() { + // 不进行任何switchover操作,直接通过参数获取配置 + WxPayConfig config1 = payService.getConfig(testMchId, testAppId1); + WxPayConfig config2 = payService.getConfig(testMchId, testAppId2); + WxPayConfig config3 = payService.getConfig(testMchId, testAppId3); + + // 验证可以同时获取到所有配置,不受 ThreadLocal 影响 + assertNotNull(config1); + assertNotNull(config2); + assertNotNull(config3); + + assertEquals(config1.getAppId(), testAppId1); + assertEquals(config2.getAppId(), testAppId2); + assertEquals(config3.getAppId(), testAppId3); + } + + /** + * 测试 getConfig 方法处理不存在的配置 + */ + @Test + public void testGetConfigWithNonexistentConfig() { + // 测试不存在的商户号和appId组合 + WxPayConfig config = payService.getConfig("nonexistent_mch_id", "nonexistent_app_id"); + assertNull(config, "获取不存在的配置应该返回null"); + + // 测试存在商户号但不存在的appId + config = payService.getConfig(testMchId, "wx9999999999999999"); + assertNull(config, "获取不存在的appId配置应该返回null"); + } + + /** + * 测试 getConfig 方法处理空参数或null参数 + */ + @Test + public void testGetConfigWithNullOrEmptyParameters() { + // 测试 null 商户号 + WxPayConfig config = payService.getConfig(null, testAppId1); + assertNull(config, "商户号为null时应该返回null"); + + // 测试空商户号 + config = payService.getConfig("", testAppId1); + assertNull(config, "商户号为空字符串时应该返回null"); + + // 测试 null appId + config = payService.getConfig(testMchId, null); + assertNull(config, "appId为null时应该返回null"); + + // 测试空 appId + config = payService.getConfig(testMchId, ""); + assertNull(config, "appId为空字符串时应该返回null"); + + // 测试仅mchId方法的null参数 + config = payService.getConfig((String) null); + assertNull(config, "商户号为null时应该返回null"); + + // 测试仅mchId方法的空字符串 + config = payService.getConfig(""); + assertNull(config, "商户号为空字符串时应该返回null"); + } + /** * 测试使用 mchId + appId 精确切换(原有功能,确保向后兼容) */