Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 101 additions & 20 deletions weixin-java-pay/MULTI_APPID_USAGE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 支持一个商户号对应多个 appId 的使用说明
# 支持一个商户号对应多个 appId 及自定义配置键的使用说明

## 背景

Expand All @@ -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();
Expand Down Expand Up @@ -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. 获取配置的方式

#### 方式一:直接获取配置(推荐,新功能)

Expand Down Expand Up @@ -122,7 +168,7 @@ payService.switchover("1234567890", "wx1111111111111111");
payService.switchover("1234567890", "wx2222222222222222");
```

#### 方式二:仅使用商户号切换(新功能)
#### 方式二:仅使用商户号切换

```java
// 仅使用商户号切换,会自动匹配该商户号的某个配置
Expand All @@ -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
// 精确切换,支持链式调用
Expand All @@ -152,7 +210,7 @@ WxPayUnifiedOrderResult result = payService
### 4. 动态添加配置

```java
// 运行时动态添加新的 appId 配置
// 方式一:使用 mchId + appId 添加配置
WxPayConfig newConfig = new WxPayConfig();
newConfig.setMchId("1234567890");
newConfig.setAppId("wx4444444444444444");
Expand All @@ -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");
```

## 实际应用场景
Expand Down Expand Up @@ -201,6 +273,7 @@ public String handlePayNotify(@RequestBody String xmlData) {
// 注意:parseOrderNotifyResult 方法内部会自动调用
// switchover(notifyResult.getMchId(), notifyResult.getAppid())
// 切换到正确的配置进行签名验证
// 若回调中 appId 为空,会自动降级为仅使用 mchId 匹配

// 处理业务逻辑
processOrder(notifyResult);
Expand Down Expand Up @@ -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. **通知回调兼容**:当 `switchover(mchId, appId)` 的 appId 为空(通知回调中可能出现此情况)时,
SDK 会自动降级为仅使用 mchId 进行匹配,避免因 appId 缺失导致的切换失败。

4. **配置隔离**:每个配置键对应独立的配置,修改一个配置不会影响其他配置。

3. **线程安全**:
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

Expand All @@ -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 | 移除指定配置(使用自定义键) |
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,23 @@ public interface WxPayService {

/**
* Map里 加入新的 {@link WxPayConfig},适用于动态添加新的微信商户配置.
* 配置键将使用 mchId + "_" + appId 的格式.
*
* @param mchId 商户id
* @param appId 微信应用id
* @param wxPayConfig 新的微信配置
*/
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},适用于动态移除微信商户配置.
*
Expand All @@ -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初始化操作
Expand All @@ -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;
}

Expand All @@ -100,15 +121,18 @@ default boolean switchover(String mchId) {
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("子类需要实现此方法");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,18 @@ public void addConfig(String mchId, String appId, WxPayConfig wxPayConfig) {
}
}

@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) {
Expand All @@ -231,6 +243,22 @@ public void removeConfig(String mchId, String appId) {
}
}

@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());
Expand All @@ -244,6 +272,10 @@ public void setMultiConfig(Map<String, WxPayConfig> wxPayConfigs, String default

@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);
Expand Down Expand Up @@ -283,6 +315,10 @@ public boolean switchover(String mchId) {

@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);
Expand Down
Loading