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
123 changes: 117 additions & 6 deletions weixin-java-pay/MULTI_APPID_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,64 @@ configMap.put(mchId + "_" + config3.getAppId(), config3);
payService.setMultiConfig(configMap);
```

### 2. 切换配置的方式
### 2. 获取配置的方式

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

直接通过商户号和 appId 获取配置,**不依赖 ThreadLocal**,适用于多商户管理场景:

```java
// 精确获取指定商户号和 appId 的配置
WxPayConfig config1 = payService.getConfig("1234567890", "wx1111111111111111");

// 仅使用商户号获取配置(会返回该商户号的任意一个配置)
// 注意:当存在多个 appId 时,返回结果基于内部存储顺序,不应依赖其稳定性
WxPayConfig config = payService.getConfig("1234567890");

// 使用获取的配置读取信息(仅用于读取配置,不用于执行支付操作)
if (config != null) {
String appId = config.getAppId();
String mchKey = config.getMchKey();
String apiV3Key = config.getApiV3Key();
// ... 使用配置信息进行业务逻辑判断或记录
}
```

**优势**:
- 不依赖 ThreadLocal,可以在任何上下文中使用
- 适合在异步场景、线程池等环境中使用
- 线程安全,不会因为线程切换导致配置丢失
- 可以同时获取多个不同的配置

**使用场景**:
- 仅需读取配置信息(如获取 mchKey、appId 等)
- 不需要执行 WxPayService 的支付相关方法
- 如需执行支付操作,请使用方式二的 switchover 方法

#### 方式二:切换配置后使用(原有方式)

通过切换配置,然后调用 `getConfig()` 获取当前配置或直接执行支付操作:

```java
// 精确切换到指定的配置
payService.switchover("1234567890", "wx1111111111111111");
WxPayConfig config = payService.getConfig(); // 获取当前切换的配置

// 仅使用商户号切换
payService.switchover("1234567890");
config = payService.getConfig(); // 获取切换后的配置

// 切换后可直接执行支付操作
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
```

**注意**:此方式依赖 ThreadLocal,需要注意线程上下文的问题。

**使用场景**:
- 需要执行 WxPayService 的支付相关方法(如 unifiedOrder、refund 等)
- 在同一线程中连续执行多个支付操作

### 3. 切换配置的方式

#### 方式一:精确切换(原有方式,向后兼容)

Expand Down Expand Up @@ -92,7 +149,7 @@ WxPayUnifiedOrderResult result = payService
.unifiedOrder(request);
```

### 3. 动态添加配置
### 4. 动态添加配置

```java
// 运行时动态添加新的 appId 配置
Expand All @@ -107,7 +164,7 @@ payService.addConfig("1234567890", "wx4444444444444444", newConfig);
payService.switchover("1234567890", "wx4444444444444444");
```

### 4. 移除配置
### 5. 移除配置

```java
// 移除特定的 appId 配置
Expand Down Expand Up @@ -174,24 +231,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) {
// 直接根据商户号切换(内部会选择该商户号下的一个配置)
if (!payService.switchover(mchId)) {
log.error("商户配置切换失败:mchId={}", mchId);
return;
}

// 在完成上下文切换后,执行退款操作
WxPayRefundRequest request = new WxPayRefundRequest();
request.setOutTradeNo(outTradeNo);
// ... 设置其他退款参数
WxPayRefundResult refundResult = payService.refund(request);
}
```

## 新增方法对比

| 方法 | 说明 | 是否依赖 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 | 精确切换,支持链式调用 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
* 设置配置对象.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,47 @@ 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 + "_";
return this.configMap.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(prefix))
.findFirst()
.map(entry -> {
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
return entry.getValue();
})
.orElseGet(() -> {
log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
return null;
});
}

@Override
public void setConfig(WxPayConfig config) {
final String defaultKey = this.getConfigKey(config.getMchId(), config.getAppId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 精确切换(原有功能,确保向后兼容)
*/
Expand Down