🎨 #3898 【微信支付】支持单参数 switchover 自定义键及通知回调空 appId 降级处理

This commit is contained in:
Copilot
2026-03-03 16:41:41 +08:00
committed by GitHub
parent d661d0c10a
commit 65deaab2a4
4 changed files with 300 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
# 支持一个商户号对应多个 appId 的使用说明
# 支持一个商户号对应多个 appId 及自定义配置键的使用说明
## 背景
@@ -7,13 +7,19 @@
- 所有小程序共用同一个支付商户号
- 支付配置(商户号、密钥、证书等)完全相同,只有 appId 不同
此外也存在多租户SaaS场景需要以自定义唯一键如租户ID来管理多个不同商户的配置。
## 解决方案
WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。
WxJava 支持以下几种多配置管理方式:
1. **mchId + appId 精确匹配**:适用于一个商户号对应多个 appId 的场景
2. **仅使用商户号mchId匹配**:适用于一个商户号对应单个 appId 或不关心具体 appId 的场景
3. **自定义唯一键**适用于多租户场景使用任意字符串如租户ID作为配置键
## 使用方式
### 1. 配置多个 appId
### 1. 配置多个 appId(按 mchId + appId 格式)
```java
WxPayService payService = new WxPayServiceImpl();
@@ -53,7 +59,47 @@ configMap.put(mchId + "_" + config3.getAppId(), config3);
payService.setMultiConfig(configMap);
```
### 2. 获取配置的方式
### 2. 使用自定义唯一键配置(多租户场景)
适用于多租户 SaaS 系统使用任意唯一标识符如租户ID管理不同商户的配置
```java
WxPayService payService = new WxPayServiceImpl();
// 使用租户ID作为配置键
WxPayConfig tenant1Config = new WxPayConfig();
tenant1Config.setMchId("1234567890");
tenant1Config.setAppId("wx1111111111111111");
tenant1Config.setMchKey("tenant1_mch_key");
// ... 其他配置
WxPayConfig tenant2Config = new WxPayConfig();
tenant2Config.setMchId("9876543210");
tenant2Config.setAppId("wx2222222222222222");
tenant2Config.setMchKey("tenant2_mch_key");
// ... 其他配置
// 方式一:通过 setMultiConfig 批量设置
Map<String, WxPayConfig> configMap = new HashMap<>();
configMap.put("tenant_001", tenant1Config);
configMap.put("tenant_002", tenant2Config);
payService.setMultiConfig(configMap);
// 方式二:通过 addConfig(key, config) 逐个添加(推荐,兼容单参数 switchover 方式)
payService.addConfig("tenant_001", tenant1Config);
payService.addConfig("tenant_002", tenant2Config);
// 切换配置:直接使用自定义键
payService.switchover("tenant_001");
WxPayConfig config = payService.getConfig(); // 获取 tenant_001 对应的配置
// 链式调用
WxPayUnifiedOrderResult result = payService
.switchoverTo("tenant_001")
.unifiedOrder(request);
```
### 3. 获取配置的方式
#### 方式一:直接获取配置(推荐,新功能)
@@ -122,7 +168,7 @@ payService.switchover("1234567890", "wx1111111111111111");
payService.switchover("1234567890", "wx2222222222222222");
```
#### 方式二:仅使用商户号切换(新功能)
#### 方式二:仅使用商户号切换
```java
// 仅使用商户号切换,会自动匹配该商户号的某个配置
@@ -135,7 +181,19 @@ boolean success = payService.switchover("1234567890");
2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置)
3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId
#### 方式三:链式调用
#### 方式三:使用自定义唯一键切换(多租户场景)
```java
// 使用通过 addConfig(key, config) 或 setMultiConfig(map) 注册的自定义键切换
boolean success = payService.switchover("tenant_001");
// 链式调用
WxPayUnifiedOrderResult result = payService
.switchoverTo("tenant_001")
.unifiedOrder(request);
```
#### 方式四:链式调用
```java
// 精确切换,支持链式调用
@@ -152,7 +210,7 @@ WxPayUnifiedOrderResult result = payService
### 4. 动态添加配置
```java
// 运行时动态添加新的 appId 配置
// 方式一:使用 mchId + appId 添加配置
WxPayConfig newConfig = new WxPayConfig();
newConfig.setMchId("1234567890");
newConfig.setAppId("wx4444444444444444");
@@ -162,13 +220,27 @@ payService.addConfig("1234567890", "wx4444444444444444", newConfig);
// 切换到新添加的配置
payService.switchover("1234567890", "wx4444444444444444");
// 方式二:使用自定义唯一键添加配置(多租户场景)
WxPayConfig tenantConfig = new WxPayConfig();
tenantConfig.setMchId("1234567890");
tenantConfig.setAppId("wx4444444444444444");
// ... 其他配置
payService.addConfig("tenant_003", tenantConfig);
// 使用自定义键切换
payService.switchover("tenant_003");
```
### 5. 移除配置
```java
// 移除特定的 appId 配置
// 方式一:使用 mchId + appId 移除配置
payService.removeConfig("1234567890", "wx1111111111111111");
// 方式二:使用自定义唯一键移除配置(多租户场景)
payService.removeConfig("tenant_001");
```
## 实际应用场景
@@ -201,6 +273,7 @@ public String handlePayNotify(@RequestBody String xmlData) {
// 注意parseOrderNotifyResult 方法内部会自动调用
// switchover(notifyResult.getMchId(), notifyResult.getAppid())
// 切换到正确的配置进行签名验证
// 若回调中 appId 为空,会自动降级为仅使用 mchId 匹配
// 处理业务逻辑
processOrder(notifyResult);
@@ -277,24 +350,30 @@ public void processRefund(String mchId, String outTradeNo) {
| `getConfig(String mchId, String appId)` | 直接获取指定配置 | **否** | 多商户管理、异步场景、线程池 |
| `getConfig(String mchId)` | 根据商户号获取配置 | **否** | 不确定 appId 的场景 |
| `switchover(String mchId, String appId)` | 精确切换配置 | 是 | 需要切换上下文的场景 |
| `switchover(String mchId)` | 根据商户号切换 | 是 | 不关心 appId 的切换场景 |
| `switchover(String mchIdOrKey)` | 根据商户号或自定义键切换 | 是 | 不关心 appId 或多租户场景 |
## 注意事项
1. **向后兼容**:所有原有的使用方式继续有效,不需要修改现有代码。
2. **配置隔离**:每个 `mchId + appId` 组合都是独立的配置,修改一个配置不会影响其他配置。
2. **自定义键支持**:可以通过 `addConfig(String configKey, WxPayConfig)``setMultiConfig(Map)` 注册任意键,
然后直接用 `switchover(key)` 进行精确匹配切换,无需关心 mchId 或 appId 的格式。
3. **线程安全**
3. **通知回调兼容**:当 `switchover(mchId, appId)` 的 appId 为空(通知回调中可能出现此情况)时,
SDK 会自动降级为仅使用 mchId 进行匹配,避免因 appId 缺失导致的切换失败。
4. **配置隔离**:每个配置键对应独立的配置,修改一个配置不会影响其他配置。
5. **线程安全**
- 配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的
- 直接获取配置方法(`getConfig(mchId, appId)`)不依赖 ThreadLocal可以在任何上下文中安全使用
4. **自动切换**在处理支付回调时SDK 会自动根据回调中的 `mchId``appId` 切换到正确的配置。
6. **自动切换**在处理支付回调时SDK 会自动根据回调中的 `mchId``appId` 切换到正确的配置。
5. **推荐实践**
- 如果知道具体的 appId建议使用精确切换或获取方式避免歧义
7. **推荐实践**
- 多租户 SaaS 场景使用自定义键如租户ID管理配置通过 `switchover(tenantId)` 切换
- 一商户多 appId 场景:使用 `mchId_appId` 格式的键,通过 `switchover(mchId, appId)` 精确切换
- 在多商户管理、异步场景、线程池等环境中,建议使用 `getConfig(mchId, appId)` 直接获取配置
- 如果使用仅商户号切换或获取,确保该商户号下至少有一个可用的配置
## 相关 API
@@ -303,9 +382,11 @@ public void processRefund(String mchId, String outTradeNo) {
| `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 | 仅使用商户号切换 |
| `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置appId 为空时自动降级为仅 mchId 匹配 |
| `switchover(String mchIdOrKey)` | 商户号或自定义键 | 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 | 移除指定配置 |
| `switchoverTo(String mchIdOrKey)` | 商户号或自定义键 | WxPayService | 根据商户号或自定义键切换,支持链式调用 |
| `addConfig(String mchId, String appId, WxPayConfig)` | 商户号, appId, 配置 | void | 动态添加配置(键为 mchId_appId 格式) |
| `addConfig(String configKey, WxPayConfig)` | 自定义键, 配置 | void | 动态添加配置(使用自定义键) |
| `removeConfig(String mchId, String appId)` | 商户号, appId | void | 移除指定配置(键为 mchId_appId 格式) |
| `removeConfig(String configKey)` | 自定义键 | void | 移除指定配置(使用自定义键) |

View File

@@ -38,6 +38,7 @@ public interface WxPayService {
/**
* Map里 加入新的 {@link WxPayConfig},适用于动态添加新的微信商户配置.
* 配置键将使用 mchId + "_" + appId 的格式.
*
* @param mchId 商户id
* @param appId 微信应用id
@@ -45,6 +46,15 @@ public interface WxPayService {
*/
void addConfig(String mchId, String appId, WxPayConfig wxPayConfig);
/**
* Map里 加入新的 {@link WxPayConfig},使用自定义配置键,适用于动态添加新的微信商户配置.
* 此方法允许使用任意唯一标识符如租户ID作为配置键兼容单参数 switchover 使用方式.
*
* @param configKey 自定义的配置键全局唯一标识符如租户ID
* @param wxPayConfig 新的微信配置
*/
void addConfig(String configKey, WxPayConfig wxPayConfig);
/**
* 从 Map中 移除 {@link String mchId} 和 {@link String appId} 所对应的 {@link WxPayConfig},适用于动态移除微信商户配置.
*
@@ -53,6 +63,14 @@ public interface WxPayService {
*/
void removeConfig(String mchId, String appId);
/**
* 从 Map中 移除指定配置键所对应的 {@link WxPayConfig},适用于动态移除微信商户配置.
* 此方法允许使用任意唯一标识符如租户ID删除配置兼容单参数 switchover 使用方式.
*
* @param configKey 自定义的配置键全局唯一标识符如租户ID
*/
void removeConfig(String configKey);
/**
* 注入多个 {@link WxPayConfig} 的实现. 并为每个 {@link WxPayConfig} 赋予不同的 {@link String mchId} 值
* 随机采用一个{@link String mchId}进行Http初始化操作
@@ -79,14 +97,17 @@ public interface WxPayService {
boolean switchover(String mchId, String appId);
/**
* 根据商户号进行切换.
* 适用于一个商户号对应多个appId的场景切换时会匹配符合该商户号的配置.
* 注意由于HashMap迭代顺序不确定当存在多个匹配项时返回的配置是不可预测的建议使用精确匹配方式.
* 根据商户号或自定义配置键进行切换.
* <ul>
* <li>当传入商户号mchId会先尝试精确匹配若未找到则前缀匹配mchId_*)。</li>
* <li>也可传入通过 {@link #addConfig(String, WxPayConfig)} 或 {@link #setMultiConfig(Map)} 注册的任意自定义配置键,此时直接精确匹配。</li>
* </ul>
* 注意:当存在多个前缀匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
*
* @param mchId 商户标识
* @param mchIdOrConfigKey 商户标识或自定义配置键
* @return 切换是否成功如果找不到匹配的配置则返回false
*/
default boolean switchover(String mchId) {
default boolean switchover(String mchIdOrConfigKey) {
return false;
}
@@ -100,15 +121,18 @@ public interface WxPayService {
WxPayService switchoverTo(String mchId, String appId);
/**
* 根据商户号进行切换.
* 适用于一个商户号对应多个appId的场景切换时会匹配符合该商户号的配置.
* 注意由于HashMap迭代顺序不确定当存在多个匹配项时返回的配置是不可预测的建议使用精确匹配方式.
* 根据商户号或自定义配置键进行切换,支持链式调用.
* <ul>
* <li>当传入商户号mchId会先尝试精确匹配若未找到则前缀匹配mchId_*)。</li>
* <li>也可传入通过 {@link #addConfig(String, WxPayConfig)} 或 {@link #setMultiConfig(Map)} 注册的任意自定义配置键,此时直接精确匹配。</li>
* </ul>
* 注意:当存在多个前缀匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
*
* @param mchId 商户标识
* @param mchIdOrConfigKey 商户标识或自定义配置键
* @return 切换成功,则返回当前对象,方便链式调用
* @throws me.chanjar.weixin.common.error.WxRuntimeException 如果找不到匹配的配置
*/
default WxPayService switchoverTo(String mchId) {
default WxPayService switchoverTo(String mchIdOrConfigKey) {
throw new me.chanjar.weixin.common.error.WxRuntimeException("子类需要实现此方法");
}

View File

@@ -214,6 +214,18 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
}
}
@Override
public void addConfig(String configKey, WxPayConfig wxPayConfig) {
synchronized (this) {
if (this.configMap == null) {
this.setMultiConfig(ImmutableMap.of(configKey, wxPayConfig), configKey);
} else {
WxPayConfigHolder.set(configKey);
this.configMap.put(configKey, wxPayConfig);
}
}
}
@Override
public void removeConfig(String mchId, String appId) {
synchronized (this) {
@@ -231,6 +243,22 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
}
}
@Override
public void removeConfig(String configKey) {
synchronized (this) {
this.configMap.remove(configKey);
if (this.configMap.isEmpty()) {
log.warn("已删除最后一个商户号配置configKey[{}]须立即使用setConfig或setMultiConfig添加配置", configKey);
return;
}
if (WxPayConfigHolder.get().equals(configKey)) {
final String nextConfigKey = this.configMap.keySet().iterator().next();
WxPayConfigHolder.set(nextConfigKey);
log.warn("已删除默认商户号配置,商户号【{}】被设为默认配置", nextConfigKey);
}
}
}
@Override
public void setMultiConfig(Map<String, WxPayConfig> wxPayConfigs) {
this.setMultiConfig(wxPayConfigs, wxPayConfigs.keySet().iterator().next());
@@ -244,6 +272,10 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Override
public boolean switchover(String mchId, String appId) {
// 如果appId为空则降级为仅使用mchId进行切换
if (StringUtils.isBlank(appId)) {
return this.switchover(mchId);
}
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);
@@ -283,6 +315,10 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Override
public WxPayService switchoverTo(String mchId, String appId) {
// 如果appId为空则降级为仅使用mchId进行切换
if (StringUtils.isBlank(appId)) {
return this.switchoverTo(mchId);
}
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);

View File

@@ -414,4 +414,133 @@ public class MultiAppIdSwitchoverTest {
assertTrue(success);
assertEquals(testService.getConfig().getAppId(), appId2);
}
/**
* 测试使用自定义唯一键非mchId格式添加配置并切换.
* 验证向后兼容性支持使用任意唯一标识符如租户ID管理配置
*/
@Test
public void testAddConfigWithCustomKey() {
WxPayService testService = new WxPayServiceImpl();
String customKey1 = "tenant_001";
String customKey2 = "tenant_002";
WxPayConfig config1 = new WxPayConfig();
config1.setMchId("mch001");
config1.setAppId("wxabc");
config1.setMchKey("key_tenant_001");
WxPayConfig config2 = new WxPayConfig();
config2.setMchId("mch002");
config2.setAppId("wxdef");
config2.setMchKey("key_tenant_002");
// 使用自定义键添加配置
testService.addConfig(customKey1, config1);
testService.addConfig(customKey2, config2);
// 使用自定义键切换配置
boolean success = testService.switchover(customKey1);
assertTrue(success, "应该能够使用自定义键切换配置");
assertEquals(testService.getConfig().getMchKey(), "key_tenant_001");
success = testService.switchover(customKey2);
assertTrue(success, "应该能够切换到第二个自定义键配置");
assertEquals(testService.getConfig().getMchKey(), "key_tenant_002");
}
/**
* 测试使用自定义唯一键删除配置.
*/
@Test
public void testRemoveConfigWithCustomKey() {
WxPayService testService = new WxPayServiceImpl();
String customKey1 = "tenant_A";
String customKey2 = "tenant_B";
WxPayConfig config1 = new WxPayConfig();
config1.setMchId("mchA");
config1.setAppId("wxA");
config1.setMchKey("key_A");
WxPayConfig config2 = new WxPayConfig();
config2.setMchId("mchB");
config2.setAppId("wxB");
config2.setMchKey("key_B");
Map<String, WxPayConfig> configMap = new HashMap<>();
configMap.put(customKey1, config1);
configMap.put(customKey2, config2);
testService.setMultiConfig(configMap);
// 删除第一个自定义键配置
testService.removeConfig(customKey1);
// 尝试切换到已删除的配置应该失败
boolean success = testService.switchover(customKey1);
assertFalse(success, "切换到已删除的配置应该失败");
// 但仍然能够切换到第二个配置
success = testService.switchover(customKey2);
assertTrue(success, "切换到未删除的配置应该成功");
assertEquals(testService.getConfig().getMchKey(), "key_B");
}
/**
* 测试 switchover(mchId, appId) 当 appId 为 null 时降级为 switchover(mchId).
* 模拟通知回调中 appId 可能为空的场景
*/
@Test
public void testSwitchoverWithNullAppIdFallsBackToMchId() {
// 切换到 appId 为 null 时,应该降级为只使用 mchId 匹配
boolean success = payService.switchover(testMchId, null);
assertTrue(success, "appId为null时应该降级为仅mchId匹配");
assertEquals(payService.getConfig().getMchId(), testMchId);
// appId 为空字符串时同样应该降级
success = payService.switchover(testMchId, "");
assertTrue(success, "appId为空字符串时应该降级为仅mchId匹配");
assertEquals(payService.getConfig().getMchId(), testMchId);
}
/**
* 测试 switchoverTo(mchId, appId) 当 appId 为 null 时降级为 switchoverTo(mchId).
*/
@Test
public void testSwitchoverToWithNullAppIdFallsBackToMchId() {
WxPayService result = payService.switchoverTo(testMchId, null);
assertNotNull(result);
assertEquals(result, payService);
assertEquals(payService.getConfig().getMchId(), testMchId);
}
/**
* 测试使用自定义键通过 setMultiConfig 注册后可以直接 switchover.
*/
@Test
public void testSwitchoverWithCustomKeyViaSetMultiConfig() {
WxPayService testService = new WxPayServiceImpl();
String tenantId = "my-unique-tenant-id";
WxPayConfig config = new WxPayConfig();
config.setMchId("mchTenant");
config.setAppId("wxTenant");
config.setMchKey("key_tenant");
Map<String, WxPayConfig> configMap = new HashMap<>();
configMap.put(tenantId, config);
testService.setMultiConfig(configMap);
// 使用自定义租户ID切换
boolean success = testService.switchover(tenantId);
assertTrue(success, "应该能够使用自定义租户ID切换配置");
assertEquals(testService.getConfig().getMchKey(), "key_tenant");
// switchoverTo 链式调用也应该支持
WxPayService result = testService.switchoverTo(tenantId);
assertNotNull(result);
assertEquals(result, testService);
}
}