Skip to content

Commit 12db287

Browse files
authored
🎨 #3849 【微信支付】支持一个商户号配置多个小程序appId
1 parent 12a9f83 commit 12db287

File tree

5 files changed

+716
-0
lines changed

5 files changed

+716
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# 支持一个商户号对应多个 appId 的使用说明
2+
3+
## 背景
4+
5+
在实际业务中,经常会遇到一个微信支付商户号需要绑定多个小程序的场景。例如:
6+
- 一个商家有多个小程序(主店、分店、活动小程序等)
7+
- 所有小程序共用同一个支付商户号
8+
- 支付配置(商户号、密钥、证书等)完全相同,只有 appId 不同
9+
10+
## 解决方案
11+
12+
WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。
13+
14+
## 使用方式
15+
16+
### 1. 配置多个 appId
17+
18+
```java
19+
WxPayService payService = new WxPayServiceImpl();
20+
21+
String mchId = "1234567890"; // 商户号
22+
23+
// 配置小程序1
24+
WxPayConfig config1 = new WxPayConfig();
25+
config1.setMchId(mchId);
26+
config1.setAppId("wx1111111111111111"); // 小程序1的appId
27+
config1.setMchKey("your_mch_key");
28+
config1.setApiV3Key("your_api_v3_key");
29+
// ... 其他配置
30+
31+
// 配置小程序2
32+
WxPayConfig config2 = new WxPayConfig();
33+
config2.setMchId(mchId);
34+
config2.setAppId("wx2222222222222222"); // 小程序2的appId
35+
config2.setMchKey("your_mch_key");
36+
config2.setApiV3Key("your_api_v3_key");
37+
// ... 其他配置
38+
39+
// 配置小程序3
40+
WxPayConfig config3 = new WxPayConfig();
41+
config3.setMchId(mchId);
42+
config3.setAppId("wx3333333333333333"); // 小程序3的appId
43+
config3.setMchKey("your_mch_key");
44+
config3.setApiV3Key("your_api_v3_key");
45+
// ... 其他配置
46+
47+
// 添加到配置映射
48+
Map<String, WxPayConfig> configMap = new HashMap<>();
49+
configMap.put(mchId + "_" + config1.getAppId(), config1);
50+
configMap.put(mchId + "_" + config2.getAppId(), config2);
51+
configMap.put(mchId + "_" + config3.getAppId(), config3);
52+
53+
payService.setMultiConfig(configMap);
54+
```
55+
56+
### 2. 切换配置的方式
57+
58+
#### 方式一:精确切换(原有方式,向后兼容)
59+
60+
```java
61+
// 切换到小程序1的配置
62+
payService.switchover("1234567890", "wx1111111111111111");
63+
64+
// 切换到小程序2的配置
65+
payService.switchover("1234567890", "wx2222222222222222");
66+
```
67+
68+
#### 方式二:仅使用商户号切换(新功能)
69+
70+
```java
71+
// 仅使用商户号切换,会自动匹配该商户号的某个配置
72+
// 适用于不关心具体使用哪个 appId 的场景
73+
boolean success = payService.switchover("1234567890");
74+
```
75+
76+
**注意**:当使用仅商户号切换时,会按照以下逻辑查找配置:
77+
1. 先尝试精确匹配商户号(针对只配置商户号、没有 appId 的情况)
78+
2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置)
79+
3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId)
80+
81+
#### 方式三:链式调用
82+
83+
```java
84+
// 精确切换,支持链式调用
85+
WxPayUnifiedOrderResult result = payService
86+
.switchoverTo("1234567890", "wx1111111111111111")
87+
.unifiedOrder(request);
88+
89+
// 仅商户号切换,支持链式调用
90+
WxPayUnifiedOrderResult result = payService
91+
.switchoverTo("1234567890")
92+
.unifiedOrder(request);
93+
```
94+
95+
### 3. 动态添加配置
96+
97+
```java
98+
// 运行时动态添加新的 appId 配置
99+
WxPayConfig newConfig = new WxPayConfig();
100+
newConfig.setMchId("1234567890");
101+
newConfig.setAppId("wx4444444444444444");
102+
// ... 其他配置
103+
104+
payService.addConfig("1234567890", "wx4444444444444444", newConfig);
105+
106+
// 切换到新添加的配置
107+
payService.switchover("1234567890", "wx4444444444444444");
108+
```
109+
110+
### 4. 移除配置
111+
112+
```java
113+
// 移除特定的 appId 配置
114+
payService.removeConfig("1234567890", "wx1111111111111111");
115+
```
116+
117+
## 实际应用场景
118+
119+
### 场景1:根据用户来源切换 appId
120+
121+
```java
122+
// 在支付前,根据订单来源切换到对应小程序的配置
123+
String orderSource = order.getSource(); // 例如: "miniapp1", "miniapp2"
124+
String appId = getAppIdBySource(orderSource);
125+
126+
// 精确切换到特定小程序
127+
payService.switchover(mchId, appId);
128+
129+
// 创建订单
130+
WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
131+
// ... 设置订单参数
132+
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
133+
```
134+
135+
### 场景2:处理支付回调
136+
137+
```java
138+
@PostMapping("/pay/notify")
139+
public String handlePayNotify(@RequestBody String xmlData) {
140+
try {
141+
// 解析回调通知
142+
WxPayOrderNotifyResult notifyResult = payService.parseOrderNotifyResult(xmlData);
143+
144+
// 注意:parseOrderNotifyResult 方法内部会自动调用
145+
// switchover(notifyResult.getMchId(), notifyResult.getAppid())
146+
// 切换到正确的配置进行签名验证
147+
148+
// 处理业务逻辑
149+
processOrder(notifyResult);
150+
151+
return WxPayNotifyResponse.success("成功");
152+
} catch (WxPayException e) {
153+
log.error("支付回调处理失败", e);
154+
return WxPayNotifyResponse.fail("失败");
155+
}
156+
}
157+
```
158+
159+
### 场景3:不关心具体 appId 的场景
160+
161+
```java
162+
// 某些场景下,只要是该商户号的配置即可,不关心具体是哪个 appId
163+
// 例如:查询订单、退款等操作
164+
165+
// 仅使用商户号切换
166+
payService.switchover(mchId);
167+
168+
// 查询订单
169+
WxPayOrderQueryResult queryResult = payService.queryOrder(null, outTradeNo);
170+
171+
// 申请退款
172+
WxPayRefundRequest refundRequest = new WxPayRefundRequest();
173+
// ... 设置退款参数
174+
WxPayRefundResult refundResult = payService.refund(refundRequest);
175+
```
176+
177+
## 注意事项
178+
179+
1. **向后兼容**:所有原有的使用方式继续有效,不需要修改现有代码。
180+
181+
2. **配置隔离**:每个 `mchId + appId` 组合都是独立的配置,修改一个配置不会影响其他配置。
182+
183+
3. **线程安全**:配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的。
184+
185+
4. **自动切换**:在处理支付回调时,SDK 会自动根据回调中的 `mchId``appId` 切换到正确的配置。
186+
187+
5. **推荐实践**
188+
- 如果知道具体的 appId,建议使用精确切换方式,避免歧义
189+
- 如果使用仅商户号切换,确保该商户号下至少有一个可用的配置
190+
191+
## 相关 API
192+
193+
| 方法 | 参数 | 返回值 | 说明 |
194+
|-----|------|--------|------|
195+
| `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置 |
196+
| `switchover(String mchId)` | 商户号 | boolean | 仅使用商户号切换 |
197+
| `switchoverTo(String mchId, String appId)` | 商户号, appId | WxPayService | 精确切换,支持链式调用 |
198+
| `switchoverTo(String mchId)` | 商户号 | WxPayService | 仅商户号切换,支持链式调用 |
199+
| `addConfig(String mchId, String appId, WxPayConfig)` | 商户号, appId, 配置 | void | 动态添加配置 |
200+
| `removeConfig(String mchId, String appId)` | 商户号, appId | void | 移除指定配置 |

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ public interface WxPayService {
7878
*/
7979
boolean switchover(String mchId, String appId);
8080

81+
/**
82+
* 仅根据商户号进行切换.
83+
* 适用于一个商户号对应多个appId的场景,切换时会匹配符合该商户号的配置.
84+
* 注意:由于HashMap迭代顺序不确定,当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
85+
*
86+
* @param mchId 商户标识
87+
* @return 切换是否成功,如果找不到匹配的配置则返回false
88+
*/
89+
default boolean switchover(String mchId) {
90+
return false;
91+
}
92+
8193
/**
8294
* 进行相应的商户切换.
8395
*
@@ -87,6 +99,19 @@ public interface WxPayService {
8799
*/
88100
WxPayService switchoverTo(String mchId, String appId);
89101

102+
/**
103+
* 仅根据商户号进行切换.
104+
* 适用于一个商户号对应多个appId的场景,切换时会匹配符合该商户号的配置.
105+
* 注意:由于HashMap迭代顺序不确定,当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
106+
*
107+
* @param mchId 商户标识
108+
* @return 切换成功,则返回当前对象,方便链式调用
109+
* @throws me.chanjar.weixin.common.error.WxRuntimeException 如果找不到匹配的配置
110+
*/
111+
default WxPayService switchoverTo(String mchId) {
112+
throw new me.chanjar.weixin.common.error.WxRuntimeException("子类需要实现此方法");
113+
}
114+
90115
/**
91116
* 发送post请求,得到响应字节数组.
92117
*

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,34 @@ public boolean switchover(String mchId, String appId) {
212212
return false;
213213
}
214214

215+
@Override
216+
public boolean switchover(String mchId) {
217+
// 参数校验
218+
if (StringUtils.isBlank(mchId)) {
219+
log.error("商户号mchId不能为空");
220+
return false;
221+
}
222+
223+
// 先尝试精确匹配(针对只有mchId没有appId的配置)
224+
if (this.configMap.containsKey(mchId)) {
225+
WxPayConfigHolder.set(mchId);
226+
return true;
227+
}
228+
229+
// 尝试前缀匹配(查找以 mchId_ 开头的配置)
230+
String prefix = mchId + "_";
231+
for (String key : this.configMap.keySet()) {
232+
if (key.startsWith(prefix)) {
233+
WxPayConfigHolder.set(key);
234+
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
235+
return true;
236+
}
237+
}
238+
239+
log.error("无法找到对应mchId=【{}】的商户号配置信息,请核实!", mchId);
240+
return false;
241+
}
242+
215243
@Override
216244
public WxPayService switchoverTo(String mchId, String appId) {
217245
String configKey = this.getConfigKey(mchId, appId);
@@ -222,6 +250,32 @@ public WxPayService switchoverTo(String mchId, String appId) {
222250
throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】,appId=【%s】的商户号配置信息,请核实!", mchId, appId));
223251
}
224252

253+
@Override
254+
public WxPayService switchoverTo(String mchId) {
255+
// 参数校验
256+
if (StringUtils.isBlank(mchId)) {
257+
throw new WxRuntimeException("商户号mchId不能为空");
258+
}
259+
260+
// 先尝试精确匹配(针对只有mchId没有appId的配置)
261+
if (this.configMap.containsKey(mchId)) {
262+
WxPayConfigHolder.set(mchId);
263+
return this;
264+
}
265+
266+
// 尝试前缀匹配(查找以 mchId_ 开头的配置)
267+
String prefix = mchId + "_";
268+
for (String key : this.configMap.keySet()) {
269+
if (key.startsWith(prefix)) {
270+
WxPayConfigHolder.set(key);
271+
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
272+
return this;
273+
}
274+
}
275+
276+
throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】的商户号配置信息,请核实!", mchId));
277+
}
278+
225279
public String getConfigKey(String mchId, String appId) {
226280
return mchId + "_" + appId;
227281
}

0 commit comments

Comments
 (0)