diff --git a/weixin-java-pay/MULTI_APPID_USAGE.md b/weixin-java-pay/MULTI_APPID_USAGE.md new file mode 100644 index 000000000..e4a7d0b9e --- /dev/null +++ b/weixin-java-pay/MULTI_APPID_USAGE.md @@ -0,0 +1,200 @@ +# 支持一个商户号对应多个 appId 的使用说明 + +## 背景 + +在实际业务中,经常会遇到一个微信支付商户号需要绑定多个小程序的场景。例如: +- 一个商家有多个小程序(主店、分店、活动小程序等) +- 所有小程序共用同一个支付商户号 +- 支付配置(商户号、密钥、证书等)完全相同,只有 appId 不同 + +## 解决方案 + +WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。 + +## 使用方式 + +### 1. 配置多个 appId + +```java +WxPayService payService = new WxPayServiceImpl(); + +String mchId = "1234567890"; // 商户号 + +// 配置小程序1 +WxPayConfig config1 = new WxPayConfig(); +config1.setMchId(mchId); +config1.setAppId("wx1111111111111111"); // 小程序1的appId +config1.setMchKey("your_mch_key"); +config1.setApiV3Key("your_api_v3_key"); +// ... 其他配置 + +// 配置小程序2 +WxPayConfig config2 = new WxPayConfig(); +config2.setMchId(mchId); +config2.setAppId("wx2222222222222222"); // 小程序2的appId +config2.setMchKey("your_mch_key"); +config2.setApiV3Key("your_api_v3_key"); +// ... 其他配置 + +// 配置小程序3 +WxPayConfig config3 = new WxPayConfig(); +config3.setMchId(mchId); +config3.setAppId("wx3333333333333333"); // 小程序3的appId +config3.setMchKey("your_mch_key"); +config3.setApiV3Key("your_api_v3_key"); +// ... 其他配置 + +// 添加到配置映射 +Map configMap = new HashMap<>(); +configMap.put(mchId + "_" + config1.getAppId(), config1); +configMap.put(mchId + "_" + config2.getAppId(), config2); +configMap.put(mchId + "_" + config3.getAppId(), config3); + +payService.setMultiConfig(configMap); +``` + +### 2. 切换配置的方式 + +#### 方式一:精确切换(原有方式,向后兼容) + +```java +// 切换到小程序1的配置 +payService.switchover("1234567890", "wx1111111111111111"); + +// 切换到小程序2的配置 +payService.switchover("1234567890", "wx2222222222222222"); +``` + +#### 方式二:仅使用商户号切换(新功能) + +```java +// 仅使用商户号切换,会自动匹配该商户号的某个配置 +// 适用于不关心具体使用哪个 appId 的场景 +boolean success = payService.switchover("1234567890"); +``` + +**注意**:当使用仅商户号切换时,会按照以下逻辑查找配置: +1. 先尝试精确匹配商户号(针对只配置商户号、没有 appId 的情况) +2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置) +3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId) + +#### 方式三:链式调用 + +```java +// 精确切换,支持链式调用 +WxPayUnifiedOrderResult result = payService + .switchoverTo("1234567890", "wx1111111111111111") + .unifiedOrder(request); + +// 仅商户号切换,支持链式调用 +WxPayUnifiedOrderResult result = payService + .switchoverTo("1234567890") + .unifiedOrder(request); +``` + +### 3. 动态添加配置 + +```java +// 运行时动态添加新的 appId 配置 +WxPayConfig newConfig = new WxPayConfig(); +newConfig.setMchId("1234567890"); +newConfig.setAppId("wx4444444444444444"); +// ... 其他配置 + +payService.addConfig("1234567890", "wx4444444444444444", newConfig); + +// 切换到新添加的配置 +payService.switchover("1234567890", "wx4444444444444444"); +``` + +### 4. 移除配置 + +```java +// 移除特定的 appId 配置 +payService.removeConfig("1234567890", "wx1111111111111111"); +``` + +## 实际应用场景 + +### 场景1:根据用户来源切换 appId + +```java +// 在支付前,根据订单来源切换到对应小程序的配置 +String orderSource = order.getSource(); // 例如: "miniapp1", "miniapp2" +String appId = getAppIdBySource(orderSource); + +// 精确切换到特定小程序 +payService.switchover(mchId, appId); + +// 创建订单 +WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); +// ... 设置订单参数 +WxPayUnifiedOrderResult result = payService.unifiedOrder(request); +``` + +### 场景2:处理支付回调 + +```java +@PostMapping("/pay/notify") +public String handlePayNotify(@RequestBody String xmlData) { + try { + // 解析回调通知 + WxPayOrderNotifyResult notifyResult = payService.parseOrderNotifyResult(xmlData); + + // 注意:parseOrderNotifyResult 方法内部会自动调用 + // switchover(notifyResult.getMchId(), notifyResult.getAppid()) + // 切换到正确的配置进行签名验证 + + // 处理业务逻辑 + processOrder(notifyResult); + + return WxPayNotifyResponse.success("成功"); + } catch (WxPayException e) { + log.error("支付回调处理失败", e); + return WxPayNotifyResponse.fail("失败"); + } +} +``` + +### 场景3:不关心具体 appId 的场景 + +```java +// 某些场景下,只要是该商户号的配置即可,不关心具体是哪个 appId +// 例如:查询订单、退款等操作 + +// 仅使用商户号切换 +payService.switchover(mchId); + +// 查询订单 +WxPayOrderQueryResult queryResult = payService.queryOrder(null, outTradeNo); + +// 申请退款 +WxPayRefundRequest refundRequest = new WxPayRefundRequest(); +// ... 设置退款参数 +WxPayRefundResult refundResult = payService.refund(refundRequest); +``` + +## 注意事项 + +1. **向后兼容**:所有原有的使用方式继续有效,不需要修改现有代码。 + +2. **配置隔离**:每个 `mchId + appId` 组合都是独立的配置,修改一个配置不会影响其他配置。 + +3. **线程安全**:配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的。 + +4. **自动切换**:在处理支付回调时,SDK 会自动根据回调中的 `mchId` 和 `appId` 切换到正确的配置。 + +5. **推荐实践**: + - 如果知道具体的 appId,建议使用精确切换方式,避免歧义 + - 如果使用仅商户号切换,确保该商户号下至少有一个可用的配置 + +## 相关 API + +| 方法 | 参数 | 返回值 | 说明 | +|-----|------|--------|------| +| `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置 | +| `switchover(String mchId)` | 商户号 | boolean | 仅使用商户号切换 | +| `switchoverTo(String mchId, String appId)` | 商户号, appId | WxPayService | 精确切换,支持链式调用 | +| `switchoverTo(String mchId)` | 商户号 | WxPayService | 仅商户号切换,支持链式调用 | +| `addConfig(String mchId, String appId, WxPayConfig)` | 商户号, appId, 配置 | void | 动态添加配置 | +| `removeConfig(String mchId, String appId)` | 商户号, appId | void | 移除指定配置 | 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 dab89a014..2db2987d1 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 @@ -78,6 +78,18 @@ public interface WxPayService { */ boolean switchover(String mchId, String appId); + /** + * 仅根据商户号进行切换. + * 适用于一个商户号对应多个appId的场景,切换时会匹配符合该商户号的配置. + * 注意:由于HashMap迭代顺序不确定,当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式. + * + * @param mchId 商户标识 + * @return 切换是否成功,如果找不到匹配的配置则返回false + */ + default boolean switchover(String mchId) { + return false; + } + /** * 进行相应的商户切换. * @@ -87,6 +99,19 @@ public interface WxPayService { */ WxPayService switchoverTo(String mchId, String appId); + /** + * 仅根据商户号进行切换. + * 适用于一个商户号对应多个appId的场景,切换时会匹配符合该商户号的配置. + * 注意:由于HashMap迭代顺序不确定,当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式. + * + * @param mchId 商户标识 + * @return 切换成功,则返回当前对象,方便链式调用 + * @throws me.chanjar.weixin.common.error.WxRuntimeException 如果找不到匹配的配置 + */ + default WxPayService switchoverTo(String mchId) { + throw new me.chanjar.weixin.common.error.WxRuntimeException("子类需要实现此方法"); + } + /** * 发送post请求,得到响应字节数组. * 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 5347099a0..4b51c498d 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 @@ -212,6 +212,34 @@ public boolean switchover(String mchId, String appId) { return false; } + @Override + public boolean switchover(String mchId) { + // 参数校验 + if (StringUtils.isBlank(mchId)) { + log.error("商户号mchId不能为空"); + return false; + } + + // 先尝试精确匹配(针对只有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; + } + @Override public WxPayService switchoverTo(String mchId, String appId) { String configKey = this.getConfigKey(mchId, appId); @@ -222,6 +250,32 @@ public WxPayService switchoverTo(String mchId, String appId) { throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】,appId=【%s】的商户号配置信息,请核实!", mchId, appId)); } + @Override + public WxPayService switchoverTo(String mchId) { + // 参数校验 + if (StringUtils.isBlank(mchId)) { + throw new WxRuntimeException("商户号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)); + } + public String getConfigKey(String mchId, String appId) { return mchId + "_" + appId; } diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java new file mode 100644 index 000000000..010f15fc6 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java @@ -0,0 +1,127 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; + +import java.util.HashMap; +import java.util.Map; + +/** + * 手动验证多appId切换功能 + */ +public class MultiAppIdSwitchoverManualTest { + + public static void main(String[] args) { + WxPayService payService = new WxPayServiceImpl(); + + String testMchId = "1234567890"; + String testAppId1 = "wx1111111111111111"; + String testAppId2 = "wx2222222222222222"; + String testAppId3 = "wx3333333333333333"; + + // 配置同一个商户号,三个不同的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 configMap = new HashMap<>(); + configMap.put(testMchId + "_" + testAppId1, config1); + configMap.put(testMchId + "_" + testAppId2, config2); + configMap.put(testMchId + "_" + testAppId3, config3); + + payService.setMultiConfig(configMap); + + // 测试1: 使用 mchId + appId 精确切换 + System.out.println("=== 测试1: 使用 mchId + appId 精确切换 ==="); + 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()); + verify(success, "切换应该成功"); + verify(testAppId1.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId1); + System.out.println("✓ 测试1通过\n"); + + // 测试2: 仅使用 mchId 切换 + System.out.println("=== 测试2: 仅使用 mchId 切换 ==="); + success = payService.switchover(testMchId); + System.out.println("切换结果: " + success); + System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey()); + verify(success, "仅使用mchId切换应该成功"); + verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId); + System.out.println("✓ 测试2通过\n"); + + // 测试3: 使用 switchoverTo 链式调用(精确匹配) + System.out.println("=== 测试3: 使用 switchoverTo 链式调用(精确匹配) ==="); + WxPayService result = payService.switchoverTo(testMchId, testAppId2); + System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例")); + System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey()); + verify(result == payService, "应该返回同一实例"); + verify(testAppId2.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId2); + System.out.println("✓ 测试3通过\n"); + + // 测试4: 使用 switchoverTo 链式调用(仅mchId) + System.out.println("=== 测试4: 使用 switchoverTo 链式调用(仅mchId) ==="); + result = payService.switchoverTo(testMchId); + System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例")); + System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey()); + verify(result == payService, "应该返回同一实例"); + verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId); + System.out.println("✓ 测试4通过\n"); + + // 测试5: 切换到不存在的商户号 + System.out.println("=== 测试5: 切换到不存在的商户号 ==="); + success = payService.switchover("nonexistent_mch_id"); + System.out.println("切换结果: " + success); + verify(!success, "切换到不存在的商户号应该失败"); + System.out.println("✓ 测试5通过\n"); + + // 测试6: 切换到不存在的 appId + System.out.println("=== 测试6: 切换到不存在的 appId ==="); + success = payService.switchover(testMchId, "wx9999999999999999"); + System.out.println("切换结果: " + success); + verify(!success, "切换到不存在的appId应该失败"); + System.out.println("✓ 测试6通过\n"); + + // 测试7: 添加新配置后切换 + System.out.println("=== 测试7: 添加新配置后切换 ==="); + String newAppId = "wx4444444444444444"; + WxPayConfig newConfig = new WxPayConfig(); + newConfig.setMchId(testMchId); + newConfig.setAppId(newAppId); + newConfig.setMchKey("test_key_4"); + payService.addConfig(testMchId, newAppId, newConfig); + + success = payService.switchover(testMchId, newAppId); + System.out.println("切换结果: " + success); + System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey()); + verify(success, "切换到新添加的配置应该成功"); + verify(newAppId.equals(payService.getConfig().getAppId()), "AppId应该是 " + newAppId); + System.out.println("✓ 测试7通过\n"); + + System.out.println("=================="); + System.out.println("所有测试通过! ✓"); + System.out.println("=================="); + } + + /** + * 验证条件是否为真,如果为假则抛出异常 + * + * @param condition 待验证的条件 + * @param message 验证失败时的错误信息 + */ + private static void verify(boolean condition, String message) { + if (!condition) { + throw new RuntimeException("验证失败: " + message); + } + } +} 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 new file mode 100644 index 000000000..c1c1460fe --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java @@ -0,0 +1,310 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; +import me.chanjar.weixin.common.error.WxRuntimeException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.*; + +/** + * 测试一个商户号配置多个appId的场景 + * + * @author Binary Wang + */ +public class MultiAppIdSwitchoverTest { + + private WxPayService payService; + private final String testMchId = "1234567890"; + private final String testAppId1 = "wx1111111111111111"; + private final String testAppId2 = "wx2222222222222222"; + private final 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 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); + } + + /** + * 测试空参数或null参数的处理 + */ + @Test + public void testSwitchoverWithNullOrEmptyMchId() { + // 测试 null 参数 + boolean success = payService.switchover(null); + assertFalse(success, "使用null作为mchId应该返回false"); + + // 测试空字符串 + success = payService.switchover(""); + assertFalse(success, "使用空字符串作为mchId应该返回false"); + + // 测试空白字符串 + success = payService.switchover(" "); + assertFalse(success, "使用空白字符串作为mchId应该返回false"); + } + + /** + * 测试 switchoverTo 方法对空参数或null参数的处理 + */ + @Test(expectedExceptions = WxRuntimeException.class) + public void testSwitchoverToWithNullMchId() { + payService.switchoverTo((String) null); + } + + @Test(expectedExceptions = WxRuntimeException.class) + public void testSwitchoverToWithEmptyMchId() { + payService.switchoverTo(""); + } + + @Test(expectedExceptions = WxRuntimeException.class) + public void testSwitchoverToWithBlankMchId() { + payService.switchoverTo(" "); + } + + /** + * 测试商户号存在包含关系的场景 + * 例如同时配置 "123" 和 "1234",验证前缀匹配不会错误匹配 + */ + @Test + public void testSwitchoverWithOverlappingMchIds() { + WxPayService testService = new WxPayServiceImpl(); + + // 配置两个有包含关系的商户号 + String mchId1 = "123"; + String mchId2 = "1234"; + String appId1 = "wx_app_123"; + String appId2 = "wx_app_1234"; + + WxPayConfig config1 = new WxPayConfig(); + config1.setMchId(mchId1); + config1.setAppId(appId1); + config1.setMchKey("key_123"); + + WxPayConfig config2 = new WxPayConfig(); + config2.setMchId(mchId2); + config2.setAppId(appId2); + config2.setMchKey("key_1234"); + + Map configMap = new HashMap<>(); + configMap.put(mchId1 + "_" + appId1, config1); + configMap.put(mchId2 + "_" + appId2, config2); + testService.setMultiConfig(configMap); + + // 切换到 "123",应该只匹配 "123_wx_app_123" + boolean success = testService.switchover(mchId1); + assertTrue(success); + assertEquals(testService.getConfig().getMchId(), mchId1); + assertEquals(testService.getConfig().getAppId(), appId1); + + // 切换到 "1234",应该只匹配 "1234_wx_app_1234" + success = testService.switchover(mchId2); + assertTrue(success); + assertEquals(testService.getConfig().getMchId(), mchId2); + assertEquals(testService.getConfig().getAppId(), appId2); + + // 精确切换验证 + success = testService.switchover(mchId1, appId1); + assertTrue(success); + assertEquals(testService.getConfig().getAppId(), appId1); + + success = testService.switchover(mchId2, appId2); + assertTrue(success); + assertEquals(testService.getConfig().getAppId(), appId2); + } +}