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
200 changes: 200 additions & 0 deletions weixin-java-pay/MULTI_APPID_USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# 支持一个商户号对应多个 appId 的使用说明

## 背景

在实际业务中,经常会遇到一个微信支付商户号需要绑定多个小程序的场景。例如:
- 一个商家有多个小程序(主店、分店、活动小程序等)
- 所有小程序共用同一个支付商户号
- 支付配置(商户号、密钥、证书等)完全相同,只有 appId 不同

## 解决方案

从 4.8.1 版本开始,WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

文档中提到该功能从 4.8.1 版本开始支持,但需要确认这个版本号是否正确。建议验证当前项目的版本号,并确保文档中的版本号与实际发布版本一致。如果这是待发布的版本,建议在 PR 合并前再次确认版本号。

Suggested change
从 4.8.1 版本开始,WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。
WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。

Copilot uses AI. Check for mistakes.

## 使用方式

### 1. 配置多个 appId

```java
WxPayService payService = new WxPayServiceImpl();

String mchId = "1234567890"; // 商户号

// 配置小程序1
WxPayConfig config1 = new WxPayConfig();
config1.setMchId(mchId);
config1.setAppId("wx1111111111111111"); // 小程序1的appId
config1.setMchKey("your_mch_key");
config1.setApiV3Key("your_api_v3_key");
// ... 其他配置

// 配置小程序2
WxPayConfig config2 = new WxPayConfig();
config2.setMchId(mchId);
config2.setAppId("wx2222222222222222"); // 小程序2的appId
config2.setMchKey("your_mch_key");
config2.setApiV3Key("your_api_v3_key");
// ... 其他配置

// 配置小程序3
WxPayConfig config3 = new WxPayConfig();
config3.setMchId(mchId);
config3.setAppId("wx3333333333333333"); // 小程序3的appId
config3.setMchKey("your_mch_key");
config3.setApiV3Key("your_api_v3_key");
// ... 其他配置

// 添加到配置映射
Map<String, WxPayConfig> configMap = new HashMap<>();
configMap.put(mchId + "_" + config1.getAppId(), config1);
configMap.put(mchId + "_" + config2.getAppId(), config2);
configMap.put(mchId + "_" + config3.getAppId(), config3);

payService.setMultiConfig(configMap);
```

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

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

```java
// 切换到小程序1的配置
payService.switchover("1234567890", "wx1111111111111111");

// 切换到小程序2的配置
payService.switchover("1234567890", "wx2222222222222222");
```

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

```java
// 仅使用商户号切换,会自动匹配该商户号的某个配置
// 适用于不关心具体使用哪个 appId 的场景
boolean success = payService.switchover("1234567890");
```

**注意**:当使用仅商户号切换时,会按照以下逻辑查找配置:
1. 先尝试精确匹配商户号(针对只配置商户号、没有 appId 的情况)
2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置)
3. 如果有多个匹配项,返回找到的第一个
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

文档在描述前缀匹配逻辑时(第 79 行)提到"如果有多个匹配项,返回找到的第一个",但没有明确说明"第一个"的定义标准。由于 HashMap 的迭代顺序不确定,这种表述可能会给用户造成误导,让他们认为返回的配置是可预测的。建议在此处明确说明返回的配置是不确定的,或者明确指出应该避免这种使用场景,推荐使用精确匹配。

Suggested change
3. 如果有多个匹配项,返回找到的第一个
3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId)

Copilot uses AI. Check for mistakes.

#### 方式三:链式调用

```java
// 精确切换,支持链式调用
WxPayUnifiedOrderResult result = payService
.switchoverTo("1234567890", "wx1111111111111111")
.unifiedOrder(request);

// 仅商户号切换,支持链式调用
WxPayUnifiedOrderResult result = payService
.switchoverTo("1234567890")
.unifiedOrder(request);
```

### 3. 动态添加配置

```java
// 运行时动态添加新的 appId 配置
WxPayConfig newConfig = new WxPayConfig();
newConfig.setMchId("1234567890");
newConfig.setAppId("wx4444444444444444");
// ... 其他配置

payService.addConfig("1234567890", "wx4444444444444444", newConfig);

// 切换到新添加的配置
payService.switchover("1234567890", "wx4444444444444444");
```

### 4. 移除配置

```java
// 移除特定的 appId 配置
payService.removeConfig("1234567890", "wx1111111111111111");
```

## 实际应用场景

### 场景1:根据用户来源切换 appId

```java
// 在支付前,根据订单来源切换到对应小程序的配置
String orderSource = order.getSource(); // 例如: "miniapp1", "miniapp2"
String appId = getAppIdBySource(orderSource);

// 精确切换到特定小程序
payService.switchover(mchId, appId);

// 创建订单
WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
// ... 设置订单参数
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
```

### 场景2:处理支付回调

```java
@PostMapping("/pay/notify")
public String handlePayNotify(@RequestBody String xmlData) {
try {
// 解析回调通知
WxPayOrderNotifyResult notifyResult = payService.parseOrderNotifyResult(xmlData);

// 注意:parseOrderNotifyResult 方法内部会自动调用
// switchover(notifyResult.getMchId(), notifyResult.getAppid())
// 切换到正确的配置进行签名验证

// 处理业务逻辑
processOrder(notifyResult);

return WxPayNotifyResponse.success("成功");
} catch (WxPayException e) {
log.error("支付回调处理失败", e);
return WxPayNotifyResponse.fail("失败");
}
}
```

### 场景3:不关心具体 appId 的场景

```java
// 某些场景下,只要是该商户号的配置即可,不关心具体是哪个 appId
// 例如:查询订单、退款等操作

// 仅使用商户号切换
payService.switchover(mchId);

// 查询订单
WxPayOrderQueryResult queryResult = payService.queryOrder(null, outTradeNo);

// 申请退款
WxPayRefundRequest refundRequest = new WxPayRefundRequest();
// ... 设置退款参数
WxPayRefundResult refundResult = payService.refund(refundRequest);
```

## 注意事项

1. **向后兼容**:所有原有的使用方式继续有效,不需要修改现有代码。

2. **配置隔离**:每个 `mchId + appId` 组合都是独立的配置,修改一个配置不会影响其他配置。

3. **线程安全**:配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的。

4. **自动切换**:在处理支付回调时,SDK 会自动根据回调中的 `mchId``appId` 切换到正确的配置。

5. **推荐实践**
- 如果知道具体的 appId,建议使用精确切换方式,避免歧义
- 如果使用仅商户号切换,确保该商户号下至少有一个可用的配置

## 相关 API

| 方法 | 参数 | 返回值 | 说明 |
|-----|------|--------|------|
| `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置 |
| `switchover(String mchId)` | 商户号 | 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 | 移除指定配置 |
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ public interface WxPayService {
*/
boolean switchover(String mchId, String appId);

/**
* 仅根据商户号进行切换.
* 适用于一个商户号对应多个appId的场景,切换时会匹配第一个符合该商户号的配置.
*
* @param mchId 商户标识
* @return 切换是否成功 boolean
*/
boolean switchover(String mchId);
Copy link

Choose a reason for hiding this comment

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

WxPayService 是对外公开接口,新增抽象方法 switchover(String mchId) / switchoverTo(String mchId) 会让任何自定义实现该接口的三方代码在升级后直接编译失败;这点在 PR 描述的“向后兼容”语境里可能需要明确说明。

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

Comment on lines +81 to +88
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

方法的 Javadoc 注释中提到"切换时会匹配第一个符合该商户号的配置",但没有说明在找不到配置时的行为。建议补充说明:当找不到匹配的配置时,方法返回 false。另外,"第一个"的定义不够明确,建议说明这是基于 HashMap 的迭代顺序,因此是不确定的。

Copilot uses AI. Check for mistakes.

/**
* 进行相应的商户切换.
*
Expand All @@ -87,6 +96,15 @@ public interface WxPayService {
*/
WxPayService switchoverTo(String mchId, String appId);

/**
* 仅根据商户号进行切换.
* 适用于一个商户号对应多个appId的场景,切换时会匹配第一个符合该商户号的配置.
*
* @param mchId 商户标识
* @return 切换成功 ,则返回当前对象,方便链式调用,否则抛出异常
*/
WxPayService switchoverTo(String mchId);
Comment on lines +99 to +106
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

方法的 Javadoc 注释不够完整。建议补充以下信息:

  1. 当找不到匹配配置时,会抛出 WxRuntimeException 异常
  2. "第一个"的定义不明确,建议说明这是基于 HashMap 的迭代顺序
  3. 建议添加 @throws 标签说明可能抛出的异常类型和情况

Copilot uses AI. Check for mistakes.

/**
* 发送post请求,得到响应字节数组.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,28 @@ public boolean switchover(String mchId, String appId) {
return false;
}

@Override
public boolean switchover(String mchId) {
// 先尝试精确匹配(针对只有mchId没有appId的配置)
if (this.configMap.containsKey(mchId)) {
WxPayConfigHolder.set(mchId);
return true;
}

// 尝试前缀匹配(查找以 mchId_ 开头的配置)
String prefix = mchId + "_";
for (String key : this.configMap.keySet()) {
Copy link

Choose a reason for hiding this comment

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

这里通过遍历 configMap.keySet() 找到“第一个” mchId_ 前缀匹配项,但 setMultiConfig 内部会把入参拷贝成 HashMap,遍历顺序不保证稳定,可能导致同一 mchId 在不同运行/环境下切到不同 appId 配置(switchoverTo(String mchId) 里的同逻辑也一样)。如果业务对具体 appId 有隐含要求,这种不确定性会比较难排查。

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

if (key.startsWith(prefix)) {
WxPayConfigHolder.set(key);
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
return true;
}
Comment on lines +223 to +230
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

当同一商户号配置了多个 appId 时,前缀匹配会返回 configMap.keySet() 中遇到的第一个匹配项。由于 HashMap 的迭代顺序不确定,这会导致非确定性行为,每次调用可能返回不同的配置。这种不可预测性可能在生产环境中引起问题。建议在文档或方法注释中明确说明这一行为,或者考虑使用确定的排序方式(如字典序)来选择配置,以保证行为的可预测性。

Copilot uses AI. Check for mistakes.
}

log.error("无法找到对应mchId=【{}】的商户号配置信息,请核实!", mchId);
return false;
}
Comment on lines +216 to +235
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

新增的 switchover(String mchId) 方法存在输入参数校验缺失的问题。当 mchId 参数为 null 或空字符串时,会导致 NullPointerException 或产生无意义的前缀匹配(如 "_")。建议在方法开始处添加参数校验,例如使用 StringUtils.isBlank(mchId) 进行检查,如果为空则直接返回 false 或抛出适当的异常。

Copilot uses AI. Check for mistakes.

@Override
public WxPayService switchoverTo(String mchId, String appId) {
String configKey = this.getConfigKey(mchId, appId);
Expand All @@ -222,6 +244,27 @@ public WxPayService switchoverTo(String mchId, String appId) {
throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】,appId=【%s】的商户号配置信息,请核实!", mchId, appId));
}

@Override
public WxPayService switchoverTo(String 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));
}
Comment on lines +248 to +266
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

新增的 switchoverTo(String mchId) 方法同样缺少输入参数校验。当 mchId 参数为 null 或空字符串时会导致运行时错误。建议在方法开始处添加参数校验,如果参数无效则抛出明确的异常信息。

Copilot uses AI. Check for mistakes.

public String getConfigKey(String mchId, String appId) {
return mchId + "_" + appId;
}
Expand Down
Loading
Loading