Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions spring-boot-starters/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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-open-multi-spring-boot-starter</module>
<module>wx-java-open-spring-boot-starter</module>
<module>wx-java-qidian-spring-boot-starter</module>
<module>wx-java-cp-multi-spring-boot-starter</module>
Expand Down
100 changes: 100 additions & 0 deletions spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# wx-java-open-multi-spring-boot-starter

## 快速开始

1. 引入依赖
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-open-multi-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
```
2. 添加配置(application.properties)
```properties
# 开放平台配置
## 应用 1 配置(必填)
wx.open.apps.tenantId1.app-id=appId
wx.open.apps.tenantId1.secret=@secret
## 选填
wx.open.apps.tenantId1.token=@token
wx.open.apps.tenantId1.aes-key=@aesKey
wx.open.apps.tenantId1.api-host-url=@apiHostUrl
wx.open.apps.tenantId1.access-token-url=@accessTokenUrl
## 应用 2 配置(必填)
wx.open.apps.tenantId2.app-id=@appId
wx.open.apps.tenantId2.secret=@secret
## 选填
wx.open.apps.tenantId2.token=@token
wx.open.apps.tenantId2.aes-key=@aesKey
wx.open.apps.tenantId2.api-host-url=@apiHostUrl
wx.open.apps.tenantId2.access-token-url=@accessTokenUrl

# ConfigStorage 配置(选填)
## 配置类型: memory(默认), jedis, redisson, redis_template
wx.open.config-storage.type=memory
Comment thread
binarywang marked this conversation as resolved.
Outdated
## 相关redis前缀配置: wx:open:multi(默认)
wx.open.config-storage.key-prefix=wx:open:multi
wx.open.config-storage.redis.host=127.0.0.1
wx.open.config-storage.redis.port=6379
## 单机和 sentinel 同时存在时,优先使用sentinel配置
Comment thread
binarywang marked this conversation as resolved.
Outdated
# wx.open.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
# wx.open.config-storage.redis.sentinel-name=mymaster

# http 客户端配置(选填)
## # http客户端类型: http_client(默认)
wx.open.config-storage.http-client-type=http_client
wx.open.config-storage.http-proxy-host=
wx.open.config-storage.http-proxy-port=
wx.open.config-storage.http-proxy-username=
wx.open.config-storage.http-proxy-password=
## 最大重试次数,默认:5 次,如果小于 0,则为 0
wx.open.config-storage.max-retry-times=5
## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
wx.open.config-storage.retry-sleep-millis=1000
## 连接超时时间,单位毫秒,默认:5000
wx.open.config-storage.connection-timeout=5000
## 读数据超时时间,即socketTimeout,单位毫秒,默认:5000
wx.open.config-storage.so-timeout=5000
## 从连接池获取链接的超时时间,单位毫秒,默认:5000
wx.open.config-storage.connection-request-timeout=5000
```
3. 自动注入的类型:`WxOpenMultiServices`

4. 使用样例

```java
import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices;
import me.chanjar.weixin.open.api.WxOpenService;
import me.chanjar.weixin.open.api.WxOpenComponentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemoService {
@Autowired
private WxOpenMultiServices wxOpenMultiServices;

public void test() {
// 应用 1 的 WxOpenService
WxOpenService wxOpenService1 = wxOpenMultiServices.getWxOpenService("tenantId1");
WxOpenComponentService componentService1 = wxOpenService1.getWxOpenComponentService();
// todo ...

// 应用 2 的 WxOpenService
WxOpenService wxOpenService2 = wxOpenMultiServices.getWxOpenService("tenantId2");
WxOpenComponentService componentService2 = wxOpenService2.getWxOpenComponentService();
// todo ...

// 应用 3 的 WxOpenService
WxOpenService wxOpenService3 = wxOpenMultiServices.getWxOpenService("tenantId3");
// 判断是否为空
if (wxOpenService3 == null) {
// todo wxOpenService3 为空,请先配置 tenantId3 微信开放平台应用参数
return;
}
WxOpenComponentService componentService3 = wxOpenService3.getWxOpenComponentService();
// todo ...
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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-open-multi-spring-boot-starter</artifactId>
<name>WxJava - Spring Boot Starter for WxOpen::支持多账号配置</name>
<description>微信开放平台开发的 Spring Boot Starter::支持多账号配置</description>

<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-open</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>provided</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.binarywang.spring.starter.wxjava.open.autoconfigure;

import com.binarywang.spring.starter.wxjava.open.configuration.WxOpenMultiServiceConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* 微信开放平台多账号自动配置
*
* @author someone
Comment thread
binarywang marked this conversation as resolved.
Outdated
*/
@Configuration
@Import(WxOpenMultiServiceConfiguration.class)
public class WxOpenMultiAutoConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.binarywang.spring.starter.wxjava.open.configuration;

import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInJedisConfiguration;
import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInMemoryConfiguration;
import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInRedisTemplateConfiguration;
import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInRedissonConfiguration;
import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* 微信开放平台相关服务自动注册
*
* @author someone
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.

作者信息不明确。当前使用 "@author someone",应使用具体的作者名称以保持项目一致性。

Suggested change
* @author someone
* @author Binary Wang

Copilot uses AI. Check for mistakes.
*/
@Configuration
@EnableConfigurationProperties(WxOpenMultiProperties.class)
@Import({
WxOpenInJedisConfiguration.class,
WxOpenInMemoryConfiguration.class,
WxOpenInRedissonConfiguration.class,
WxOpenInRedisTemplateConfiguration.class
})
public class WxOpenMultiServiceConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.binarywang.spring.starter.wxjava.open.configuration.services;

import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties;
import com.binarywang.spring.starter.wxjava.open.properties.WxOpenSingleProperties;
import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices;
import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServicesImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.open.api.WxOpenConfigStorage;
import me.chanjar.weixin.open.api.WxOpenService;
import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl;
import org.apache.commons.lang3.StringUtils;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* WxOpenConfigStorage 抽象配置类
*
* @author someone
Comment thread
binarywang marked this conversation as resolved.
Outdated
*/
@RequiredArgsConstructor
@Slf4j
public abstract class AbstractWxOpenConfiguration {

protected WxOpenMultiServices wxOpenMultiServices(WxOpenMultiProperties wxOpenMultiProperties) {
Map<String, WxOpenSingleProperties> appsMap = wxOpenMultiProperties.getApps();
if (appsMap == null || appsMap.isEmpty()) {
log.warn("微信开放平台应用参数未配置,通过 WxOpenMultiServices#getWxOpenService(\"tenantId\")获取实例将返回空");
return new WxOpenMultiServicesImpl();
}
/**
* 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
*/
Collection<WxOpenSingleProperties> apps = appsMap.values();
if (apps.size() > 1) {
// 校验 appId 是否唯一
String nullAppIdPlaceholder = "__NULL_APP_ID__";
boolean multi = apps.stream()
// 没有 appId,如果不判断是否为空,这里会报 NPE 异常
.collect(Collectors.groupingBy(c -> c.getAppId() == null ? nullAppIdPlaceholder : c.getAppId(), Collectors.counting()))
.entrySet().stream().anyMatch(e -> e.getValue() > 1);
if (multi) {
throw new RuntimeException("请确保微信开放平台配置 appId 的唯一性");
}
}
WxOpenMultiServicesImpl services = new WxOpenMultiServicesImpl();

Set<Map.Entry<String, WxOpenSingleProperties>> entries = appsMap.entrySet();
for (Map.Entry<String, WxOpenSingleProperties> entry : entries) {
String tenantId = entry.getKey();
WxOpenSingleProperties wxOpenSingleProperties = entry.getValue();
WxOpenInMemoryConfigStorage storage = this.wxOpenConfigStorage(wxOpenMultiProperties);
this.configApp(storage, wxOpenSingleProperties);
this.configHttp(storage, wxOpenMultiProperties.getConfigStorage());
WxOpenService wxOpenService = this.wxOpenService(storage, wxOpenMultiProperties);
services.addWxOpenService(tenantId, wxOpenService);
}
return services;
}

/**
* 配置 WxOpenInMemoryConfigStorage
*
* @param wxOpenMultiProperties 参数
* @return WxOpenInMemoryConfigStorage
*/
protected abstract WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties);

public WxOpenService wxOpenService(WxOpenConfigStorage configStorage, WxOpenMultiProperties wxOpenMultiProperties) {
Comment thread
binarywang marked this conversation as resolved.
WxOpenService wxOpenService = new WxOpenServiceImpl();
wxOpenService.setWxOpenConfigStorage(configStorage);
return wxOpenService;
}

private void configApp(WxOpenInMemoryConfigStorage config, WxOpenSingleProperties appProperties) {
String appId = appProperties.getAppId();
String secret = appProperties.getSecret();
String token = appProperties.getToken();
String aesKey = appProperties.getAesKey();
String apiHostUrl = appProperties.getApiHostUrl();
String accessTokenUrl = appProperties.getAccessTokenUrl();

// appId 和 secret 是必需的
if (StringUtils.isBlank(appId)) {
throw new IllegalArgumentException("微信开放平台 appId 不能为空");
}
if (StringUtils.isBlank(secret)) {
throw new IllegalArgumentException("微信开放平台 secret 不能为空");
}

config.setComponentAppId(appId);
config.setComponentAppSecret(secret);
if (StringUtils.isNotBlank(token)) {
config.setComponentToken(token);
}
if (StringUtils.isNotBlank(aesKey)) {
config.setComponentAesKey(aesKey);
}
// 设置URL配置
config.setApiHostUrl(StringUtils.trimToNull(apiHostUrl));
config.setAccessTokenUrl(StringUtils.trimToNull(accessTokenUrl));
}

private void configHttp(WxOpenInMemoryConfigStorage config, WxOpenMultiProperties.ConfigStorage storage) {
String httpProxyHost = storage.getHttpProxyHost();
Integer httpProxyPort = storage.getHttpProxyPort();
String httpProxyUsername = storage.getHttpProxyUsername();
String httpProxyPassword = storage.getHttpProxyPassword();
if (StringUtils.isNotBlank(httpProxyHost)) {
config.setHttpProxyHost(httpProxyHost);
if (httpProxyPort != null) {
config.setHttpProxyPort(httpProxyPort);
}
if (StringUtils.isNotBlank(httpProxyUsername)) {
config.setHttpProxyUsername(httpProxyUsername);
}
if (StringUtils.isNotBlank(httpProxyPassword)) {
config.setHttpProxyPassword(httpProxyPassword);
}
}

// 设置重试配置
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
}
int retrySleepMillis = storage.getRetrySleepMillis();
if (retrySleepMillis < 0) {
retrySleepMillis = 1000;
}
config.setRetrySleepMillis(retrySleepMillis);
config.setMaxRetryTimes(maxRetryTimes);

// 设置自定义的HttpClient超时配置
ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder();
if (clientBuilder == null) {
clientBuilder = DefaultApacheHttpClientBuilder.get();
}
if (clientBuilder instanceof DefaultApacheHttpClientBuilder) {
DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder;
defaultBuilder.setConnectionTimeout(storage.getConnectionTimeout());
defaultBuilder.setSoTimeout(storage.getSoTimeout());
defaultBuilder.setConnectionRequestTimeout(storage.getConnectionRequestTimeout());
config.setApacheHttpClientBuilder(defaultBuilder);
}
}
}
Loading
Loading