Skip to content
Open
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
109 changes: 103 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,50 @@ configMap.put(mchId + "_" + config3.getAppId(), config3);
payService.setMultiConfig(configMap);
```

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

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

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

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

// 仅使用商户号获取配置(会返回该商户号的任意一个配置)
WxPayConfig config = payService.getConfig("1234567890");

// 使用获取的配置进行支付操作
if (config != null) {
String appId = config.getAppId();
String mchKey = config.getMchKey();
// ... 使用配置信息
}
```

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

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

通过切换配置,然后调用 `getConfig()` 获取当前配置:

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

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

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

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

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

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

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

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

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

```java
// 移除特定的 appId 配置
Expand Down Expand Up @@ -174,24 +217,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) {
// 获取该商户号的任意一个配置
WxPayConfig config = payService.getConfig(mchId);

if (config == null) {
log.error("找不到商户配置:mchId={}", mchId);
return;
}

// 先切换到该配置,然后进行退款
payService.switchover(mchId, config.getAppId());
// ... 执行退款操作
Comment on lines +242 to +254
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

文档示例中的使用模式存在不一致。在场景4的第二个例子(processRefund方法)中,先使用 getConfig(mchId) 获取配置,然后仍然调用 switchover 方法切换配置。

这种模式的问题:

  1. 既然已经获取到了配置,理论上应该可以直接使用,不需要再次切换
  2. 违背了新方法"不依赖 ThreadLocal"的设计初衷
  3. 可能会让用户困惑:到底应该直接使用 config 还是需要先切换

建议改进示例:

  • 如果用户需要调用 payService 的其他方法(如 refund),应该直接使用 switchover 方法
  • 或者更清楚地说明:getConfig 方法仅用于读取配置信息,如果需要执行支付操作,仍需要调用 switchover 设置 ThreadLocal

更好的示例可能是:

// 方式1:仅读取配置信息
WxPayConfig config = payService.getConfig(mchId);
String apiV3Key = config.getApiV3Key();

// 方式2:需要执行支付操作时,直接使用 switchover
payService.switchover(mchId);
payService.refund(refundRequest);
Suggested change
// 或者在不确定 appId 的情况下
public void processRefund(String mchId, String outTradeNo) {
// 获取该商户号的任意一个配置
WxPayConfig config = payService.getConfig(mchId);
if (config == null) {
log.error("找不到商户配置:mchId={}", mchId);
return;
}
// 先切换到该配置,然后进行退款
payService.switchover(mchId, config.getAppId());
// ... 执行退款操作
// 或者在不确定 appId 的情况下,仅通过商户号发起退款
public void processRefund(String mchId, String outTradeNo) {
// 直接根据商户号切换(内部会选择该商户号下的一个配置)
if (!payService.switchover(mchId)) {
log.error("商户配置切换失败:mchId={}", mchId);
return;
}
// 在完成上下文切换后,执行退款操作
// ... 执行退款操作,如:
// WxPayRefundRequest request = new WxPayRefundRequest();
// request.setOutTradeNo(outTradeNo);
// payService.refund(request);

Copilot uses AI. Check for mistakes.
}
```

## 新增方法对比

| 方法 | 说明 | 是否依赖 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WxPayService 接口新增非 default 方法会对第三方自定义实现造成源码/二进制不兼容(实现类需要新增实现才能编译/加载)。这可能与 PR 描述里的“完全向后兼容”不一致,建议在变更说明中明确这一点。

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎


/**
* 根据商户号直接获取配置.
* 此方法不依赖 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,45 @@ 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 + "_";
for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getConfig(String mchId) 这里遍历 configMap.entrySet(),若 configMap 在运行时被设置为非并发 Map 或与 addConfig/removeConfig 并发发生修改,可能触发 ConcurrentModificationException 或读到不一致数据。鉴于该方法主打异步/线程池场景,建议确认并发语义与文档表述一致。

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

if (entry.getKey().startsWith(prefix)) {
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
return entry.getValue();
}
}

log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
return null;
Comment on lines +185 to +193
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getConfig(String mchId) 方法在处理大量配置时可能存在性能问题。当前实现使用 for 循环遍历所有 configMap 条目来查找前缀匹配的配置,时间复杂度为 O(n)。

建议优化:

  1. 可以考虑使用 Stream API 的 findFirst() 来提前结束遍历
  2. 或者维护一个从 mchId 到 configKey 的索引映射来实现 O(1) 查找

例如使用 Stream:

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;
  });
Suggested change
for (Map.Entry<String, WxPayConfig> entry : this.configMap.entrySet()) {
if (entry.getKey().startsWith(prefix)) {
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
return entry.getValue();
}
}
log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
return null;
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;
});

Copilot uses AI. Check for mistakes.
}
Comment on lines +172 to +194
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getConfig(String mchId) 方法的行为存在不确定性问题。文档中提到"当存在多个匹配项时返回的配置是不可预测的",但实际实现总是返回第一个匹配的配置(取决于 HashMap 的遍历顺序)。

这种不确定性可能导致:

  1. 不同环境下返回不同的配置
  2. 相同代码在不同时间执行可能得到不同结果
  3. 难以调试和定位问题

建议:

  1. 在文档中更明确地说明这是基于内部存储顺序的,不应依赖
  2. 或者考虑使用确定性的排序(如字典序)来返回最小的 key 对应的配置
  3. 在日志中输出实际返回的配置 key,帮助调试

Copilot uses AI. Check for mistakes.

@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
Loading