# 支持一个商户号对应多个 appId 的使用说明 ## 背景 在实际业务中,经常会遇到一个微信支付商户号需要绑定多个小程序的场景。例如: - 一个商家有多个小程序(主店、分店、活动小程序等) - 所有小程序共用同一个支付商户号 - 支付配置(商户号、密钥、证书等)完全相同,只有 appId 不同 ## 解决方案 WxJava 支持在配置多个相同商户号、不同 appId 的情况下,**可以仅通过商户号进行配置切换**,无需每次都指定 appId。 ## 使用方式 ### 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 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. 获取配置的方式 #### 方式一:直接获取配置(推荐,新功能) 直接通过商户号和 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. 切换配置的方式 #### 方式一:精确切换(原有方式,向后兼容) ```java // 切换到小程序1的配置 payService.switchover("1234567890", "wx1111111111111111"); // 切换到小程序2的配置 payService.switchover("1234567890", "wx2222222222222222"); ``` #### 方式二:仅使用商户号切换(新功能) ```java // 仅使用商户号切换,会自动匹配该商户号的某个配置 // 适用于不关心具体使用哪个 appId 的场景 boolean success = payService.switchover("1234567890"); ``` **注意**:当使用仅商户号切换时,会按照以下逻辑查找配置: 1. 先尝试精确匹配商户号(针对只配置商户号、没有 appId 的情况) 2. 如果未找到,则尝试前缀匹配(查找以 `商户号_` 开头的配置) 3. 如果有多个匹配项,将返回其中任意一个匹配项,具体选择结果不保证稳定或可预测,如需确定性行为请使用精确匹配方式(同时指定商户号和 appId) #### 方式三:链式调用 ```java // 精确切换,支持链式调用 WxPayUnifiedOrderResult result = payService .switchoverTo("1234567890", "wx1111111111111111") .unifiedOrder(request); // 仅商户号切换,支持链式调用 WxPayUnifiedOrderResult result = payService .switchoverTo("1234567890") .unifiedOrder(request); ``` ### 4. 动态添加配置 ```java // 运行时动态添加新的 appId 配置 WxPayConfig newConfig = new WxPayConfig(); newConfig.setMchId("1234567890"); newConfig.setAppId("wx4444444444444444"); // ... 其他配置 payService.addConfig("1234567890", "wx4444444444444444", newConfig); // 切换到新添加的配置 payService.switchover("1234567890", "wx4444444444444444"); ``` ### 5. 移除配置 ```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); ``` ### 场景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()); // ... 执行退款操作 } ``` ## 新增方法对比 | 方法 | 说明 | 是否依赖 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`),是线程安全的 - 直接获取配置方法(`getConfig(mchId, appId)`)不依赖 ThreadLocal,可以在任何上下文中安全使用 4. **自动切换**:在处理支付回调时,SDK 会自动根据回调中的 `mchId` 和 `appId` 切换到正确的配置。 5. **推荐实践**: - 如果知道具体的 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 | 精确切换,支持链式调用 | | `switchoverTo(String mchId)` | 商户号 | WxPayService | 仅商户号切换,支持链式调用 | | `addConfig(String mchId, String appId, WxPayConfig)` | 商户号, appId, 配置 | void | 动态添加配置 | | `removeConfig(String mchId, String appId)` | 商户号, appId | void | 移除指定配置 |