Skip to content

Commit d661d0c

Browse files
authored
🎨 #3863 【微信支付】添加直接获取配置的方法,解决多商户管理场景下的 ThreadLocal 限制
1 parent e93380c commit d661d0c

File tree

4 files changed

+287
-6
lines changed

4 files changed

+287
-6
lines changed

weixin-java-pay/MULTI_APPID_USAGE.md

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,64 @@ configMap.put(mchId + "_" + config3.getAppId(), config3);
5353
payService.setMultiConfig(configMap);
5454
```
5555

56-
### 2. 切换配置的方式
56+
### 2. 获取配置的方式
57+
58+
#### 方式一:直接获取配置(推荐,新功能)
59+
60+
直接通过商户号和 appId 获取配置,**不依赖 ThreadLocal**,适用于多商户管理场景:
61+
62+
```java
63+
// 精确获取指定商户号和 appId 的配置
64+
WxPayConfig config1 = payService.getConfig("1234567890", "wx1111111111111111");
65+
66+
// 仅使用商户号获取配置(会返回该商户号的任意一个配置)
67+
// 注意:当存在多个 appId 时,返回结果基于内部存储顺序,不应依赖其稳定性
68+
WxPayConfig config = payService.getConfig("1234567890");
69+
70+
// 使用获取的配置读取信息(仅用于读取配置,不用于执行支付操作)
71+
if (config != null) {
72+
String appId = config.getAppId();
73+
String mchKey = config.getMchKey();
74+
String apiV3Key = config.getApiV3Key();
75+
// ... 使用配置信息进行业务逻辑判断或记录
76+
}
77+
```
78+
79+
**优势**
80+
- 不依赖 ThreadLocal,可以在任何上下文中使用
81+
- 适合在异步场景、线程池等环境中使用
82+
- 线程安全,不会因为线程切换导致配置丢失
83+
- 可以同时获取多个不同的配置
84+
85+
**使用场景**
86+
- 仅需读取配置信息(如获取 mchKey、appId 等)
87+
- 不需要执行 WxPayService 的支付相关方法
88+
- 如需执行支付操作,请使用方式二的 switchover 方法
89+
90+
#### 方式二:切换配置后使用(原有方式)
91+
92+
通过切换配置,然后调用 `getConfig()` 获取当前配置或直接执行支付操作:
93+
94+
```java
95+
// 精确切换到指定的配置
96+
payService.switchover("1234567890", "wx1111111111111111");
97+
WxPayConfig config = payService.getConfig(); // 获取当前切换的配置
98+
99+
// 仅使用商户号切换
100+
payService.switchover("1234567890");
101+
config = payService.getConfig(); // 获取切换后的配置
102+
103+
// 切换后可直接执行支付操作
104+
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
105+
```
106+
107+
**注意**:此方式依赖 ThreadLocal,需要注意线程上下文的问题。
108+
109+
**使用场景**
110+
- 需要执行 WxPayService 的支付相关方法(如 unifiedOrder、refund 等)
111+
- 在同一线程中连续执行多个支付操作
112+
113+
### 3. 切换配置的方式
57114

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

@@ -92,7 +149,7 @@ WxPayUnifiedOrderResult result = payService
92149
.unifiedOrder(request);
93150
```
94151

95-
### 3. 动态添加配置
152+
### 4. 动态添加配置
96153

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

110-
### 4. 移除配置
167+
### 5. 移除配置
111168

112169
```java
113170
// 移除特定的 appId 配置
@@ -174,24 +231,78 @@ WxPayRefundRequest refundRequest = new WxPayRefundRequest();
174231
WxPayRefundResult refundResult = payService.refund(refundRequest);
175232
```
176233

234+
### 场景4:多商户管理(推荐使用直接获取配置)
235+
236+
```java
237+
// 在多商户管理系统中,可以直接获取指定商户的配置
238+
// 这种方式不依赖 ThreadLocal,适合异步场景和线程池环境
239+
240+
public void processMerchantOrder(String mchId, String appId, Order order) {
241+
// 直接获取配置,无需切换
242+
WxPayConfig config = payService.getConfig(mchId, appId);
243+
244+
if (config == null) {
245+
log.error("找不到商户配置:mchId={}, appId={}", mchId, appId);
246+
return;
247+
}
248+
249+
// 使用配置信息
250+
String merchantKey = config.getMchKey();
251+
String apiV3Key = config.getApiV3Key();
252+
253+
// ... 处理订单逻辑
254+
}
255+
256+
// 或者在不确定 appId 的情况下,仅通过商户号发起退款
257+
public void processRefund(String mchId, String outTradeNo) {
258+
// 直接根据商户号切换(内部会选择该商户号下的一个配置)
259+
if (!payService.switchover(mchId)) {
260+
log.error("商户配置切换失败:mchId={}", mchId);
261+
return;
262+
}
263+
264+
// 在完成上下文切换后,执行退款操作
265+
WxPayRefundRequest request = new WxPayRefundRequest();
266+
request.setOutTradeNo(outTradeNo);
267+
// ... 设置其他退款参数
268+
WxPayRefundResult refundResult = payService.refund(request);
269+
}
270+
```
271+
272+
## 新增方法对比
273+
274+
| 方法 | 说明 | 是否依赖 ThreadLocal | 适用场景 |
275+
|-----|------|---------------------|---------|
276+
| `getConfig()` | 获取当前配置 || 单线程同步场景 |
277+
| `getConfig(String mchId, String appId)` | 直接获取指定配置 | **** | 多商户管理、异步场景、线程池 |
278+
| `getConfig(String mchId)` | 根据商户号获取配置 | **** | 不确定 appId 的场景 |
279+
| `switchover(String mchId, String appId)` | 精确切换配置 || 需要切换上下文的场景 |
280+
| `switchover(String mchId)` | 根据商户号切换 || 不关心 appId 的切换场景 |
281+
177282
## 注意事项
178283

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

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

183-
3. **线程安全**:配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的。
288+
3. **线程安全**
289+
- 配置切换使用 `WxPayConfigHolder`(基于 `ThreadLocal`),是线程安全的
290+
- 直接获取配置方法(`getConfig(mchId, appId)`)不依赖 ThreadLocal,可以在任何上下文中安全使用
184291

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

187294
5. **推荐实践**
188-
- 如果知道具体的 appId,建议使用精确切换方式,避免歧义
189-
- 如果使用仅商户号切换,确保该商户号下至少有一个可用的配置
295+
- 如果知道具体的 appId,建议使用精确切换或获取方式,避免歧义
296+
- 在多商户管理、异步场景、线程池等环境中,建议使用 `getConfig(mchId, appId)` 直接获取配置
297+
- 如果使用仅商户号切换或获取,确保该商户号下至少有一个可用的配置
190298

191299
## 相关 API
192300

193301
| 方法 | 参数 | 返回值 | 说明 |
194302
|-----|------|--------|------|
303+
| `getConfig()` || WxPayConfig | 获取当前配置(依赖 ThreadLocal) |
304+
| `getConfig(String mchId, String appId)` | 商户号, appId | WxPayConfig | 直接获取指定配置(不依赖 ThreadLocal) |
305+
| `getConfig(String mchId)` | 商户号 | WxPayConfig | 根据商户号获取配置(不依赖 ThreadLocal) |
195306
| `switchover(String mchId, String appId)` | 商户号, appId | boolean | 精确切换到指定配置 |
196307
| `switchover(String mchId)` | 商户号 | boolean | 仅使用商户号切换 |
197308
| `switchoverTo(String mchId, String appId)` | 商户号, appId | WxPayService | 精确切换,支持链式调用 |

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,11 +785,33 @@ default WxPayService switchoverTo(String mchId) {
785785

786786
/**
787787
* 获取配置.
788+
* 在多商户配置场景下,会根据 WxPayConfigHolder 中的值获取对应的配置.
788789
*
789790
* @return the config
790791
*/
791792
WxPayConfig getConfig();
792793

794+
/**
795+
* 根据商户号和 appId 直接获取配置.
796+
* 此方法不依赖 ThreadLocal,可以在任何上下文中使用,适用于多商户管理场景.
797+
*
798+
* @param mchId 商户号
799+
* @param appId 微信应用 id
800+
* @return 对应的配置对象,如果不存在则返回 null
801+
*/
802+
WxPayConfig getConfig(String mchId, String appId);
803+
804+
/**
805+
* 根据商户号直接获取配置.
806+
* 此方法不依赖 ThreadLocal,可以在任何上下文中使用.
807+
* 适用于一个商户号对应多个 appId 的场景,会返回该商户号的任意一个配置.
808+
* 注意:当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
809+
*
810+
* @param mchId 商户号
811+
* @return 对应的配置对象,如果不存在则返回 null
812+
*/
813+
WxPayConfig getConfig(String mchId);
814+
793815
/**
794816
* 设置配置对象.
795817
*

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,47 @@ public WxPayConfig getConfig() {
154154
return this.configMap.get(WxPayConfigHolder.get());
155155
}
156156

157+
@Override
158+
public WxPayConfig getConfig(String mchId, String appId) {
159+
if (StringUtils.isBlank(mchId)) {
160+
log.warn("商户号mchId不能为空");
161+
return null;
162+
}
163+
if (StringUtils.isBlank(appId)) {
164+
log.warn("应用ID appId不能为空");
165+
return null;
166+
}
167+
String configKey = this.getConfigKey(mchId, appId);
168+
return this.configMap.get(configKey);
169+
}
170+
171+
@Override
172+
public WxPayConfig getConfig(String mchId) {
173+
if (StringUtils.isBlank(mchId)) {
174+
log.warn("商户号mchId不能为空");
175+
return null;
176+
}
177+
178+
// 先尝试精确匹配(针对只有mchId没有appId的配置)
179+
if (this.configMap.containsKey(mchId)) {
180+
return this.configMap.get(mchId);
181+
}
182+
183+
// 尝试前缀匹配(查找以 mchId_ 开头的配置)
184+
String prefix = mchId + "_";
185+
return this.configMap.entrySet().stream()
186+
.filter(entry -> entry.getKey().startsWith(prefix))
187+
.findFirst()
188+
.map(entry -> {
189+
log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
190+
return entry.getValue();
191+
})
192+
.orElseGet(() -> {
193+
log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
194+
return null;
195+
});
196+
}
197+
157198
@Override
158199
public void setConfig(WxPayConfig config) {
159200
final String defaultKey = this.getConfigKey(config.getMchId(), config.getAppId());

weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,113 @@ public void setup() {
5252
payService.setMultiConfig(configMap);
5353
}
5454

55+
/**
56+
* 测试直接通过 mchId 和 appId 获取配置(新功能)
57+
*/
58+
@Test
59+
public void testGetConfigWithMchIdAndAppId() {
60+
// 测试获取第一个配置
61+
WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
62+
assertNotNull(config1, "应该能够获取到配置");
63+
assertEquals(config1.getMchId(), testMchId);
64+
assertEquals(config1.getAppId(), testAppId1);
65+
assertEquals(config1.getMchKey(), "test_key_1");
66+
67+
// 测试获取第二个配置
68+
WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
69+
assertNotNull(config2);
70+
assertEquals(config2.getAppId(), testAppId2);
71+
assertEquals(config2.getMchKey(), "test_key_2");
72+
73+
// 测试获取第三个配置
74+
WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
75+
assertNotNull(config3);
76+
assertEquals(config3.getAppId(), testAppId3);
77+
assertEquals(config3.getMchKey(), "test_key_3");
78+
}
79+
80+
/**
81+
* 测试直接通过 mchId 获取配置(新功能)
82+
*/
83+
@Test
84+
public void testGetConfigWithMchIdOnly() {
85+
WxPayConfig config = payService.getConfig(testMchId);
86+
assertNotNull(config, "应该能够通过mchId获取配置");
87+
assertEquals(config.getMchId(), testMchId);
88+
89+
// appId应该是三个中的一个
90+
String currentAppId = config.getAppId();
91+
assertTrue(
92+
testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId),
93+
"获取的配置的appId应该是配置的appId之一"
94+
);
95+
}
96+
97+
/**
98+
* 测试 getConfig 方法不依赖 ThreadLocal
99+
* 在不切换配置的情况下也能直接获取
100+
*/
101+
@Test
102+
public void testGetConfigWithoutSwitchover() {
103+
// 不进行任何switchover操作,直接通过参数获取配置
104+
WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
105+
WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
106+
WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
107+
108+
// 验证可以同时获取到所有配置,不受 ThreadLocal 影响
109+
assertNotNull(config1);
110+
assertNotNull(config2);
111+
assertNotNull(config3);
112+
113+
assertEquals(config1.getAppId(), testAppId1);
114+
assertEquals(config2.getAppId(), testAppId2);
115+
assertEquals(config3.getAppId(), testAppId3);
116+
}
117+
118+
/**
119+
* 测试 getConfig 方法处理不存在的配置
120+
*/
121+
@Test
122+
public void testGetConfigWithNonexistentConfig() {
123+
// 测试不存在的商户号和appId组合
124+
WxPayConfig config = payService.getConfig("nonexistent_mch_id", "nonexistent_app_id");
125+
assertNull(config, "获取不存在的配置应该返回null");
126+
127+
// 测试存在商户号但不存在的appId
128+
config = payService.getConfig(testMchId, "wx9999999999999999");
129+
assertNull(config, "获取不存在的appId配置应该返回null");
130+
}
131+
132+
/**
133+
* 测试 getConfig 方法处理空参数或null参数
134+
*/
135+
@Test
136+
public void testGetConfigWithNullOrEmptyParameters() {
137+
// 测试 null 商户号
138+
WxPayConfig config = payService.getConfig(null, testAppId1);
139+
assertNull(config, "商户号为null时应该返回null");
140+
141+
// 测试空商户号
142+
config = payService.getConfig("", testAppId1);
143+
assertNull(config, "商户号为空字符串时应该返回null");
144+
145+
// 测试 null appId
146+
config = payService.getConfig(testMchId, null);
147+
assertNull(config, "appId为null时应该返回null");
148+
149+
// 测试空 appId
150+
config = payService.getConfig(testMchId, "");
151+
assertNull(config, "appId为空字符串时应该返回null");
152+
153+
// 测试仅mchId方法的null参数
154+
config = payService.getConfig((String) null);
155+
assertNull(config, "商户号为null时应该返回null");
156+
157+
// 测试仅mchId方法的空字符串
158+
config = payService.getConfig("");
159+
assertNull(config, "商户号为空字符串时应该返回null");
160+
}
161+
55162
/**
56163
* 测试使用 mchId + appId 精确切换(原有功能,确保向后兼容)
57164
*/

0 commit comments

Comments
 (0)