🆕 #3842 【微信支付】添加 wx-java-pay-multi-spring-boot-starter 模块支持多公众号关联配置
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled

This commit is contained in:
Copilot
2026-01-16 16:33:04 +08:00
committed by GitHub
parent 47b4431aa8
commit 12a9f83b98
12 changed files with 1041 additions and 0 deletions

View File

@@ -23,6 +23,7 @@
<module>wx-java-mp-multi-spring-boot-starter</module>
<module>wx-java-mp-spring-boot-starter</module>
<module>wx-java-pay-spring-boot-starter</module>
<module>wx-java-pay-multi-spring-boot-starter</module>
<module>wx-java-open-multi-spring-boot-starter</module>
<module>wx-java-open-spring-boot-starter</module>
<module>wx-java-qidian-spring-boot-starter</module>

View File

@@ -0,0 +1,316 @@
# wx-java-pay-multi-spring-boot-starter
## 快速开始
本starter支持微信支付多公众号关联配置适用于以下场景
- 一个服务商需要为多个公众号提供支付服务
- 一个系统需要支持多个公众号的支付业务
- 需要根据不同的appId动态切换支付配置
## 使用说明
### 1. 引入依赖
在项目的 `pom.xml` 中添加以下依赖:
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-pay-multi-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
```
### 2. 添加配置
`application.yml``application.properties` 中配置多个公众号的支付信息。
#### 配置示例application.yml
##### V2版本配置
```yml
wx:
pay:
configs:
# 配置1 - 可以使用appId作为key
wx1234567890abcdef:
appId: wx1234567890abcdef
mchId: 1234567890
mchKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
keyPath: classpath:cert/app1/apiclient_cert.p12
notifyUrl: https://example.com/pay/notify
# 配置2 - 也可以使用自定义标识作为key
config2:
appId: wx9876543210fedcba
mchId: 9876543210
mchKey: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
keyPath: classpath:cert/app2/apiclient_cert.p12
notifyUrl: https://example.com/pay/notify
```
##### V3版本配置
```yml
wx:
pay:
configs:
# 公众号1配置
wx1234567890abcdef:
appId: wx1234567890abcdef
mchId: 1234567890
apiV3Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/app1/apiclient_key.pem
privateCertPath: classpath:cert/app1/apiclient_cert.pem
notifyUrl: https://example.com/pay/notify
# 公众号2配置
wx9876543210fedcba:
appId: wx9876543210fedcba
mchId: 9876543210
apiV3Key: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
certSerialNo: 73D7DFBB471CDxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/app2/apiclient_key.pem
privateCertPath: classpath:cert/app2/apiclient_cert.pem
notifyUrl: https://example.com/pay/notify
```
##### V3服务商版本配置
```yml
wx:
pay:
configs:
# 服务商为公众号1提供服务
config1:
appId: wxe97b2x9c2b3d # 服务商appId
mchId: 16486610 # 服务商商户号
subAppId: wx118cexxe3c07679 # 子商户公众号appId
subMchId: 16496705 # 子商户号
apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr
privateKeyPath: classpath:cert/apiclient_key.pem
privateCertPath: classpath:cert/apiclient_cert.pem
# 服务商为公众号2提供服务
config2:
appId: wxe97b2x9c2b3d # 服务商appId可以相同
mchId: 16486610 # 服务商商户号(可以相同)
subAppId: wx228dexxf4d18890 # 子商户公众号appId不同
subMchId: 16496706 # 子商户号(不同)
apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr
privateKeyPath: classpath:cert/apiclient_key.pem
privateCertPath: classpath:cert/apiclient_cert.pem
```
#### 配置示例application.properties
```properties
# 公众号1配置
wx.pay.configs.wx1234567890abcdef.app-id=wx1234567890abcdef
wx.pay.configs.wx1234567890abcdef.mch-id=1234567890
wx.pay.configs.wx1234567890abcdef.apiv3-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
wx.pay.configs.wx1234567890abcdef.cert-serial-no=62C6CEAA360BCxxxxxxxxxxxxxxx
wx.pay.configs.wx1234567890abcdef.private-key-path=classpath:cert/app1/apiclient_key.pem
wx.pay.configs.wx1234567890abcdef.private-cert-path=classpath:cert/app1/apiclient_cert.pem
wx.pay.configs.wx1234567890abcdef.notify-url=https://example.com/pay/notify
# 公众号2配置
wx.pay.configs.wx9876543210fedcba.app-id=wx9876543210fedcba
wx.pay.configs.wx9876543210fedcba.mch-id=9876543210
wx.pay.configs.wx9876543210fedcba.apiv3-key=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
wx.pay.configs.wx9876543210fedcba.cert-serial-no=73D7DFBB471CDxxxxxxxxxxxxxxx
wx.pay.configs.wx9876543210fedcba.private-key-path=classpath:cert/app2/apiclient_key.pem
wx.pay.configs.wx9876543210fedcba.private-cert-path=classpath:cert/app2/apiclient_cert.pem
wx.pay.configs.wx9876543210fedcba.notify-url=https://example.com/pay/notify
```
### 3. 使用示例
自动注入的类型:`WxPayMultiServices`
```java
import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.service.WxPayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PayService {
@Autowired
private WxPayMultiServices wxPayMultiServices;
/**
* 为不同的公众号创建支付订单
*
* @param configKey 配置标识(即 wx.pay.configs.&lt;configKey&gt; 中的 key可以是 appId 或自定义标识)
*/
public void createOrder(String configKey, String openId, Integer totalFee, String body) throws Exception {
// 根据配置标识获取对应的WxPayService
WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
if (wxPayService == null) {
throw new IllegalArgumentException("未找到配置标识对应的微信支付配置: " + configKey);
}
// 使用WxPayService进行支付操作
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(generateOutTradeNo());
request.setDescription(body);
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl());
// V3统一下单
WxPayUnifiedOrderV3Result.JsapiResult result =
wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
// 返回给前端用于调起支付
// ...
}
/**
* 服务商模式示例
*/
public void serviceProviderExample(String configKey) throws Exception {
// 使用配置标识获取WxPayService
WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
if (wxPayService == null) {
throw new IllegalArgumentException("未找到配置: " + configKey);
}
// 获取子商户的配置信息
String subAppId = wxPayService.getConfig().getSubAppId();
String subMchId = wxPayService.getConfig().getSubMchId();
// 进行支付操作
// ...
}
/**
* 查询订单示例
*
* @param configKey 配置标识(即 wx.pay.configs.&lt;configKey&gt; 中的 key
*/
public void queryOrder(String configKey, String outTradeNo) throws Exception {
WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
if (wxPayService == null) {
throw new IllegalArgumentException("未找到配置标识对应的微信支付配置: " + configKey);
}
// 查询订单
WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo);
// 处理查询结果
// ...
}
private String generateOutTradeNo() {
// 生成商户订单号
return "ORDER_" + System.currentTimeMillis();
}
}
```
### 4. 配置说明
#### 必填配置项
| 配置项 | 说明 | 示例 |
|--------|------|------|
| appId | 公众号或小程序的appId | wx1234567890abcdef |
| mchId | 商户号 | 1234567890 |
#### V2版本配置项
| 配置项 | 说明 | 是否必填 |
|--------|------|----------|
| mchKey | 商户密钥 | 是V2 |
| keyPath | p12证书文件路径 | 部分接口需要 |
#### V3版本配置项
| 配置项 | 说明 | 是否必填 |
|--------|------|----------|
| apiV3Key | API V3密钥 | 是V3 |
| certSerialNo | 证书序列号 | 是V3 |
| privateKeyPath | apiclient_key.pem路径 | 是V3 |
| privateCertPath | apiclient_cert.pem路径 | 是V3 |
#### 服务商模式配置项
| 配置项 | 说明 | 是否必填 |
|--------|------|----------|
| subAppId | 子商户公众号appId | 服务商模式必填 |
| subMchId | 子商户号 | 服务商模式必填 |
#### 可选配置项
| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| notifyUrl | 支付结果通知URL | 无 |
| refundNotifyUrl | 退款结果通知URL | 无 |
| serviceId | 微信支付分serviceId | 无 |
| payScoreNotifyUrl | 支付分回调地址 | 无 |
| payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 |
| useSandboxEnv | 是否使用沙箱环境 | false |
| apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com |
| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | false |
| fullPublicKeyModel | 是否完全使用公钥模式 | false |
| publicKeyId | 公钥ID | 无 |
| publicKeyPath | 公钥文件路径 | 无 |
## 常见问题
### 1. 如何选择配置的key
配置的key`wx.pay.configs.<configKey>` 中的 `<configKey>` 部分)可以自由选择:
- 可以使用appId作为key`wx.pay.configs.wx1234567890abcdef`),这样调用 `getWxPayService("wx1234567890abcdef")` 时就像直接用 appId 获取服务
- 可以使用自定义标识(如 `wx.pay.configs.config1`),调用时使用 `getWxPayService("config1")`
**注意**`getWxPayService(configKey)` 方法的参数是配置文件中定义的 key而不是 appId。只有当你使用 appId 作为配置 key 时,才能直接传入 appId。
### 2. V2和V3配置可以混用吗
可以。不同的配置可以使用不同的版本,例如:
```yml
wx:
pay:
configs:
app1: # V2配置
appId: wx111
mchId: 111
mchKey: xxx
app2: # V3配置
appId: wx222
mchId: 222
apiV3Key: yyy
privateKeyPath: xxx
```
### 3. 证书文件如何放置?
证书文件可以放在以下位置:
- `src/main/resources` 目录下,使用 `classpath:` 前缀
- 服务器绝对路径,直接填写完整路径
- 建议为不同配置使用不同的目录组织证书
### 4. 服务商模式如何配置?
服务商模式需要同时配置服务商信息和子商户信息:
- `appId``mchId` 填写服务商的信息
- `subAppId``subMchId` 填写子商户的信息
## 注意事项
1. **配置安全**:生产环境中的密钥、证书等敏感信息,建议使用配置中心或环境变量管理
2. **证书管理**:不同公众号的证书文件要分开存放,避免混淆
3. **懒加载**WxPayService 实例采用懒加载策略,只有在首次调用时才会创建
4. **线程安全**WxPayMultiServices 的实现是线程安全的
5. **配置更新**:如需动态更新配置,可调用 `removeWxPayService(configKey)` 方法移除缓存的实例
## 更多信息
- [WxJava 项目首页](https://github.com/Wechat-Group/WxJava)
- [微信支付官方文档](https://pay.weixin.qq.com/wiki/doc/api/)
- [微信支付V3接口文档](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.8.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wx-java-pay-multi-spring-boot-starter</artifactId>
<name>WxJava - Spring Boot Starter for Pay::支持多公众号关联配置</name>
<description>微信支付开发的 Spring Boot Starter::支持多公众号关联配置</description>
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,38 @@
package com.binarywang.spring.starter.wxjava.pay.config;
import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties;
import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServicesImpl;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 微信支付多公众号关联自动配置.
*
* @author Binary Wang
*/
@Slf4j
@Configuration
@EnableConfigurationProperties(WxPayMultiProperties.class)
@ConditionalOnClass(WxPayService.class)
@ConditionalOnProperty(prefix = WxPayMultiProperties.PREFIX, value = "enabled", matchIfMissing = true)
public class WxPayMultiAutoConfiguration {
/**
* 构造微信支付多服务管理对象.
*
* @param wxPayMultiProperties 多配置属性
* @return 微信支付多服务管理对象
*/
@Bean
@ConditionalOnMissingBean(WxPayMultiServices.class)
public WxPayMultiServices wxPayMultiServices(WxPayMultiProperties wxPayMultiProperties) {
return new WxPayMultiServicesImpl(wxPayMultiProperties);
}
}

View File

@@ -0,0 +1,27 @@
package com.binarywang.spring.starter.wxjava.pay.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 微信支付多公众号关联配置属性类.
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
@ConfigurationProperties(WxPayMultiProperties.PREFIX)
public class WxPayMultiProperties implements Serializable {
private static final long serialVersionUID = -8015955705346835955L;
public static final String PREFIX = "wx.pay";
/**
* 多个公众号的配置信息key 可以是 appId 或自定义的标识.
*/
private Map<String, WxPaySingleProperties> configs = new HashMap<>();
}

View File

@@ -0,0 +1,124 @@
package com.binarywang.spring.starter.wxjava.pay.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 微信支付单个公众号配置属性类.
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class WxPaySingleProperties implements Serializable {
private static final long serialVersionUID = 3978986361098922525L;
/**
* 设置微信公众号或者小程序等的appid.
*/
private String appId;
/**
* 微信支付商户号.
*/
private String mchId;
/**
* 微信支付商户密钥.
*/
private String mchKey;
/**
* 服务商模式下的子商户公众账号ID普通模式请不要配置.
*/
private String subAppId;
/**
* 服务商模式下的子商户号,普通模式请不要配置.
*/
private String subMchId;
/**
* apiclient_cert.p12文件的绝对路径或者如果放在项目中请以classpath:开头指定.
*/
private String keyPath;
/**
* 微信支付分serviceId.
*/
private String serviceId;
/**
* 证书序列号.
*/
private String certSerialNo;
/**
* apiV3秘钥.
*/
private String apiv3Key;
/**
* 微信支付异步回调地址通知url必须为直接可访问的url不能携带参数.
*/
private String notifyUrl;
/**
* 退款结果异步回调地址通知url必须为直接可访问的url不能携带参数.
*/
private String refundNotifyUrl;
/**
* 微信支付分回调地址.
*/
private String payScoreNotifyUrl;
/**
* 微信支付分授权回调地址.
*/
private String payScorePermissionNotifyUrl;
/**
* apiv3 商户apiclient_key.pem.
*/
private String privateKeyPath;
/**
* apiv3 商户apiclient_cert.pem.
*/
private String privateCertPath;
/**
* 公钥ID.
*/
private String publicKeyId;
/**
* pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String publicKeyPath;
/**
* 微信支付是否使用仿真测试环境.
* 默认不使用.
*/
private boolean useSandboxEnv = false;
/**
* 自定义API主机地址用于替换默认的 https://api.mch.weixin.qq.com.
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头默认不添加.
*/
private boolean strictlyNeedWechatPaySerial = false;
/**
* 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用.
*/
private boolean fullPublicKeyModel = false;
}

View File

@@ -0,0 +1,33 @@
package com.binarywang.spring.starter.wxjava.pay.service;
import com.github.binarywang.wxpay.service.WxPayService;
/**
* 微信支付 {@link WxPayService} 所有实例存放类.
*
* @author Binary Wang
*/
public interface WxPayMultiServices {
/**
* 通过配置标识获取 WxPayService.
* <p>
* 注意configKey 是配置文件中定义的 key如 wx.pay.configs.&lt;configKey&gt;.xxx
* 而不是 appId。如果使用 appId 作为配置 key则可以直接传入 appId。
* </p>
*
* @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key
* @return WxPayService
*/
WxPayService getWxPayService(String configKey);
/**
* 根据配置标识,从列表中移除一个 WxPayService 实例.
* <p>
* 注意configKey 是配置文件中定义的 key如 wx.pay.configs.&lt;configKey&gt;.xxx
* 而不是 appId。如果使用 appId 作为配置 key则可以直接传入 appId。
* </p>
*
* @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key
*/
void removeWxPayService(String configKey);
}

View File

@@ -0,0 +1,92 @@
package com.binarywang.spring.starter.wxjava.pay.service;
import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties;
import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 微信支付多服务管理实现类.
*
* @author Binary Wang
*/
@Slf4j
public class WxPayMultiServicesImpl implements WxPayMultiServices {
private final Map<String, WxPayService> services = new ConcurrentHashMap<>();
private final WxPayMultiProperties wxPayMultiProperties;
public WxPayMultiServicesImpl(WxPayMultiProperties wxPayMultiProperties) {
this.wxPayMultiProperties = wxPayMultiProperties;
}
@Override
public WxPayService getWxPayService(String configKey) {
if (StringUtils.isBlank(configKey)) {
log.warn("配置标识为空无法获取WxPayService");
return null;
}
// 使用 computeIfAbsent 实现线程安全的懒加载,避免使用 synchronized(this) 带来的性能问题
return services.computeIfAbsent(configKey, key -> {
WxPaySingleProperties properties = wxPayMultiProperties.getConfigs().get(key);
if (properties == null) {
log.warn("未找到配置标识为[{}]的微信支付配置", key);
return null;
}
return this.buildWxPayService(properties);
});
}
@Override
public void removeWxPayService(String configKey) {
if (StringUtils.isBlank(configKey)) {
log.warn("配置标识为空无法移除WxPayService");
return;
}
services.remove(configKey);
}
/**
* 根据配置构建 WxPayService.
*
* @param properties 单个配置属性
* @return WxPayService
*/
private WxPayService buildWxPayService(WxPaySingleProperties properties) {
WxPayServiceImpl wxPayService = new WxPayServiceImpl();
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(StringUtils.trimToNull(properties.getAppId()));
payConfig.setMchId(StringUtils.trimToNull(properties.getMchId()));
payConfig.setMchKey(StringUtils.trimToNull(properties.getMchKey()));
payConfig.setSubAppId(StringUtils.trimToNull(properties.getSubAppId()));
payConfig.setSubMchId(StringUtils.trimToNull(properties.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(properties.getKeyPath()));
payConfig.setUseSandboxEnv(properties.isUseSandboxEnv());
payConfig.setNotifyUrl(StringUtils.trimToNull(properties.getNotifyUrl()));
payConfig.setRefundNotifyUrl(StringUtils.trimToNull(properties.getRefundNotifyUrl()));
// 以下是apiv3以及支付分相关
payConfig.setServiceId(StringUtils.trimToNull(properties.getServiceId()));
payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(properties.getPayScoreNotifyUrl()));
payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(properties.getPayScorePermissionNotifyUrl()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(properties.getPrivateKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(properties.getPrivateCertPath()));
payConfig.setCertSerialNo(StringUtils.trimToNull(properties.getCertSerialNo()));
payConfig.setApiV3Key(StringUtils.trimToNull(properties.getApiv3Key()));
payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel());
wxPayService.setConfig(payConfig);
return wxPayService;
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration

View File

@@ -0,0 +1,2 @@
com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration

View File

@@ -0,0 +1,104 @@
package com.binarywang.spring.starter.wxjava.pay;
import com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration;
import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties;
import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties;
import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
import com.github.binarywang.wxpay.service.WxPayService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import static org.junit.jupiter.api.Assertions.*;
/**
* 微信支付多公众号关联配置测试.
*
* @author Binary Wang
*/
@SpringBootTest(classes = {WxPayMultiAutoConfiguration.class, WxPayMultiServicesTest.TestApplication.class})
@TestPropertySource(properties = {
"wx.pay.configs.app1.app-id=wx1111111111111111",
"wx.pay.configs.app1.mch-id=1111111111",
"wx.pay.configs.app1.mch-key=11111111111111111111111111111111",
"wx.pay.configs.app1.notify-url=https://example.com/pay/notify",
"wx.pay.configs.app2.app-id=wx2222222222222222",
"wx.pay.configs.app2.mch-id=2222222222",
"wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222",
"wx.pay.configs.app2.cert-serial-no=2222222222222222",
"wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem",
"wx.pay.configs.app2.private-cert-path=classpath:cert/apiclient_cert.pem"
})
public class WxPayMultiServicesTest {
@Autowired
private WxPayMultiServices wxPayMultiServices;
@Autowired
private WxPayMultiProperties wxPayMultiProperties;
@Test
public void testConfiguration() {
assertNotNull(wxPayMultiServices, "WxPayMultiServices should be autowired");
assertNotNull(wxPayMultiProperties, "WxPayMultiProperties should be autowired");
// 验证配置正确加载
assertEquals(2, wxPayMultiProperties.getConfigs().size(), "Should have 2 configurations");
WxPaySingleProperties app1Config = wxPayMultiProperties.getConfigs().get("app1");
assertNotNull(app1Config, "app1 configuration should exist");
assertEquals("wx1111111111111111", app1Config.getAppId());
assertEquals("1111111111", app1Config.getMchId());
assertEquals("11111111111111111111111111111111", app1Config.getMchKey());
WxPaySingleProperties app2Config = wxPayMultiProperties.getConfigs().get("app2");
assertNotNull(app2Config, "app2 configuration should exist");
assertEquals("wx2222222222222222", app2Config.getAppId());
assertEquals("2222222222", app2Config.getMchId());
assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
}
@Test
public void testGetWxPayService() {
WxPayService app1Service = wxPayMultiServices.getWxPayService("app1");
assertNotNull(app1Service, "Should get WxPayService for app1");
assertEquals("wx1111111111111111", app1Service.getConfig().getAppId());
assertEquals("1111111111", app1Service.getConfig().getMchId());
WxPayService app2Service = wxPayMultiServices.getWxPayService("app2");
assertNotNull(app2Service, "Should get WxPayService for app2");
assertEquals("wx2222222222222222", app2Service.getConfig().getAppId());
assertEquals("2222222222", app2Service.getConfig().getMchId());
// 测试相同key返回相同实例
WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1");
assertSame(app1Service, app1ServiceAgain, "Should return the same instance for the same key");
}
@Test
public void testGetWxPayServiceWithInvalidKey() {
WxPayService service = wxPayMultiServices.getWxPayService("nonexistent");
assertNull(service, "Should return null for non-existent key");
}
@Test
public void testRemoveWxPayService() {
// 首先获取一个服务实例
WxPayService app1Service = wxPayMultiServices.getWxPayService("app1");
assertNotNull(app1Service, "Should get WxPayService for app1");
// 移除服务
wxPayMultiServices.removeWxPayService("app1");
// 再次获取时应该创建新实例
WxPayService app1ServiceNew = wxPayMultiServices.getWxPayService("app1");
assertNotNull(app1ServiceNew, "Should get new WxPayService for app1");
assertNotSame(app1Service, app1ServiceNew, "Should return a new instance after removal");
}
@SpringBootApplication
static class TestApplication {
}
}

View File

@@ -0,0 +1,249 @@
package com.binarywang.spring.starter.wxjava.pay.example;
import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 微信支付多公众号关联使用示例.
* <p>
* 本示例展示了如何使用 wx-java-pay-multi-spring-boot-starter 来管理多个公众号的支付配置。
* </p>
*
* @author Binary Wang
*/
@Slf4j
@Service
public class WxPayMultiExample {
@Autowired
private WxPayMultiServices wxPayMultiServices;
/**
* 示例1根据appId创建支付订单.
* <p>
* 适用场景:系统需要支持多个公众号,根据用户所在的公众号动态选择支付配置
* </p>
*
* @param appId 公众号appId
* @param openId 用户的openId
* @param totalFee 支付金额(分)
* @param body 商品描述
* @return JSAPI支付参数
*/
public WxPayUnifiedOrderV3Result.JsapiResult createJsapiOrder(String appId, String openId,
Integer totalFee, String body) {
try {
// 根据appId获取对应的WxPayService
WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
if (wxPayService == null) {
log.error("未找到appId对应的微信支付配置: {}", appId);
throw new IllegalArgumentException("未找到appId对应的微信支付配置");
}
// 构建支付请求
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(generateOutTradeNo());
request.setDescription(body);
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl());
// 调用微信支付API创建订单
WxPayUnifiedOrderV3Result.JsapiResult result =
wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
log.info("创建JSAPI支付订单成功appId: {}, outTradeNo: {}", appId, request.getOutTradeNo());
return result;
} catch (Exception e) {
log.error("创建JSAPI支付订单失败appId: {}", appId, e);
throw new RuntimeException("创建支付订单失败", e);
}
}
/**
* 示例2服务商模式 - 为不同子商户创建订单.
* <p>
* 适用场景:服务商为多个子商户提供支付服务
* </p>
*
* @param configKey 配置标识(在配置文件中定义)
* @param subOpenId 子商户用户的openId
* @param totalFee 支付金额(分)
* @param body 商品描述
* @return JSAPI支付参数
*/
public WxPayUnifiedOrderV3Result.JsapiResult createPartnerOrder(String configKey, String subOpenId,
Integer totalFee, String body) {
try {
// 根据配置标识获取WxPayService
WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
if (wxPayService == null) {
log.error("未找到配置: {}", configKey);
throw new IllegalArgumentException("未找到配置");
}
// 获取子商户信息
String subAppId = wxPayService.getConfig().getSubAppId();
String subMchId = wxPayService.getConfig().getSubMchId();
log.info("使用服务商模式子商户appId: {}, 子商户号: {}", subAppId, subMchId);
// 构建支付请求
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(generateOutTradeNo());
request.setDescription(body);
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(subOpenId));
request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl());
// 调用微信支付API创建订单
WxPayUnifiedOrderV3Result.JsapiResult result =
wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
log.info("创建服务商支付订单成功,配置: {}, outTradeNo: {}", configKey, request.getOutTradeNo());
return result;
} catch (Exception e) {
log.error("创建服务商支付订单失败,配置: {}", configKey, e);
throw new RuntimeException("创建支付订单失败", e);
}
}
/**
* 示例3查询订单状态.
* <p>
* 适用场景:查询不同公众号的订单支付状态
* </p>
*
* @param appId 公众号appId
* @param outTradeNo 商户订单号
* @return 订单状态
*/
public String queryOrderStatus(String appId, String outTradeNo) {
try {
WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
if (wxPayService == null) {
log.error("未找到appId对应的微信支付配置: {}", appId);
throw new IllegalArgumentException("未找到appId对应的微信支付配置");
}
// 查询订单
WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo);
String tradeState = result.getTradeState();
log.info("查询订单状态成功appId: {}, outTradeNo: {}, 状态: {}", appId, outTradeNo, tradeState);
return tradeState;
} catch (Exception e) {
log.error("查询订单状态失败appId: {}, outTradeNo: {}", appId, outTradeNo, e);
throw new RuntimeException("查询订单失败", e);
}
}
/**
* 示例4申请退款.
* <p>
* 适用场景:为不同公众号的订单申请退款
* </p>
*
* @param appId 公众号appId
* @param outTradeNo 商户订单号
* @param refundFee 退款金额(分)
* @param totalFee 订单总金额(分)
* @param reason 退款原因
* @return 退款单号
*/
public String refund(String appId, String outTradeNo, Integer refundFee,
Integer totalFee, String reason) {
try {
WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
if (wxPayService == null) {
log.error("未找到appId对应的微信支付配置: {}", appId);
throw new IllegalArgumentException("未找到appId对应的微信支付配置");
}
// 构建退款请求
com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request request =
new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request();
request.setOutTradeNo(outTradeNo);
request.setOutRefundNo(generateRefundNo());
request.setReason(reason);
request.setNotifyUrl(wxPayService.getConfig().getRefundNotifyUrl());
com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount amount =
new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount();
amount.setRefund(refundFee);
amount.setTotal(totalFee);
amount.setCurrency("CNY");
request.setAmount(amount);
// 调用微信支付API申请退款
WxPayRefundV3Result result = wxPayService.refundV3(request);
log.info("申请退款成功appId: {}, outTradeNo: {}, outRefundNo: {}",
appId, outTradeNo, request.getOutRefundNo());
return request.getOutRefundNo();
} catch (Exception e) {
log.error("申请退款失败appId: {}, outTradeNo: {}", appId, outTradeNo, e);
throw new RuntimeException("申请退款失败", e);
}
}
/**
* 示例5动态管理配置.
* <p>
* 适用场景:需要在运行时更新配置(如证书更新后需要重新加载)
* </p>
*
* @param configKey 配置标识
*/
public void reloadConfig(String configKey) {
try {
// 移除缓存的WxPayService实例
wxPayMultiServices.removeWxPayService(configKey);
log.info("移除配置成功,下次获取时将重新创建: {}", configKey);
// 下次调用 getWxPayService 时会重新创建实例
WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
if (wxPayService != null) {
log.info("重新加载配置成功: {}", configKey);
}
} catch (Exception e) {
log.error("重新加载配置失败: {}", configKey, e);
throw new RuntimeException("重新加载配置失败", e);
}
}
/**
* 生成商户订单号.
*
* @return 商户订单号
*/
private String generateOutTradeNo() {
return "ORDER_" + System.currentTimeMillis();
}
/**
* 生成商户退款单号.
*
* @return 商户退款单号
*/
private String generateRefundNo() {
return "REFUND_" + System.currentTimeMillis();
}
}