1
0
mirror of synced 2026-02-09 04:07:52 +08:00

🎨 #3849 【微信支付】支持一个商户号配置多个小程序appId

This commit is contained in:
Copilot
2026-01-16 17:28:51 +08:00
committed by GitHub
parent 12a9f83b98
commit 12db287ae0
5 changed files with 716 additions and 0 deletions

View File

@@ -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请求得到响应字节数组.
*

View File

@@ -212,6 +212,34 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
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 abstract class BaseWxPayServiceImpl implements WxPayService {
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;
}

View File

@@ -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<String, WxPayConfig> 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);
}
}
}

View File

@@ -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<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);
}
/**
* 测试空参数或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<String, WxPayConfig> 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);
}
}