corps = new HashMap<>();
+
+ /**
+ * 配置存储策略,默认内存
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wx:cp";
+
+ /**
+ * redis连接配置
+ */
+ private WxCpMultiRedisProperties redis = new WxCpMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP
+ }
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java
new file mode 100644
index 0000000000..14952d69d9
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java
@@ -0,0 +1,48 @@
+package com.binarywang.solon.wxjava.cp_multi.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java
new file mode 100644
index 0000000000..6f7f633c3f
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java
@@ -0,0 +1,68 @@
+package com.binarywang.solon.wxjava.cp_multi.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信企业相关配置属性
+ *
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpSingleProperties implements Serializable {
+ private static final long serialVersionUID = -7502823825007859418L;
+ /**
+ * 微信企业号 corpId
+ */
+ private String corpId;
+ /**
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
+ */
+ private String corpSecret;
+ /**
+ * 微信企业号应用 token
+ */
+ private String token;
+ /**
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
+ */
+ private Integer agentId;
+ /**
+ * 微信企业号应用 EncodingAESKey
+ */
+ private String aesKey;
+ /**
+ * 微信企业号应用 会话存档私钥
+ */
+ private String msgAuditPriKey;
+ /**
+ * 微信企业号应用 会话存档类库路径
+ */
+ private String msgAuditLibPath;
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java
new file mode 100644
index 0000000000..c66c28233d
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.cp_multi.service;
+
+import me.chanjar.weixin.cp.api.WxCpService;
+
+/**
+ * 企业微信 {@link WxCpService} 所有实例存放类.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public interface WxCpMultiServices {
+ /**
+ * 通过租户 Id 获取 WxCpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpService
+ */
+ WxCpService getWxCpService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxCpService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxCpService(String tenantId);
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java
new file mode 100644
index 0000000000..d7833a05f9
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java
@@ -0,0 +1,42 @@
+package com.binarywang.solon.wxjava.cp_multi.service;
+
+import me.chanjar.weixin.cp.api.WxCpService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 企业微信 {@link WxCpMultiServices} 默认实现
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public class WxCpMultiServicesImpl implements WxCpMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ /**
+ * 通过租户 Id 获取 WxCpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpService
+ */
+ @Override
+ public WxCpService getWxCpService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxCpService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxCpService WxCpService 实例
+ */
+ public void addWxCpService(String tenantId, WxCpService wxCpService) {
+ this.services.put(tenantId, wxCpService);
+ }
+
+ @Override
+ public void removeWxCpService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties
new file mode 100644
index 0000000000..eb537e9a66
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.cp_multi.integration.WxCpMultiPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..0602c0a807
--- /dev/null
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,19 @@
+# ?? 1 ??
+wx.cp.corps.tenantId1.corp-id = @corp-id
+wx.cp.corps.tenantId1.corp-secret = @corp-secret
+ ## ??
+wx.cp.corps.tenantId1.agent-id = @agent-id
+wx.cp.corps.tenantId1.token = @token
+wx.cp.corps.tenantId1.aes-key = @aes-key
+wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey
+wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path
+
+ # ?? 2 ??
+wx.cp.corps.tenantId2.corp-id = @corp-id
+wx.cp.corps.tenantId2.corp-secret = @corp-secret
+ ## ??
+wx.cp.corps.tenantId2.agent-id = @agent-id
+wx.cp.corps.tenantId2.token = @token
+wx.cp.corps.tenantId2.aes-key = @aes-key
+wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey
+wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path
diff --git a/solon-plugins/wx-java-cp-solon-plugin/README.md b/solon-plugins/wx-java-cp-solon-plugin/README.md
new file mode 100644
index 0000000000..04d5dfab58
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/README.md
@@ -0,0 +1,41 @@
+# wx-java-cp-solon-plugin
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-cp-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 企业微信号配置(必填)
+ wx.cp.corp-id = @corp-id
+ wx.cp.corp-secret = @corp-secret
+ # 选填
+ wx.cp.agent-id = @agent-id
+ wx.cp.token = @token
+ wx.cp.aes-key = @aes-key
+ wx.cp.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.msg-audit-lib-path = @msg-audit-lib-path
+ # ConfigStorage 配置(选填)
+ wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate
+ # http 客户端配置(选填)
+ wx.cp.config-storage.http-proxy-host=
+ wx.cp.config-storage.http-proxy-port=
+ wx.cp.config-storage.http-proxy-username=
+ wx.cp.config-storage.http-proxy-password=
+ # 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.cp.config-storage.max-retry-times=5
+ # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.cp.config-storage.retry-sleep-millis=1000
+ ```
+3. 支持自动注入的类型: `WxCpService`, `WxCpConfigStorage`
+
+4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的
+
+- WxCpService
+- WxCpConfigStorage
diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
new file mode 100644
index 0000000000..78d994f172
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-cp-solon-plugin
+ WxJava - Solon Plugin for WxCp
+ 微信企业号开发的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-cp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+
+
+ org.redisson
+ redisson
+
+
+
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java
new file mode 100644
index 0000000000..82aeeaf859
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java
@@ -0,0 +1,43 @@
+package com.binarywang.solon.wxjava.cp.config;
+
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 企业微信平台相关服务自动注册
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@RequiredArgsConstructor
+public class WxCpServiceAutoConfiguration {
+ private final WxCpProperties wxCpProperties;
+
+ @Bean
+ @Condition(onMissingBean = WxCpService.class,
+ onBean = WxCpConfigStorage.class)
+ public WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage) {
+ WxCpService wxCpService = new WxCpServiceImpl();
+ wxCpService.setWxCpConfigStorage(wxCpConfigStorage);
+
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxCpService.setRetrySleepMillis(retrySleepMillis);
+ wxCpService.setMaxRetryTimes(maxRetryTimes);
+ return wxCpService;
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java
new file mode 100644
index 0000000000..fda64b3a17
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java
@@ -0,0 +1,25 @@
+package com.binarywang.solon.wxjava.cp.integration;
+
+import com.binarywang.solon.wxjava.cp.config.WxCpServiceAutoConfiguration;
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import com.binarywang.solon.wxjava.cp.storage.WxCpInJedisConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.cp.storage.WxCpInMemoryConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.cp.storage.WxCpInRedissonConfigStorageConfiguration;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxCpPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxCpProperties.class);
+
+ context.beanMake(WxCpServiceAutoConfiguration.class);
+
+ context.beanMake(WxCpInMemoryConfigStorageConfiguration.class);
+ context.beanMake(WxCpInJedisConfigStorageConfiguration.class);
+ context.beanMake(WxCpInRedissonConfigStorageConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java
new file mode 100644
index 0000000000..60524f5228
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java
@@ -0,0 +1,133 @@
+package com.binarywang.solon.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信接入相关配置属性
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Data
+@NoArgsConstructor
+@Configuration
+@Inject("${" + WxCpProperties.PREFIX + "}")
+public class WxCpProperties {
+ public static final String PREFIX = "wx.cp";
+
+ /**
+ * 微信企业号 corpId
+ */
+ private String corpId;
+ /**
+ * 微信企业号 corpSecret
+ */
+ private String corpSecret;
+ /**
+ * 微信企业号应用 token
+ */
+ private String token;
+ /**
+ * 微信企业号应用 ID
+ */
+ private Integer agentId;
+ /**
+ * 微信企业号应用 EncodingAESKey
+ */
+ private String aesKey;
+ /**
+ * 微信企业号应用 会话存档私钥
+ */
+ private String msgAuditPriKey;
+ /**
+ * 微信企业号应用 会话存档类库路径
+ */
+ private String msgAuditLibPath;
+
+ /**
+ * 配置存储策略,默认内存
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wx:cp";
+
+ /**
+ * redis连接配置
+ */
+ private WxCpRedisProperties redis = new WxCpRedisProperties();
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java
new file mode 100644
index 0000000000..43b8788d3f
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java
@@ -0,0 +1,46 @@
+package com.binarywang.solon.wxjava.cp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Data
+public class WxCpRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java
new file mode 100644
index 0000000000..9fcdd5779a
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java
@@ -0,0 +1,61 @@
+package com.binarywang.solon.wxjava.cp.storage;
+
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * WxCpConfigStorage 抽象配置类
+ *
+ * @author yl & Wang_Wong
+ * created on 2021/12/6
+ */
+public abstract class AbstractWxCpConfigStorageConfiguration {
+
+ protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpProperties properties) {
+ String corpId = properties.getCorpId();
+ String corpSecret = properties.getCorpSecret();
+ Integer agentId = properties.getAgentId();
+ String token = properties.getToken();
+ String aesKey = properties.getAesKey();
+ // 企业微信,私钥,会话存档路径
+ String msgAuditPriKey = properties.getMsgAuditPriKey();
+ String msgAuditLibPath = properties.getMsgAuditLibPath();
+
+ config.setCorpId(corpId);
+ config.setCorpSecret(corpSecret);
+ config.setAgentId(agentId);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditPriKey)) {
+ config.setMsgAuditPriKey(msgAuditPriKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditLibPath)) {
+ config.setMsgAuditLibPath(msgAuditLibPath);
+ }
+
+ WxCpProperties.ConfigStorage storage = properties.getConfigStorage();
+ 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);
+ }
+ }
+ return config;
+ }
+
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..f6f6931992
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java
@@ -0,0 +1,74 @@
+package com.binarywang.solon.wxjava.cp.storage;
+
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import com.binarywang.solon.wxjava.cp.properties.WxCpRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type} = jedis",
+ onClass = JedisPool.class
+)
+@RequiredArgsConstructor
+public class WxCpInJedisConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = getConfigStorage();
+ return this.config(config, wxCpProperties);
+ }
+
+ private WxCpJedisConfigImpl getConfigStorage() {
+ WxCpRedisProperties wxCpRedisProperties = wxCpProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxCpRedisProperties != null && StringUtils.isNotEmpty(wxCpRedisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxCpJedisConfigImpl(jedisPool, wxCpProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ WxCpRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..2776fea368
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,31 @@
+package com.binarywang.solon.wxjava.cp.storage;
+
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxCpInMemoryConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+
+ @Bean
+ @Condition(onMissingBean=WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl();
+ return this.config(config, wxCpProperties);
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..0aef4d520a
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,65 @@
+package com.binarywang.solon.wxjava.cp.storage;
+
+import com.binarywang.solon.wxjava.cp.properties.WxCpProperties;
+import com.binarywang.solon.wxjava.cp.properties.WxCpRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxCpInRedissonConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = getConfigStorage();
+ return this.config(config, wxCpProperties);
+ }
+
+ private WxCpRedissonConfigImpl getConfigStorage() {
+ WxCpRedisProperties redisProperties = wxCpProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxCpRedissonConfigImpl(redissonClient, wxCpProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ WxCpRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties b/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties
new file mode 100644
index 0000000000..c765affecb
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.cp.integration.WxCpPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..0c99c8b64d
--- /dev/null
+++ b/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,20 @@
+# ???????(??)
+wx.cp.corp-id = @corp-id
+wx.cp.corp-secret = @corp-secret
+# ??
+wx.cp.agent-id = @agent-id
+wx.cp.token = @token
+wx.cp.aes-key = @aes-key
+wx.cp.msg-audit-priKey = @msg-audit-priKey
+wx.cp.msg-audit-lib-path = @msg-audit-lib-path
+# ConfigStorage ??????
+wx.cp.config-storage.type=memory # ????: memory(??), jedis, redisson, redistemplate
+# http ?????????
+wx.cp.config-storage.http-proxy-host=
+wx.cp.config-storage.http-proxy-port=
+wx.cp.config-storage.http-proxy-username=
+wx.cp.config-storage.http-proxy-password=
+# ??????????5 ?????? 0??? 0
+wx.cp.config-storage.max-retry-times=5
+# ????????????1000 ??????? 0??? 1000
+wx.cp.config-storage.retry-sleep-millis=1000
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md b/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md
new file mode 100644
index 0000000000..4555a4fc5e
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md
@@ -0,0 +1,95 @@
+# wx-java-miniapp-multi-solon-plugin
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-miniapp-multi-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置
+ ## 应用 1 配置(必填)
+ wx.ma.apps.tenantId1.app-id=appId
+ wx.ma.apps.tenantId1.app-secret=@secret
+ ## 选填
+ wx.ma.apps.tenantId1.token=@token
+ wx.ma.apps.tenantId1.aes-key=@aesKey
+ wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+ ## 应用 2 配置(必填)
+ wx.ma.apps.tenantId2.app-id=@appId
+ wx.ma.apps.tenantId2.app-secret =@secret
+ ## 选填
+ wx.ma.apps.tenantId2.token=@token
+ wx.ma.apps.tenantId2.aes-key=@aesKey
+ wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson
+ wx.ma.config-storage.type=memory
+ ## 相关redis前缀配置: wx:ma:multi(默认)
+ wx.ma.config-storage.key-prefix=wx:ma:multi
+ wx.ma.config-storage.redis.host=127.0.0.1
+ wx.ma.config-storage.redis.port=6379
+ ## 单机和 sentinel 同时存在时,优先使用sentinel配置
+ # wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ # wx.ma.config-storage.redis.sentinel-name=mymaster
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.ma.config-storage.http-client-type=http_client
+ wx.ma.config-storage.http-proxy-host=
+ wx.ma.config-storage.http-proxy-port=
+ wx.ma.config-storage.http-proxy-username=
+ wx.ma.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.ma.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.ma.config-storage.retry-sleep-millis=1000
+ ```
+3. 自动注入的类型:`WxMaMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaUserService;
+import org.noear.solon.annotation.Component;
+import org.noear.solon.annotation.Inject;
+
+@Component
+public class DemoService {
+ @Inject
+ private WxMaMultiServices wxMaMultiServices;
+
+ public void test() {
+ // 应用 1 的 WxMaService
+ WxMaService wxMaService1 = wxMaMultiServices.getWxMaService("tenantId1");
+ WxMaUserService userService1 = wxMaService1.getUserService();
+ userService1.userInfo("xxx");
+ // todo ...
+
+ // 应用 2 的 WxMaService
+ WxMaService wxMaService2 = wxMaMultiServices.getWxMaService("tenantId2");
+ WxMaUserService userService2 = wxMaService2.getUserService();
+ userService2.userInfo("xxx");
+ // todo ...
+
+ // 应用 3 的 WxMaService
+ WxMaService wxMaService3 = wxMaMultiServices.getWxMaService("tenantId3");
+ // 判断是否为空
+ if (wxMaService3 == null) {
+ // todo wxMaService3 为空,请先配置 tenantId3 微信公众号应用参数
+ return;
+ }
+ WxMaUserService userService3 = wxMaService3.getUserService();
+ userService3.userInfo("xxx");
+ // todo ...
+ }
+}
+```
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
new file mode 100644
index 0000000000..38569db2a7
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
@@ -0,0 +1,43 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-miniapp-multi-solon-plugin
+ WxJava - Solon Plugin for MiniApp::支持多账号配置
+ 微信公众号开发的 Solon Plugin::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java
new file mode 100644
index 0000000000..8ad85c96b8
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java
@@ -0,0 +1,151 @@
+package com.binarywang.solon.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaSingleProperties;
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices;
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServicesImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * WxMaConfigStorage 抽象配置类
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxMaConfiguration {
+
+ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) {
+ Map appsMap = wxMaMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信公众号应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空");
+ return new WxMaMultiServicesImpl();
+ }
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 查看 {@link cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl#setAppId(String)}
+ */
+ Collection apps = appsMap.values();
+ if (apps.size() > 1) {
+ // 校验 appId 是否唯一
+ boolean multi = apps.stream()
+ // 没有 appId,如果不判断是否为空,这里会报 NPE 异常
+ .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保微信公众号配置 appId 的唯一性");
+ }
+ }
+ WxMaMultiServicesImpl services = new WxMaMultiServicesImpl();
+
+ Set> entries = appsMap.entrySet();
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxMaSingleProperties wxMaSingleProperties = entry.getValue();
+ WxMaDefaultConfigImpl storage = this.wxMaConfigStorage(wxMaMultiProperties);
+ this.configApp(storage, wxMaSingleProperties);
+ this.configHttp(storage, wxMaMultiProperties.getConfigStorage());
+ WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties);
+ services.addWxMaService(tenantId, wxMaService);
+ }
+ return services;
+ }
+
+ /**
+ * 配置 WxMaDefaultConfigImpl
+ *
+ * @param wxMaMultiProperties 参数
+ * @return WxMaDefaultConfigImpl
+ */
+ protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties);
+
+ public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
+ WxMaService wxMaService;
+ switch (httpClientType) {
+ case OK_HTTP:
+ wxMaService = new WxMaServiceOkHttpImpl();
+ break;
+ case JODD_HTTP:
+ wxMaService = new WxMaServiceJoddHttpImpl();
+ break;
+ case HTTP_CLIENT:
+ wxMaService = new WxMaServiceHttpClientImpl();
+ break;
+ case HTTP_COMPONENTS:
+ wxMaService = new WxMaServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMaService = new WxMaServiceImpl();
+ break;
+ }
+
+ wxMaService.setWxMaConfig(wxMaConfig);
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxMaService.setRetrySleepMillis(retrySleepMillis);
+ wxMaService.setMaxRetryTimes(maxRetryTimes);
+ return wxMaService;
+ }
+
+ private void configApp(WxMaDefaultConfigImpl config, WxMaSingleProperties corpProperties) {
+ String appId = corpProperties.getAppId();
+ String appSecret = corpProperties.getAppSecret();
+ String token = corpProperties.getToken();
+ String aesKey = corpProperties.getAesKey();
+ boolean useStableAccessToken = corpProperties.isUseStableAccessToken();
+
+ config.setAppid(appId);
+ config.setSecret(appSecret);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ config.useStableAccessToken(useStableAccessToken);
+ }
+
+ private void configHttp(WxMaDefaultConfigImpl config, WxMaMultiProperties.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);
+ }
+ }
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java
new file mode 100644
index 0000000000..24950fae10
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java
@@ -0,0 +1,77 @@
+package com.binarywang.solon.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiRedisProperties;
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type} = jedis",
+ onClass = JedisPool.class
+)
+@RequiredArgsConstructor
+public class WxMaInJedisConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configRedis(wxMaMultiProperties);
+ }
+
+ private WxMaDefaultConfigImpl configRedis(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiRedisProperties wxMaMultiRedisProperties = wxMaMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxMaMultiRedisProperties != null && StringUtils.isNotEmpty(wxMaMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxMaMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxMaRedisConfigImpl(jedisPool);
+ }
+
+ private JedisPool getJedisPool(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java
new file mode 100644
index 0000000000..0b9ef1c4a8
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java
@@ -0,0 +1,39 @@
+package com.binarywang.solon.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxMaInMemoryConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxMaDefaultConfigImpl configInMemory() {
+ return new WxMaDefaultConfigImpl();
+ // return new WxMaDefaultConfigImpl();
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java
new file mode 100644
index 0000000000..4e97071f01
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java
@@ -0,0 +1,68 @@
+package com.binarywang.solon.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiRedisProperties;
+import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxMaInRedissonConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configRedisson(wxMaMultiProperties);
+ }
+
+ private WxMaDefaultConfigImpl configRedisson(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiRedisProperties redisProperties = wxMaMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxMaMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMaRedissonConfigImpl(redissonClient, wxMaMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java
new file mode 100644
index 0000000000..c1153be1bb
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java
@@ -0,0 +1,22 @@
+package com.binarywang.solon.wxjava.miniapp.integration;
+
+import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInJedisConfiguration;
+import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInMemoryConfiguration;
+import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInRedissonConfiguration;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/10/9 created
+ */
+public class WxMiniappMultiPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxMaMultiProperties.class);
+
+ context.beanMake(WxMaInJedisConfiguration.class);
+ context.beanMake(WxMaInMemoryConfiguration.class);
+ context.beanMake(WxMaInRedissonConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java
new file mode 100644
index 0000000000..f99d6280ec
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java
@@ -0,0 +1,158 @@
+package com.binarywang.solon.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author monch created on 2024/9/6
+ * @author noear
+ */
+@Data
+@NoArgsConstructor
+@Configuration
+@Inject("${" + WxMaMultiProperties.PREFIX + "}")
+public class WxMaMultiProperties implements Serializable {
+ private static final long serialVersionUID = -5358245184407791011L;
+ public static final String PREFIX = "wx.ma";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class HostConfig implements Serializable {
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.MEMORY;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:ma:multi";
+
+ /**
+ * redis连接配置.
+ */
+ private final WxMaMultiRedisProperties redis = new WxMaMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link cn.binarywang.wx.miniapp.api.WxMaService#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link cn.binarywang.wx.miniapp.api.WxMaService#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ MEMORY,
+ /**
+ * jedis
+ */
+ JEDIS,
+ /**
+ * redisson
+ */
+ REDISSON,
+ /**
+ * redisTemplate
+ */
+ REDIS_TEMPLATE
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java
new file mode 100644
index 0000000000..1f4c07806e
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.solon.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Data
+@NoArgsConstructor
+public class WxMaMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java
new file mode 100644
index 0000000000..f61985716e
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java
@@ -0,0 +1,40 @@
+package com.binarywang.solon.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Data
+@NoArgsConstructor
+public class WxMaSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+ /**
+ * 设置微信公众号的 appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的 app secret.
+ */
+ private String appSecret;
+
+ /**
+ * 设置微信公众号的 token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的 EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java
new file mode 100644
index 0000000000..80d073cceb
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java
@@ -0,0 +1,27 @@
+package com.binarywang.solon.wxjava.miniapp.service;
+
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+
+/**
+ * 微信小程序 {@link WxMaService} 所有实例存放类.
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+public interface WxMaMultiServices {
+ /**
+ * 通过租户 Id 获取 WxMaService
+ *
+ * @param tenantId 租户 Id
+ * @return WxMaService
+ */
+ WxMaService getWxMaService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxMaService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxMaService(String tenantId);
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java
new file mode 100644
index 0000000000..d0ba21cdb8
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java
@@ -0,0 +1,36 @@
+package com.binarywang.solon.wxjava.miniapp.service;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 微信小程序 {@link WxMaMultiServices} 默认实现
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+public class WxMaMultiServicesImpl implements WxMaMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxMaService getWxMaService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxMaService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxMaService WxMaService 实例
+ */
+ public void addWxMaService(String tenantId, WxMaService wxMaService) {
+ this.services.put(tenantId, wxMaService);
+ }
+
+ @Override
+ public void removeWxMaService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties
new file mode 100644
index 0000000000..9d3e2557a8
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.miniapp.integration.WxMiniappMultiPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..6522b172c6
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,38 @@
+# 公众号配置
+## 应用 1 配置(必填)
+wx.ma.apps.tenantId1.app-id=appId
+wx.ma.apps.tenantId1.app-secret=@secret
+## 选填
+wx.ma.apps.tenantId1.token=@token
+wx.ma.apps.tenantId1.aes-key=@aesKey
+wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+## 应用 2 配置(必填)
+wx.ma.apps.tenantId2.app-id=@appId
+wx.ma.apps.tenantId2.app-secret =@secret
+## 选填
+wx.ma.apps.tenantId2.token=@token
+wx.ma.apps.tenantId2.aes-key=@aesKey
+wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+# ConfigStorage 配置(选填)
+## 配置类型: memory(默认), jedis, redisson
+wx.ma.config-storage.type=memory
+## 相关redis前缀配置: wx:ma:multi(默认)
+wx.ma.config-storage.key-prefix=wx:ma:multi
+wx.ma.config-storage.redis.host=127.0.0.1
+wx.ma.config-storage.redis.port=6379
+## 单机和 sentinel 同时存在时,优先使用sentinel配置
+# wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+# wx.ma.config-storage.redis.sentinel-name=mymaster
+
+# http 客户端配置(选填)
+## # http客户端类型: http_client(默认), ok_http, jodd_http
+wx.ma.config-storage.http-client-type=http_client
+wx.ma.config-storage.http-proxy-host=
+wx.ma.config-storage.http-proxy-port=
+wx.ma.config-storage.http-proxy-username=
+wx.ma.config-storage.http-proxy-password=
+## 最大重试次数,默认:5 次,如果小于 0,则为 0
+wx.ma.config-storage.max-retry-times=5
+## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+wx.ma.config-storage.retry-sleep-millis=1000
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/README.md b/solon-plugins/wx-java-miniapp-solon-plugin/README.md
new file mode 100644
index 0000000000..3d1d7517f7
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/README.md
@@ -0,0 +1,35 @@
+# wx-java-miniapp-solon-plugin
+## 快速开始
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-miniapp-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.miniapp.appid = appId
+ wx.miniapp.secret = @secret
+ wx.miniapp.token = @token
+ wx.miniapp.aesKey = @aesKey
+ wx.miniapp.msgDataFormat = @msgDataFormat # 消息格式,XML或者JSON.
+ # 存储配置redis(可选)
+ # 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool)
+ wx.miniapp.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.miniapp.config-storage.key-prefix = wa # 相关redis前缀配置: wa(默认)
+ wx.miniapp.config-storage.redis.host = 127.0.0.1
+ wx.miniapp.config-storage.redis.port = 6379
+ # http客户端配置
+ wx.miniapp.config-storage.http-client-type=HttpClient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
+ wx.miniapp.config-storage.http-proxy-host=
+ wx.miniapp.config-storage.http-proxy-port=
+ wx.miniapp.config-storage.http-proxy-username=
+ wx.miniapp.config-storage.http-proxy-password=
+ ```
+3. 自动注入的类型
+- `WxMaService`
+- `WxMaConfig`
+
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
new file mode 100644
index 0000000000..0879651425
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
@@ -0,0 +1,43 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-miniapp-solon-plugin
+ WxJava - Solon Plugin for MiniApp
+ 微信小程序开发的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java
new file mode 100644
index 0000000000..78f95380b2
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java
@@ -0,0 +1,58 @@
+package com.binarywang.solon.wxjava.miniapp.config;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import com.binarywang.solon.wxjava.miniapp.enums.HttpClientType;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import lombok.AllArgsConstructor;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 微信小程序平台相关服务自动注册.
+ *
+ * @author someone TaoYu
+ */
+@Configuration
+@AllArgsConstructor
+public class WxMaServiceAutoConfiguration {
+
+ private final WxMaProperties wxMaProperties;
+
+ /**
+ * 小程序service.
+ *
+ * @return 小程序service
+ */
+ @Bean
+ @Condition(onMissingBean=WxMaService.class, onBean=WxMaConfig.class)
+ public WxMaService wxMaService(WxMaConfig wxMaConfig) {
+ HttpClientType httpClientType = wxMaProperties.getConfigStorage().getHttpClientType();
+ WxMaService wxMaService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxMaService = new WxMaServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxMaService = new WxMaServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxMaService = new WxMaServiceHttpClientImpl();
+ break;
+ case HttpComponents:
+ wxMaService = new WxMaServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMaService = new WxMaServiceImpl();
+ break;
+ }
+ wxMaService.setWxMaConfig(wxMaConfig);
+ return wxMaService;
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java
new file mode 100644
index 0000000000..acc147a705
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.solon.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author yl TaoYu
+ */
+public abstract class AbstractWxMaConfigStorageConfiguration {
+
+ protected WxMaDefaultConfigImpl config(WxMaDefaultConfigImpl config, WxMaProperties properties) {
+ config.setAppid(StringUtils.trimToNull(properties.getAppid()));
+ config.setSecret(StringUtils.trimToNull(properties.getSecret()));
+ config.setToken(StringUtils.trimToNull(properties.getToken()));
+ config.setAesKey(StringUtils.trimToNull(properties.getAesKey()));
+ config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat()));
+ config.useStableAccessToken(properties.isUseStableAccessToken());
+
+ WxMaProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+
+ int maxRetryTimes = configStorageProperties.getMaxRetryTimes();
+ if (configStorageProperties.getMaxRetryTimes() < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = configStorageProperties.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ config.setRetrySleepMillis(retrySleepMillis);
+ config.setMaxRetryTimes(maxRetryTimes);
+ return config;
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..da8c4701ba
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java
@@ -0,0 +1,72 @@
+package com.binarywang.solon.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.RedisProperties;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type} = jedis",
+ onClass = JedisPool.class
+)
+@RequiredArgsConstructor
+public class WxMaInJedisConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaRedisBetterConfigImpl config = getWxMaRedisBetterConfigImpl();
+ return this.config(config, properties);
+ }
+
+ private WxMaRedisBetterConfigImpl getWxMaRedisBetterConfigImpl() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxMaProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..958742d2aa
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,28 @@
+package com.binarywang.solon.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxMaInMemoryConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+
+ @Bean
+ @Condition(onMissingBean=WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
+ return this.config(config, properties);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..af7c11448e
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,61 @@
+package com.binarywang.solon.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl;
+import com.binarywang.solon.wxjava.miniapp.properties.RedisProperties;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxMaInRedissonConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaRedissonConfigImpl config = getWxMaInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMaRedissonConfigImpl getWxMaInRedissonConfigStorage() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMaRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxMaProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java
new file mode 100644
index 0000000000..d116a30cf6
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.miniapp.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-05-25
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java
new file mode 100644
index 0000000000..b82261ba8a
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.miniapp.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-05-25
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java
new file mode 100644
index 0000000000..88d1c3023a
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java
@@ -0,0 +1,25 @@
+package com.binarywang.solon.wxjava.miniapp.integration;
+
+import com.binarywang.solon.wxjava.miniapp.config.WxMaServiceAutoConfiguration;
+import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInJedisConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInMemoryConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInRedissonConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxMiniappPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxMaProperties.class);
+
+ context.beanMake(WxMaServiceAutoConfiguration.class);
+
+ context.beanMake(WxMaInMemoryConfigStorageConfiguration.class);
+ context.beanMake(WxMaInJedisConfigStorageConfiguration.class);
+ context.beanMake(WxMaInRedissonConfigStorageConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java
new file mode 100644
index 0000000000..021a4b1b6b
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java
@@ -0,0 +1,43 @@
+package com.binarywang.solon.wxjava.miniapp.properties;
+
+import lombok.Data;
+
+/**
+ * redis 配置.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties {
+
+ /**
+ * 主机地址.不填则从solon容器内获取JedisPool
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java
new file mode 100644
index 0000000000..4493b6aec5
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java
@@ -0,0 +1,117 @@
+package com.binarywang.solon.wxjava.miniapp.properties;
+
+import com.binarywang.solon.wxjava.miniapp.enums.HttpClientType;
+import com.binarywang.solon.wxjava.miniapp.enums.StorageType;
+import lombok.Data;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import static com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties.PREFIX;
+
+/**
+ * 属性配置类.
+ *
+ * @author Binary Wang
+ * created on 2019-08-10
+ */
+@Data
+@Configuration
+@Inject("${" + PREFIX + "}")
+public class WxMaProperties {
+ public static final String PREFIX = "wx.miniapp";
+
+ /**
+ * 设置微信小程序的appid.
+ */
+ private String appid;
+
+ /**
+ * 设置微信小程序的Secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信小程序消息服务器配置的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信小程序消息服务器配置的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 消息格式,XML或者JSON.
+ */
+ private String msgDataFormat;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage {
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wa";
+
+ /**
+ * redis连接配置.
+ */
+ private final RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+ }
+
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties
new file mode 100644
index 0000000000..ba1049647e
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.miniapp.integration.WxMiniappPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..22a9a4e627
--- /dev/null
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,18 @@
+# ?????(??)
+wx.miniapp.appid = appId
+wx.miniapp.secret = @secret
+wx.miniapp.token = @token
+wx.miniapp.aesKey = @aesKey
+wx.miniapp.msgDataFormat = @msgDataFormat # ?????XML??JSON.
+# ????redis(??)
+# ??: ??redis.host???????????redis??(JedisPool)
+wx.miniapp.config-storage.type = Jedis # ????: Memory(??), Jedis, RedisTemplate
+wx.miniapp.config-storage.key-prefix = wa # ??redis????: wa(??)
+wx.miniapp.config-storage.redis.host = 127.0.0.1
+wx.miniapp.config-storage.redis.port = 6379
+# http?????
+wx.miniapp.config-storage.http-client-type=HttpClient # http?????: HttpClient(??), OkHttp, JoddHttp
+wx.miniapp.config-storage.http-proxy-host=
+wx.miniapp.config-storage.http-proxy-port=
+wx.miniapp.config-storage.http-proxy-username=
+wx.miniapp.config-storage.http-proxy-password=
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/README.md b/solon-plugins/wx-java-mp-multi-solon-plugin/README.md
new file mode 100644
index 0000000000..0d2b332d5a
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/README.md
@@ -0,0 +1,100 @@
+# wx-java-mp-multi-solon-plugin
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-mp-multi-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置
+ ## 应用 1 配置(必填)
+ wx.mp.apps.tenantId1.app-id=appId
+ wx.mp.apps.tenantId1.app-secret=@secret
+ ## 选填
+ wx.mp.apps.tenantId1.token=@token
+ wx.mp.apps.tenantId1.aes-key=@aesKey
+ wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+ ## 应用 2 配置(必填)
+ wx.mp.apps.tenantId2.app-id=@appId
+ wx.mp.apps.tenantId2.app-secret =@secret
+ ## 选填
+ wx.mp.apps.tenantId2.token=@token
+ wx.mp.apps.tenantId2.aes-key=@aesKey
+ wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson, redis_template
+ wx.mp.config-storage.type=memory
+ ## 相关redis前缀配置: wx:mp:multi(默认)
+ wx.mp.config-storage.key-prefix=wx:mp:multi
+ wx.mp.config-storage.redis.host=127.0.0.1
+ wx.mp.config-storage.redis.port=6379
+ ## 单机和 sentinel 同时存在时,优先使用sentinel配置
+ # wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ # wx.mp.config-storage.redis.sentinel-name=mymaster
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.mp.config-storage.http-client-type=http_client
+ wx.mp.config-storage.http-proxy-host=
+ wx.mp.config-storage.http-proxy-port=
+ wx.mp.config-storage.http-proxy-username=
+ wx.mp.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.mp.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.mp.config-storage.retry-sleep-millis=1000
+
+ # 公众号地址 host 配置
+ # wx.mp.hosts.api-host=http://proxy.com/
+ # wx.mp.hosts.open-host=http://proxy.com/
+ # wx.mp.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型:`WxMpMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.WxMpUserService;
+import org.noear.solon.annotation.Component;
+import org.noear.solon.annotation.Inject;
+
+@Component
+public class DemoService {
+ @Inject
+ private WxMpMultiServices wxMpMultiServices;
+
+ public void test() {
+ // 应用 1 的 WxMpService
+ WxMpService wxMpService1 = wxMpMultiServices.getWxMpService("tenantId1");
+ WxMpUserService userService1 = wxMpService1.getUserService();
+ userService1.userInfo("xxx");
+ // todo ...
+
+ // 应用 2 的 WxMpService
+ WxMpService wxMpService2 = wxMpMultiServices.getWxMpService("tenantId2");
+ WxMpUserService userService2 = wxMpService2.getUserService();
+ userService2.userInfo("xxx");
+ // todo ...
+
+ // 应用 3 的 WxMpService
+ WxMpService wxMpService3 = wxMpMultiServices.getWxMpService("tenantId3");
+ // 判断是否为空
+ if (wxMpService3 == null) {
+ // todo wxMpService3 为空,请先配置 tenantId3 微信公众号应用参数
+ return;
+ }
+ WxMpUserService userService3 = wxMpService3.getUserService();
+ userService3.userInfo("xxx");
+ // todo ...
+ }
+}
+```
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
new file mode 100644
index 0000000000..47ae9e94a9
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-mp-multi-solon-plugin
+ WxJava - Solon Plugin for MP::支持多账号配置
+ 微信公众号开发的 Solon Plugin::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java
new file mode 100644
index 0000000000..a51c6eaaea
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java
@@ -0,0 +1,169 @@
+package com.binarywang.solon.wxjava.mp_multi.configuration.services;
+
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties;
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpSingleProperties;
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices;
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServicesImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.WxMpHostConfig;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * WxMpConfigStorage 抽象配置类
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxMpConfiguration {
+
+ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxCpMultiProperties) {
+ Map appsMap = wxCpMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空");
+ return new WxMpMultiServicesImpl();
+ }
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 查看 {@link me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl#setAppId(String)}
+ */
+ Collection apps = appsMap.values();
+ if (apps.size() > 1) {
+ // 校验 appId 是否唯一
+ boolean multi = apps.stream()
+ // 没有 appId,如果不判断是否为空,这里会报 NPE 异常
+ .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保微信公众号配置 appId 的唯一性");
+ }
+ }
+ WxMpMultiServicesImpl services = new WxMpMultiServicesImpl();
+
+ Set> entries = appsMap.entrySet();
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxMpSingleProperties wxMpSingleProperties = entry.getValue();
+ WxMpDefaultConfigImpl storage = this.wxMpConfigStorage(wxCpMultiProperties);
+ this.configApp(storage, wxMpSingleProperties);
+ this.configHttp(storage, wxCpMultiProperties.getConfigStorage());
+ this.configHost(storage, wxCpMultiProperties.getHosts());
+ WxMpService wxCpService = this.wxMpService(storage, wxCpMultiProperties);
+ services.addWxMpService(tenantId, wxCpService);
+ }
+ return services;
+ }
+
+ /**
+ * 配置 WxMpDefaultConfigImpl
+ *
+ * @param wxMpMultiProperties 参数
+ * @return WxMpDefaultConfigImpl
+ */
+ protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties);
+
+ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
+ WxMpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
+ WxMpService wxMpService;
+ switch (httpClientType) {
+ case OK_HTTP:
+ wxMpService = new WxMpServiceOkHttpImpl();
+ break;
+ case JODD_HTTP:
+ wxMpService = new WxMpServiceJoddHttpImpl();
+ break;
+ case HTTP_CLIENT:
+ wxMpService = new WxMpServiceHttpClientImpl();
+ break;
+ case HTTP_COMPONENTS:
+ wxMpService = new WxMpServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMpService = new WxMpServiceImpl();
+ break;
+ }
+
+ wxMpService.setWxMpConfigStorage(configStorage);
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxMpService.setRetrySleepMillis(retrySleepMillis);
+ wxMpService.setMaxRetryTimes(maxRetryTimes);
+ return wxMpService;
+ }
+
+ private void configApp(WxMpDefaultConfigImpl config, WxMpSingleProperties corpProperties) {
+ String appId = corpProperties.getAppId();
+ String appSecret = corpProperties.getAppSecret();
+ String token = corpProperties.getToken();
+ String aesKey = corpProperties.getAesKey();
+ boolean useStableAccessToken = corpProperties.isUseStableAccessToken();
+
+ config.setAppId(appId);
+ config.setSecret(appSecret);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ config.setUseStableAccessToken(useStableAccessToken);
+ }
+
+ private void configHttp(WxMpDefaultConfigImpl config, WxMpMultiProperties.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);
+ }
+ }
+ }
+
+ /**
+ * wx host config
+ */
+ private void configHost(WxMpDefaultConfigImpl config, WxMpMultiProperties.HostConfig hostConfig) {
+ if (hostConfig != null) {
+ String apiHost = hostConfig.getApiHost();
+ String mpHost = hostConfig.getMpHost();
+ String openHost = hostConfig.getOpenHost();
+ WxMpHostConfig wxMpHostConfig = new WxMpHostConfig();
+ wxMpHostConfig.setApiHost(StringUtils.isNotBlank(apiHost) ? apiHost : null);
+ wxMpHostConfig.setMpHost(StringUtils.isNotBlank(mpHost) ? mpHost : null);
+ wxMpHostConfig.setOpenHost(StringUtils.isNotBlank(openHost) ? openHost : null);
+ config.setHostConfig(wxMpHostConfig);
+ }
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java
new file mode 100644
index 0000000000..c00898a82d
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java
@@ -0,0 +1,78 @@
+package com.binarywang.solon.wxjava.mp_multi.configuration.services;
+
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties;
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiRedisProperties;
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type} = jedis",
+ onClass = JedisPool.class
+)
+@RequiredArgsConstructor
+public class WxMpInJedisConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxCpMultiProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) {
+ return this.configRedis(wxCpMultiProperties);
+ }
+
+ private WxMpDefaultConfigImpl configRedis(WxMpMultiProperties wxCpMultiProperties) {
+ WxMpMultiRedisProperties wxCpMultiRedisProperties = wxCpMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxCpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxCpMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxCpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxMpMultiProperties wxCpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage();
+ WxMpMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java
new file mode 100644
index 0000000000..74bc13e03e
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.solon.wxjava.mp_multi.configuration.services;
+
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties;
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxMpInMemoryConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxCpMultiProperties;
+
+ @Bean
+ public WxMpMultiServices wxCpMultiServices() {
+ return this.wxMpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxMpDefaultConfigImpl configInMemory() {
+ return new WxMpMapConfigImpl();
+ // return new WxMpDefaultConfigImpl();
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java
new file mode 100644
index 0000000000..89ffdfd912
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java
@@ -0,0 +1,68 @@
+package com.binarywang.solon.wxjava.mp_multi.configuration.services;
+
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties;
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiRedisProperties;
+import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxMpInRedissonConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxCpMultiProperties;
+ private final AppContext applicationContext;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) {
+ return this.configRedisson(wxCpMultiProperties);
+ }
+
+ private WxMpDefaultConfigImpl configRedisson(WxMpMultiProperties wxCpMultiProperties) {
+ WxMpMultiRedisProperties redisProperties = wxCpMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxCpMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMpRedissonConfigImpl(redissonClient, wxCpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxMpMultiProperties wxCpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage();
+ WxMpMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java
new file mode 100644
index 0000000000..3629a8f78f
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java
@@ -0,0 +1,23 @@
+package com.binarywang.solon.wxjava.mp_multi.integration;
+
+import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInJedisConfiguration;
+import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInMemoryConfiguration;
+import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInRedissonConfiguration;
+import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxMpMultiPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxMpMultiProperties.class);
+
+ context.beanMake(WxMpInJedisConfiguration.class);
+ context.beanMake(WxMpInMemoryConfiguration.class);
+ context.beanMake(WxMpInRedissonConfiguration.class);
+
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java
new file mode 100644
index 0000000000..3d47f71381
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java
@@ -0,0 +1,158 @@
+package com.binarywang.solon.wxjava.mp_multi.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+@Configuration
+@Inject("${"+WxMpMultiProperties.PREFIX+"}")
+public class WxMpMultiProperties implements Serializable {
+ private static final long serialVersionUID = -5358245184407791011L;
+ public static final String PREFIX = "wx.mp";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class HostConfig implements Serializable {
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.MEMORY;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:mp:multi";
+
+ /**
+ * redis连接配置.
+ */
+ private final WxMpMultiRedisProperties redis = new WxMpMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.mp.api.WxMpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.WxMpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ MEMORY,
+ /**
+ * jedis
+ */
+ JEDIS,
+ /**
+ * redisson
+ */
+ REDISSON,
+ /**
+ * redisTemplate
+ */
+ REDIS_TEMPLATE
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java
new file mode 100644
index 0000000000..12646d4eaf
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.solon.wxjava.mp_multi.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+public class WxMpMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java
new file mode 100644
index 0000000000..22938cb67c
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java
@@ -0,0 +1,40 @@
+package com.binarywang.solon.wxjava.mp_multi.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+public class WxMpSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+ /**
+ * 设置微信公众号的 appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的 app secret.
+ */
+ private String appSecret;
+
+ /**
+ * 设置微信公众号的 token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的 EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java
new file mode 100644
index 0000000000..a59b5962ad
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java
@@ -0,0 +1,27 @@
+package com.binarywang.solon.wxjava.mp_multi.service;
+
+
+import me.chanjar.weixin.mp.api.WxMpService;
+
+/**
+ * 企业微信 {@link WxMpService} 所有实例存放类.
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+public interface WxMpMultiServices {
+ /**
+ * 通过租户 Id 获取 WxMpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxMpService
+ */
+ WxMpService getWxMpService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxMpService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxMpService(String tenantId);
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java
new file mode 100644
index 0000000000..d87cd4e8df
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java
@@ -0,0 +1,36 @@
+package com.binarywang.solon.wxjava.mp_multi.service;
+
+import me.chanjar.weixin.mp.api.WxMpService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 企业微信 {@link WxMpMultiServices} 默认实现
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+public class WxMpMultiServicesImpl implements WxMpMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxMpService getWxMpService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxMpService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxMpService WxMpService 实例
+ */
+ public void addWxMpService(String tenantId, WxMpService wxMpService) {
+ this.services.put(tenantId, wxMpService);
+ }
+
+ @Override
+ public void removeWxMpService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties
new file mode 100644
index 0000000000..11c68ccc81
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.mp_multi.integration.WxMpMultiPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..3f3b21657c
--- /dev/null
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,23 @@
+# ?????
+## ?? 1 ??(??)
+wx.mp.apps.tenantId1.app-id=appId
+wx.mp.apps.tenantId1.app-secret=@secret
+## ??
+wx.mp.apps.tenantId1.token=@token
+wx.mp.apps.tenantId1.aes-key=@aesKey
+wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+## ?? 2 ??(??)
+wx.mp.apps.tenantId2.app-id=@appId
+wx.mp.apps.tenantId2.app-secret =@secret
+## ??
+wx.mp.apps.tenantId2.token=@token
+wx.mp.apps.tenantId2.aes-key=@aesKey
+wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+# ConfigStorage ??????
+## ????: memory(??), jedis, redisson, redis_template
+wx.mp.config-storage.type=memory
+## ??redis????: wx:mp:multi(??)
+wx.mp.config-storage.key-prefix=wx:mp:multi
+wx.mp.config-storage.redis.host=127.0.0.1
+wx.mp.config-storage.redis.port=6379
diff --git a/solon-plugins/wx-java-mp-solon-plugin/README.md b/solon-plugins/wx-java-mp-solon-plugin/README.md
new file mode 100644
index 0000000000..58dcbfddbe
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/README.md
@@ -0,0 +1,46 @@
+# wx-java-mp-solon-plugin
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-mp-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.mp.app-id=appId
+ wx.mp.secret=@secret
+ wx.mp.token=@token
+ wx.mp.aes-key=@aesKey
+ wx.mp.use-stable-access-token=@useStableAccessToken
+ # 存储配置redis(可选)
+ wx.mp.config-storage.type= edis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.mp.config-storage.key-prefix=wx # 相关redis前缀配置: wx(默认)
+ wx.mp.config-storage.redis.host=127.0.0.1
+ wx.mp.config-storage.redis.port=6379
+ #单机和sentinel同时存在时,优先使用sentinel配置
+ #wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ #wx.mp.config-storage.redis.sentinel-name=mymaster
+ # http客户端配置
+ wx.mp.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
+ wx.mp.config-storage.http-proxy-host=
+ wx.mp.config-storage.http-proxy-port=
+ wx.mp.config-storage.http-proxy-username=
+ wx.mp.config-storage.http-proxy-password=
+ # 公众号地址host配置
+ #wx.mp.hosts.api-host=http://proxy.com/
+ #wx.mp.hosts.open-host=http://proxy.com/
+ #wx.mp.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型
+
+- `WxMpService`
+- `WxMpConfigStorage`
+
+4、参考demo:
+https://github.com/binarywang/wx-java-mp-demo
diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
new file mode 100644
index 0000000000..90787bdb04
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-mp-solon-plugin
+ WxJava - Solon Plugin for MP
+ 微信公众号开发的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java
new file mode 100644
index 0000000000..334ccf7abe
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java
@@ -0,0 +1,71 @@
+package com.binarywang.solon.wxjava.mp.config;
+
+import com.binarywang.solon.wxjava.mp.enums.HttpClientType;
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 微信公众号相关服务自动注册.
+ *
+ * @author someone
+ */
+@Configuration
+public class WxMpServiceAutoConfiguration {
+
+ @Bean
+ @Condition(onMissingBean = WxMpService.class)
+ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties wxMpProperties) {
+ HttpClientType httpClientType = wxMpProperties.getConfigStorage().getHttpClientType();
+ WxMpService wxMpService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxMpService = newWxMpServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxMpService = newWxMpServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxMpService = newWxMpServiceHttpClientImpl();
+ break;
+ case HttpComponents:
+ wxMpService = newWxMpServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMpService = newWxMpServiceImpl();
+ break;
+ }
+
+ wxMpService.setWxMpConfigStorage(configStorage);
+ return wxMpService;
+ }
+
+ private WxMpService newWxMpServiceImpl() {
+ return new WxMpServiceImpl();
+ }
+
+ private WxMpService newWxMpServiceHttpClientImpl() {
+ return new WxMpServiceHttpClientImpl();
+ }
+
+ private WxMpService newWxMpServiceOkHttpImpl() {
+ return new WxMpServiceOkHttpImpl();
+ }
+
+ private WxMpService newWxMpServiceJoddHttpImpl() {
+ return new WxMpServiceJoddHttpImpl();
+ }
+
+ private WxMpService newWxMpServiceHttpComponentsImpl() {
+ return new WxMpServiceHttpComponentsImpl();
+ }
+
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java
new file mode 100644
index 0000000000..663bb13340
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java
@@ -0,0 +1,27 @@
+package com.binarywang.solon.wxjava.mp.config.storage;
+
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+
+/**
+ * @author zhangyl
+ */
+public abstract class AbstractWxMpConfigStorageConfiguration {
+
+ protected WxMpDefaultConfigImpl config(WxMpDefaultConfigImpl config, WxMpProperties properties) {
+ config.setAppId(properties.getAppId());
+ config.setSecret(properties.getSecret());
+ config.setToken(properties.getToken());
+ config.setAesKey(properties.getAesKey());
+ config.setUseStableAccessToken(properties.isUseStableAccessToken());
+
+ WxMpProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+ return config;
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..a949ccfaca
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java
@@ -0,0 +1,76 @@
+package com.binarywang.solon.wxjava.mp.config.storage;
+
+import com.binarywang.solon.wxjava.mp.properties.RedisProperties;
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@Condition(
+ onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type} = jedis",
+ onClass = Jedis.class
+)
+@RequiredArgsConstructor
+public class WxMpInJedisConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean = WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpRedisConfigImpl config = getWxMpRedisConfigImpl();
+ return this.config(config, properties);
+ }
+
+ private WxMpRedisConfigImpl getWxMpRedisConfigImpl() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = applicationContext.getBean("wxMpJedisPool");
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ @Bean
+ @Condition(onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.redis.host}")
+ public JedisPool wxMpJedisPool() {
+ WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+ redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..88994fcf42
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,29 @@
+package com.binarywang.solon.wxjava.mp.config.storage;
+
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@Condition(
+ onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxMpInMemoryConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+
+ @Bean
+ @Condition(onMissingBean = WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
+ config(config, properties);
+ return config;
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..7121f2d700
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,65 @@
+package com.binarywang.solon.wxjava.mp.config.storage;
+
+import com.binarywang.solon.wxjava.mp.properties.RedisProperties;
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@Condition(
+ onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxMpInRedissonConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean = WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMaConfig() {
+ WxMpRedissonConfigImpl config = getWxMpInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() {
+ String configuredHost = applicationContext.cfg().get(WxMpProperties.PREFIX + ".config-storage.redis.host");
+ RedissonClient redissonClient;
+ if (StringUtils.isNotEmpty(configuredHost)) {
+ redissonClient = applicationContext.getBean("wxMpRedissonClient");
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMpRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ @Bean
+ @Condition(onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.redis.host}")
+ public RedissonClient wxMpRedissonClient() {
+ WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase());
+ if (StringUtils.isNotBlank(redis.getPassword())) {
+ config.useSingleServer().setPassword(redis.getPassword());
+ }
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java
new file mode 100644
index 0000000000..2858d77e43
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.mp.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java
new file mode 100644
index 0000000000..34433a8230
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.mp.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java
new file mode 100644
index 0000000000..285d871f25
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java
@@ -0,0 +1,29 @@
+package com.binarywang.solon.wxjava.mp.integration;
+
+import com.binarywang.solon.wxjava.mp.config.WxMpServiceAutoConfiguration;
+import com.binarywang.solon.wxjava.mp.config.storage.WxMpInJedisConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.mp.config.storage.WxMpInMemoryConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.mp.config.storage.WxMpInRedissonConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+import org.noear.solon.core.util.ClassUtil;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxMpPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxMpProperties.class);
+ context.beanMake(WxMpServiceAutoConfiguration.class);
+
+ context.beanMake(WxMpInMemoryConfigStorageConfiguration.class);
+ if (ClassUtil.loadClass("redis.clients.jedis.Jedis") != null) {
+ context.beanMake(WxMpInJedisConfigStorageConfiguration.class);
+ }
+ if (ClassUtil.loadClass("org.redisson.api.RedissonClient") != null) {
+ context.beanMake(WxMpInRedissonConfigStorageConfiguration.class);
+ }
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java
new file mode 100644
index 0000000000..8ccedf9294
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java
@@ -0,0 +1,27 @@
+package com.binarywang.solon.wxjava.mp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class HostConfig implements Serializable {
+
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java
new file mode 100644
index 0000000000..0376f947a7
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.solon.wxjava.mp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * redis 配置属性.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java
new file mode 100644
index 0000000000..0dcc6b41d3
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java
@@ -0,0 +1,106 @@
+package com.binarywang.solon.wxjava.mp.properties;
+
+import com.binarywang.solon.wxjava.mp.enums.HttpClientType;
+import com.binarywang.solon.wxjava.mp.enums.StorageType;
+import lombok.Data;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+
+import static com.binarywang.solon.wxjava.mp.enums.StorageType.Memory;
+import static com.binarywang.solon.wxjava.mp.properties.WxMpProperties.PREFIX;
+
+/**
+ * 微信接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@Configuration
+@Inject("${" + PREFIX + "}")
+public class WxMpProperties {
+ public static final String PREFIX = "wx.mp";
+
+ /**
+ * 设置微信公众号的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信公众号的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx";
+
+ /**
+ * redis连接配置.
+ */
+ private final RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ }
+
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties b/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties
new file mode 100644
index 0000000000..c80357184c
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.mp.integration.WxMpPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..06abfa5bb8
--- /dev/null
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,11 @@
+# ?????(??)
+wx.mp.app-id=appId
+wx.mp.secret=@secret
+wx.mp.token=@token
+wx.mp.aes-key=@aesKey
+wx.mp.use-stable-access-token=@useStableAccessToken
+# ????redis(??) # ????: Memory(??), Jedis, RedisTemplate
+wx.mp.config-storage.type=memory
+wx.mp.config-storage.key-prefix=wx
+wx.mp.config-storage.redis.host=127.0.0.1
+wx.mp.config-storage.redis.port=6379
diff --git a/solon-plugins/wx-java-open-solon-plugin/README.md b/solon-plugins/wx-java-open-solon-plugin/README.md
new file mode 100644
index 0000000000..619e28dbdd
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/README.md
@@ -0,0 +1,39 @@
+# wx-java-open-solon-plugin
+## 快速开始
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-open-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.open.appId = appId
+ wx.open.secret = @secret
+ wx.open.token = @token
+ wx.open.aesKey = @aesKey
+ # 存储配置redis(可选)
+ # 优先注入容器的(JedisPool, RedissonClient), 当配置了wx.open.config-storage.redis.host, 不会使用容器注入redis连接配置
+ wx.open.config-storage.type = redis # 配置类型: memory(默认), redis(jedis), jedis, redisson, redistemplate
+ wx.open.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认)
+ wx.open.config-storage.redis.host = 127.0.0.1
+ wx.open.config-storage.redis.port = 6379
+ # http客户端配置
+ wx.open.config-storage.http-client-type=httpclient # http客户端类型: httpclient(默认)
+ 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
+ ```
+3. 支持自动注入的类型: `WxOpenService, WxOpenMessageRouter, WxOpenComponentService`
+
+4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的
+ - WxOpenConfigStorage
+ - WxOpenService
diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml
new file mode 100644
index 0000000000..45bf5e0c52
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-open-solon-plugin
+ WxJava - Solon Plugin for WxOpen
+ 微信开放平台开发的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-open
+ ${project.version}
+
+
+ redis.clients
+ jedis
+
+
+ org.redisson
+ redisson
+
+
+
+
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java
new file mode 100644
index 0000000000..7bda6816ed
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java
@@ -0,0 +1,37 @@
+package com.binarywang.solon.wxjava.open.config;
+
+import me.chanjar.weixin.open.api.WxOpenComponentService;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.WxOpenService;
+import me.chanjar.weixin.open.api.impl.WxOpenMessageRouter;
+import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 微信开放平台相关服务自动注册.
+ *
+ * @author someone
+ */
+@Configuration
+public class WxOpenServiceAutoConfiguration {
+
+ @Bean
+ @Condition(onMissingBean = WxOpenService.class, onBean = WxOpenConfigStorage.class)
+ public WxOpenService wxOpenService(WxOpenConfigStorage wxOpenConfigStorage) {
+ WxOpenService wxOpenService = new WxOpenServiceImpl();
+ wxOpenService.setWxOpenConfigStorage(wxOpenConfigStorage);
+ return wxOpenService;
+ }
+
+ @Bean
+ public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) {
+ return new WxOpenMessageRouter(wxOpenService);
+ }
+
+ @Bean
+ public WxOpenComponentService wxOpenComponentService(WxOpenService wxOpenService) {
+ return wxOpenService.getWxOpenComponentService();
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java
new file mode 100644
index 0000000000..4a65b311d9
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java
@@ -0,0 +1,33 @@
+package com.binarywang.solon.wxjava.open.config.storage;
+
+import com.binarywang.solon.wxjava.open.properties.WxOpenProperties;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+
+/**
+ * @author yl
+ */
+public abstract class AbstractWxOpenConfigStorageConfiguration {
+
+ protected WxOpenInMemoryConfigStorage config(WxOpenInMemoryConfigStorage config, WxOpenProperties properties) {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
+ config.setHttpProxyHost(storage.getHttpProxyHost());
+ config.setHttpProxyUsername(storage.getHttpProxyUsername());
+ config.setHttpProxyPassword(storage.getHttpProxyPassword());
+ Integer httpProxyPort = storage.getHttpProxyPort();
+ if (httpProxyPort != null) {
+ config.setHttpProxyPort(httpProxyPort);
+ }
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ config.setRetrySleepMillis(retrySleepMillis);
+ config.setMaxRetryTimes(maxRetryTimes);
+ return config;
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..59e65ef48c
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java
@@ -0,0 +1,71 @@
+package com.binarywang.solon.wxjava.open.config.storage;
+
+import com.binarywang.solon.wxjava.open.properties.WxOpenProperties;
+import com.binarywang.solon.wxjava.open.properties.WxOpenRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author yl
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type} = jedis",
+ onClass = JedisPool.class
+)
+@RequiredArgsConstructor
+public class WxOpenInJedisConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = getWxOpenInRedisConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() {
+ WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxOpenInRedisConfigStorage(jedisPool, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ WxOpenRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..756b6525fc
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,28 @@
+package com.binarywang.solon.wxjava.open.config.storage;
+
+import com.binarywang.solon.wxjava.open.properties.WxOpenProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * @author yl
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type:memory} = memory"
+)
+@RequiredArgsConstructor
+public class WxOpenInMemoryConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+
+ @Bean
+ @Condition(onMissingBean=WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage();
+ return this.config(config, properties);
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..70844e2888
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,62 @@
+package com.binarywang.solon.wxjava.open.config.storage;
+
+import com.binarywang.solon.wxjava.open.properties.WxOpenProperties;
+import com.binarywang.solon.wxjava.open.properties.WxOpenRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.core.AppContext;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+
+/**
+ * @author yl
+ */
+@Configuration
+@Condition(
+ onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type} = redisson",
+ onClass = Redisson.class
+)
+@RequiredArgsConstructor
+public class WxOpenInRedissonConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+ private final AppContext applicationContext;
+
+ @Bean
+ @Condition(onMissingBean=WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = getWxOpenInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() {
+ WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxOpenInRedissonConfigStorage(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ WxOpenRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java
new file mode 100644
index 0000000000..29352d81f0
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java
@@ -0,0 +1,25 @@
+package com.binarywang.solon.wxjava.open.integration;
+
+import com.binarywang.solon.wxjava.open.config.WxOpenServiceAutoConfiguration;
+import com.binarywang.solon.wxjava.open.config.storage.WxOpenInJedisConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.open.config.storage.WxOpenInMemoryConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.open.config.storage.WxOpenInRedissonConfigStorageConfiguration;
+import com.binarywang.solon.wxjava.open.properties.WxOpenProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxOpenPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxOpenProperties.class);
+
+ context.beanMake(WxOpenServiceAutoConfiguration.class);
+
+ context.beanMake(WxOpenInMemoryConfigStorageConfiguration.class);
+ context.beanMake(WxOpenInJedisConfigStorageConfiguration.class);
+ context.beanMake(WxOpenInRedissonConfigStorageConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java
new file mode 100644
index 0000000000..4ec34c02b8
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java
@@ -0,0 +1,138 @@
+package com.binarywang.solon.wxjava.open.properties;
+
+import lombok.Data;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+
+import static com.binarywang.solon.wxjava.open.properties.WxOpenProperties.PREFIX;
+import static com.binarywang.solon.wxjava.open.properties.WxOpenProperties.StorageType.memory;
+
+
+/**
+ * 微信接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@Configuration
+@Inject("${"+PREFIX+"}")
+public class WxOpenProperties {
+ public static final String PREFIX = "wx.open";
+
+ /**
+ * 设置微信开放平台的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信开放平台的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信开放平台的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信开放平台的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 存储策略.
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:open";
+
+ /**
+ * redis连接配置.
+ */
+ private WxOpenRedisProperties redis = new WxOpenRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.httpclient;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ }
+
+ public enum StorageType {
+ /**
+ * 内存.
+ */
+ memory,
+ /**
+ * jedis.
+ */
+ jedis,
+ /**
+ * redisson.
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ httpclient
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java
new file mode 100644
index 0000000000..6b7a2d8654
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java
@@ -0,0 +1,45 @@
+package com.binarywang.solon.wxjava.open.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author someone
+ */
+@Data
+public class WxOpenRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties b/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties
new file mode 100644
index 0000000000..289aca5eeb
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.open.integration.WxOpenPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties
new file mode 100644
index 0000000000..fc2e79c95b
--- /dev/null
+++ b/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties
@@ -0,0 +1,11 @@
+# ?????(??)
+wx.open.appId = appId
+wx.open.secret = @secret
+wx.open.token = @token
+wx.open.aesKey = @aesKey
+# ????redis(??)
+# ???????(JedisPool, RedissonClient), ????wx.open.config-storage.redis.host, ????????redis????
+wx.open.config-storage.type = redis # ????: memory(??), redis(jedis), jedis, redisson, redistemplate
+wx.open.config-storage.key-prefix = wx # ??redis????: wx(??)
+wx.open.config-storage.redis.host = 127.0.0.1
+wx.open.config-storage.redis.port = 6379
diff --git a/solon-plugins/wx-java-pay-solon-plugin/README.md b/solon-plugins/wx-java-pay-solon-plugin/README.md
new file mode 100644
index 0000000000..8ff3416293
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/README.md
@@ -0,0 +1,45 @@
+# 使用说明
+1. 在自己的Solon项目里,引入maven依赖
+```xml
+
+ com.github.binarywang
+ wx-java-pay-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.yml)
+###### 1)V2版本
+```yml
+wx:
+ pay:
+ appId:
+ mchId:
+ mchKey:
+ keyPath:
+```
+###### 2)V3版本
+```yml
+wx:
+ pay:
+ appId: xxxxxxxxxxx
+ mchId: 15xxxxxxxxx #商户id
+ apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
+ apiHostUrlPath: /api-weixin # 可选:代理入口前缀
+ apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
+ certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
+ privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
+ privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
+```
+###### 3)V3服务商版本
+```yml
+wx:
+ pay: #微信服务商支付
+ configs:
+ - appId: wxe97b2x9c2b3d #spAppId
+ mchId: 16486610 #服务商商户
+ subAppId: wx118cexxe3c07679 #子appId
+ subMchId: 16496705 #子商户
+ apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr #apiV3密钥
+ privateKeyPath: classpath:cert/apiclient_key.pem #服务商证书文件,apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径(可以配置绝对路径)
+ privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
+```
diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
new file mode 100644
index 0000000000..7bd0a955f1
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-pay-solon-plugin
+ WxJava - Solon Plugin for WxPay
+ 微信支付开发的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-pay
+ ${project.version}
+
+
+
+
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java
new file mode 100644
index 0000000000..c311a099a2
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java
@@ -0,0 +1,70 @@
+package com.binarywang.solon.wxjava.pay.config;
+
+import com.binarywang.solon.wxjava.pay.properties.WxPayProperties;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ *
+ * 微信支付自动配置
+ * Created by BinaryWang on 2019/4/17.
+ *
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@Condition(
+ onProperty = "${wx.pay.enabled:true} = true",
+ onClass=WxPayService.class
+)
+public class WxPayAutoConfiguration {
+ private WxPayProperties properties;
+
+ public WxPayAutoConfiguration(WxPayProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * 构造微信支付服务对象.
+ *
+ * @return 微信支付service
+ */
+ @Bean
+ @Condition(onMissingBean=WxPayService.class)
+ public WxPayService wxPayService() {
+ final WxPayServiceImpl wxPayService = new WxPayServiceImpl();
+ WxPayConfig payConfig = new WxPayConfig();
+ payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
+ payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
+ payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
+ payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
+ payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
+ payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
+ payConfig.setUseSandboxEnv(this.properties.isUseSandboxEnv());
+ payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl()));
+ payConfig.setRefundNotifyUrl(StringUtils.trimToNull(this.properties.getRefundNotifyUrl()));
+ //以下是apiv3以及支付分相关
+ payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
+ payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
+ payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(this.properties.getPayScorePermissionNotifyUrl()));
+ payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
+ payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
+ payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
+ payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key()));
+ payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
+ payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
+ payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
+ payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
+ payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
+ payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());
+
+ wxPayService.setConfig(payConfig);
+ return wxPayService;
+ }
+
+}
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java
new file mode 100644
index 0000000000..e7ba275ca0
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java
@@ -0,0 +1,18 @@
+package com.binarywang.solon.wxjava.pay.integration;
+
+import com.binarywang.solon.wxjava.pay.config.WxPayAutoConfiguration;
+import com.binarywang.solon.wxjava.pay.properties.WxPayProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxPayPluginImpl implements Plugin {
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxPayProperties.class);
+
+ context.beanMake(WxPayAutoConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java
new file mode 100644
index 0000000000..fe024f59f1
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java
@@ -0,0 +1,132 @@
+package com.binarywang.solon.wxjava.pay.properties;
+
+import lombok.Data;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+/**
+ *
+ * 微信支付属性配置类
+ * Created by Binary Wang on 2019/4/17.
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@Configuration
+@Inject("${wx.pay}")
+public class WxPayProperties {
+ /**
+ * 设置微信公众号或者小程序等的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;
+
+ /**
+ * 微信支付分回调地址
+ */
+ private String payScoreNotifyUrl;
+
+ /**
+ * apiv3 商户apiclient_key.pem
+ */
+ private String privateKeyPath;
+
+ /**
+ * apiv3 商户apiclient_cert.pem
+ */
+ private String privateCertPath;
+
+ /**
+ * 微信支付是否使用仿真测试环境.
+ * 默认不使用
+ */
+ private boolean useSandboxEnv;
+
+ /**
+ * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数
+ */
+ private String notifyUrl;
+
+ /**
+ * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数.
+ */
+ private String refundNotifyUrl;
+
+ /**
+ * 微信支付分授权回调地址
+ */
+ private String payScorePermissionNotifyUrl;
+
+ /**
+ * 公钥ID
+ */
+ private String publicKeyId;
+
+ /**
+ * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+ */
+ private String publicKeyPath;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.mch.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
+ /**
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
+ */
+ private boolean strictlyNeedWechatPaySerial = true;
+
+ /**
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用
+ */
+ private boolean fullPublicKeyModel = true;
+
+}
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties b/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties
new file mode 100644
index 0000000000..98783176e2
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.pay.integration.WxPayPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml b/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml
new file mode 100644
index 0000000000..1d6a61d7e5
--- /dev/null
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml
@@ -0,0 +1,6 @@
+wx:
+ pay:
+ appId:
+ mchId:
+ mchKey:
+ keyPath:
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/README.md b/solon-plugins/wx-java-qidian-solon-plugin/README.md
new file mode 100644
index 0000000000..42daa3e4c8
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/README.md
@@ -0,0 +1,45 @@
+# wx-java-qidian-solon-plugin
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-qidian-solon-plugin
+ ${version}
+
+ ```
+2. 添加配置(app.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.qidian.appId = appId
+ wx.qidian.secret = @secret
+ wx.qidian.token = @token
+ wx.qidian.aesKey = @aesKey
+ # 存储配置redis(可选)
+ wx.qidian.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.qidian.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认)
+ wx.qidian.config-storage.redis.host = 127.0.0.1
+ wx.qidian.config-storage.redis.port = 6379
+ #单机和sentinel同时存在时,优先使用sentinel配置
+ #wx.qidian.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ #wx.qidian.config-storage.redis.sentinel-name=mymaster
+ # http客户端配置
+ wx.qidian.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
+ wx.qidian.config-storage.http-proxy-host=
+ wx.qidian.config-storage.http-proxy-port=
+ wx.qidian.config-storage.http-proxy-username=
+ wx.qidian.config-storage.http-proxy-password=
+ # 公众号地址host配置
+ #wx.qidian.hosts.api-host=http://proxy.com/
+ #wx.qidian.hosts.open-host=http://proxy.com/
+ #wx.qidian.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型
+
+- `WxQidianService`
+- `WxQidianConfigStorage`
+
+4、参考 demo:
+https://github.com/binarywang/wx-java-mp-demo
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
new file mode 100644
index 0000000000..0e47ac7c4e
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
@@ -0,0 +1,38 @@
+
+
+
+ wx-java-solon-plugins
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-qidian-solon-plugin
+ WxJava - Solon Plugin for QiDian
+ 腾讯企点的 Solon Plugin
+
+
+
+ com.github.binarywang
+ weixin-java-qidian
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ 4.3.2
+ compile
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java
new file mode 100644
index 0000000000..02ec06cd25
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java
@@ -0,0 +1,71 @@
+package com.binarywang.solon.wxjava.qidian.config;
+
+import com.binarywang.solon.wxjava.qidian.enums.HttpClientType;
+import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties;
+import me.chanjar.weixin.qidian.api.WxQidianService;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpComponentsImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceJoddHttpImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceOkHttpImpl;
+import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+
+/**
+ * 腾讯企点相关服务自动注册.
+ *
+ * @author alegria
+ */
+@Configuration
+public class WxQidianServiceAutoConfiguration {
+
+ @Bean
+ @Condition(onMissingBean = WxQidianService.class)
+ public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQidianProperties wxQidianProperties) {
+ HttpClientType httpClientType = wxQidianProperties.getConfigStorage().getHttpClientType();
+ WxQidianService wxQidianService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxQidianService = newWxQidianServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxQidianService = newWxQidianServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxQidianService = newWxQidianServiceHttpClientImpl();
+ break;
+ case HttpComponents:
+ wxQidianService = newWxQidianServiceHttpComponentsImpl();
+ break;
+ default:
+ wxQidianService = newWxQidianServiceImpl();
+ break;
+ }
+
+ wxQidianService.setWxMpConfigStorage(configStorage);
+ return wxQidianService;
+ }
+
+ private WxQidianService newWxQidianServiceImpl() {
+ return new WxQidianServiceImpl();
+ }
+
+ private WxQidianService newWxQidianServiceHttpClientImpl() {
+ return new WxQidianServiceHttpClientImpl();
+ }
+
+ private WxQidianService newWxQidianServiceOkHttpImpl() {
+ return new WxQidianServiceOkHttpImpl();
+ }
+
+ private WxQidianService newWxQidianServiceJoddHttpImpl() {
+ return new WxQidianServiceJoddHttpImpl();
+ }
+
+ private WxQidianService newWxQidianServiceHttpComponentsImpl() {
+ return new WxQidianServiceHttpComponentsImpl();
+ }
+
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java
new file mode 100644
index 0000000000..a99a8178c9
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java
@@ -0,0 +1,137 @@
+package com.binarywang.solon.wxjava.qidian.config;
+
+import com.binarywang.solon.wxjava.qidian.enums.StorageType;
+import com.binarywang.solon.wxjava.qidian.properties.RedisProperties;
+import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties;
+import com.google.common.collect.Sets;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
+import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
+import me.chanjar.weixin.qidian.config.impl.WxQidianDefaultConfigImpl;
+import me.chanjar.weixin.qidian.config.impl.WxQidianRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Condition;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+import org.noear.solon.core.AppContext;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.JedisSentinelPool;
+import redis.clients.jedis.util.Pool;
+
+import java.time.Duration;
+import java.util.Set;
+
+/**
+ * 腾讯企点存储策略自动配置.
+ *
+ * @author alegria
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class WxQidianStorageAutoConfiguration {
+ private final AppContext applicationContext;
+
+ private final WxQidianProperties wxQidianProperties;
+
+ @Inject("${wx.mp.config-storage.redis.host:")
+ private String redisHost;
+
+ @Inject("${wx.mp.configStorage.redis.host:")
+ private String redisHost2;
+
+ @Bean
+ @Condition(onMissingBean=WxQidianConfigStorage.class)
+ public WxQidianConfigStorage wxQidianConfigStorage() {
+ StorageType type = wxQidianProperties.getConfigStorage().getType();
+ WxQidianConfigStorage config;
+ switch (type) {
+ case Jedis:
+ config = jedisConfigStorage();
+ break;
+ default:
+ config = defaultConfigStorage();
+ break;
+ }
+ // wx host config
+ if (null != wxQidianProperties.getHosts() && StringUtils.isNotEmpty(wxQidianProperties.getHosts().getApiHost())) {
+ WxQidianHostConfig hostConfig = new WxQidianHostConfig();
+ hostConfig.setApiHost(wxQidianProperties.getHosts().getApiHost());
+ hostConfig.setQidianHost(wxQidianProperties.getHosts().getQidianHost());
+ hostConfig.setOpenHost(wxQidianProperties.getHosts().getOpenHost());
+ config.setHostConfig(hostConfig);
+ }
+ return config;
+ }
+
+ private WxQidianConfigStorage defaultConfigStorage() {
+ WxQidianDefaultConfigImpl config = new WxQidianDefaultConfigImpl();
+ setWxMpInfo(config);
+ return config;
+ }
+
+ private WxQidianConfigStorage jedisConfigStorage() {
+ Pool jedisPool;
+ if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ WxQidianRedisConfigImpl wxQidianRedisConfig = new WxQidianRedisConfigImpl(redisOps,
+ wxQidianProperties.getConfigStorage().getKeyPrefix());
+ setWxMpInfo(wxQidianRedisConfig);
+ return wxQidianRedisConfig;
+ }
+
+ private void setWxMpInfo(WxQidianDefaultConfigImpl config) {
+ WxQidianProperties properties = wxQidianProperties;
+ WxQidianProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setAppId(properties.getAppId());
+ config.setSecret(properties.getSecret());
+ config.setToken(properties.getToken());
+ config.setAesKey(properties.getAesKey());
+
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+ }
+
+ private Pool getJedisPool() {
+ WxQidianProperties.ConfigStorage storage = wxQidianProperties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWait(Duration.ofMillis(redis.getMaxWaitMillis()));
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+ if (StringUtils.isNotEmpty(redis.getSentinelIps())) {
+
+ Set sentinels = Sets.newHashSet(redis.getSentinelIps().split(","));
+ return new JedisSentinelPool(redis.getSentinelName(), sentinels,config);
+ }
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+ redis.getDatabase());
+ }
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java
new file mode 100644
index 0000000000..5729ab8fda
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.qidian.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java
new file mode 100644
index 0000000000..c4bd2f72cf
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.solon.wxjava.qidian.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java
new file mode 100644
index 0000000000..2a97b512fd
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java
@@ -0,0 +1,20 @@
+package com.binarywang.solon.wxjava.qidian.integration;
+
+import com.binarywang.solon.wxjava.qidian.config.WxQidianServiceAutoConfiguration;
+import com.binarywang.solon.wxjava.qidian.config.WxQidianStorageAutoConfiguration;
+import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties;
+import org.noear.solon.core.AppContext;
+import org.noear.solon.core.Plugin;
+
+/**
+ * @author noear 2024/9/2 created
+ */
+public class WxQidianPluginImpl implements Plugin{
+ @Override
+ public void start(AppContext context) throws Throwable {
+ context.beanMake(WxQidianProperties.class);
+
+ context.beanMake(WxQidianStorageAutoConfiguration.class);
+ context.beanMake(WxQidianServiceAutoConfiguration.class);
+ }
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java
new file mode 100644
index 0000000000..08546d8da6
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java
@@ -0,0 +1,18 @@
+package com.binarywang.solon.wxjava.qidian.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class HostConfig implements Serializable {
+
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ private String apiHost;
+
+ private String openHost;
+
+ private String qidianHost;
+
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java
new file mode 100644
index 0000000000..a6b10a9e0f
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.solon.wxjava.qidian.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * redis 配置属性.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java
new file mode 100644
index 0000000000..e99f882e6f
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java
@@ -0,0 +1,101 @@
+package com.binarywang.solon.wxjava.qidian.properties;
+
+import com.binarywang.solon.wxjava.qidian.enums.HttpClientType;
+import com.binarywang.solon.wxjava.qidian.enums.StorageType;
+import lombok.Data;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+
+import java.io.Serializable;
+
+import static com.binarywang.solon.wxjava.qidian.enums.StorageType.Memory;
+import static com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties.PREFIX;
+
+/**
+ * 企点接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@Configuration
+@Inject("${" + PREFIX + "}")
+public class WxQidianProperties {
+ public static final String PREFIX = "wx.qidian";
+
+ /**
+ * 设置腾讯企点的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置腾讯企点的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置腾讯企点的token.
+ */
+ private String token;
+
+ /**
+ * 设置腾讯企点的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx";
+
+ /**
+ * redis连接配置.
+ */
+ private RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ }
+
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties b/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties
new file mode 100644
index 0000000000..a60c450b06
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties
@@ -0,0 +1,2 @@
+solon.plugin=com.binarywang.solon.wxjava.qidian.integration.WxQidianPluginImpl
+solon.plugin.priority=10
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java
new file mode 100644
index 0000000000..d049f5a51a
--- /dev/null
+++ b/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java
@@ -0,0 +1,15 @@
+package features.test;
+
+import org.junit.jupiter.api.Test;
+import org.noear.solon.test.SolonTest;
+
+/**
+ * @author noear 2024/9/4 created
+ */
+@SolonTest
+public class LoadTest {
+ @Test
+ public void load(){
+
+ }
+}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/test/resources/app.yml b/solon-plugins/wx-java-qidian-solon-plugin/src/test/resources/app.yml
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md
new file mode 100644
index 0000000000..6581f6207d
--- /dev/null
+++ b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md
@@ -0,0 +1,160 @@
+# 多租户模式配置改进说明
+
+## 问题背景
+
+用户在 issue #3835 中提出了一个架构设计问题:
+
+> 基础 Wx 实现类中已经有 configMap 了,可以用 configMap 来存储不同的小程序配置。不同的配置,都是复用同一个 http 客户端。为什么在各个 spring-boot-starter 中又单独创建类来存储不同的配置?从 spring 的配置来看,http 客户端只有一个,不同小程序配置可以实现多租户,所以似乎没必要单独再建新类存放?重复创建,增加了 http 客户端的成本?直接使用 Wx 实现类中已经有 configMap 不是更好吗?
+
+## 解决方案
+
+从 4.8.0 版本开始,我们为多租户 Spring Boot Starter 提供了**两种实现模式**供用户选择:
+
+### 1. 隔离模式(ISOLATED,默认)
+
+**实现方式**:为每个租户创建独立的 WxService 实例,每个实例拥有独立的 HTTP 客户端。
+
+**优点**:
+- ✅ 线程安全,无需担心并发问题
+- ✅ 不依赖 ThreadLocal,适合异步/响应式编程
+- ✅ 租户间完全隔离,互不影响
+
+**缺点**:
+- ❌ 每个租户创建独立的 HTTP 客户端,资源占用较多
+- ❌ 适合租户数量不多的场景(建议 < 50 个租户)
+
+**代码实现**:`WxMaMultiServicesImpl`, `WxMpMultiServicesImpl` 等
+
+### 2. 共享模式(SHARED,新增)
+
+**实现方式**:使用单个 WxService 实例管理所有租户配置,通过 ThreadLocal 切换租户,所有租户共享同一个 HTTP 客户端。
+
+**优点**:
+- ✅ 共享 HTTP 客户端,大幅节省资源
+- ✅ 适合租户数量较多的场景(支持 100+ 租户)
+- ✅ 内存占用更小
+
+**缺点**:
+- ❌ 依赖 ThreadLocal 切换配置,在异步场景需要特别注意
+- ❌ 需要注意线程上下文传递
+
+**代码实现**:`WxMaMultiServicesSharedImpl`, `WxMpMultiServicesSharedImpl` 等
+
+## 使用方式
+
+### 配置示例
+
+```yaml
+wx:
+ ma: # 或 mp, cp, channel
+ apps:
+ tenant1:
+ app-id: wxd898fcb01713c555
+ app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
+ tenant2:
+ app-id: wx1234567890abcdef
+ app-secret: 1234567890abcdef1234567890abcdef
+
+ config-storage:
+ type: memory
+ http-client-type: http_client
+ # 多租户模式配置(新增)
+ multi-tenant-mode: shared # isolated(默认)或 shared
+```
+
+### 代码使用(两种模式代码完全相同)
+
+```java
+@RestController
+public class WxController {
+ @Autowired
+ private WxMaMultiServices wxMaMultiServices; // 或 WxMpMultiServices
+
+ @GetMapping("/api/{tenantId}")
+ public String handle(@PathVariable String tenantId) {
+ WxMaService wxService = wxMaMultiServices.getWxMaService(tenantId);
+ // 使用 wxService 调用微信 API
+ return wxService.getAccessToken();
+ }
+}
+```
+
+## 性能对比
+
+以 100 个租户为例:
+
+| 指标 | 隔离模式 | 共享模式 |
+|------|---------|---------|
+| HTTP 客户端数量 | 100 个 | 1 个 |
+| 内存占用(估算) | ~500MB | ~50MB |
+| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 |
+| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) |
+| 适用场景 | 中小规模 | 大规模 |
+
+## 支持的模块
+
+目前已实现共享模式支持的模块:
+
+- ✅ **小程序(MiniApp)**:`wx-java-miniapp-multi-spring-boot-starter`
+- ✅ **公众号(MP)**:`wx-java-mp-multi-spring-boot-starter`
+
+后续版本将支持:
+- ⏳ 企业微信(CP)
+- ⏳ 视频号(Channel)
+- ⏳ 企业微信第三方应用(CP-TP)
+
+## 迁移指南
+
+### 从旧版本升级
+
+升级到 4.8.0+ 后:
+
+1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致)
+2. **向后兼容**:所有现有代码无需修改
+3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式
+
+### 选择建议
+
+**使用隔离模式(ISOLATED)的场景**:
+- 租户数量较少(< 50 个)
+- 使用异步编程、响应式编程
+- 对线程安全有严格要求
+- 对资源占用不敏感
+
+**使用共享模式(SHARED)的场景**:
+- 租户数量较多(> 50 个)
+- 同步编程场景
+- 对资源占用敏感
+- 可以接受 ThreadLocal 的约束
+
+## 注意事项
+
+### 共享模式下的异步编程
+
+如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递:
+
+```java
+// ❌ 错误:异步线程无法获取到正确的配置
+CompletableFuture.runAsync(() -> {
+ wxService.getUserService().getUserInfo(...); // 可能使用错误的租户配置
+});
+
+// ✅ 正确:在主线程获取必要信息,传递给异步线程
+String appId = wxService.getWxMaConfig().getAppid();
+CompletableFuture.runAsync(() -> {
+ log.info("AppId: {}", appId); // 使用已获取的配置信息
+});
+```
+
+## 详细文档
+
+- 小程序模块详细说明:[spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md](spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md)
+
+## 相关链接
+
+- Issue: [#3835](https://github.com/binarywang/WxJava/issues/3835)
+- Pull Request: [#3840](https://github.com/binarywang/WxJava/pull/3840)
+
+## 致谢
+
+感谢 issue 提出者对项目架构的深入思考和建议,这帮助我们提供了更灵活、更高效的多租户解决方案。
diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml
new file mode 100644
index 0000000000..9de6f2d82d
--- /dev/null
+++ b/spring-boot-starters/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.5.B
+
+ pom
+ wx-java-spring-boot-starters
+ WxJava - Spring Boot Starters
+ WxJava 各个模块的 Spring Boot Starter
+
+
+ 2.5.15
+
+
+
+ wx-java-miniapp-multi-spring-boot-starter
+ wx-java-miniapp-spring-boot-starter
+ wx-java-mp-multi-spring-boot-starter
+ wx-java-mp-spring-boot-starter
+ wx-java-pay-spring-boot-starter
+ wx-java-pay-multi-spring-boot-starter
+ wx-java-open-multi-spring-boot-starter
+ wx-java-open-spring-boot-starter
+ wx-java-qidian-spring-boot-starter
+ wx-java-cp-multi-spring-boot-starter
+ wx-java-cp-tp-multi-spring-boot-starter
+ wx-java-cp-spring-boot-starter
+ wx-java-channel-spring-boot-starter
+ wx-java-channel-multi-spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ ${spring.boot.version}
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ ${spring.boot.version}
+ true
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..c2f082bec8
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md
@@ -0,0 +1,123 @@
+# wx-java-channel-multi-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+
+ com.github.binarywang
+ wx-java-channel-multi-spring-boot-starter
+ ${version}
+
+
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
+
+
+ org.redisson
+ redisson
+ ${redisson.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 视频号配置
+ ## 应用 1 配置(必填)
+ wx.channel.apps.tenantId1.app-id=@appId
+ wx.channel.apps.tenantId1.secret=@secret
+ ## 选填
+ wx.channel.apps.tenantId1.use-stable-access-token=false
+ wx.channel.apps.tenantId1.token=
+ wx.channel.apps.tenantId1.aes-key=
+ ## 应用 2 配置(必填)
+ wx.channel.apps.tenantId2.app-id=@appId
+ wx.channel.apps.tenantId2.secret=@secret
+ ## 选填
+ wx.channel.apps.tenantId2.use-stable-access-token=false
+ wx.channel.apps.tenantId2.token=
+ wx.channel.apps.tenantId2.aes-key=
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson, redis_template
+ wx.channel.config-storage.type=memory
+ ## 相关redis前缀配置: wx:channel:multi(默认)
+ wx.channel.config-storage.key-prefix=wx:channel:multi
+ wx.channel.config-storage.redis.host=127.0.0.1
+ wx.channel.config-storage.redis.port=6379
+ wx.channel.config-storage.redis.password=123456
+
+ # redis_template 方式使用spring data redis配置
+ spring.data.redis.database=0
+ spring.data.redis.host=127.0.0.1
+ spring.data.redis.password=123456
+ spring.data.redis.port=6379
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认)
+ wx.channel.config-storage.http-client-type=http_client
+ wx.channel.config-storage.http-proxy-host=
+ wx.channel.config-storage.http-proxy-port=
+ wx.channel.config-storage.http-proxy-username=
+ wx.channel.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.channel.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.channel.config-storage.retry-sleep-millis=1000
+ ```
+3. 自动注入的类型:`WxChannelMultiServices`
+
+4. 使用样例
+
+ ```java
+ import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+ import me.chanjar.weixin.channel.api.WxChannelService;
+ import me.chanjar.weixin.channel.api.WxFinderLiveService;
+ import me.chanjar.weixin.channel.bean.lead.component.response.FinderAttrResponse;
+ import me.chanjar.weixin.common.error.WxErrorException;
+ import org.springframework.beans.factory.annotation.Autowired;
+ import org.springframework.stereotype.Service;
+
+ @Service
+ public class DemoService {
+ @Autowired
+ private WxChannelMultiServices wxChannelMultiServices;
+
+ public void test() throws WxErrorException {
+ // 应用 1 的 WxChannelService
+ WxChannelService wxChannelService1 = wxChannelMultiServices.getWxChannelService("tenantId1");
+ WxFinderLiveService finderLiveService = wxChannelService1.getFinderLiveService();
+ FinderAttrResponse response1 = finderLiveService.getFinderAttrByAppid();
+ // todo ...
+
+ // 应用 2 的 WxChannelService
+ WxChannelService wxChannelService2 = wxChannelMultiServices.getWxChannelService("tenantId2");
+ WxFinderLiveService finderLiveService2 = wxChannelService2.getFinderLiveService();
+ FinderAttrResponse response2 = finderLiveService2.getFinderAttrByAppid();
+ // todo ...
+
+ // 应用 3 的 WxChannelService
+ WxChannelService wxChannelService3 = wxChannelMultiServices.getWxChannelService("tenantId3");
+ // 判断是否为空
+ if (wxChannelService3 == null) {
+ // todo wxChannelService3 为空,请先配置 tenantId3 微信视频号应用参数
+ return;
+ }
+ WxFinderLiveService finderLiveService3 = wxChannelService3.getFinderLiveService();
+ FinderAttrResponse response3 = finderLiveService3.getFinderAttrByAppid();
+ // todo ...
+ }
+ }
+ ```
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..a1acf3a571
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-channel-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for Channel::支持多账号配置
+ 微信视频号开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-channel
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java
new file mode 100644
index 0000000000..e6ef922b43
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java
@@ -0,0 +1,15 @@
+package com.binarywang.spring.starter.wxjava.channel.autoconfigure;
+
+import com.binarywang.spring.starter.wxjava.channel.configuration.WxChannelMultiServiceConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信视频号自动注册
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@Import(WxChannelMultiServiceConfiguration.class)
+public class WxChannelMultiAutoConfiguration {}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java
new file mode 100644
index 0000000000..17cd0da723
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java
@@ -0,0 +1,21 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration;
+
+import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInJedisConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInMemoryConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInRedisTemplateConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInRedissonConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信视频号相关服务自动注册
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@EnableConfigurationProperties(WxChannelMultiProperties.class)
+@Import({WxChannelInJedisConfiguration.class, WxChannelInMemoryConfiguration.class, WxChannelInRedissonConfiguration.class, WxChannelInRedisTemplateConfiguration.class})
+public class WxChannelMultiServiceConfiguration {}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java
new file mode 100644
index 0000000000..e2f9f87f5a
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java
@@ -0,0 +1,148 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelSingleProperties;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServicesImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpClientImpl;
+import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpComponentsImpl;
+import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * WxChannelConfigStorage 抽象配置类
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxChannelConfiguration {
+ protected WxChannelMultiServices wxChannelMultiServices(WxChannelMultiProperties wxChannelMultiProperties) {
+ Map appsMap = wxChannelMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信视频号应用参数未配置,通过 WxChannelMultiServices#getWxChannelService(\"tenantId\")获取实例将返回空");
+ return new WxChannelMultiServicesImpl();
+ }
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 查看 {@link me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl#setAppid(String)}
+ */
+ Collection apps = appsMap.values();
+ if (apps.size() > 1) {
+ // 校验 appId 是否唯一
+ boolean multi = apps.stream()
+ // 没有 appId,如果不判断是否为空,这里会报 NPE 异常
+ .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保微信视频号配置 appId 的唯一性");
+ }
+ }
+ WxChannelMultiServicesImpl services = new WxChannelMultiServicesImpl();
+
+ Set> entries = appsMap.entrySet();
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxChannelSingleProperties wxChannelSingleProperties = entry.getValue();
+ WxChannelDefaultConfigImpl storage = this.wxChannelConfigStorage(wxChannelMultiProperties);
+ this.configApp(storage, wxChannelSingleProperties);
+ this.configHttp(storage, wxChannelMultiProperties.getConfigStorage());
+ WxChannelService wxChannelService = this.wxChannelService(storage, wxChannelMultiProperties);
+ services.addWxChannelService(tenantId, wxChannelService);
+ }
+ return services;
+ }
+
+ /**
+ * 配置 WxChannelDefaultConfigImpl
+ *
+ * @param wxChannelMultiProperties 参数
+ * @return WxChannelDefaultConfigImpl
+ */
+ protected abstract WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties);
+
+ public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig, WxChannelMultiProperties wxChannelMultiProperties) {
+ WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage();
+ HttpClientType httpClientType = storage.getHttpClientType();
+ WxChannelService wxChannelService;
+ switch (httpClientType) {
+// case OK_HTTP:
+// wxChannelService = new WxChannelServiceOkHttpImpl(false, false);
+// break;
+ case HTTP_CLIENT:
+ wxChannelService = new WxChannelServiceHttpClientImpl();
+ break;
+ case HTTP_COMPONENTS:
+ wxChannelService = new WxChannelServiceHttpComponentsImpl();
+ break;
+ default:
+ wxChannelService = new WxChannelServiceImpl();
+ break;
+ }
+
+ wxChannelService.setConfig(wxChannelConfig);
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxChannelService.setRetrySleepMillis(retrySleepMillis);
+ wxChannelService.setMaxRetryTimes(maxRetryTimes);
+ return wxChannelService;
+ }
+
+ private void configApp(WxChannelDefaultConfigImpl config, WxChannelSingleProperties wxChannelSingleProperties) {
+ String appId = wxChannelSingleProperties.getAppId();
+ String appSecret = wxChannelSingleProperties.getSecret();
+ String token = wxChannelSingleProperties.getToken();
+ String aesKey = wxChannelSingleProperties.getAesKey();
+ boolean useStableAccessToken = wxChannelSingleProperties.isUseStableAccessToken();
+
+ config.setAppid(appId);
+ config.setSecret(appSecret);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ config.setStableAccessToken(useStableAccessToken);
+ config.setApiHostUrl(StringUtils.trimToNull(wxChannelSingleProperties.getApiHostUrl()));
+ config.setAccessTokenUrl(StringUtils.trimToNull(wxChannelSingleProperties.getAccessTokenUrl()));
+ }
+
+ private void configHttp(WxChannelDefaultConfigImpl config, WxChannelMultiProperties.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);
+ }
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java
new file mode 100644
index 0000000000..d19b0fc4b5
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java
@@ -0,0 +1,74 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis")
+@RequiredArgsConstructor
+public class WxChannelInJedisConfiguration extends AbstractWxChannelConfiguration {
+ private final WxChannelMultiProperties wxChannelMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxChannelMultiServices wxChannelMultiServices() {
+ return this.wxChannelMultiServices(wxChannelMultiProperties);
+ }
+
+ @Override
+ protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) {
+ return this.configRedis(wxChannelMultiProperties);
+ }
+
+ private WxChannelDefaultConfigImpl configRedis(WxChannelMultiProperties wxChannelMultiProperties) {
+ WxChannelMultiRedisProperties wxChannelMultiRedisProperties = wxChannelMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxChannelMultiRedisProperties != null && StringUtils.isNotEmpty(wxChannelMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxChannelMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxChannelRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxChannelMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxChannelMultiProperties wxChannelMultiProperties) {
+ WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage();
+ WxChannelMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java
new file mode 100644
index 0000000000..77bb221f25
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java
@@ -0,0 +1,36 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true)
+@RequiredArgsConstructor
+public class WxChannelInMemoryConfiguration extends AbstractWxChannelConfiguration {
+ private final WxChannelMultiProperties wxChannelMultiProperties;
+
+ @Bean
+ public WxChannelMultiServices wxChannelMultiServices() {
+ return this.wxChannelMultiServices(wxChannelMultiProperties);
+ }
+
+ @Override
+ protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxChannelDefaultConfigImpl configInMemory() {
+ return new WxChannelDefaultConfigImpl();
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java
new file mode 100644
index 0000000000..f9defdb94a
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java
@@ -0,0 +1,42 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template")
+@RequiredArgsConstructor
+public class WxChannelInRedisTemplateConfiguration extends AbstractWxChannelConfiguration {
+ private final WxChannelMultiProperties wxChannelMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxChannelMultiServices wxChannelMultiServices() {
+ return this.wxChannelMultiServices(wxChannelMultiProperties);
+ }
+
+ @Override
+ protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) {
+ return this.configRedisTemplate(wxChannelMultiProperties);
+ }
+
+ private WxChannelDefaultConfigImpl configRedisTemplate(WxChannelMultiProperties wxChannelMultiProperties) {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxChannelRedisConfigImpl(new RedisTemplateWxRedisOps(redisTemplate), wxChannelMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java
new file mode 100644
index 0000000000..fa4798a18b
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java
@@ -0,0 +1,62 @@
+package com.binarywang.spring.starter.wxjava.channel.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson")
+@RequiredArgsConstructor
+public class WxChannelInRedissonConfiguration extends AbstractWxChannelConfiguration {
+ private final WxChannelMultiProperties wxChannelMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxChannelMultiServices wxChannelMultiServices() {
+ return this.wxChannelMultiServices(wxChannelMultiProperties);
+ }
+
+ @Override
+ protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) {
+ return this.configRedisson(wxChannelMultiProperties);
+ }
+
+ private WxChannelDefaultConfigImpl configRedisson(WxChannelMultiProperties wxChannelMultiProperties) {
+ WxChannelMultiRedisProperties redisProperties = wxChannelMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxChannelMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxChannelRedissonConfigImpl(redissonClient, wxChannelMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxChannelMultiProperties wxChannelMultiProperties) {
+ WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage();
+ WxChannelMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer().setAddress("redis://" + redis.getHost() + ":" + redis.getPort()).setDatabase(redis.getDatabase()).setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java
new file mode 100644
index 0000000000..adc8a2b52b
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java
@@ -0,0 +1,23 @@
+package com.binarywang.spring.starter.wxjava.channel.enums;
+
+/**
+ * httpclient类型
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HTTP_CLIENT,
+ // WxChannelServiceOkHttpImpl 实现经测试无法正常完成业务固暂不支持OK_HTTP方式
+// /**
+// * OkHttp.
+// */
+// OK_HTTP,
+ /**
+ * HttpComponents.
+ */
+ HTTP_COMPONENTS,
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java
new file mode 100644
index 0000000000..0ee69eca73
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.channel.enums;
+
+/**
+ * storage类型
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+public enum StorageType {
+ /**
+ * 内存
+ */
+ MEMORY,
+ /**
+ * redis(JedisClient)
+ */
+ JEDIS,
+ /**
+ * redis(Redisson)
+ */
+ REDISSON,
+ /**
+ * redis(RedisTemplate)
+ */
+ REDIS_TEMPLATE
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java
new file mode 100644
index 0000000000..d22f560282
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java
@@ -0,0 +1,96 @@
+package com.binarywang.spring.starter.wxjava.channel.properties;
+
+import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.channel.enums.StorageType;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信多视频号接入相关配置属性
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(WxChannelMultiProperties.PREFIX)
+public class WxChannelMultiProperties implements Serializable {
+ private static final long serialVersionUID = - 8361973118805546037L;
+ public static final String PREFIX = "wx.channel";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = - 5152619132544179942L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.MEMORY;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:channel:multi";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final WxChannelMultiRedisProperties redis = new WxChannelMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.channel.api.WxChannelService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setMaxRetryTimes(int)}
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.channel.api.WxChannelService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setRetrySleepMillis(int)}
+ */
+ private int retrySleepMillis = 1000;
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java
new file mode 100644
index 0000000000..99c426765c
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java
@@ -0,0 +1,63 @@
+package com.binarywang.spring.starter.wxjava.channel.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Data
+@NoArgsConstructor
+public class WxChannelMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = 9061055444734277357L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * 最大活动连接数
+ */
+ private Integer maxActive;
+
+ /**
+ * 最大空闲连接数
+ */
+ private Integer maxIdle;
+
+ /**
+ * 最小空闲连接数
+ */
+ private Integer minIdle;
+
+ /**
+ * 最大等待时间
+ */
+ private Integer maxWaitMillis;
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java
new file mode 100644
index 0000000000..4b613e3bf6
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java
@@ -0,0 +1,55 @@
+package com.binarywang.spring.starter.wxjava.channel.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信视频号相关配置属性
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+@Data
+@NoArgsConstructor
+public class WxChannelSingleProperties implements Serializable {
+ private static final long serialVersionUID = 5306630351265124825L;
+
+ /**
+ * 设置微信视频号的 appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信视频号的 secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信视频号的 token.
+ */
+ private String token;
+
+ /**
+ * 设置微信视频号的 EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java
new file mode 100644
index 0000000000..acd4ebf20b
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.channel.service;
+
+import me.chanjar.weixin.channel.api.WxChannelService;
+
+/**
+ * 视频号 {@link WxChannelService} 所有实例存放类.
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+public interface WxChannelMultiServices {
+ /**
+ * 通过租户 Id 获取 WxChannelService
+ *
+ * @param tenantId 租户 Id
+ * @return WxChannelService
+ */
+ WxChannelService getWxChannelService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxChannelService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxChannelService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java
new file mode 100644
index 0000000000..1673289cb5
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java
@@ -0,0 +1,36 @@
+package com.binarywang.spring.starter.wxjava.channel.service;
+
+import me.chanjar.weixin.channel.api.WxChannelService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 视频号 {@link WxChannelMultiServices} 实现
+ *
+ * @author Winnie
+ * @date 2024/9/13
+ */
+public class WxChannelMultiServicesImpl implements WxChannelMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxChannelService getWxChannelService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxChannelService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxChannelService WxChannelService 实例
+ */
+ public void addWxChannelService(String tenantId, WxChannelService wxChannelService) {
+ this.services.put(tenantId, wxChannelService);
+ }
+
+ @Override
+ public void removeWxChannelService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..2c5a939c32
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.channel.autoconfigure.WxChannelMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..d21a2cdc8d
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.channel.autoconfigure.WxChannelMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md b/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md
new file mode 100644
index 0000000000..398001a286
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md
@@ -0,0 +1,103 @@
+# wx-java-channel-spring-boot-starter
+
+## 快速开始
+1. 引入依赖
+ ```xml
+
+
+ com.github.binarywang
+ wx-java-channel-spring-boot-starter
+ ${version}
+
+
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
+
+
+ org.redisson
+ redisson
+ ${redisson.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 视频号配置(必填)
+ ## 视频号小店的appId和secret
+ wx.channel.app-id=@appId
+ wx.channel.secret=@secret
+ # 视频号配置 选填
+ ## 设置视频号小店消息服务器配置的token
+ wx.channel.token=@token
+ ## 设置视频号小店消息服务器配置的EncodingAESKey
+ wx.channel.aes-key=
+ ## 支持JSON或者XML格式,默认JSON
+ wx.channel.msg-data-format=JSON
+ ## 是否使用稳定版 Access Token
+ wx.channel.use-stable-access-token=false
+
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson, redis_template
+ wx.channel.config-storage.type=memory
+ ## 相关redis前缀配置: wx:channel(默认)
+ wx.channel.config-storage.key-prefix=wx:channel
+ wx.channel.config-storage.redis.host=127.0.0.1
+ wx.channel.config-storage.redis.port=6379
+ wx.channel.config-storage.redis.password=123456
+
+ # redis_template 方式使用spring data redis配置
+ spring.data.redis.database=0
+ spring.data.redis.host=127.0.0.1
+ spring.data.redis.password=123456
+ spring.data.redis.port=6379
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认)
+ wx.channel.config-storage.http-client-type=http_client
+ wx.channel.config-storage.http-proxy-host=
+ wx.channel.config-storage.http-proxy-port=
+ wx.channel.config-storage.http-proxy-username=
+ wx.channel.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.channel.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.channel.config-storage.retry-sleep-millis=1000
+ ```
+3. 自动注入的类型
+- `WxChannelService`
+- `WxChannelConfig`
+4. 使用样例
+```java
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse;
+import me.chanjar.weixin.channel.util.JsonUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+ @Autowired
+ private WxChannelService wxChannelService;
+
+ public String getShopInfo() throws WxErrorException {
+ // 获取店铺基本信息
+ ShopInfoResponse response = wxChannelService.getBasicService().getShopInfo();
+ // 此处为演示,如果要返回response的结果,建议自己封装一个VO,避免直接返回response
+ return JsonUtils.encode(response);
+ }
+}
+```
+
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..8c8620f072
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
@@ -0,0 +1,61 @@
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-channel-spring-boot-starter
+ WxJava - Spring Boot Starter for Channel
+ 微信视频号开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-channel
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java
new file mode 100644
index 0000000000..ad9d90b28d
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java
@@ -0,0 +1,20 @@
+package com.binarywang.spring.starter.wxjava.channel.config;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 自动配置
+ *
+ * @author Zeyes
+ */
+@Configuration
+@EnableConfigurationProperties(WxChannelProperties.class)
+@Import({
+ WxChannelStorageAutoConfiguration.class,
+ WxChannelServiceAutoConfiguration.class
+})
+public class WxChannelAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java
new file mode 100644
index 0000000000..5276a803e7
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java
@@ -0,0 +1,37 @@
+package com.binarywang.spring.starter.wxjava.channel.config;
+
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import lombok.AllArgsConstructor;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信小程序平台相关服务自动注册
+ *
+ * @author Zeyes
+ */
+@Configuration
+@AllArgsConstructor
+public class WxChannelServiceAutoConfiguration {
+ private final WxChannelProperties properties;
+
+ /**
+ * Channel Service
+ *
+ * @return Channel Service
+ */
+ @Bean
+ @ConditionalOnMissingBean(WxChannelService.class)
+ @ConditionalOnBean(WxChannelConfig.class)
+ public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig) {
+ WxChannelService wxChannelService = new WxChannelServiceImpl();
+ wxChannelService.setConfig(wxChannelConfig);
+ return wxChannelService;
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java
new file mode 100644
index 0000000000..66f2276a35
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java
@@ -0,0 +1,23 @@
+package com.binarywang.spring.starter.wxjava.channel.config;
+
+import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInJedisConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInMemoryConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInRedisTemplateConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInRedissonConfigStorageConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信小程序存储策略自动配置
+ *
+ * @author Zeyes
+ */
+@Configuration
+@Import({
+ WxChannelInMemoryConfigStorageConfiguration.class,
+ WxChannelInJedisConfigStorageConfiguration.class,
+ WxChannelInRedisTemplateConfigStorageConfiguration.class,
+ WxChannelInRedissonConfigStorageConfiguration.class
+})
+public class WxChannelStorageAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java
new file mode 100644
index 0000000000..2a7978640d
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java
@@ -0,0 +1,42 @@
+package com.binarywang.spring.starter.wxjava.channel.config.storage;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author Zeyes
+ */
+public abstract class AbstractWxChannelConfigStorageConfiguration {
+
+ protected WxChannelDefaultConfigImpl config(WxChannelDefaultConfigImpl config, WxChannelProperties properties) {
+ config.setAppid(StringUtils.trimToNull(properties.getAppid()));
+ config.setSecret(StringUtils.trimToNull(properties.getSecret()));
+ config.setToken(StringUtils.trimToNull(properties.getToken()));
+ config.setAesKey(StringUtils.trimToNull(properties.getAesKey()));
+ config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat()));
+ config.setStableAccessToken(properties.isUseStableAccessToken());
+ config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
+
+ WxChannelProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+
+ int maxRetryTimes = configStorageProperties.getMaxRetryTimes();
+ if (configStorageProperties.getMaxRetryTimes() < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = configStorageProperties.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ config.setRetrySleepMillis(retrySleepMillis);
+ config.setMaxRetryTimes(maxRetryTimes);
+ return config;
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..f88548c3e9
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java
@@ -0,0 +1,73 @@
+package com.binarywang.spring.starter.wxjava.channel.config.storage;
+
+
+import com.binarywang.spring.starter.wxjava.channel.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author Zeyes
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis")
+@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class})
+@RequiredArgsConstructor
+public class WxChannelInJedisConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration {
+ private final WxChannelProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxChannelConfig.class)
+ public WxChannelConfig wxChannelConfig() {
+ WxChannelRedisConfigImpl config = getWxChannelRedisConfig();
+ return this.config(config, properties);
+ }
+
+ private WxChannelRedisConfigImpl getWxChannelRedisConfig() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ return new WxChannelRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxChannelProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..deb586ae7b
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,29 @@
+package com.binarywang.spring.starter.wxjava.channel.config.storage;
+
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Zeyes
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type",
+ matchIfMissing = true, havingValue = "memory")
+@RequiredArgsConstructor
+public class WxChannelInMemoryConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration {
+ private final WxChannelProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean(WxChannelProperties.class)
+ public WxChannelConfig wxChannelConfig() {
+ WxChannelDefaultConfigImpl config = new WxChannelDefaultConfigImpl();
+ return this.config(config, properties);
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java
new file mode 100644
index 0000000000..e190fbd755
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.spring.starter.wxjava.channel.config.storage;
+
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * @author Zeyes
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate")
+@ConditionalOnClass(StringRedisTemplate.class)
+@RequiredArgsConstructor
+public class WxChannelInRedisTemplateConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration {
+ private final WxChannelProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxChannelConfig.class)
+ public WxChannelConfig wxChannelConfig() {
+ WxChannelRedisConfigImpl config = getWxChannelInRedisTemplateConfig();
+ return this.config(config, properties);
+ }
+
+ private WxChannelRedisConfigImpl getWxChannelInRedisTemplateConfig() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+ return new WxChannelRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..16db4395a7
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,62 @@
+package com.binarywang.spring.starter.wxjava.channel.config.storage;
+
+
+import com.binarywang.spring.starter.wxjava.channel.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Zeyes
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson")
+@ConditionalOnClass({Redisson.class, RedissonClient.class})
+@RequiredArgsConstructor
+public class WxChannelInRedissonConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration {
+ private final WxChannelProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxChannelConfig.class)
+ public WxChannelConfig wxChannelConfig() {
+ WxChannelRedissonConfigImpl config = getWxChannelRedissonConfig();
+ return this.config(config, properties);
+ }
+
+ private WxChannelRedissonConfigImpl getWxChannelRedissonConfig() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxChannelRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxChannelProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java
new file mode 100644
index 0000000000..e4b3f3ad16
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java
@@ -0,0 +1,17 @@
+package com.binarywang.spring.starter.wxjava.channel.enums;
+
+/**
+ * httpclient类型
+ *
+ * @author Zeyes
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java
new file mode 100644
index 0000000000..59b27fc022
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java
@@ -0,0 +1,25 @@
+package com.binarywang.spring.starter.wxjava.channel.enums;
+
+/**
+ * storage类型
+ *
+ * @author Zeyes
+ */
+public enum StorageType {
+ /**
+ * 内存
+ */
+ Memory,
+ /**
+ * redis(JedisClient)
+ */
+ Jedis,
+ /**
+ * redis(Redisson)
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate)
+ */
+ RedisTemplate
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java
new file mode 100644
index 0000000000..19f27d0682
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java
@@ -0,0 +1,42 @@
+package com.binarywang.spring.starter.wxjava.channel.properties;
+
+import lombok.Data;
+
+/**
+ * redis 配置
+ *
+ * @author Zeyes
+ */
+@Data
+public class RedisProperties {
+
+ /**
+ * 主机地址,不填则从spring容器内获取JedisPool
+ */
+ private String host;
+
+ /**
+ * 端口号
+ */
+ private int port = 6379;
+
+ /**
+ * 密码
+ */
+ private String password;
+
+ /**
+ * 超时
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java
new file mode 100644
index 0000000000..f43d297e0b
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java
@@ -0,0 +1,126 @@
+package com.binarywang.spring.starter.wxjava.channel.properties;
+
+import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.channel.enums.StorageType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * 属性配置类
+ *
+ * @author Zeyes
+ */
+@Data
+@ConfigurationProperties(prefix = WxChannelProperties.PREFIX)
+public class WxChannelProperties {
+ public static final String PREFIX = "wx.channel";
+
+ /**
+ * 设置视频号小店的appid
+ */
+ private String appid;
+
+ /**
+ * 设置视频号小店的Secret
+ */
+ private String secret;
+
+ /**
+ * 设置视频号小店消息服务器配置的token.
+ */
+ private String token;
+
+ /**
+ * 设置视频号小店消息服务器配置的EncodingAESKey
+ */
+ private String aesKey;
+
+ /**
+ * 消息格式,XML或者JSON
+ */
+ private String msgDataFormat = "JSON";
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage {
+
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.Memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wh";
+
+ /**
+ * redis连接配置
+ */
+ @NestedConfigurationProperty
+ private final RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..a9401752a0
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ com.binarywang.spring.starter.wxjava.channel.config.WxChannelAutoConfiguration
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..99ccbadbbc
--- /dev/null
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.channel.config.WxChannelAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..0f0b74695e
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md
@@ -0,0 +1,112 @@
+# wx-java-cp-multi-spring-boot-starter
+
+企业微信多账号配置
+
+- 实现多 WxCpService 初始化。
+- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+
+## 关于 corp-secret 的说明
+
+企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限:
+
+| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id |
+|---|---|---|---|
+| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** |
+| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** |
+| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 |
+
+> **常见问题**:
+> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限)
+> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错**
+
+如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。
+
+> **配置限制说明**:
+> - 当前 starter 实现会校验:同一 `corp-id` 下,`agent-id` **必须唯一**
+> - 同一 `corp-id` 下,**只能有一个条目不填 `agent-id`**
+> - 否则会因为 token/ticket 缓存 key 冲突而在启动时直接抛异常
+>
+> 因此,像"通讯录同步 Secret""客户联系 Secret"这类通常不填写 `agent-id` 的配置,**不能**在同一个 `corp-id` 下同时配置多个 `agent-id` 均为空的条目;如确有多个条目,请确保其中最多只有一个未填写 `agent-id`。
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-cp-multi-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id)
+ wx.cp.corps.app1.corp-id = @corp-id
+ wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看)
+ wx.cp.corps.app1.agent-id = @自建应用的AgentId
+ ## 选填
+ wx.cp.corps.app1.token = @token
+ wx.cp.corps.app1.aes-key = @aes-key
+ wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path
+
+ # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id)
+ # 此配置用于部门、成员的增删改查等通讯录管理操作
+ wx.cp.corps.contact.corp-id = @corp-id
+ wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看)
+ ## agent-id 不填,通讯录同步不需要 agentId
+
+ # 公共配置
+ ## ConfigStorage 配置(选填)
+ wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate
+ ## http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.cp.config-storage.http-client-type=http_client
+ wx.cp.config-storage.http-proxy-host=
+ wx.cp.config-storage.http-proxy-port=
+ wx.cp.config-storage.http-proxy-username=
+ wx.cp.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.cp.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.cp.config-storage.retry-sleep-millis=1000
+ ```
+3. 支持自动注入的类型: `WxCpMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import me.chanjar.weixin.cp.api.WxCpDepartmentService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.WxCpUserService;
+import me.chanjar.weixin.cp.bean.WxCpDepart;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+ @Autowired
+ private WxCpMultiServices wxCpMultiServices;
+
+ public void test() {
+ // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret)
+ WxCpService appService = wxCpMultiServices.getWxCpService("app1");
+ WxCpUserService userService = appService.getUserService();
+ userService.getUserId("xxx");
+ // todo ...
+
+ // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret)
+ // 通讯录同步 Secret 具有部门/成员增删改查等权限
+ WxCpService contactService = wxCpMultiServices.getWxCpService("contact");
+ WxCpDepartmentService departmentService = contactService.getDepartmentService();
+ // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段)
+ WxCpDepart depart = new WxCpDepart();
+ depart.setId(100L);
+ depart.setName("新部门名称");
+ departmentService.update(depart);
+ // todo ...
+ }
+}
+```
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..f00b416488
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-cp-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for WxCp::支持多账号配置
+ 微信企业号开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-cp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java
new file mode 100644
index 0000000000..40a6d9048d
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java
@@ -0,0 +1,16 @@
+package com.binarywang.spring.starter.wxjava.cp.autoconfigure;
+
+import com.binarywang.spring.starter.wxjava.cp.configuration.WxCpMultiServicesAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信自动注册
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@Import(WxCpMultiServicesAutoConfiguration.class)
+public class WxCpMultiAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java
new file mode 100644
index 0000000000..12a4947301
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration;
+
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInJedisConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInMemoryConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInRedisTemplateConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInRedissonConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信平台相关服务自动注册
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@EnableConfigurationProperties(WxCpMultiProperties.class)
+@Import({
+ WxCpInJedisConfiguration.class,
+ WxCpInMemoryConfiguration.class,
+ WxCpInRedissonConfiguration.class,
+ WxCpInRedisTemplateConfiguration.class
+})
+public class WxCpMultiServicesAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java
new file mode 100644
index 0000000000..a793a8501f
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java
@@ -0,0 +1,173 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpSingleProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServicesImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceApacheHttpClientImpl;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceJoddHttpImpl;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceOkHttpImpl;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * WxCpConfigStorage 抽象配置类
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxCpConfiguration {
+
+ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiProperties) {
+ Map corps = wxCpMultiProperties.getCorps();
+ if (corps == null || corps.isEmpty()) {
+ log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpService(\"tenantId\")获取实例将返回空");
+ return new WxCpMultiServicesImpl();
+ }
+ /**
+ * 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:
+ *
+ * - 自建应用条目:填写应用对应的 corpSecret 和 agentId
+ * - 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
+ *
+ * 但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。
+ *
+ * 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)}
+ */
+ Collection corpList = corps.values();
+ if (corpList.size() > 1) {
+ // 先按 corpId 分组统计
+ Map> corpsMap = corpList.stream()
+ .collect(Collectors.groupingBy(WxCpSingleProperties::getCorpId));
+ Set>> entries = corpsMap.entrySet();
+ for (Map.Entry> entry : entries) {
+ String corpId = entry.getKey();
+ // 校验每个企业下,agentId 是否唯一
+ boolean multi = entry.getValue().stream()
+ // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突
+ .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]");
+ }
+ }
+ }
+ WxCpMultiServicesImpl services = new WxCpMultiServicesImpl();
+
+ Set> entries = corps.entrySet();
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxCpSingleProperties wxCpSingleProperties = entry.getValue();
+ WxCpDefaultConfigImpl storage = this.wxCpConfigStorage(wxCpMultiProperties);
+ this.configCorp(storage, wxCpSingleProperties);
+ this.configHttp(storage, wxCpMultiProperties.getConfigStorage());
+ WxCpService wxCpService = this.wxCpService(storage, wxCpMultiProperties.getConfigStorage());
+ services.addWxCpService(tenantId, wxCpService);
+ }
+ return services;
+ }
+
+ /**
+ * 配置 WxCpDefaultConfigImpl
+ *
+ * @param wxCpMultiProperties 参数
+ * @return WxCpDefaultConfigImpl
+ */
+ protected abstract WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties);
+
+ private WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage, WxCpMultiProperties.ConfigStorage storage) {
+ WxCpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
+ WxCpService wxCpService;
+ switch (httpClientType) {
+ case OK_HTTP:
+ wxCpService = new WxCpServiceOkHttpImpl();
+ break;
+ case JODD_HTTP:
+ wxCpService = new WxCpServiceJoddHttpImpl();
+ break;
+ case HTTP_CLIENT:
+ wxCpService = new WxCpServiceApacheHttpClientImpl();
+ break;
+ default:
+ wxCpService = new WxCpServiceImpl();
+ break;
+ }
+ wxCpService.setWxCpConfigStorage(wxCpConfigStorage);
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxCpService.setRetrySleepMillis(retrySleepMillis);
+ wxCpService.setMaxRetryTimes(maxRetryTimes);
+ return wxCpService;
+ }
+
+ private void configCorp(WxCpDefaultConfigImpl config, WxCpSingleProperties wxCpSingleProperties) {
+ String corpId = wxCpSingleProperties.getCorpId();
+ String corpSecret = wxCpSingleProperties.getCorpSecret();
+ Long agentId = wxCpSingleProperties.getAgentId();
+ String token = wxCpSingleProperties.getToken();
+ String aesKey = wxCpSingleProperties.getAesKey();
+ // 企业微信,私钥,会话存档路径
+ String msgAuditPriKey = wxCpSingleProperties.getMsgAuditPriKey();
+ String msgAuditLibPath = wxCpSingleProperties.getMsgAuditLibPath();
+
+ config.setCorpId(corpId);
+ config.setCorpSecret(corpSecret);
+ config.setAgentId(agentId);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditPriKey)) {
+ config.setMsgAuditPriKey(msgAuditPriKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditLibPath)) {
+ config.setMsgAuditLibPath(msgAuditLibPath);
+ }
+ if (StringUtils.isNotBlank(wxCpSingleProperties.getBaseApiUrl())) {
+ config.setBaseApiUrl(wxCpSingleProperties.getBaseApiUrl());
+ }
+ }
+
+ private void configHttp(WxCpDefaultConfigImpl config, WxCpMultiProperties.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);
+ }
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java
new file mode 100644
index 0000000000..e03647cb63
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java
@@ -0,0 +1,76 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@RequiredArgsConstructor
+public class WxCpInJedisConfiguration extends AbstractWxCpConfiguration {
+ private final WxCpMultiProperties wxCpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) {
+ return this.configRedis(wxCpMultiProperties);
+ }
+
+ private WxCpDefaultConfigImpl configRedis(WxCpMultiProperties wxCpMultiProperties) {
+ WxCpMultiRedisProperties wxCpMultiRedisProperties = wxCpMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxCpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxCpMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxCpJedisConfigImpl(jedisPool, wxCpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxCpMultiProperties wxCpMultiProperties) {
+ WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage();
+ WxCpMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java
new file mode 100644
index 0000000000..29593667ed
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java
@@ -0,0 +1,38 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxCpInMemoryConfiguration extends AbstractWxCpConfiguration {
+ private final WxCpMultiProperties wxCpMultiProperties;
+
+ @Bean
+ public WxCpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxCpDefaultConfigImpl configInMemory() {
+ return new WxCpDefaultConfigImpl();
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java
new file mode 100644
index 0000000000..374c5cdfb0
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java
@@ -0,0 +1,43 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
+)
+@RequiredArgsConstructor
+public class WxCpInRedisTemplateConfiguration extends AbstractWxCpConfiguration {
+ private final WxCpMultiProperties wxCpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) {
+ return this.configRedisTemplate(wxCpMultiProperties);
+ }
+
+ private WxCpDefaultConfigImpl configRedisTemplate(WxCpMultiProperties wxCpMultiProperties) {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxCpRedisTemplateConfigImpl(redisTemplate, wxCpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java
new file mode 100644
index 0000000000..c0753a44aa
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java
@@ -0,0 +1,67 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@RequiredArgsConstructor
+public class WxCpInRedissonConfiguration extends AbstractWxCpConfiguration {
+ private final WxCpMultiProperties wxCpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpMultiProperties);
+ }
+
+ @Override
+ protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) {
+ return this.configRedisson(wxCpMultiProperties);
+ }
+
+ private WxCpDefaultConfigImpl configRedisson(WxCpMultiProperties wxCpMultiProperties) {
+ WxCpMultiRedisProperties redisProperties = wxCpMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxCpMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxCpRedissonConfigImpl(redissonClient, wxCpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxCpMultiProperties wxCpMultiProperties) {
+ WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage();
+ WxCpMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java
new file mode 100644
index 0000000000..ab694a30b2
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java
@@ -0,0 +1,129 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 企业微信多企业接入相关配置属性
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(prefix = WxCpMultiProperties.PREFIX)
+public class WxCpMultiProperties implements Serializable {
+ private static final long serialVersionUID = -1569510477055668503L;
+ public static final String PREFIX = "wx.cp";
+
+ private Map corps = new HashMap<>();
+
+ /**
+ * 配置存储策略,默认内存
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wx:cp";
+
+ /**
+ * redis连接配置
+ */
+ @NestedConfigurationProperty
+ private WxCpMultiRedisProperties redis = new WxCpMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java
new file mode 100644
index 0000000000..ea1f257c41
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java
@@ -0,0 +1,48 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java
new file mode 100644
index 0000000000..740f5a2a3b
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java
@@ -0,0 +1,74 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信企业相关配置属性
+ *
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpSingleProperties implements Serializable {
+ private static final long serialVersionUID = -7502823825007859418L;
+ /**
+ * 微信企业号 corpId
+ */
+ private String corpId;
+ /**
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
+ */
+ private String corpSecret;
+ /**
+ * 微信企业号应用 token
+ */
+ private String token;
+ /**
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
+ */
+ private Long agentId;
+ /**
+ * 微信企业号应用 EncodingAESKey
+ */
+ private String aesKey;
+ /**
+ * 微信企业号应用 会话存档私钥
+ */
+ private String msgAuditPriKey;
+ /**
+ * 微信企业号应用 会话存档类库路径
+ */
+ private String msgAuditLibPath;
+
+ /**
+ * 自定义企业微信服务器baseUrl,用于替换默认的 https://qyapi.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String baseApiUrl;
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java
new file mode 100644
index 0000000000..dfcb25631d
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.cp.service;
+
+import me.chanjar.weixin.cp.api.WxCpService;
+
+/**
+ * 企业微信 {@link WxCpService} 所有实例存放类.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public interface WxCpMultiServices {
+ /**
+ * 通过租户 Id 获取 WxCpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpService
+ */
+ WxCpService getWxCpService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxCpService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxCpService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java
new file mode 100644
index 0000000000..19eae24159
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java
@@ -0,0 +1,42 @@
+package com.binarywang.spring.starter.wxjava.cp.service;
+
+import me.chanjar.weixin.cp.api.WxCpService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 企业微信 {@link WxCpMultiServices} 默认实现
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public class WxCpMultiServicesImpl implements WxCpMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ /**
+ * 通过租户 Id 获取 WxCpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpService
+ */
+ @Override
+ public WxCpService getWxCpService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxCpService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxCpService WxCpService 实例
+ */
+ public void addWxCpService(String tenantId, WxCpService wxCpService) {
+ this.services.put(tenantId, wxCpService);
+ }
+
+ @Override
+ public void removeWxCpService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..6010561a96
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..3c48ec34e1
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md
new file mode 100644
index 0000000000..d6c1abc945
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md
@@ -0,0 +1,41 @@
+# wx-java-cp-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-cp-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 企业微信号配置(必填)
+ wx.cp.corp-id = @corp-id
+ wx.cp.corp-secret = @corp-secret
+ # 选填
+ wx.cp.agent-id = @agent-id
+ wx.cp.token = @token
+ wx.cp.aes-key = @aes-key
+ wx.cp.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.msg-audit-lib-path = @msg-audit-lib-path
+ # ConfigStorage 配置(选填)
+ wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate
+ # http 客户端配置(选填)
+ wx.cp.config-storage.http-proxy-host=
+ wx.cp.config-storage.http-proxy-port=
+ wx.cp.config-storage.http-proxy-username=
+ wx.cp.config-storage.http-proxy-password=
+ # 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.cp.config-storage.max-retry-times=5
+ # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.cp.config-storage.retry-sleep-millis=1000
+ ```
+3. 支持自动注入的类型: `WxCpService`, `WxCpConfigStorage`
+
+4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的
+
+- WxCpService
+- WxCpConfigStorage
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..9e204e82cb
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
@@ -0,0 +1,60 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-cp-spring-boot-starter
+ WxJava - Spring Boot Starter for WxCp
+ 微信企业号开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-cp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+
+
+ org.redisson
+ redisson
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java
new file mode 100644
index 0000000000..f78c39dd45
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java
@@ -0,0 +1,21 @@
+package com.binarywang.spring.starter.wxjava.cp.config;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信自动注册
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@EnableConfigurationProperties(WxCpProperties.class)
+@Import({
+ WxCpStorageAutoConfiguration.class,
+ WxCpServiceAutoConfiguration.class
+})
+public class WxCpAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java
new file mode 100644
index 0000000000..70c4045259
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java
@@ -0,0 +1,44 @@
+package com.binarywang.spring.starter.wxjava.cp.config;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 企业微信平台相关服务自动注册
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@RequiredArgsConstructor
+public class WxCpServiceAutoConfiguration {
+ private final WxCpProperties wxCpProperties;
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(WxCpConfigStorage.class)
+ public WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage) {
+ WxCpService wxCpService = new WxCpServiceImpl();
+ wxCpService.setWxCpConfigStorage(wxCpConfigStorage);
+
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxCpService.setRetrySleepMillis(retrySleepMillis);
+ wxCpService.setMaxRetryTimes(maxRetryTimes);
+ return wxCpService;
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java
new file mode 100644
index 0000000000..1c7d80b84e
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java
@@ -0,0 +1,24 @@
+package com.binarywang.spring.starter.wxjava.cp.config;
+
+import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInJedisConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInMemoryConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInRedisTemplateConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInRedissonConfigStorageConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信存储策略自动配置
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@Import({
+ WxCpInMemoryConfigStorageConfiguration.class,
+ WxCpInJedisConfigStorageConfiguration.class,
+ WxCpInRedissonConfigStorageConfiguration.class,
+ WxCpInRedisTemplateConfigStorageConfiguration.class
+})
+public class WxCpStorageAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java
new file mode 100644
index 0000000000..63e730d1b4
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java
@@ -0,0 +1,139 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信接入相关配置属性
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(prefix = WxCpProperties.PREFIX)
+public class WxCpProperties {
+ public static final String PREFIX = "wx.cp";
+
+ /**
+ * 微信企业号 corpId
+ */
+ private String corpId;
+ /**
+ * 微信企业号 corpSecret
+ */
+ private String corpSecret;
+ /**
+ * 微信企业号应用 token
+ */
+ private String token;
+ /**
+ * 微信企业号应用 ID
+ */
+ private Long agentId;
+ /**
+ * 微信企业号应用 EncodingAESKey
+ */
+ private String aesKey;
+ /**
+ * 微信企业号应用 会话存档私钥
+ */
+ private String msgAuditPriKey;
+ /**
+ * 微信企业号应用 会话存档类库路径
+ */
+ private String msgAuditLibPath;
+
+ /**
+ * 自定义企业微信服务器baseUrl,用于替换默认的 https://qyapi.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String baseApiUrl;
+
+ /**
+ * 配置存储策略,默认内存
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wx:cp";
+
+ /**
+ * redis连接配置
+ */
+ @NestedConfigurationProperty
+ private WxCpRedisProperties redis = new WxCpRedisProperties();
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java
new file mode 100644
index 0000000000..63a7fe01e0
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java
@@ -0,0 +1,46 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Data
+public class WxCpRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java
new file mode 100644
index 0000000000..70f620a58d
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java
@@ -0,0 +1,64 @@
+package com.binarywang.spring.starter.wxjava.cp.storage;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * WxCpConfigStorage 抽象配置类
+ *
+ * @author yl & Wang_Wong
+ * created on 2021/12/6
+ */
+public abstract class AbstractWxCpConfigStorageConfiguration {
+
+ protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpProperties properties) {
+ String corpId = properties.getCorpId();
+ String corpSecret = properties.getCorpSecret();
+ Long agentId = properties.getAgentId();
+ String token = properties.getToken();
+ String aesKey = properties.getAesKey();
+ // 企业微信,私钥,会话存档路径
+ String msgAuditPriKey = properties.getMsgAuditPriKey();
+ String msgAuditLibPath = properties.getMsgAuditLibPath();
+
+ config.setCorpId(corpId);
+ config.setCorpSecret(corpSecret);
+ config.setAgentId(agentId);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditPriKey)) {
+ config.setMsgAuditPriKey(msgAuditPriKey);
+ }
+ if (StringUtils.isNotBlank(msgAuditLibPath)) {
+ config.setMsgAuditLibPath(msgAuditLibPath);
+ }
+ if (StringUtils.isNotBlank(properties.getBaseApiUrl())) {
+ config.setBaseApiUrl(properties.getBaseApiUrl());
+ }
+
+ WxCpProperties.ConfigStorage storage = properties.getConfigStorage();
+ 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);
+ }
+ }
+ return config;
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..246971baed
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java
@@ -0,0 +1,74 @@
+package com.binarywang.spring.starter.wxjava.cp.storage;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@RequiredArgsConstructor
+public class WxCpInJedisConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = getConfigStorage();
+ return this.config(config, wxCpProperties);
+ }
+
+ private WxCpJedisConfigImpl getConfigStorage() {
+ WxCpRedisProperties wxCpRedisProperties = wxCpProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxCpRedisProperties != null && StringUtils.isNotEmpty(wxCpRedisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxCpJedisConfigImpl(jedisPool, wxCpProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ WxCpRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..3722bd07d1
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,33 @@
+package com.binarywang.spring.starter.wxjava.cp.storage;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2021/12/6
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpProperties.PREFIX + ".config-storage", name = "type",
+ matchIfMissing = true, havingValue = "memory"
+)
+@RequiredArgsConstructor
+public class WxCpInMemoryConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+
+ @Bean
+ @ConditionalOnMissingBean(WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl();
+ return this.config(config, wxCpProperties);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java
new file mode 100644
index 0000000000..879568b16a
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java
@@ -0,0 +1,41 @@
+package com.binarywang.spring.starter.wxjava.cp.storage;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
+)
+@RequiredArgsConstructor
+public class WxCpInRedisTemplateConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = getConfigStorage();
+ return this.config(config, wxCpProperties);
+ }
+
+ private WxCpRedisTemplateConfigImpl getConfigStorage() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxCpRedisTemplateConfigImpl(redisTemplate, wxCpProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..060b894fd1
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,65 @@
+package com.binarywang.spring.starter.wxjava.cp.storage;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2023/04/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@RequiredArgsConstructor
+public class WxCpInRedissonConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration {
+ private final WxCpProperties wxCpProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxCpConfigStorage.class)
+ public WxCpConfigStorage wxCpConfigStorage() {
+ WxCpDefaultConfigImpl config = getConfigStorage();
+ return this.config(config, wxCpProperties);
+ }
+
+ private WxCpRedissonConfigImpl getConfigStorage() {
+ WxCpRedisProperties redisProperties = wxCpProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxCpRedissonConfigImpl(redissonClient, wxCpProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage();
+ WxCpRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..c2ef7f6354
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.cp.config.WxCpAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..0beff3f862
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.cp.config.WxCpAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..624c6b3150
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md
@@ -0,0 +1,97 @@
+# wx-java-cp-multi-spring-boot-starter
+
+企业微信多账号配置
+
+- 实现多 WxCpService 初始化。
+- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-cp-multi-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 应用 1 配置
+ wx.cp.corps.tenantId1.corp-id = @corp-id
+ wx.cp.corps.tenantId1.corp-secret = @corp-secret
+ ## 选填
+ wx.cp.corps.tenantId1.agent-id = @agent-id
+ wx.cp.corps.tenantId1.token = @token
+ wx.cp.corps.tenantId1.aes-key = @aes-key
+ wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path
+
+ # 应用 2 配置
+ wx.cp.corps.tenantId2.corp-id = @corp-id
+ wx.cp.corps.tenantId2.corp-secret = @corp-secret
+ ## 选填
+ wx.cp.corps.tenantId2.agent-id = @agent-id
+ wx.cp.corps.tenantId2.token = @token
+ wx.cp.corps.tenantId2.aes-key = @aes-key
+ wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path
+
+ # 公共配置
+ ## ConfigStorage 配置(选填)
+ wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate
+ ## http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.cp.config-storage.http-client-type=http_client
+ wx.cp.config-storage.http-proxy-host=
+ wx.cp.config-storage.http-proxy-port=
+ wx.cp.config-storage.http-proxy-username=
+ wx.cp.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.cp.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.cp.config-storage.retry-sleep-millis=1000
+ ```
+3. 支持自动注入的类型: `WxCpMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.WxCpUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+ @Autowired
+ private WxCpTpMultiServices wxCpTpMultiServices;
+
+ public void test() {
+ // 应用 1 的 WxCpService
+ WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1");
+ WxCpUserService userService1 = wxCpService1.getUserService();
+ userService1.getUserId("xxx");
+ // todo ...
+
+ // 应用 2 的 WxCpService
+ WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2");
+ WxCpUserService userService2 = wxCpService2.getUserService();
+ userService2.getUserId("xxx");
+ // todo ...
+
+ // 应用 3 的 WxCpService
+ WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
+ // 判断是否为空
+ if (wxCpService3 == null) {
+ // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
+ return;
+ }
+ WxCpUserService userService3 = wxCpService3.getUserService();
+ userService3.getUserId("xxx");
+ // todo ...
+ }
+}
+```
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..3a66ab1b8a
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-cp-tp-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for WxCp::支持多账号配置
+ 微信企业号开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-cp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java
new file mode 100644
index 0000000000..1ec07c5c5b
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java
@@ -0,0 +1,16 @@
+package com.binarywang.spring.starter.wxjava.cp.autoconfigure;
+
+import com.binarywang.spring.starter.wxjava.cp.configuration.WxCpTpMultiServicesAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信自动注册
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@Import(WxCpTpMultiServicesAutoConfiguration.class)
+public class WxCpTpMultiAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java
new file mode 100644
index 0000000000..1f6e784236
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration;
+
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInJedisTpConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInMemoryTpConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedisTemplateTpConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedissonTpConfiguration;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 企业微信平台相关服务自动注册
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@EnableConfigurationProperties(WxCpTpMultiProperties.class)
+@Import({
+ WxCpTpInJedisTpConfiguration.class,
+ WxCpTpInMemoryTpConfiguration.class,
+ WxCpTpInRedissonTpConfiguration.class,
+ WxCpTpInRedisTemplateTpConfiguration.class
+})
+public class WxCpTpMultiServicesAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java
new file mode 100644
index 0000000000..2404dee068
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java
@@ -0,0 +1,139 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpSingleProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServicesImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImpl;
+import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceImpl;
+import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceJoddHttpImpl;
+import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceOkHttpImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * WxCpConfigStorage 抽象配置类
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxCpTpConfiguration {
+
+ /**
+ *
+ * @param wxCpTpMultiProperties 应用列表配置
+ * @param services 用于支持,应用启动之后,可以调用这个接口添加新服务对象。主要是配置是从数据库中读取的
+ * @return
+ */
+ public WxCpTpMultiServices wxCpMultiServices(WxCpTpMultiProperties wxCpTpMultiProperties,WxCpTpMultiServices services) {
+ Map corps = wxCpTpMultiProperties.getCorps();
+ if (corps == null || corps.isEmpty()) {
+ log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpTpService(\"tenantId\")获取实例将返回空");
+ return new WxCpTpMultiServicesImpl();
+ }
+
+ if (services == null) {
+ services = new WxCpTpMultiServicesImpl();
+ }
+
+ Set> entries = corps.entrySet();
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxCpTpSingleProperties wxCpTpSingleProperties = entry.getValue();
+ WxCpTpDefaultConfigImpl storage = this.wxCpTpConfigStorage(wxCpTpMultiProperties);
+ this.configCorp(storage, wxCpTpSingleProperties);
+ this.configHttp(storage, wxCpTpMultiProperties.getConfigStorage());
+ WxCpTpService wxCpTpService = this.wxCpTpService(storage, wxCpTpMultiProperties.getConfigStorage());
+ if (services.getWxCpTpService(tenantId) == null) {
+ // 不存在的才会添加到服务列表中
+ services.addWxCpTpService(tenantId, wxCpTpService);
+ }
+ }
+ return services;
+ }
+
+ /**
+ * 配置 WxCpDefaultConfigImpl
+ *
+ * @param wxCpTpMultiProperties 参数
+ * @return WxCpDefaultConfigImpl
+ */
+ protected abstract WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties);
+
+ private WxCpTpService wxCpTpService(WxCpTpConfigStorage wxCpTpConfigStorage, WxCpTpMultiProperties.ConfigStorage storage) {
+ WxCpTpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
+ WxCpTpService cpTpService;
+ switch (httpClientType) {
+ case OK_HTTP:
+ cpTpService = new WxCpTpServiceOkHttpImpl();
+ break;
+ case JODD_HTTP:
+ cpTpService = new WxCpTpServiceJoddHttpImpl();
+ break;
+ case HTTP_CLIENT:
+ cpTpService = new WxCpTpServiceApacheHttpClientImpl();
+ break;
+ default:
+ cpTpService = new WxCpTpServiceImpl();
+ break;
+ }
+ cpTpService.setWxCpTpConfigStorage(wxCpTpConfigStorage);
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ cpTpService.setRetrySleepMillis(retrySleepMillis);
+ cpTpService.setMaxRetryTimes(maxRetryTimes);
+ return cpTpService;
+ }
+
+ private void configCorp(WxCpTpDefaultConfigImpl config, WxCpTpSingleProperties wxCpTpSingleProperties) {
+ String corpId = wxCpTpSingleProperties.getCorpId();
+ String providerSecret = wxCpTpSingleProperties.getProviderSecret();
+ String suiteId = wxCpTpSingleProperties.getSuiteId();
+ String token = wxCpTpSingleProperties.getToken();
+ String suiteSecret = wxCpTpSingleProperties.getSuiteSecret();
+ // 企业微信,私钥,会话存档路径
+ config.setCorpId(corpId);
+ config.setProviderSecret(providerSecret);
+ config.setEncodingAESKey(wxCpTpSingleProperties.getEncodingAESKey());
+ config.setSuiteId(suiteId);
+ config.setToken(token);
+ config.setSuiteSecret(suiteSecret);
+ }
+
+ private void configHttp(WxCpTpDefaultConfigImpl config, WxCpTpMultiProperties.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);
+ }
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java
new file mode 100644
index 0000000000..f3034ac007
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java
@@ -0,0 +1,78 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpJedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@RequiredArgsConstructor
+public class WxCpTpInJedisTpConfiguration extends AbstractWxCpTpConfiguration {
+ private final WxCpTpMultiProperties wxCpTpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpTpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpTpMultiProperties,null);
+ }
+
+ @Override
+ protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ return this.configRedis(wxCpTpMultiProperties);
+ }
+
+ private WxCpTpDefaultConfigImpl configRedis(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ WxCpTpMultiRedisProperties wxCpTpMultiRedisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxCpTpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpTpMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxCpTpMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxCpTpJedisConfigImpl(jedisPool, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage();
+ WxCpTpMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java
new file mode 100644
index 0000000000..5e460abb26
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java
@@ -0,0 +1,39 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxCpTpInMemoryTpConfiguration extends AbstractWxCpTpConfiguration {
+ private final WxCpTpMultiProperties wxCpTpMultiProperties;
+
+ @Bean
+ public WxCpTpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpTpMultiProperties,null);
+ }
+
+ @Override
+ protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxCpTpDefaultConfigImpl configInMemory() {
+ return new WxCpTpDefaultConfigImpl();
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java
new file mode 100644
index 0000000000..1faa37862c
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java
@@ -0,0 +1,45 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpRedisTemplateConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
+)
+@RequiredArgsConstructor
+public class WxCpTpInRedisTemplateTpConfiguration extends AbstractWxCpTpConfiguration {
+ private final WxCpTpMultiProperties wxCpTpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpTpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpTpMultiProperties,null);
+ }
+
+ @Override
+ protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ return this.configRedisTemplate(wxCpTpMultiProperties);
+ }
+
+ private WxCpTpDefaultConfigImpl configRedisTemplate(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxCpTpRedisTemplateConfigImpl(redisTemplate, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java
new file mode 100644
index 0000000000..bd16db37ea
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java
@@ -0,0 +1,68 @@
+package com.binarywang.spring.starter.wxjava.cp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
+import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import me.chanjar.weixin.cp.config.impl.AbstractWxCpTpInRedisConfigImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@RequiredArgsConstructor
+public class WxCpTpInRedissonTpConfiguration extends AbstractWxCpTpConfiguration {
+ private final WxCpTpMultiProperties wxCpTpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxCpTpMultiServices wxCpMultiServices() {
+ return this.wxCpMultiServices(wxCpTpMultiProperties,null);
+ }
+
+ @Override
+ protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ return this.configRedisson(wxCpTpMultiProperties);
+ }
+
+ private WxCpTpDefaultConfigImpl configRedisson(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ WxCpTpMultiRedisProperties redisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxCpTpMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxCpTpRedissonConfigImpl(redissonClient, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxCpTpMultiProperties wxCpTpMultiProperties) {
+ WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage();
+ WxCpTpMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java
new file mode 100644
index 0000000000..771b1b6de7
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java
@@ -0,0 +1,129 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 企业微信多企业接入相关配置属性
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(prefix = WxCpTpMultiProperties.PREFIX)
+public class WxCpTpMultiProperties implements Serializable {
+ private static final long serialVersionUID = -1569510477055668503L;
+ public static final String PREFIX = "wx.cp.tp";
+
+ private Map corps = new HashMap<>();
+
+ /**
+ * 配置存储策略,默认内存
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+ /**
+ * 存储类型
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * 指定key前缀
+ */
+ private String keyPrefix = "wx:cp:tp";
+
+ /**
+ * redis连接配置
+ */
+ @NestedConfigurationProperty
+ private WxCpTpMultiRedisProperties redis = new WxCpTpMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+
+ /**
+ * http代理主机
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java
new file mode 100644
index 0000000000..b94711216f
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java
@@ -0,0 +1,48 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpTpMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java
new file mode 100644
index 0000000000..02a52657db
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java
@@ -0,0 +1,43 @@
+package com.binarywang.spring.starter.wxjava.cp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信企业相关配置属性
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+@Data
+@NoArgsConstructor
+public class WxCpTpSingleProperties implements Serializable {
+ private static final long serialVersionUID = -7502823825007859418L;
+ /**
+ * 微信企业号 corpId
+ */
+ private String corpId;
+ /**
+ * 微信企业号 服务商 providerSecret
+ */
+ private String providerSecret;
+ /**
+ * 微信企业号应用 token
+ */
+ private String token;
+
+ private String encodingAESKey;
+
+ /**
+ * 微信企业号 第三方 应用 ID
+ */
+ private String suiteId;
+ /**
+ * 微信企业号应用
+ */
+ private String suiteSecret;
+
+
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java
new file mode 100644
index 0000000000..c0a9faf51e
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java
@@ -0,0 +1,29 @@
+package com.binarywang.spring.starter.wxjava.cp.service;
+
+
+import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+
+/**
+ * 企业微信 {@link WxCpTpService} 所有实例存放类.
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public interface WxCpTpMultiServices {
+ /**
+ * 通过租户 Id 获取 WxCpTpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpTpService
+ */
+ WxCpTpService getWxCpTpService(String tenantId);
+
+ void addWxCpTpService(String tenantId, WxCpTpService wxCpService);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxCpTpService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxCpTpService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java
new file mode 100644
index 0000000000..84b381230c
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java
@@ -0,0 +1,44 @@
+package com.binarywang.spring.starter.wxjava.cp.service;
+
+
+import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 企业微信 {@link WxCpTpMultiServices} 默认实现
+ *
+ * @author yl
+ * created on 2023/10/16
+ */
+public class WxCpTpMultiServicesImpl implements WxCpTpMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ /**
+ * 通过租户 Id 获取 WxCpTpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxCpTpService
+ */
+ @Override
+ public WxCpTpService getWxCpTpService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxCpTpService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxCpService WxCpTpService 实例
+ */
+ @Override
+ public void addWxCpTpService(String tenantId, WxCpTpService wxCpService) {
+ this.services.put(tenantId, wxCpService);
+ }
+
+ @Override
+ public void removeWxCpTpService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..9d11107229
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..5de0e9f139
--- /dev/null
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md
new file mode 100644
index 0000000000..6dd1d110c3
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md
@@ -0,0 +1,205 @@
+# 微信小程序多租户配置说明
+
+## 多租户模式对比
+
+从 4.8.0 版本开始,wx-java-miniapp-multi-spring-boot-starter 支持两种多租户实现模式:
+
+### 1. 隔离模式(ISOLATED,默认)
+
+每个租户创建独立的 `WxMaService` 实例,各自拥有独立的 HTTP 客户端。
+
+**优点:**
+- 线程安全,无需担心并发问题
+- 不依赖 ThreadLocal,适合异步/响应式编程
+- 租户间完全隔离,互不影响
+
+**缺点:**
+- 每个租户创建独立的 HTTP 客户端,资源占用较多
+- 适合租户数量不多的场景(建议 < 50 个租户)
+
+**适用场景:**
+- SaaS 应用,租户数量较少
+- 异步编程、响应式编程场景
+- 对线程安全有严格要求
+
+### 2. 共享模式(SHARED)
+
+使用单个 `WxMaService` 实例管理所有租户配置,所有租户共享同一个 HTTP 客户端。
+
+**优点:**
+- 共享 HTTP 客户端,大幅节省资源
+- 适合租户数量较多的场景(支持 100+ 租户)
+- 内存占用更小
+
+**缺点:**
+- 依赖 ThreadLocal 切换配置,在异步场景需要特别注意
+- 需要注意线程上下文传递
+
+**适用场景:**
+- 租户数量较多(> 50 个)
+- 同步编程场景
+- 对资源占用有严格要求
+
+## 配置方式
+
+### 使用隔离模式(默认)
+
+```yaml
+wx:
+ ma:
+ # 多租户配置
+ apps:
+ tenant1:
+ app-id: wxd898fcb01713c555
+ app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
+ token: aBcDeFg123456
+ aes-key: abcdefgh123456abcdefgh123456abc
+ tenant2:
+ app-id: wx1234567890abcdef
+ app-secret: 1234567890abcdef1234567890abcdef
+ token: token123
+ aes-key: aeskey123aeskey123aeskey123aes
+
+ # 配置存储(可选)
+ config-storage:
+ type: memory # memory, jedis, redisson, redis_template
+ http-client-type: http_client # http_client, ok_http, jodd_http
+ # multi-tenant-mode: isolated # 默认值,可以不配置
+```
+
+### 使用共享模式
+
+```yaml
+wx:
+ ma:
+ # 多租户配置
+ apps:
+ tenant1:
+ app-id: wxd898fcb01713c555
+ app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
+ tenant2:
+ app-id: wx1234567890abcdef
+ app-secret: 1234567890abcdef1234567890abcdef
+ # ... 可配置更多租户
+
+ # 配置存储
+ config-storage:
+ type: memory
+ http-client-type: http_client
+ multi-tenant-mode: shared # 启用共享模式
+```
+
+## 代码使用
+
+两种模式下的代码使用方式**完全相同**:
+
+```java
+@RestController
+@RequestMapping("/ma")
+public class MiniAppController {
+
+ @Autowired
+ private WxMaMultiServices wxMaMultiServices;
+
+ @GetMapping("/userInfo/{tenantId}")
+ public String getUserInfo(@PathVariable String tenantId, @RequestParam String code) {
+ // 获取指定租户的 WxMaService
+ WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId);
+
+ try {
+ WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code);
+ return "OpenId: " + session.getOpenid();
+ } catch (WxErrorException e) {
+ return "错误: " + e.getMessage();
+ }
+ }
+}
+```
+
+## 性能对比
+
+以 100 个租户为例:
+
+| 指标 | 隔离模式 | 共享模式 |
+|------|---------|---------|
+| HTTP 客户端数量 | 100 个 | 1 个 |
+| 内存占用(估算) | ~500MB | ~50MB |
+| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 |
+| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) |
+| 适用场景 | 中小规模 | 大规模 |
+
+## 注意事项
+
+### 共享模式下的异步编程
+
+如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递:
+
+```java
+@Service
+public class MiniAppService {
+
+ @Autowired
+ private WxMaMultiServices wxMaMultiServices;
+
+ public void asyncOperation(String tenantId) {
+ WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId);
+
+ // ❌ 错误:异步线程无法获取到正确的配置
+ CompletableFuture.runAsync(() -> {
+ // 这里 wxMaService.getWxMaConfig() 可能返回错误的配置
+ wxMaService.getUserService().getUserInfo(...);
+ });
+
+ // ✅ 正确:在主线程获取配置,传递给异步线程
+ WxMaConfig config = wxMaService.getWxMaConfig();
+ String appId = config.getAppid();
+ CompletableFuture.runAsync(() -> {
+ // 使用已获取的配置信息
+ log.info("AppId: {}", appId);
+ });
+ }
+}
+```
+
+### 动态添加/删除租户
+
+两种模式都支持运行时动态添加或删除租户配置。
+
+## 迁移指南
+
+如果您正在使用旧版本,升级到 4.8.0+ 后:
+
+1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致)
+2. **向后兼容**:所有现有代码无需修改
+3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式
+
+## 源码分析
+
+issue讨论地址:[#3835](https://github.com/binarywang/WxJava/issues/3835)
+
+### 为什么有两种设计?
+
+1. **基础实现类的 `configMap`**:
+ - 位置:`BaseWxMaServiceImpl`
+ - 特点:单个 Service 实例 + 多个配置 + ThreadLocal 切换
+ - 设计目的:支持在一个应用中管理多个小程序账号
+
+2. **Spring Boot Starter 的 `services` Map**:
+ - 位置:`WxMaMultiServicesImpl`
+ - 特点:多个 Service 实例 + 每个实例一个配置
+ - 设计目的:为 Spring Boot 提供更符合依赖注入风格的多租户支持
+
+### 新版本改进
+
+新版本通过配置项让用户自主选择实现方式:
+
+```
+用户 → WxMaMultiServices 接口
+ ↓
+ ┌────┴────┐
+ ↓ ↓
+隔离模式 共享模式
+(多Service) (单Service+configMap)
+```
+
+这样既保留了线程安全的优势(隔离模式),又提供了资源节省的选项(共享模式)。
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..ccc0d5bf5f
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md
@@ -0,0 +1,96 @@
+# wx-java-miniapp-multi-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-miniapp-multi-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 公众号配置
+ ## 应用 1 配置(必填)
+ wx.ma.apps.tenantId1.app-id=appId
+ wx.ma.apps.tenantId1.app-secret=@secret
+ ## 选填
+ wx.ma.apps.tenantId1.token=@token
+ wx.ma.apps.tenantId1.aes-key=@aesKey
+ wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+ ## 应用 2 配置(必填)
+ wx.ma.apps.tenantId2.app-id=@appId
+ wx.ma.apps.tenantId2.app-secret =@secret
+ ## 选填
+ wx.ma.apps.tenantId2.token=@token
+ wx.ma.apps.tenantId2.aes-key=@aesKey
+ wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson
+ wx.ma.config-storage.type=memory
+ ## 相关redis前缀配置: wx:ma:multi(默认)
+ wx.ma.config-storage.key-prefix=wx:ma:multi
+ wx.ma.config-storage.redis.host=127.0.0.1
+ wx.ma.config-storage.redis.port=6379
+ ## 单机和 sentinel 同时存在时,优先使用sentinel配置
+ # wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ # wx.ma.config-storage.redis.sentinel-name=mymaster
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.ma.config-storage.http-client-type=http_client
+ wx.ma.config-storage.http-proxy-host=
+ wx.ma.config-storage.http-proxy-port=
+ wx.ma.config-storage.http-proxy-username=
+ wx.ma.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.ma.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.ma.config-storage.retry-sleep-millis=1000
+ ```
+3. 自动注入的类型:`WxMaMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+ @Autowired
+ private WxMaMultiServices wxMaMultiServices;
+
+ public void test() {
+ // 应用 1 的 WxMaService
+ WxMaService wxMaService1 = wxMaMultiServices.getWxMaService("tenantId1");
+ WxMaUserService userService1 = wxMaService1.getUserService();
+ userService1.userInfo("xxx");
+ // todo ...
+
+ // 应用 2 的 WxMaService
+ WxMaService wxMaService2 = wxMaMultiServices.getWxMaService("tenantId2");
+ WxMaUserService userService2 = wxMaService2.getUserService();
+ userService2.userInfo("xxx");
+ // todo ...
+
+ // 应用 3 的 WxMaService
+ WxMaService wxMaService3 = wxMaMultiServices.getWxMaService("tenantId3");
+ // 判断是否为空
+ if (wxMaService3 == null) {
+ // todo wxMaService3 为空,请先配置 tenantId3 微信公众号应用参数
+ return;
+ }
+ WxMaUserService userService3 = wxMaService3.getUserService();
+ userService3.userInfo("xxx");
+ // todo ...
+ }
+}
+```
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..7f561529b0
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,74 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-miniapp-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for MiniApp::支持多账号配置
+ 微信公众号开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java
new file mode 100644
index 0000000000..bd03751c37
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java
@@ -0,0 +1,14 @@
+package com.binarywang.spring.starter.wxjava.miniapp.autoconfigure;
+
+import com.binarywang.spring.starter.wxjava.miniapp.configuration.WxMaMultiServiceConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@Import(WxMaMultiServiceConfiguration.class)
+public class WxMaMultiAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java
new file mode 100644
index 0000000000..e1db56cfc7
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration;
+
+import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInJedisConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInMemoryConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedisTemplateConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedissonConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信小程序相关服务自动注册
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@EnableConfigurationProperties(WxMaMultiProperties.class)
+@Import({
+ WxMaInJedisConfiguration.class,
+ WxMaInMemoryConfiguration.class,
+ WxMaInRedissonConfiguration.class,
+ WxMaInRedisTemplateConfiguration.class
+})
+public class WxMaMultiServiceConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java
new file mode 100644
index 0000000000..fba9d875ee
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java
@@ -0,0 +1,212 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaSingleProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesSharedImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * WxMaConfigStorage 抽象配置类
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxMaConfiguration {
+
+ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) {
+ Map appsMap = wxMaMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信小程序应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空");
+ return new WxMaMultiServicesImpl();
+ }
+
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 查看 {@link cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl#setAppId(String)}
+ */
+ Collection apps = appsMap.values();
+ if (apps.size() > 1) {
+ // 校验 appId 是否唯一
+ boolean multi = apps.stream()
+ // 没有 appId,如果不判断是否为空,这里会报 NPE 异常
+ .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保微信小程序配置 appId 的唯一性");
+ }
+ }
+
+ // 根据配置选择多租户模式
+ WxMaMultiProperties.MultiTenantMode mode = wxMaMultiProperties.getConfigStorage().getMultiTenantMode();
+ if (mode == WxMaMultiProperties.MultiTenantMode.SHARED) {
+ return createSharedMultiServices(appsMap, wxMaMultiProperties);
+ } else {
+ return createIsolatedMultiServices(appsMap, wxMaMultiProperties);
+ }
+ }
+
+ /**
+ * 创建隔离模式的多租户服务(每个租户独立 WxMaService 实例)
+ */
+ private WxMaMultiServices createIsolatedMultiServices(
+ Map appsMap,
+ WxMaMultiProperties wxMaMultiProperties) {
+
+ WxMaMultiServicesImpl services = new WxMaMultiServicesImpl();
+ Set> entries = appsMap.entrySet();
+
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxMaSingleProperties wxMaSingleProperties = entry.getValue();
+ WxMaDefaultConfigImpl storage = this.wxMaConfigStorage(wxMaMultiProperties);
+ this.configApp(storage, wxMaSingleProperties);
+ this.configHttp(storage, wxMaMultiProperties.getConfigStorage());
+ WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties);
+ services.addWxMaService(tenantId, wxMaService);
+ }
+
+ log.info("微信小程序多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size());
+ return services;
+ }
+
+ /**
+ * 创建共享模式的多租户服务(单个 WxMaService 实例管理多个配置)
+ */
+ private WxMaMultiServices createSharedMultiServices(
+ Map appsMap,
+ WxMaMultiProperties wxMaMultiProperties) {
+
+ // 创建共享的 WxMaService 实例
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaService sharedService = createWxMaServiceByType(storage.getHttpClientType());
+ configureWxMaService(sharedService, storage);
+
+ // 准备所有租户的配置,使用 TreeMap 保证顺序一致性
+ Map configsMap = new HashMap<>();
+ String defaultTenantId = new TreeMap<>(appsMap).firstKey();
+
+ for (Map.Entry entry : appsMap.entrySet()) {
+ String tenantId = entry.getKey();
+ WxMaSingleProperties wxMaSingleProperties = entry.getValue();
+ WxMaDefaultConfigImpl config = this.wxMaConfigStorage(wxMaMultiProperties);
+ this.configApp(config, wxMaSingleProperties);
+ this.configHttp(config, storage);
+ configsMap.put(tenantId, config);
+ }
+
+ // 设置多配置到共享的 WxMaService
+ sharedService.setMultiConfigs(configsMap, defaultTenantId);
+
+ log.info("微信小程序多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size());
+ return new WxMaMultiServicesSharedImpl(sharedService);
+ }
+
+ /**
+ * 根据类型创建 WxMaService 实例
+ */
+ private WxMaService createWxMaServiceByType(WxMaMultiProperties.HttpClientType httpClientType) {
+ switch (httpClientType) {
+ case OK_HTTP:
+ return new WxMaServiceOkHttpImpl();
+ case JODD_HTTP:
+ return new WxMaServiceJoddHttpImpl();
+ case HTTP_CLIENT:
+ return new WxMaServiceHttpClientImpl();
+ default:
+ return new WxMaServiceImpl();
+ }
+ }
+
+ /**
+ * 配置 WxMaService 的通用参数
+ */
+ private void configureWxMaService(WxMaService wxMaService, WxMaMultiProperties.ConfigStorage storage) {
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxMaService.setRetrySleepMillis(retrySleepMillis);
+ wxMaService.setMaxRetryTimes(maxRetryTimes);
+ }
+
+ /**
+ * 配置 WxMaDefaultConfigImpl
+ *
+ * @param wxMaMultiProperties 参数
+ * @return WxMaDefaultConfigImpl
+ */
+ protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties);
+
+ public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaService wxMaService = createWxMaServiceByType(storage.getHttpClientType());
+ wxMaService.setWxMaConfig(wxMaConfig);
+ configureWxMaService(wxMaService, storage);
+ return wxMaService;
+ }
+
+ private void configApp(WxMaDefaultConfigImpl config, WxMaSingleProperties properties) {
+ String appId = properties.getAppId();
+ String appSecret = properties.getAppSecret();
+ String token = properties.getToken();
+ String aesKey = properties.getAesKey();
+ boolean useStableAccessToken = properties.isUseStableAccessToken();
+
+ config.setAppid(appId);
+ config.setSecret(appSecret);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ config.setMsgDataFormat(properties.getMsgDataFormat());
+ config.useStableAccessToken(useStableAccessToken);
+ config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
+ }
+
+ private void configHttp(WxMaDefaultConfigImpl config, WxMaMultiProperties.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);
+ }
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java
new file mode 100644
index 0000000000..52eeffe7e4
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java
@@ -0,0 +1,76 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@RequiredArgsConstructor
+public class WxMaInJedisConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configRedis(wxMaMultiProperties);
+ }
+
+ private WxMaDefaultConfigImpl configRedis(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiRedisProperties wxMaMultiRedisProperties = wxMaMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxMaMultiRedisProperties != null && StringUtils.isNotEmpty(wxMaMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxMaMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxMaRedisConfigImpl(jedisPool);
+ }
+
+ private JedisPool getJedisPool(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java
new file mode 100644
index 0000000000..3c8202a6b3
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java
@@ -0,0 +1,39 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxMaInMemoryConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxMaDefaultConfigImpl configInMemory() {
+ return new WxMaDefaultConfigImpl();
+ // return new WxMaDefaultConfigImpl();
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java
new file mode 100644
index 0000000000..fc88a0578a
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java
@@ -0,0 +1,43 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author hb0730 2025/9/10
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template")
+@RequiredArgsConstructor
+public class WxMaInRedisTemplateConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configRedisTemplate(wxMaMultiProperties);
+ }
+
+ private WxMaDefaultConfigImpl configRedisTemplate(WxMaMultiProperties wxMaMultiProperties) {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ RedisTemplateWxRedisOps wxRedisOps = new RedisTemplateWxRedisOps(redisTemplate);
+ return new WxMaRedisBetterConfigImpl(wxRedisOps, wxMaMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java
new file mode 100644
index 0000000000..c1915400d3
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java
@@ -0,0 +1,67 @@
+package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@RequiredArgsConstructor
+public class WxMaInRedissonConfiguration extends AbstractWxMaConfiguration {
+ private final WxMaMultiProperties wxMaMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMaMultiServices wxMaMultiServices() {
+ return this.wxMaMultiServices(wxMaMultiProperties);
+ }
+
+ @Override
+ protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
+ return this.configRedisson(wxMaMultiProperties);
+ }
+
+ private WxMaDefaultConfigImpl configRedisson(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiRedisProperties redisProperties = wxMaMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxMaMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMaRedissonConfigImpl(redissonClient, wxMaMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxMaMultiProperties wxMaMultiProperties) {
+ WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
+ WxMaMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java
new file mode 100644
index 0000000000..201aceb8bf
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java
@@ -0,0 +1,178 @@
+package com.binarywang.spring.starter.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(WxMaMultiProperties.PREFIX)
+public class WxMaMultiProperties implements Serializable {
+ private static final long serialVersionUID = -5358245184407791011L;
+ public static final String PREFIX = "wx.ma";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class HostConfig implements Serializable {
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.MEMORY;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:ma:multi";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final WxMaMultiRedisProperties redis = new WxMaMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link cn.binarywang.wx.miniapp.api.WxMaService#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link cn.binarywang.wx.miniapp.api.WxMaService#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+
+ /**
+ * 多租户实现模式.
+ *
+ * - ISOLATED: 为每个租户创建独立的 WxMaService 实例(默认)
+ * - SHARED: 使用单个 WxMaService 实例管理所有租户配置,共享 HTTP 客户端
+ *
+ */
+ private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ MEMORY,
+ /**
+ * jedis
+ */
+ JEDIS,
+ /**
+ * redisson
+ */
+ REDISSON,
+ /**
+ * redisTemplate
+ */
+ REDIS_TEMPLATE
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP
+ }
+
+ public enum MultiTenantMode {
+ /**
+ * 隔离模式:为每个租户创建独立的 WxMaService 实例.
+ * 优点:线程安全,不依赖 ThreadLocal
+ * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多
+ */
+ ISOLATED,
+ /**
+ * 共享模式:使用单个 WxMaService 实例管理所有租户配置.
+ * 优点:共享 HTTP 客户端,节省资源
+ * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意
+ */
+ SHARED
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java
new file mode 100644
index 0000000000..67562c69a4
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.spring.starter.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Data
+@NoArgsConstructor
+public class WxMaMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java
new file mode 100644
index 0000000000..5defae5514
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java
@@ -0,0 +1,57 @@
+package com.binarywang.spring.starter.wxjava.miniapp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author monch
+ * created on 2024/9/6
+ */
+@Data
+@NoArgsConstructor
+public class WxMaSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+ /**
+ * 设置微信公众号的 appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的 app secret.
+ */
+ private String appSecret;
+
+ /**
+ * 设置微信公众号的 token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的 EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 消息格式,XML或者JSON.
+ */
+ private String msgDataFormat;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java
new file mode 100644
index 0000000000..90fce690c7
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.miniapp.service;
+
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+
+/**
+ * 微信小程序 {@link WxMaService} 所有实例存放类.
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+public interface WxMaMultiServices {
+ /**
+ * 通过租户 Id 获取 WxMaService
+ *
+ * @param tenantId 租户 Id
+ * @return WxMaService
+ */
+ WxMaService getWxMaService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxMaService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxMaService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java
new file mode 100644
index 0000000000..913a371f52
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java
@@ -0,0 +1,36 @@
+package com.binarywang.spring.starter.wxjava.miniapp.service;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 微信小程序 {@link com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices} 默认实现
+ *
+ * @author monch
+ * created on 2024/9/6
+ */
+public class WxMaMultiServicesImpl implements com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxMaService getWxMaService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxMaService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxMaService WxMaService 实例
+ */
+ public void addWxMaService(String tenantId, WxMaService wxMaService) {
+ this.services.put(tenantId, wxMaService);
+ }
+
+ @Override
+ public void removeWxMaService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java
new file mode 100644
index 0000000000..40a01fb52e
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java
@@ -0,0 +1,53 @@
+package com.binarywang.spring.starter.wxjava.miniapp.service;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 微信小程序 {@link WxMaMultiServices} 共享式实现.
+ *
+ * 使用单个 WxMaService 实例管理多个租户配置,通过 switchover 切换租户。
+ * 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。
+ *
+ *
+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。
+ *
+ *
+ * @author Binary Wang
+ * created on 2026/1/9
+ */
+@RequiredArgsConstructor
+public class WxMaMultiServicesSharedImpl implements WxMaMultiServices {
+ private final WxMaService sharedWxMaService;
+
+ @Override
+ public WxMaService getWxMaService(String tenantId) {
+ if (tenantId == null) {
+ return null;
+ }
+ // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null)
+ if (!sharedWxMaService.switchover(tenantId)) {
+ return null;
+ }
+ return sharedWxMaService;
+ }
+
+ @Override
+ public void removeWxMaService(String tenantId) {
+ if (tenantId != null) {
+ sharedWxMaService.removeConfig(tenantId);
+ }
+ }
+
+ /**
+ * 添加租户配置到共享的 WxMaService 实例
+ *
+ * @param tenantId 租户 ID
+ * @param wxMaService 要添加配置的 WxMaService(仅使用其配置,不使用其实例)
+ */
+ public void addWxMaService(String tenantId, WxMaService wxMaService) {
+ if (tenantId != null && wxMaService != null) {
+ sharedWxMaService.addConfig(tenantId, wxMaService.getWxMaConfig());
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..bc9bec9bfb
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.miniapp.autoconfigure.WxMaMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..3023f06bdd
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.miniapp.autoconfigure.WxMaMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md
new file mode 100644
index 0000000000..cbf0b53925
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md
@@ -0,0 +1,36 @@
+# wx-java-miniapp-spring-boot-starter
+## 快速开始
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-miniapp-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 小程序配置(必填)
+ wx.miniapp.appid = appId
+ wx.miniapp.secret = @secret
+ wx.miniapp.token = @token
+ wx.miniapp.aesKey = @aesKey
+ wx.miniapp.msgDataFormat = @msgDataFormat # 消息格式,XML或者JSON.
+ wx.miniapp.use-stable-access-token=@useStableAccessToken
+ # 存储配置redis(可选)
+ # 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool)
+ wx.miniapp.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.miniapp.config-storage.key-prefix = wa # 相关redis前缀配置: wa(默认)
+ wx.miniapp.config-storage.redis.host = 127.0.0.1
+ wx.miniapp.config-storage.redis.port = 6379
+ # http客户端配置
+ wx.miniapp.config-storage.http-client-type=HttpClient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
+ wx.miniapp.config-storage.http-proxy-host=
+ wx.miniapp.config-storage.http-proxy-port=
+ wx.miniapp.config-storage.http-proxy-username=
+ wx.miniapp.config-storage.http-proxy-password=
+ ```
+3. 自动注入的类型
+- `WxMaService`
+- `WxMaConfig`
+
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..40a18523e9
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-miniapp-spring-boot-starter
+ WxJava - Spring Boot Starter for MiniApp
+ 微信小程序开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java
new file mode 100644
index 0000000000..67a7efaecf
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java
@@ -0,0 +1,21 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config;
+
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 自动配置.
+ *
+ * @author Binary Wang
+ * created on 2019-08-10
+ */
+@Configuration
+@EnableConfigurationProperties(WxMaProperties.class)
+@Import({
+ WxMaStorageAutoConfiguration.class,
+ WxMaServiceAutoConfiguration.class
+})
+public class WxMaAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java
new file mode 100644
index 0000000000..f03d3f1493
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java
@@ -0,0 +1,60 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import com.binarywang.spring.starter.wxjava.miniapp.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import lombok.AllArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信小程序平台相关服务自动注册.
+ *
+ * @author someone TaoYu
+ */
+@Configuration
+@AllArgsConstructor
+public class WxMaServiceAutoConfiguration {
+
+ private final WxMaProperties wxMaProperties;
+
+ /**
+ * 小程序service.
+ *
+ * @return 小程序service
+ */
+ @Bean
+ @ConditionalOnMissingBean(WxMaService.class)
+ @ConditionalOnBean(WxMaConfig.class)
+ public WxMaService wxMaService(WxMaConfig wxMaConfig) {
+ HttpClientType httpClientType = wxMaProperties.getConfigStorage().getHttpClientType();
+ WxMaService wxMaService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxMaService = new WxMaServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxMaService = new WxMaServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxMaService = new WxMaServiceHttpClientImpl();
+ break;
+ case HttpComponents:
+ wxMaService = new WxMaServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMaService = new WxMaServiceImpl();
+ break;
+ }
+ wxMaService.setWxMaConfig(wxMaConfig);
+ return wxMaService;
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java
new file mode 100644
index 0000000000..0f0477a8b0
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java
@@ -0,0 +1,23 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config;
+
+import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInJedisConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInMemoryConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInRedisTemplateConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInRedissonConfigStorageConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信小程序存储策略自动配置.
+ *
+ * @author someone TaoYu
+ */
+@Configuration
+@Import({
+ WxMaInMemoryConfigStorageConfiguration.class,
+ WxMaInJedisConfigStorageConfiguration.class,
+ WxMaInRedisTemplateConfigStorageConfiguration.class,
+ WxMaInRedissonConfigStorageConfiguration.class
+})
+public class WxMaStorageAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java
new file mode 100644
index 0000000000..abcd83e848
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java
@@ -0,0 +1,58 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author yl TaoYu
+ */
+public abstract class AbstractWxMaConfigStorageConfiguration {
+
+ protected WxMaDefaultConfigImpl config(WxMaDefaultConfigImpl config, WxMaProperties properties) {
+ WxMaProperties.ConfigStorage storage = properties.getConfigStorage();
+ config.setAppid(StringUtils.trimToNull(properties.getAppid()));
+ config.setSecret(StringUtils.trimToNull(properties.getSecret()));
+ config.setToken(StringUtils.trimToNull(properties.getToken()));
+ config.setAesKey(StringUtils.trimToNull(properties.getAesKey()));
+ config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat()));
+ config.useStableAccessToken(properties.isUseStableAccessToken());
+ config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
+
+ WxMaProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+
+ // 设置自定义的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);
+ }
+
+ int maxRetryTimes = configStorageProperties.getMaxRetryTimes();
+ if (configStorageProperties.getMaxRetryTimes() < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = configStorageProperties.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ config.setRetrySleepMillis(retrySleepMillis);
+ config.setMaxRetryTimes(maxRetryTimes);
+ return config;
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..93b901ebf8
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java
@@ -0,0 +1,72 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis")
+@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class})
+@RequiredArgsConstructor
+public class WxMaInJedisConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaRedisBetterConfigImpl config = getWxMaRedisBetterConfigImpl();
+ return this.config(config, properties);
+ }
+
+ private WxMaRedisBetterConfigImpl getWxMaRedisBetterConfigImpl() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxMaProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..44e727af83
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,28 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type",
+ matchIfMissing = true, havingValue = "memory")
+@RequiredArgsConstructor
+public class WxMaInMemoryConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
+ return this.config(config, properties);
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java
new file mode 100644
index 0000000000..81cf8c6559
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate")
+@ConditionalOnClass(StringRedisTemplate.class)
+@RequiredArgsConstructor
+public class WxMaInRedisTemplateConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaRedisBetterConfigImpl config = getWxMaInRedisTemplateConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMaRedisBetterConfigImpl getWxMaInRedisTemplateConfigStorage() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+ return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..2a030b5f9e
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,61 @@
+package com.binarywang.spring.starter.wxjava.miniapp.config.storage;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author yl TaoYu
+ */
+@Configuration
+@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson")
+@ConditionalOnClass({Redisson.class, RedissonClient.class})
+@RequiredArgsConstructor
+public class WxMaInRedissonConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration {
+ private final WxMaProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMaConfig.class)
+ public WxMaConfig wxMaConfig() {
+ WxMaRedissonConfigImpl config = getWxMaInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMaRedissonConfigImpl getWxMaInRedissonConfigStorage() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMaRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxMaProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java
new file mode 100644
index 0000000000..48549e4399
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.miniapp.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-05-25
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient (Apache HttpClient 4.x).
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents (Apache HttpClient 5.x).
+ */
+ HttpComponents,
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java
new file mode 100644
index 0000000000..31c6e4b602
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.miniapp.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-05-25
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java
new file mode 100644
index 0000000000..75e3740a19
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java
@@ -0,0 +1,43 @@
+package com.binarywang.spring.starter.wxjava.miniapp.properties;
+
+import lombok.Data;
+
+/**
+ * redis 配置.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties {
+
+ /**
+ * 主机地址.不填则从spring容器内获取JedisPool
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java
new file mode 100644
index 0000000000..82f1500941
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java
@@ -0,0 +1,144 @@
+package com.binarywang.spring.starter.wxjava.miniapp.properties;
+
+import com.binarywang.spring.starter.wxjava.miniapp.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.miniapp.enums.StorageType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import static com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties.PREFIX;
+
+/**
+ * 属性配置类.
+ *
+ * @author Binary Wang
+ * created on 2019-08-10
+ */
+@Data
+@ConfigurationProperties(prefix = PREFIX)
+public class WxMaProperties {
+ public static final String PREFIX = "wx.miniapp";
+
+ /**
+ * 设置微信小程序的appid.
+ */
+ private String appid;
+
+ /**
+ * 设置微信小程序的Secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信小程序消息服务器配置的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信小程序消息服务器配置的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 消息格式,XML或者JSON.
+ */
+ private String msgDataFormat;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage {
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wa";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * 连接超时时间,单位毫秒
+ */
+ private int connectionTimeout = 5000;
+
+ /**
+ * 读数据超时时间,即socketTimeout,单位毫秒
+ */
+ private int soTimeout = 5000;
+
+ /**
+ * 从连接池获取链接的超时时间,单位毫秒
+ */
+ private int connectionRequestTimeout = 5000;
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..4bb1c3d604
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..6644fa9701
--- /dev/null
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..26b593addd
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md
@@ -0,0 +1,100 @@
+# wx-java-mp-multi-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-mp-multi-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 公众号配置
+ ## 应用 1 配置(必填)
+ wx.mp.apps.tenantId1.app-id=appId
+ wx.mp.apps.tenantId1.app-secret=@secret
+ ## 选填
+ wx.mp.apps.tenantId1.token=@token
+ wx.mp.apps.tenantId1.aes-key=@aesKey
+ wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken
+ ## 应用 2 配置(必填)
+ wx.mp.apps.tenantId2.app-id=@appId
+ wx.mp.apps.tenantId2.app-secret =@secret
+ ## 选填
+ wx.mp.apps.tenantId2.token=@token
+ wx.mp.apps.tenantId2.aes-key=@aesKey
+ wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken
+
+ # ConfigStorage 配置(选填)
+ ## 配置类型: memory(默认), jedis, redisson, redis_template
+ wx.mp.config-storage.type=memory
+ ## 相关redis前缀配置: wx:mp:multi(默认)
+ wx.mp.config-storage.key-prefix=wx:mp:multi
+ wx.mp.config-storage.redis.host=127.0.0.1
+ wx.mp.config-storage.redis.port=6379
+ ## 单机和 sentinel 同时存在时,优先使用sentinel配置
+ # wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ # wx.mp.config-storage.redis.sentinel-name=mymaster
+
+ # http 客户端配置(选填)
+ ## # http客户端类型: http_client(默认), ok_http, jodd_http
+ wx.mp.config-storage.http-client-type=http_client
+ wx.mp.config-storage.http-proxy-host=
+ wx.mp.config-storage.http-proxy-port=
+ wx.mp.config-storage.http-proxy-username=
+ wx.mp.config-storage.http-proxy-password=
+ ## 最大重试次数,默认:5 次,如果小于 0,则为 0
+ wx.mp.config-storage.max-retry-times=5
+ ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000
+ wx.mp.config-storage.retry-sleep-millis=1000
+
+ # 公众号地址 host 配置
+ # wx.mp.hosts.api-host=http://proxy.com/
+ # wx.mp.hosts.open-host=http://proxy.com/
+ # wx.mp.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型:`WxMpMultiServices`
+
+4. 使用样例
+
+```java
+import com.binarywang.spring.starter.wxjava.mp.service.WxMaMultiServices;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.WxMpUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+ @Autowired
+ private WxMpMultiServices wxMpMultiServices;
+
+ public void test() {
+ // 应用 1 的 WxMpService
+ WxMpService wxMpService1 = wxMpMultiServices.getWxMpService("tenantId1");
+ WxMpUserService userService1 = wxMpService1.getUserService();
+ userService1.userInfo("xxx");
+ // todo ...
+
+ // 应用 2 的 WxMpService
+ WxMpService wxMpService2 = wxMpMultiServices.getWxMpService("tenantId2");
+ WxMpUserService userService2 = wxMpService2.getUserService();
+ userService2.userInfo("xxx");
+ // todo ...
+
+ // 应用 3 的 WxMpService
+ WxMpService wxMpService3 = wxMpMultiServices.getWxMpService("tenantId3");
+ // 判断是否为空
+ if (wxMpService3 == null) {
+ // todo wxMpService3 为空,请先配置 tenantId3 微信公众号应用参数
+ return;
+ }
+ WxMpUserService userService3 = wxMpService3.getUserService();
+ userService3.userInfo("xxx");
+ // todo ...
+ }
+}
+```
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..90318a0d57
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-mp-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for MP::支持多账号配置
+ 微信公众号开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java
new file mode 100644
index 0000000000..21ec0925d3
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java
@@ -0,0 +1,14 @@
+package com.binarywang.spring.starter.wxjava.mp.autoconfigure;
+
+import com.binarywang.spring.starter.wxjava.mp.configuration.WxMpMultiServiceConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@Import(WxMpMultiServiceConfiguration.class)
+public class WxMpMultiAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java
new file mode 100644
index 0000000000..35a53d0ccd
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration;
+
+import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInJedisConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInMemoryConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInRedisTemplateConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInRedissonConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信公众号相关服务自动注册
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@EnableConfigurationProperties(WxMpMultiProperties.class)
+@Import({
+ WxMpInJedisConfiguration.class,
+ WxMpInMemoryConfiguration.class,
+ WxMpInRedissonConfiguration.class,
+ WxMpInRedisTemplateConfiguration.class
+})
+public class WxMpMultiServiceConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java
new file mode 100644
index 0000000000..46724c625f
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java
@@ -0,0 +1,231 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpSingleProperties;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesImpl;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesSharedImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.WxMpHostConfig;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * WxMpConfigStorage 抽象配置类
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxMpConfiguration {
+
+ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiProperties) {
+ Map appsMap = wxMpMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空");
+ return new WxMpMultiServicesImpl();
+ }
+
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ *
+ * 查看 {@link me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl#setAppId(String)}
+ */
+ Collection apps = appsMap.values();
+ if (apps.size() > 1) {
+ // 校验 appId 是否唯一
+ boolean multi = apps.stream()
+ // 没有 appId,如果不判断是否为空,这里会报 NPE 异常
+ .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting()))
+ .entrySet().stream().anyMatch(e -> e.getValue() > 1);
+ if (multi) {
+ throw new RuntimeException("请确保微信公众号配置 appId 的唯一性");
+ }
+ }
+
+ // 根据配置选择多租户模式
+ WxMpMultiProperties.MultiTenantMode mode = wxMpMultiProperties.getConfigStorage().getMultiTenantMode();
+ if (mode == WxMpMultiProperties.MultiTenantMode.SHARED) {
+ return createSharedMultiServices(appsMap, wxMpMultiProperties);
+ } else {
+ return createIsolatedMultiServices(appsMap, wxMpMultiProperties);
+ }
+ }
+
+ /**
+ * 创建隔离模式的多租户服务(每个租户独立 WxMpService 实例)
+ */
+ private WxMpMultiServices createIsolatedMultiServices(
+ Map appsMap,
+ WxMpMultiProperties wxMpMultiProperties) {
+
+ WxMpMultiServicesImpl services = new WxMpMultiServicesImpl();
+ Set> entries = appsMap.entrySet();
+
+ for (Map.Entry entry : entries) {
+ String tenantId = entry.getKey();
+ WxMpSingleProperties wxMpSingleProperties = entry.getValue();
+ WxMpDefaultConfigImpl storage = this.wxMpConfigStorage(wxMpMultiProperties);
+ this.configApp(storage, wxMpSingleProperties);
+ this.configHttp(storage, wxMpMultiProperties.getConfigStorage());
+ this.configHost(storage, wxMpMultiProperties.getHosts());
+ WxMpService wxMpService = this.wxMpService(storage, wxMpMultiProperties);
+ services.addWxMpService(tenantId, wxMpService);
+ }
+
+ log.info("微信公众号多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size());
+ return services;
+ }
+
+ /**
+ * 创建共享模式的多租户服务(单个 WxMpService 实例管理多个配置)
+ */
+ private WxMpMultiServices createSharedMultiServices(
+ Map appsMap,
+ WxMpMultiProperties wxMpMultiProperties) {
+
+ // 创建共享的 WxMpService 实例
+ WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
+ WxMpService sharedService = createWxMpServiceByType(storage.getHttpClientType());
+ configureWxMpService(sharedService, storage);
+
+ // 准备所有租户的配置,使用 TreeMap 保证顺序一致性
+ Map configsMap = new HashMap<>();
+ String defaultTenantId = new TreeMap<>(appsMap).firstKey();
+
+ for (Map.Entry entry : appsMap.entrySet()) {
+ String tenantId = entry.getKey();
+ WxMpSingleProperties wxMpSingleProperties = entry.getValue();
+ WxMpDefaultConfigImpl config = this.wxMpConfigStorage(wxMpMultiProperties);
+ this.configApp(config, wxMpSingleProperties);
+ this.configHttp(config, storage);
+ this.configHost(config, wxMpMultiProperties.getHosts());
+ configsMap.put(tenantId, config);
+ }
+
+ // 设置多配置到共享的 WxMpService
+ sharedService.setMultiConfigStorages(configsMap, defaultTenantId);
+
+ log.info("微信公众号多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size());
+ return new WxMpMultiServicesSharedImpl(sharedService);
+ }
+
+ /**
+ * 根据类型创建 WxMpService 实例
+ */
+ private WxMpService createWxMpServiceByType(WxMpMultiProperties.HttpClientType httpClientType) {
+ switch (httpClientType) {
+ case OK_HTTP:
+ return new WxMpServiceOkHttpImpl();
+ case JODD_HTTP:
+ return new WxMpServiceJoddHttpImpl();
+ case HTTP_CLIENT:
+ return new WxMpServiceHttpClientImpl();
+ case HTTP_COMPONENTS:
+ return new WxMpServiceHttpComponentsImpl();
+ default:
+ return new WxMpServiceImpl();
+ }
+ }
+
+ /**
+ * 配置 WxMpService 的通用参数
+ */
+ private void configureWxMpService(WxMpService wxMpService, WxMpMultiProperties.ConfigStorage storage) {
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ wxMpService.setRetrySleepMillis(retrySleepMillis);
+ wxMpService.setMaxRetryTimes(maxRetryTimes);
+ }
+
+ /**
+ * 配置 WxMpDefaultConfigImpl
+ *
+ * @param wxMpMultiProperties 参数
+ * @return WxMpDefaultConfigImpl
+ */
+ protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties);
+
+ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
+ WxMpService wxMpService = createWxMpServiceByType(storage.getHttpClientType());
+ wxMpService.setWxMpConfigStorage(configStorage);
+ configureWxMpService(wxMpService, storage);
+ return wxMpService;
+ }
+
+ private void configApp(WxMpDefaultConfigImpl config, WxMpSingleProperties corpProperties) {
+ String appId = corpProperties.getAppId();
+ String appSecret = corpProperties.getAppSecret();
+ String token = corpProperties.getToken();
+ String aesKey = corpProperties.getAesKey();
+ boolean useStableAccessToken = corpProperties.isUseStableAccessToken();
+
+ config.setAppId(appId);
+ config.setSecret(appSecret);
+ if (StringUtils.isNotBlank(token)) {
+ config.setToken(token);
+ }
+ if (StringUtils.isNotBlank(aesKey)) {
+ config.setAesKey(aesKey);
+ }
+ config.setUseStableAccessToken(useStableAccessToken);
+ }
+
+ private void configHttp(WxMpDefaultConfigImpl config, WxMpMultiProperties.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);
+ }
+ }
+ }
+
+ /**
+ * wx host config
+ */
+ private void configHost(WxMpDefaultConfigImpl config, WxMpMultiProperties.HostConfig hostConfig) {
+ if (hostConfig != null) {
+ String apiHost = hostConfig.getApiHost();
+ String mpHost = hostConfig.getMpHost();
+ String openHost = hostConfig.getOpenHost();
+ WxMpHostConfig wxMpHostConfig = new WxMpHostConfig();
+ wxMpHostConfig.setApiHost(StringUtils.isNotBlank(apiHost) ? apiHost : null);
+ wxMpHostConfig.setMpHost(StringUtils.isNotBlank(mpHost) ? mpHost : null);
+ wxMpHostConfig.setOpenHost(StringUtils.isNotBlank(openHost) ? openHost : null);
+ config.setHostConfig(wxMpHostConfig);
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java
new file mode 100644
index 0000000000..c137d0c087
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java
@@ -0,0 +1,77 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@RequiredArgsConstructor
+public class WxMpInJedisConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxMpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(wxMpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) {
+ return this.configRedis(wxMpMultiProperties);
+ }
+
+ private WxMpDefaultConfigImpl configRedis(WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiRedisProperties wxMpMultiRedisProperties = wxMpMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxMpMultiRedisProperties != null && StringUtils.isNotEmpty(wxMpMultiRedisProperties.getHost())) {
+ jedisPool = getJedisPool(wxMpMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxMpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
+ WxMpMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java
new file mode 100644
index 0000000000..cd90eba114
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxMpInMemoryConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxMpMultiProperties;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(wxMpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) {
+ return this.configInMemory();
+ }
+
+ private WxMpDefaultConfigImpl configInMemory() {
+ return new WxMpMapConfigImpl();
+ // return new WxMpDefaultConfigImpl();
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java
new file mode 100644
index 0000000000..fd96176a8a
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java
@@ -0,0 +1,45 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redisTemplate 策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template"
+)
+@RequiredArgsConstructor
+public class WxMpInRedisTemplateConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties WxMpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(WxMpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) {
+ return this.configRedisTemplate(WxMpMultiProperties);
+ }
+
+ private WxMpDefaultConfigImpl configRedisTemplate(WxMpMultiProperties wxMpMultiProperties) {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxMpRedisConfigImpl(new RedisTemplateWxRedisOps(redisTemplate),
+ wxMpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java
new file mode 100644
index 0000000000..a2b606c4a6
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java
@@ -0,0 +1,67 @@
+package com.binarywang.spring.starter.wxjava.mp.configuration.services;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@RequiredArgsConstructor
+public class WxMpInRedissonConfiguration extends AbstractWxMpConfiguration {
+ private final WxMpMultiProperties wxMpMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxMpMultiServices wxMpMultiServices() {
+ return this.wxMpMultiServices(wxMpMultiProperties);
+ }
+
+ @Override
+ protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) {
+ return this.configRedisson(wxMpMultiProperties);
+ }
+
+ private WxMpDefaultConfigImpl configRedisson(WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiRedisProperties redisProperties = wxMpMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxMpMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMpRedissonConfigImpl(redissonClient, wxMpMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxMpMultiProperties wxMpMultiProperties) {
+ WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
+ WxMpMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java
new file mode 100644
index 0000000000..9dd95f9531
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java
@@ -0,0 +1,182 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(WxMpMultiProperties.PREFIX)
+public class WxMpMultiProperties implements Serializable {
+ private static final long serialVersionUID = -5358245184407791011L;
+ public static final String PREFIX = "wx.mp";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class HostConfig implements Serializable {
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.MEMORY;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:mp:multi";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final WxMpMultiRedisProperties redis = new WxMpMultiRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.mp.api.WxMpService#setMaxRetryTimes(int)}
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.WxMpService#setRetrySleepMillis(int)}
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+
+ /**
+ * 多租户实现模式.
+ *
+ * - ISOLATED: 为每个租户创建独立的 WxMpService 实例(默认)
+ * - SHARED: 使用单个 WxMpService 实例管理所有租户配置,共享 HTTP 客户端
+ *
+ */
+ private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ MEMORY,
+ /**
+ * jedis
+ */
+ JEDIS,
+ /**
+ * redisson
+ */
+ REDISSON,
+ /**
+ * redisTemplate
+ */
+ REDIS_TEMPLATE
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient
+ */
+ HTTP_CLIENT,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS,
+ /**
+ * OkHttp
+ */
+ OK_HTTP,
+ /**
+ * JoddHttp
+ */
+ JODD_HTTP
+ }
+
+ public enum MultiTenantMode {
+ /**
+ * 隔离模式:为每个租户创建独立的 WxMpService 实例.
+ * 优点:线程安全,不依赖 ThreadLocal
+ * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多
+ */
+ ISOLATED,
+ /**
+ * 共享模式:使用单个 WxMpService 实例管理所有租户配置.
+ * 优点:共享 HTTP 客户端,节省资源
+ * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意
+ */
+ SHARED
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java
new file mode 100644
index 0000000000..38cae8bdac
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+public class WxMpMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java
new file mode 100644
index 0000000000..6302784bf0
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java
@@ -0,0 +1,40 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author yl
+ * created on 2024/1/23
+ */
+@Data
+@NoArgsConstructor
+public class WxMpSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+ /**
+ * 设置微信公众号的 appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的 app secret.
+ */
+ private String appSecret;
+
+ /**
+ * 设置微信公众号的 token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的 EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java
new file mode 100644
index 0000000000..69122e5277
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.mp.service;
+
+
+import me.chanjar.weixin.mp.api.WxMpService;
+
+/**
+ * 企业微信 {@link WxMpService} 所有实例存放类.
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+public interface WxMpMultiServices {
+ /**
+ * 通过租户 Id 获取 WxMpService
+ *
+ * @param tenantId 租户 Id
+ * @return WxMpService
+ */
+ WxMpService getWxMpService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxMpService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxMpService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java
new file mode 100644
index 0000000000..e5f358abe2
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java
@@ -0,0 +1,36 @@
+package com.binarywang.spring.starter.wxjava.mp.service;
+
+import me.chanjar.weixin.mp.api.WxMpService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 企业微信 {@link WxMpMultiServices} 默认实现
+ *
+ * @author yl
+ * created on 2024/1/23
+ */
+public class WxMpMultiServicesImpl implements WxMpMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxMpService getWxMpService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxMpService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxMpService WxMpService 实例
+ */
+ public void addWxMpService(String tenantId, WxMpService wxMpService) {
+ this.services.put(tenantId, wxMpService);
+ }
+
+ @Override
+ public void removeWxMpService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java
new file mode 100644
index 0000000000..ca9123c572
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java
@@ -0,0 +1,53 @@
+package com.binarywang.spring.starter.wxjava.mp.service;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.api.WxMpService;
+
+/**
+ * 微信公众号 {@link WxMpMultiServices} 共享式实现.
+ *
+ * 使用单个 WxMpService 实例管理多个租户配置,通过 switchover 切换租户。
+ * 相比 {@link WxMpMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。
+ *
+ *
+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。
+ *
+ *
+ * @author Binary Wang
+ * created on 2026/1/9
+ */
+@RequiredArgsConstructor
+public class WxMpMultiServicesSharedImpl implements WxMpMultiServices {
+ private final WxMpService sharedWxMpService;
+
+ @Override
+ public WxMpService getWxMpService(String tenantId) {
+ if (tenantId == null) {
+ return null;
+ }
+ // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null)
+ if (!sharedWxMpService.switchover(tenantId)) {
+ return null;
+ }
+ return sharedWxMpService;
+ }
+
+ @Override
+ public void removeWxMpService(String tenantId) {
+ if (tenantId != null) {
+ sharedWxMpService.removeConfigStorage(tenantId);
+ }
+ }
+
+ /**
+ * 添加租户配置到共享的 WxMpService 实例
+ *
+ * @param tenantId 租户 ID
+ * @param wxMpService 要添加配置的 WxMpService(仅使用其配置,不使用其实例)
+ */
+ public void addWxMpService(String tenantId, WxMpService wxMpService) {
+ if (tenantId != null && wxMpService != null) {
+ sharedWxMpService.addConfigStorage(tenantId, wxMpService.getWxMpConfigStorage());
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..d20dc22dc3
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.mp.autoconfigure.WxMpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..324e3555ba
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.mp.autoconfigure.WxMpMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md
new file mode 100644
index 0000000000..091912cfad
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md
@@ -0,0 +1,46 @@
+# wx-java-mp-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-mp-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.mp.app-id=appId
+ wx.mp.secret=@secret
+ wx.mp.token=@token
+ wx.mp.aes-key=@aesKey
+ wx.mp.use-stable-access-token=@useStableAccessToken
+ # 存储配置redis(可选)
+ wx.mp.config-storage.type= edis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.mp.config-storage.key-prefix=wx # 相关redis前缀配置: wx(默认)
+ wx.mp.config-storage.redis.host=127.0.0.1
+ wx.mp.config-storage.redis.port=6379
+ #单机和sentinel同时存在时,优先使用sentinel配置
+ #wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ #wx.mp.config-storage.redis.sentinel-name=mymaster
+ # http客户端配置
+ wx.mp.config-storage.http-client-type=HttpComponents # http客户端类型: HttpComponents(Apache HttpClient 5.x,推荐), HttpClient(Apache HttpClient 4.x), OkHttp, JoddHttp
+ wx.mp.config-storage.http-proxy-host=
+ wx.mp.config-storage.http-proxy-port=
+ wx.mp.config-storage.http-proxy-username=
+ wx.mp.config-storage.http-proxy-password=
+ # 公众号地址host配置
+ #wx.mp.hosts.api-host=http://proxy.com/
+ #wx.mp.hosts.open-host=http://proxy.com/
+ #wx.mp.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型
+
+- `WxMpService`
+- `WxMpConfigStorage`
+
+4、参考demo:
+https://github.com/binarywang/wx-java-mp-demo
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..6b4993ca77
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-mp-spring-boot-starter
+ WxJava - Spring Boot Starter for MP
+ 微信公众号开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java
new file mode 100644
index 0000000000..b2e3848ab8
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java
@@ -0,0 +1,17 @@
+package com.binarywang.spring.starter.wxjava.mp.config;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * .
+ *
+ * @author someone
+ */
+@Configuration
+@EnableConfigurationProperties(WxMpProperties.class)
+@Import({ WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class })
+public class WxMpAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java
new file mode 100644
index 0000000000..dc6dcafb82
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java
@@ -0,0 +1,71 @@
+package com.binarywang.spring.starter.wxjava.mp.config;
+
+import com.binarywang.spring.starter.wxjava.mp.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信公众号相关服务自动注册.
+ *
+ * @author someone
+ */
+@Configuration
+public class WxMpServiceAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties wxMpProperties) {
+ HttpClientType httpClientType = wxMpProperties.getConfigStorage().getHttpClientType();
+ WxMpService wxMpService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxMpService = newWxMpServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxMpService = newWxMpServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxMpService = newWxMpServiceHttpClientImpl();
+ break;
+ case HttpComponents:
+ wxMpService = newWxMpServiceHttpComponentsImpl();
+ break;
+ default:
+ wxMpService = newWxMpServiceImpl();
+ break;
+ }
+
+ wxMpService.setWxMpConfigStorage(configStorage);
+ return wxMpService;
+ }
+
+ private WxMpService newWxMpServiceImpl() {
+ return new WxMpServiceImpl();
+ }
+
+ private WxMpService newWxMpServiceHttpClientImpl() {
+ return new WxMpServiceHttpClientImpl();
+ }
+
+ private WxMpService newWxMpServiceOkHttpImpl() {
+ return new WxMpServiceOkHttpImpl();
+ }
+
+ private WxMpService newWxMpServiceJoddHttpImpl() {
+ return new WxMpServiceJoddHttpImpl();
+ }
+
+ private WxMpService newWxMpServiceHttpComponentsImpl() {
+ return new WxMpServiceHttpComponentsImpl();
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java
new file mode 100644
index 0000000000..cab3cb17b2
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.mp.config;
+
+import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInJedisConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInMemoryConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInRedisTemplateConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInRedissonConfigStorageConfiguration;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信公众号存储策略自动配置.
+ *
+ * @author Luo
+ */
+@Configuration
+@Import({
+ WxMpInMemoryConfigStorageConfiguration.class,
+ WxMpInJedisConfigStorageConfiguration.class,
+ WxMpInRedisTemplateConfigStorageConfiguration.class,
+ WxMpInRedissonConfigStorageConfiguration.class
+})
+@RequiredArgsConstructor
+public class WxMpStorageAutoConfiguration {
+
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java
new file mode 100644
index 0000000000..e39a8bf4d9
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java
@@ -0,0 +1,54 @@
+package com.binarywang.spring.starter.wxjava.mp.config.storage;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+import me.chanjar.weixin.mp.config.WxMpHostConfig;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author zhangyl
+ */
+public abstract class AbstractWxMpConfigStorageConfiguration {
+
+ protected WxMpDefaultConfigImpl config(WxMpDefaultConfigImpl config, WxMpProperties properties) {
+ config.setAppId(properties.getAppId());
+ config.setSecret(properties.getSecret());
+ config.setToken(properties.getToken());
+ config.setAesKey(properties.getAesKey());
+ config.setUseStableAccessToken(properties.isUseStableAccessToken());
+
+ WxMpProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+
+ // 设置自定义的 HttpClient 超时配置
+ ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder();
+ if (clientBuilder == null) {
+ clientBuilder = DefaultApacheHttpClientBuilder.get();
+ }
+ if (clientBuilder instanceof DefaultApacheHttpClientBuilder) {
+ DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder;
+ defaultBuilder.setConnectionTimeout(configStorageProperties.getConnectionTimeout());
+ defaultBuilder.setSoTimeout(configStorageProperties.getSoTimeout());
+ defaultBuilder.setConnectionRequestTimeout(configStorageProperties.getConnectionRequestTimeout());
+ config.setApacheHttpClientBuilder(defaultBuilder);
+ }
+
+ // wx host config
+ if (null != properties.getHosts() && StringUtils.isNotEmpty(properties.getHosts().getApiHost())) {
+ WxMpHostConfig hostConfig = new WxMpHostConfig();
+ hostConfig.setApiHost(properties.getHosts().getApiHost());
+ hostConfig.setOpenHost(properties.getHosts().getOpenHost());
+ hostConfig.setMpHost(properties.getHosts().getMpHost());
+ config.setHostConfig(hostConfig);
+ }
+
+ return config;
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..c21418a6f6
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java
@@ -0,0 +1,80 @@
+package com.binarywang.spring.starter.wxjava.mp.config.storage;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpProperties.PREFIX + ".config-storage",
+ name = "type",
+ havingValue = "jedis"
+)
+@ConditionalOnClass(Jedis.class)
+@RequiredArgsConstructor
+public class WxMpInJedisConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpRedisConfigImpl config = getWxMpRedisConfigImpl();
+ return this.config(config, properties);
+ }
+
+ private WxMpRedisConfigImpl getWxMpRedisConfigImpl() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = applicationContext.getBean("wxMpJedisPool", JedisPool.class);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = WxMpProperties.PREFIX + ".config-storage.redis", name = "host")
+ public JedisPool wxMpJedisPool() {
+ WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+ redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..16eada73ae
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,33 @@
+package com.binarywang.spring.starter.wxjava.mp.config.storage;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpProperties.PREFIX + ".config-storage",
+ name = "type",
+ havingValue = "memory",
+ matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxMpInMemoryConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
+ config(config, properties);
+ return config;
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java
new file mode 100644
index 0000000000..0305ca4f8e
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java
@@ -0,0 +1,46 @@
+package com.binarywang.spring.starter.wxjava.mp.config.storage;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * @author zhangyl
+ */
+@Slf4j
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpProperties.PREFIX + ".config-storage",
+ name = "type",
+ havingValue = "redistemplate"
+)
+@ConditionalOnClass(StringRedisTemplate.class)
+@RequiredArgsConstructor
+public class WxMpInRedisTemplateConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpRedisConfigImpl config = getWxMpInRedisTemplateConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMpRedisConfigImpl getWxMpInRedisTemplateConfigStorage() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+ return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..75b736f53f
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,69 @@
+package com.binarywang.spring.starter.wxjava.mp.config.storage;
+
+import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.mp.config.WxMpConfigStorage;
+import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author zhangyl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxMpProperties.PREFIX + ".config-storage",
+ name = "type",
+ havingValue = "redisson"
+)
+@ConditionalOnClass({Redisson.class, RedissonClient.class})
+@RequiredArgsConstructor
+public class WxMpInRedissonConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration {
+ private final WxMpProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxMpConfigStorage.class)
+ public WxMpConfigStorage wxMpConfigStorage() {
+ WxMpRedissonConfigImpl config = getWxMpInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() {
+ RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = applicationContext.getBean("wxMpRedissonClient", RedissonClient.class);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxMpRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = WxMpProperties.PREFIX + ".config-storage.redis", name = "host")
+ public RedissonClient wxMpRedissonClient() {
+ WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase());
+ if (StringUtils.isNotBlank(redis.getPassword())) {
+ config.useSingleServer().setPassword(redis.getPassword());
+ }
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java
new file mode 100644
index 0000000000..0bf034417f
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.mp.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents (Apache HttpClient 5.x).
+ */
+ HttpComponents,
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java
new file mode 100644
index 0000000000..05ed6ce393
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.mp.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java
new file mode 100644
index 0000000000..5b29400738
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java
@@ -0,0 +1,27 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class HostConfig implements Serializable {
+
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ /**
+ * 对应于:https://api.weixin.qq.com
+ */
+ private String apiHost;
+
+ /**
+ * 对应于:https://open.weixin.qq.com
+ */
+ private String openHost;
+
+ /**
+ * 对应于:https://mp.weixin.qq.com
+ */
+ private String mpHost;
+
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java
new file mode 100644
index 0000000000..573c87630f
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * redis 配置属性.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java
new file mode 100644
index 0000000000..a6c6e3b6bd
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java
@@ -0,0 +1,122 @@
+package com.binarywang.spring.starter.wxjava.mp.properties;
+
+import com.binarywang.spring.starter.wxjava.mp.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.mp.enums.StorageType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+
+import static com.binarywang.spring.starter.wxjava.mp.enums.StorageType.Memory;
+import static com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties.PREFIX;
+
+/**
+ * 微信接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@ConfigurationProperties(PREFIX)
+public class WxMpProperties {
+ public static final String PREFIX = "wx.mp";
+
+ /**
+ * 设置微信公众号的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信公众号的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 是否使用稳定版 Access Token
+ */
+ private boolean useStableAccessToken = false;
+
+ /**
+ * 自定义host配置
+ */
+ @NestedConfigurationProperty
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * 连接超时时间,单位毫秒
+ */
+ private int connectionTimeout = 5000;
+
+ /**
+ * 读数据超时时间,即socketTimeout,单位毫秒
+ */
+ private int soTimeout = 5000;
+
+ /**
+ * 从连接池获取链接的超时时间,单位毫秒
+ */
+ private int connectionRequestTimeout = 5000;
+
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..6634593ff2
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.mp.config.WxMpAutoConfiguration
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..cdffb05c9e
--- /dev/null
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.mp.config.WxMpAutoConfiguration
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..ab5afa5449
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md
@@ -0,0 +1,98 @@
+# wx-java-open-multi-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-open-multi-spring-boot-starter
+ ${version}
+
+ ```
+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, redistemplate
+ wx.open.config-storage.type=memory
+ ## 相关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 配置,以下配置仅作为预留
+ # 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 客户端配置(选填)
+ 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 ...
+ }
+}
+```
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..5b9e2d4df7
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-open-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for WxOpen::支持多账号配置
+ 微信开放平台开发的 Spring Boot Starter::支持多账号配置
+
+
+
+ com.github.binarywang
+ weixin-java-open
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ provided
+
+
+ org.redisson
+ redisson
+ provided
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java
new file mode 100644
index 0000000000..749130f517
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java
@@ -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 Binary Wang
+ */
+@Configuration
+@Import(WxOpenMultiServiceConfiguration.class)
+public class WxOpenMultiAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java
new file mode 100644
index 0000000000..e858185e30
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java
@@ -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 Binary Wang
+ */
+@Configuration
+@EnableConfigurationProperties(WxOpenMultiProperties.class)
+@Import({
+ WxOpenInJedisConfiguration.class,
+ WxOpenInMemoryConfiguration.class,
+ WxOpenInRedissonConfiguration.class,
+ WxOpenInRedisTemplateConfiguration.class
+})
+public class WxOpenMultiServiceConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java
new file mode 100644
index 0000000000..f4245402e0
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java
@@ -0,0 +1,162 @@
+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.WxOpenServiceApacheHttpClientImpl;
+import me.chanjar.weixin.open.api.impl.WxOpenServiceHttpComponentsImpl;
+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 Binary Wang
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractWxOpenConfiguration {
+
+ protected WxOpenMultiServices wxOpenMultiServices(WxOpenMultiProperties wxOpenMultiProperties) {
+ Map appsMap = wxOpenMultiProperties.getApps();
+ if (appsMap == null || appsMap.isEmpty()) {
+ log.warn("微信开放平台应用参数未配置,通过 WxOpenMultiServices#getWxOpenService(\"tenantId\")获取实例将返回空");
+ return new WxOpenMultiServicesImpl();
+ }
+ /**
+ * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
+ */
+ Collection 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> entries = appsMap.entrySet();
+ for (Map.Entry 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) {
+ WxOpenMultiProperties.HttpClientType httpClientType = wxOpenMultiProperties.getConfigStorage().getHttpClientType();
+ WxOpenService wxOpenService;
+ if (httpClientType == WxOpenMultiProperties.HttpClientType.APACHE_HTTP) {
+ wxOpenService = new WxOpenServiceApacheHttpClientImpl();
+ } else {
+ wxOpenService = new WxOpenServiceHttpComponentsImpl();
+ }
+ 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);
+
+ // 仅在使用 Apache HttpClient 4.x 时配置 ApacheHttpClientBuilder 超时参数
+ if (storage.getHttpClientType() == WxOpenMultiProperties.HttpClientType.APACHE_HTTP) {
+ 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);
+ }
+ }
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java
new file mode 100644
index 0000000000..bb9577b99b
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java
@@ -0,0 +1,78 @@
+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.WxOpenMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+
+/**
+ * 自动装配基于 jedis 策略配置
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "JEDIS"
+)
+@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class})
+@RequiredArgsConstructor
+public class WxOpenInJedisConfiguration extends AbstractWxOpenConfiguration {
+ private final WxOpenMultiProperties wxOpenMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxOpenMultiServices wxOpenMultiServices() {
+ return this.wxOpenMultiServices(wxOpenMultiProperties);
+ }
+
+ @Override
+ protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) {
+ return this.configJedis(wxOpenMultiProperties);
+ }
+
+ private WxOpenInRedisConfigStorage configJedis(WxOpenMultiProperties wxOpenMultiProperties) {
+ WxOpenMultiRedisProperties redisProperties = wxOpenMultiProperties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ jedisPool = getJedisPool(wxOpenMultiProperties);
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxOpenInRedisConfigStorage(jedisPool, wxOpenMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool(WxOpenMultiProperties wxOpenMultiProperties) {
+ WxOpenMultiProperties.ConfigStorage storage = wxOpenMultiProperties.getConfigStorage();
+ WxOpenMultiRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java
new file mode 100644
index 0000000000..f7448a0875
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java
@@ -0,0 +1,33 @@
+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.service.WxOpenMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于内存策略配置
+ *
+ * @author someone
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "MEMORY", matchIfMissing = true
+)
+@RequiredArgsConstructor
+public class WxOpenInMemoryConfiguration extends AbstractWxOpenConfiguration {
+ private final WxOpenMultiProperties wxOpenMultiProperties;
+
+ @Bean
+ public WxOpenMultiServices wxOpenMultiServices() {
+ return this.wxOpenMultiServices(wxOpenMultiProperties);
+ }
+
+ @Override
+ protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) {
+ return new WxOpenInMemoryConfigStorage();
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java
new file mode 100644
index 0000000000..6208c90fe5
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java
@@ -0,0 +1,44 @@
+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.service.WxOpenMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedisTemplateConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 自动装配基于 redis template 策略配置
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
+)
+@ConditionalOnClass(StringRedisTemplate.class)
+@RequiredArgsConstructor
+public class WxOpenInRedisTemplateConfiguration extends AbstractWxOpenConfiguration {
+ private final WxOpenMultiProperties wxOpenMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxOpenMultiServices wxOpenMultiServices() {
+ return this.wxOpenMultiServices(wxOpenMultiProperties);
+ }
+
+ @Override
+ protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) {
+ return this.configRedisTemplate();
+ }
+
+ private WxOpenInRedisTemplateConfigStorage configRedisTemplate() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxOpenInRedisTemplateConfigStorage(redisTemplate, wxOpenMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java
new file mode 100644
index 0000000000..97569f3baf
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java
@@ -0,0 +1,68 @@
+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.WxOpenMultiRedisProperties;
+import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 自动装配基于 redisson 策略配置
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@ConditionalOnClass({Redisson.class, RedissonClient.class})
+@RequiredArgsConstructor
+public class WxOpenInRedissonConfiguration extends AbstractWxOpenConfiguration {
+ private final WxOpenMultiProperties wxOpenMultiProperties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ public WxOpenMultiServices wxOpenMultiServices() {
+ return this.wxOpenMultiServices(wxOpenMultiProperties);
+ }
+
+ @Override
+ protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) {
+ return this.configRedisson(wxOpenMultiProperties);
+ }
+
+ private WxOpenInRedissonConfigStorage configRedisson(WxOpenMultiProperties wxOpenMultiProperties) {
+ WxOpenMultiRedisProperties redisProperties = wxOpenMultiProperties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ redissonClient = getRedissonClient(wxOpenMultiProperties);
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxOpenInRedissonConfigStorage(redissonClient, wxOpenMultiProperties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient(WxOpenMultiProperties wxOpenMultiProperties) {
+ WxOpenMultiProperties.ConfigStorage storage = wxOpenMultiProperties.getConfigStorage();
+ WxOpenMultiRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java
new file mode 100644
index 0000000000..0c6a222891
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java
@@ -0,0 +1,141 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信开放平台多账号配置属性.
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+@ConfigurationProperties(WxOpenMultiProperties.PREFIX)
+public class WxOpenMultiProperties implements Serializable {
+ private static final long serialVersionUID = -5358245184407791011L;
+ public static final String PREFIX = "wx.open";
+
+ private Map apps = new HashMap<>();
+
+ /**
+ * 存储策略
+ */
+ private final ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ @NoArgsConstructor
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = StorageType.memory;
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:open:multi";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private final WxOpenMultiRedisProperties redis = new WxOpenMultiRedisProperties();
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+
+ /**
+ * 连接超时时间,单位毫秒
+ */
+ private int connectionTimeout = 5000;
+
+ /**
+ * 读数据超时时间,即socketTimeout,单位毫秒
+ */
+ private int soTimeout = 5000;
+
+ /**
+ * 从连接池获取链接的超时时间,单位毫秒
+ */
+ private int connectionRequestTimeout = 5000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redisTemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * Apache HttpClient 4.x
+ */
+ APACHE_HTTP,
+ /**
+ * Apache HttpClient 5.x (HttpComponents)
+ */
+ HTTP_COMPONENTS
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java
new file mode 100644
index 0000000000..ae6d5368d7
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java
@@ -0,0 +1,57 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信开放平台多账号Redis配置.
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class WxOpenMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java
new file mode 100644
index 0000000000..116da323dc
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java
@@ -0,0 +1,49 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信开放平台单个应用配置.
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class WxOpenSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+
+ /**
+ * 设置微信开放平台的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信开放平台的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信开放平台的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信开放平台的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java
new file mode 100644
index 0000000000..9228071a10
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.open.service;
+
+
+import me.chanjar.weixin.open.api.WxOpenService;
+
+/**
+ * 微信开放平台 {@link WxOpenService} 所有实例存放类.
+ *
+ * @author binarywang
+ */
+public interface WxOpenMultiServices {
+ /**
+ * 通过租户 Id 获取 WxOpenService
+ *
+ * @param tenantId 租户 Id
+ * @return WxOpenService
+ */
+ WxOpenService getWxOpenService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxOpenService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxOpenService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java
new file mode 100644
index 0000000000..76fb139e6c
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java
@@ -0,0 +1,35 @@
+package com.binarywang.spring.starter.wxjava.open.service;
+
+import me.chanjar.weixin.open.api.WxOpenService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 微信开放平台 {@link WxOpenMultiServices} 默认实现
+ *
+ * @author Binary Wang
+ */
+public class WxOpenMultiServicesImpl implements WxOpenMultiServices {
+ private final Map services = new ConcurrentHashMap<>();
+
+ @Override
+ public WxOpenService getWxOpenService(String tenantId) {
+ return this.services.get(tenantId);
+ }
+
+ /**
+ * 根据租户 Id,添加一个 WxOpenService 到列表
+ *
+ * @param tenantId 租户 Id
+ * @param wxOpenService WxOpenService 实例
+ */
+ public void addWxOpenService(String tenantId, WxOpenService wxOpenService) {
+ this.services.put(tenantId, wxOpenService);
+ }
+
+ @Override
+ public void removeWxOpenService(String tenantId) {
+ this.services.remove(tenantId);
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..a61d0018db
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.open.autoconfigure.WxOpenMultiAutoConfiguration
\ No newline at end of file
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..ddc66af02c
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.open.autoconfigure.WxOpenMultiAutoConfiguration
\ No newline at end of file
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/README.md b/spring-boot-starters/wx-java-open-spring-boot-starter/README.md
new file mode 100644
index 0000000000..12650ac931
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/README.md
@@ -0,0 +1,39 @@
+# wx-java-open-spring-boot-starter
+## 快速开始
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-open-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.open.appId = appId
+ wx.open.secret = @secret
+ wx.open.token = @token
+ wx.open.aesKey = @aesKey
+ # 存储配置redis(可选)
+ # 优先注入容器的(JedisPool, RedissonClient), 当配置了wx.open.config-storage.redis.host, 不会使用容器注入redis连接配置
+ wx.open.config-storage.type = redis # 配置类型: memory(默认), redis(jedis), jedis, redisson, redistemplate
+ wx.open.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认)
+ wx.open.config-storage.redis.host = 127.0.0.1
+ wx.open.config-storage.redis.port = 6379
+ # http客户端配置
+ wx.open.config-storage.http-client-type=httpclient # http客户端类型: httpclient(默认)
+ 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
+ ```
+3. 支持自动注入的类型: `WxOpenService, WxOpenMessageRouter, WxOpenComponentService`
+
+4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的
+ - WxOpenConfigStorage
+ - WxOpenService
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..e6cc597ebc
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-open-spring-boot-starter
+ WxJava - Spring Boot Starter for WxOpen
+ 微信开放平台开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-open
+ ${project.version}
+
+
+ redis.clients
+ jedis
+
+
+ org.redisson
+ redisson
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java
new file mode 100644
index 0000000000..724d4a2f80
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java
@@ -0,0 +1,20 @@
+package com.binarywang.spring.starter.wxjava.open.config;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * .
+ *
+ * @author someone
+ */
+@Configuration
+@EnableConfigurationProperties(WxOpenProperties.class)
+@Import({
+ WxOpenStorageAutoConfiguration.class,
+ WxOpenServiceAutoConfiguration.class
+})
+public class WxOpenAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java
new file mode 100644
index 0000000000..e532f3c160
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java
@@ -0,0 +1,40 @@
+package com.binarywang.spring.starter.wxjava.open.config;
+
+import me.chanjar.weixin.open.api.WxOpenComponentService;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.WxOpenService;
+import me.chanjar.weixin.open.api.impl.WxOpenMessageRouter;
+import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信开放平台相关服务自动注册.
+ *
+ * @author someone
+ */
+@Configuration
+public class WxOpenServiceAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(WxOpenConfigStorage.class)
+ public WxOpenService wxOpenService(WxOpenConfigStorage wxOpenConfigStorage) {
+ WxOpenService wxOpenService = new WxOpenServiceImpl();
+ wxOpenService.setWxOpenConfigStorage(wxOpenConfigStorage);
+ return wxOpenService;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) {
+ return new WxOpenMessageRouter(wxOpenService);
+ }
+
+ @Bean
+ public WxOpenComponentService wxOpenComponentService(WxOpenService wxOpenService) {
+ return wxOpenService.getWxOpenComponentService();
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java
new file mode 100644
index 0000000000..efbefbe0a1
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java
@@ -0,0 +1,23 @@
+package com.binarywang.spring.starter.wxjava.open.config;
+
+import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInJedisConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInMemoryConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInRedisTemplateConfigStorageConfiguration;
+import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInRedissonConfigStorageConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 微信公众号存储策略自动配置.
+ *
+ * @author someone
+ */
+@Configuration
+@Import({
+ WxOpenInMemoryConfigStorageConfiguration.class,
+ WxOpenInRedisTemplateConfigStorageConfiguration.class,
+ WxOpenInJedisConfigStorageConfiguration.class,
+ WxOpenInRedissonConfigStorageConfiguration.class
+})
+public class WxOpenStorageAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java
new file mode 100644
index 0000000000..91db545ab9
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java
@@ -0,0 +1,54 @@
+package com.binarywang.spring.starter.wxjava.open.config.storage;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author yl
+ */
+public abstract class AbstractWxOpenConfigStorageConfiguration {
+
+ protected WxOpenInMemoryConfigStorage config(WxOpenInMemoryConfigStorage config, WxOpenProperties properties) {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
+ config.setHttpProxyHost(storage.getHttpProxyHost());
+ config.setHttpProxyUsername(storage.getHttpProxyUsername());
+ config.setHttpProxyPassword(storage.getHttpProxyPassword());
+ Integer httpProxyPort = storage.getHttpProxyPort();
+ if (httpProxyPort != null) {
+ config.setHttpProxyPort(httpProxyPort);
+ }
+ int maxRetryTimes = storage.getMaxRetryTimes();
+ if (maxRetryTimes < 0) {
+ maxRetryTimes = 0;
+ }
+ int retrySleepMillis = storage.getRetrySleepMillis();
+ if (retrySleepMillis < 0) {
+ retrySleepMillis = 1000;
+ }
+ config.setRetrySleepMillis(retrySleepMillis);
+ config.setMaxRetryTimes(maxRetryTimes);
+
+ // 设置URL配置
+ config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
+
+ // 设置自定义的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);
+ }
+
+ return config;
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java
new file mode 100644
index 0000000000..73a0183d72
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java
@@ -0,0 +1,73 @@
+package com.binarywang.spring.starter.wxjava.open.config.storage;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * @author yl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
+)
+@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class})
+@RequiredArgsConstructor
+public class WxOpenInJedisConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = getWxOpenInRedisConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() {
+ WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis();
+ JedisPool jedisPool;
+ if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ return new WxOpenInRedisConfigStorage(jedisPool, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private JedisPool getJedisPool() {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ WxOpenRedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(),
+ redis.getTimeout(), redis.getPassword(), redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java
new file mode 100644
index 0000000000..ef17905493
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java
@@ -0,0 +1,30 @@
+package com.binarywang.spring.starter.wxjava.open.config.storage;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author yl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type",
+ matchIfMissing = true, havingValue = "memory"
+)
+@RequiredArgsConstructor
+public class WxOpenInMemoryConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean(WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage();
+ return this.config(config, properties);
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java
new file mode 100644
index 0000000000..79521c921a
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java
@@ -0,0 +1,41 @@
+package com.binarywang.spring.starter.wxjava.open.config.storage;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedisTemplateConfigStorage;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * @author yl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
+)
+@ConditionalOnClass(StringRedisTemplate.class)
+@RequiredArgsConstructor
+public class WxOpenInRedisTemplateConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = getWxOpenInRedisTemplateConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxOpenInRedisTemplateConfigStorage getWxOpenInRedisTemplateConfigStorage() {
+ StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ return new WxOpenInRedisTemplateConfigStorage(redisTemplate, properties.getConfigStorage().getKeyPrefix());
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java
new file mode 100644
index 0000000000..ea1dce3670
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java
@@ -0,0 +1,64 @@
+package com.binarywang.spring.starter.wxjava.open.config.storage;
+
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
+import com.binarywang.spring.starter.wxjava.open.properties.WxOpenRedisProperties;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.open.api.WxOpenConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
+import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author yl
+ */
+@Configuration
+@ConditionalOnProperty(
+ prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
+)
+@ConditionalOnClass({Redisson.class, RedissonClient.class})
+@RequiredArgsConstructor
+public class WxOpenInRedissonConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration {
+ private final WxOpenProperties properties;
+ private final ApplicationContext applicationContext;
+
+ @Bean
+ @ConditionalOnMissingBean(WxOpenConfigStorage.class)
+ public WxOpenConfigStorage wxOpenConfigStorage() {
+ WxOpenInMemoryConfigStorage config = getWxOpenInRedissonConfigStorage();
+ return this.config(config, properties);
+ }
+
+ private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() {
+ WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis();
+ RedissonClient redissonClient;
+ if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) {
+ redissonClient = getRedissonClient();
+ } else {
+ redissonClient = applicationContext.getBean(RedissonClient.class);
+ }
+ return new WxOpenInRedissonConfigStorage(redissonClient, properties.getConfigStorage().getKeyPrefix());
+ }
+
+ private RedissonClient getRedissonClient() {
+ WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
+ WxOpenRedisProperties redis = storage.getRedis();
+
+ Config config = new Config();
+ config.useSingleServer()
+ .setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
+ .setDatabase(redis.getDatabase())
+ .setPassword(redis.getPassword());
+ config.setTransportMode(TransportMode.NIO);
+ return Redisson.create(config);
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java
new file mode 100644
index 0000000000..248c6eedf6
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java
@@ -0,0 +1,165 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.io.Serializable;
+
+import static com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties.PREFIX;
+import static com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties.StorageType.memory;
+
+
+/**
+ * 微信接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@ConfigurationProperties(PREFIX)
+public class WxOpenProperties {
+ public static final String PREFIX = "wx.open";
+
+ /**
+ * 设置微信开放平台的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信开放平台的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信开放平台的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信开放平台的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+
+ /**
+ * 存储策略.
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx:open";
+
+ /**
+ * redis连接配置.
+ */
+ @NestedConfigurationProperty
+ private WxOpenRedisProperties redis = new WxOpenRedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.httpclient;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+ /**
+ * http 请求最大重试次数
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * 连接超时时间,单位毫秒
+ */
+ private int connectionTimeout = 5000;
+
+ /**
+ * 读数据超时时间,即socketTimeout,单位毫秒
+ */
+ private int soTimeout = 5000;
+
+ /**
+ * 从连接池获取链接的超时时间,单位毫秒
+ */
+ private int connectionRequestTimeout = 5000;
+
+ }
+
+ public enum StorageType {
+ /**
+ * 内存.
+ */
+ memory,
+ /**
+ * jedis.
+ */
+ jedis,
+ /**
+ * redisson.
+ */
+ redisson,
+ /**
+ * redistemplate
+ */
+ redistemplate
+ }
+
+ public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ httpclient
+ }
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java
new file mode 100644
index 0000000000..0aafc73da6
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java
@@ -0,0 +1,45 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * Redis配置.
+ *
+ * @author someone
+ */
+@Data
+public class WxOpenRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host;
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..0e5975cf18
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.open.config.WxOpenAutoConfiguration
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..ce327ba462
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.open.config.WxOpenAutoConfiguration
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md
new file mode 100644
index 0000000000..1ae4ac6299
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md
@@ -0,0 +1,317 @@
+# wx-java-pay-multi-spring-boot-starter
+
+## 快速开始
+
+本starter支持微信支付多公众号关联配置,适用于以下场景:
+- 一个服务商需要为多个公众号提供支付服务
+- 一个系统需要支持多个公众号的支付业务
+- 需要根据不同的appId动态切换支付配置
+
+## 使用说明
+
+### 1. 引入依赖
+
+在项目的 `pom.xml` 中添加以下依赖:
+
+```xml
+
+ com.github.binarywang
+ wx-java-pay-multi-spring-boot-starter
+ ${version}
+
+```
+
+### 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.api-v3-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.api-v3-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.<configKey> 中的 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.<configKey> 中的 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 |
+| apiHostUrlPath | 自定义API主机路径前缀(代理入口前缀) | 空 |
+| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | true |
+| fullPublicKeyModel | 是否完全使用公钥模式 | true |
+| publicKeyId | 公钥ID | 无 |
+| publicKeyPath | 公钥文件路径 | 无 |
+
+## 常见问题
+
+### 1. 如何选择配置的key?
+
+配置的key(即 `wx.pay.configs.` 中的 `` 部分)可以自由选择:
+- 可以使用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)
+- [微信支付V2文档](https://pay.weixin.qq.com/doc/v2)
+- [微信支付V3接口文档](https://pay.weixin.qq.com/doc/v3/merchant/4012062524)
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..39cb67b4a8
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,53 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-pay-multi-spring-boot-starter
+ WxJava - Spring Boot Starter for Pay::支持多公众号关联配置
+ 微信支付开发的 Spring Boot Starter::支持多公众号关联配置
+
+
+
+ com.github.binarywang
+ weixin-java-pay
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring.boot.version}
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java
new file mode 100644
index 0000000000..08ddafbf9c
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java
@@ -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);
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java
new file mode 100644
index 0000000000..8d1180b0e4
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java
@@ -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 configs = new HashMap<>();
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java
new file mode 100644
index 0000000000..ef936fc234
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java
@@ -0,0 +1,130 @@
+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;
+
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀).
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
+ /**
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加.
+ */
+ private boolean strictlyNeedWechatPaySerial = true;
+
+ /**
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用.
+ */
+ private boolean fullPublicKeyModel = true;
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java
new file mode 100644
index 0000000000..3e0b7a999f
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java
@@ -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.
+ *
+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx),
+ * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。
+ *
+ *
+ * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key)
+ * @return WxPayService
+ */
+ WxPayService getWxPayService(String configKey);
+
+ /**
+ * 根据配置标识,从列表中移除一个 WxPayService 实例.
+ *
+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx),
+ * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。
+ *
+ *
+ * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key)
+ */
+ void removeWxPayService(String configKey);
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java
new file mode 100644
index 0000000000..7cbcceabb4
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java
@@ -0,0 +1,93 @@
+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 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.setApiHostUrlPath(StringUtils.trimToNull(properties.getApiHostUrlPath()));
+ payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial());
+ payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel());
+
+ wxPayService.setConfig(payConfig);
+ return wxPayService;
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..d257d37276
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..39e3342f4a
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration
+
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
new file mode 100644
index 0000000000..87132fdcf3
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
@@ -0,0 +1,109 @@
+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.api-host-url=http://10.0.0.1:3128",
+ "wx.pay.configs.app2.api-host-url-path=/api-weixin",
+ "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("http://10.0.0.1:3128", app2Config.getApiHostUrl());
+ assertEquals("/api-weixin", app2Config.getApiHostUrlPath());
+ 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());
+ assertEquals("/api-weixin", app2Service.getConfig().getApiHostUrlPath());
+
+ // 测试相同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 {
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java
new file mode 100644
index 0000000000..48ae32d5b4
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java
@@ -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;
+
+/**
+ * 微信支付多公众号关联使用示例.
+ *
+ * 本示例展示了如何使用 wx-java-pay-multi-spring-boot-starter 来管理多个公众号的支付配置。
+ *
+ *
+ * @author Binary Wang
+ */
+@Slf4j
+@Service
+public class WxPayMultiExample {
+
+ @Autowired
+ private WxPayMultiServices wxPayMultiServices;
+
+ /**
+ * 示例1:根据appId创建支付订单.
+ *
+ * 适用场景:系统需要支持多个公众号,根据用户所在的公众号动态选择支付配置
+ *
+ *
+ * @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:服务商模式 - 为不同子商户创建订单.
+ *
+ * 适用场景:服务商为多个子商户提供支付服务
+ *
+ *
+ * @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:查询订单状态.
+ *
+ * 适用场景:查询不同公众号的订单支付状态
+ *
+ *
+ * @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:申请退款.
+ *
+ * 适用场景:为不同公众号的订单申请退款
+ *
+ *
+ * @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:动态管理配置.
+ *
+ * 适用场景:需要在运行时更新配置(如证书更新后需要重新加载)
+ *
+ *
+ * @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();
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md
new file mode 100644
index 0000000000..bed890d5e8
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md
@@ -0,0 +1,45 @@
+# 使用说明
+1. 在自己的Spring Boot项目里,引入maven依赖
+```xml
+
+ com.github.binarywang
+ wx-java-pay-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.yml)
+###### 1)V2版本
+```yml
+wx:
+ pay:
+ appId:
+ mchId:
+ mchKey:
+ keyPath:
+```
+###### 2)V3版本
+```yml
+wx:
+ pay:
+ appId: xxxxxxxxxxx
+ mchId: 15xxxxxxxxx #商户id
+ apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
+ apiHostUrlPath: /api-weixin # 可选:代理入口前缀
+ apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
+ certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
+ privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
+ privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
+```
+###### 3)V3服务商版本
+```yml
+wx:
+ pay: #微信服务商支付
+ configs:
+ - appId: wxe97b2x9c2b3d #spAppId
+ mchId: 16486610 #服务商商户
+ subAppId: wx118cexxe3c07679 #子appId
+ subMchId: 16496705 #子商户
+ apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr #apiV3密钥
+ privateKeyPath: classpath:cert/apiclient_key.pem #服务商证书文件,apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径(可以配置绝对路径)
+ privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
+```
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..4c1a99c0a5
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-pay-spring-boot-starter
+ WxJava - Spring Boot Starter for WxPay
+ 微信支付开发的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-pay
+ ${project.version}
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
new file mode 100644
index 0000000000..7e748ba1a3
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
@@ -0,0 +1,74 @@
+package com.binarywang.spring.starter.wxjava.pay.config;
+
+import com.binarywang.spring.starter.wxjava.pay.properties.WxPayProperties;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+/**
+ *
+ * 微信支付自动配置
+ * Created by BinaryWang on 2019/4/17.
+ *
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@EnableConfigurationProperties(WxPayProperties.class)
+@ConditionalOnClass(WxPayService.class)
+@ConditionalOnProperty(prefix = "wx.pay", value = "enabled", matchIfMissing = true)
+public class WxPayAutoConfiguration {
+ private WxPayProperties properties;
+
+ @Autowired
+ public WxPayAutoConfiguration(WxPayProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * 构造微信支付服务对象.
+ *
+ * @return 微信支付service
+ */
+ @Bean
+ @ConditionalOnMissingBean(WxPayService.class)
+ public WxPayService wxPayService() {
+ final WxPayServiceImpl wxPayService = new WxPayServiceImpl();
+ WxPayConfig payConfig = new WxPayConfig();
+ payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
+ payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
+ payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
+ payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
+ payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
+ payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
+ payConfig.setUseSandboxEnv(this.properties.isUseSandboxEnv());
+ payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl()));
+ payConfig.setRefundNotifyUrl(StringUtils.trimToNull(this.properties.getRefundNotifyUrl()));
+ //以下是apiv3以及支付分相关
+ payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
+ payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
+ payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(this.properties.getPayScorePermissionNotifyUrl()));
+ payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
+ payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
+ payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
+ payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key()));
+ payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
+ payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
+ payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
+ payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
+ payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
+ payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());
+
+ wxPayService.setConfig(payConfig);
+ return wxPayService;
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
new file mode 100644
index 0000000000..49045c4ee0
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
@@ -0,0 +1,130 @@
+package com.binarywang.spring.starter.wxjava.pay.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ *
+ * 微信支付属性配置类
+ * Created by Binary Wang on 2019/4/17.
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@ConfigurationProperties(prefix = "wx.pay")
+public class WxPayProperties {
+ /**
+ * 设置微信公众号或者小程序等的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;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.mch.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
+ /**
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
+ */
+ private boolean strictlyNeedWechatPaySerial = true;
+
+ /**
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用
+ */
+ private boolean fullPublicKeyModel = true;
+
+}
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..37fe6c20e4
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.pay.config.WxPayAutoConfiguration
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..28cbbace5f
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.pay.config.WxPayAutoConfiguration
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md
new file mode 100644
index 0000000000..34069fa1fe
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md
@@ -0,0 +1,45 @@
+# wx-java-qidian-spring-boot-starter
+
+## 快速开始
+
+1. 引入依赖
+ ```xml
+
+ com.github.binarywang
+ wx-java-qidian-spring-boot-starter
+ ${version}
+
+ ```
+2. 添加配置(application.properties)
+ ```properties
+ # 公众号配置(必填)
+ wx.qidian.appId = appId
+ wx.qidian.secret = @secret
+ wx.qidian.token = @token
+ wx.qidian.aesKey = @aesKey
+ # 存储配置redis(可选)
+ wx.qidian.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate
+ wx.qidian.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认)
+ wx.qidian.config-storage.redis.host = 127.0.0.1
+ wx.qidian.config-storage.redis.port = 6379
+ #单机和sentinel同时存在时,优先使用sentinel配置
+ #wx.qidian.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379
+ #wx.qidian.config-storage.redis.sentinel-name=mymaster
+ # http客户端配置
+ wx.qidian.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
+ wx.qidian.config-storage.http-proxy-host=
+ wx.qidian.config-storage.http-proxy-port=
+ wx.qidian.config-storage.http-proxy-username=
+ wx.qidian.config-storage.http-proxy-password=
+ # 公众号地址host配置
+ #wx.qidian.hosts.api-host=http://proxy.com/
+ #wx.qidian.hosts.open-host=http://proxy.com/
+ #wx.qidian.hosts.mp-host=http://proxy.com/
+ ```
+3. 自动注入的类型
+
+- `WxQidianService`
+- `WxQidianConfigStorage`
+
+4、参考 demo:
+https://github.com/binarywang/wx-java-mp-demo
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
new file mode 100644
index 0000000000..71851fd59d
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
@@ -0,0 +1,68 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.B
+
+ 4.0.0
+
+ wx-java-qidian-spring-boot-starter
+ WxJava - Spring Boot Starter for QiDian
+ 腾讯企点的 Spring Boot Starter
+
+
+
+ com.github.binarywang
+ weixin-java-qidian
+ ${project.version}
+
+
+ redis.clients
+ jedis
+ 4.3.2
+ compile
+
+
+ org.springframework.data
+ spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java
new file mode 100644
index 0000000000..bb66fde262
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java
@@ -0,0 +1,17 @@
+package com.binarywang.spring.starter.wxjava.qidian.config;
+
+import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * .
+ *
+ * @author someone
+ */
+@Configuration
+@EnableConfigurationProperties(WxQidianProperties.class)
+@Import({ WxQidianStorageAutoConfiguration.class, WxQidianServiceAutoConfiguration.class })
+public class WxQidianAutoConfiguration {
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java
new file mode 100644
index 0000000000..3af628d01e
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java
@@ -0,0 +1,63 @@
+package com.binarywang.spring.starter.wxjava.qidian.config;
+
+import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties;
+import me.chanjar.weixin.qidian.api.WxQidianService;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceJoddHttpImpl;
+import me.chanjar.weixin.qidian.api.impl.WxQidianServiceOkHttpImpl;
+import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 腾讯企点相关服务自动注册.
+ *
+ * @author alegria
+ */
+@Configuration
+public class WxQidianServiceAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQidianProperties wxQidianProperties) {
+ HttpClientType httpClientType = wxQidianProperties.getConfigStorage().getHttpClientType();
+ WxQidianService wxQidianService;
+ switch (httpClientType) {
+ case OkHttp:
+ wxQidianService = newWxQidianServiceOkHttpImpl();
+ break;
+ case JoddHttp:
+ wxQidianService = newWxQidianServiceJoddHttpImpl();
+ break;
+ case HttpClient:
+ wxQidianService = newWxQidianServiceHttpClientImpl();
+ break;
+ default:
+ wxQidianService = newWxQidianServiceImpl();
+ break;
+ }
+
+ wxQidianService.setWxMpConfigStorage(configStorage);
+ return wxQidianService;
+ }
+
+ private WxQidianService newWxQidianServiceImpl() {
+ return new WxQidianServiceImpl();
+ }
+
+ private WxQidianService newWxQidianServiceHttpClientImpl() {
+ return new WxQidianServiceHttpClientImpl();
+ }
+
+ private WxQidianService newWxQidianServiceOkHttpImpl() {
+ return new WxQidianServiceOkHttpImpl();
+ }
+
+ private WxQidianService newWxQidianServiceJoddHttpImpl() {
+ return new WxQidianServiceJoddHttpImpl();
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java
new file mode 100644
index 0000000000..01ba91b565
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java
@@ -0,0 +1,168 @@
+package com.binarywang.spring.starter.wxjava.qidian.config;
+
+import com.binarywang.spring.starter.wxjava.qidian.enums.StorageType;
+import com.binarywang.spring.starter.wxjava.qidian.properties.RedisProperties;
+import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties;
+import com.google.common.collect.Sets;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
+import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
+import me.chanjar.weixin.qidian.config.impl.WxQidianDefaultConfigImpl;
+import me.chanjar.weixin.qidian.config.impl.WxQidianRedisConfigImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.JedisSentinelPool;
+import redis.clients.jedis.util.Pool;
+
+import java.util.Set;
+
+/**
+ * 腾讯企点存储策略自动配置.
+ *
+ * @author alegria
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class WxQidianStorageAutoConfiguration {
+ private final ApplicationContext applicationContext;
+
+ private final WxQidianProperties wxQidianProperties;
+
+ @Value("${wx.mp.config-storage.redis.host:")
+ private String redisHost;
+
+ @Value("${wx.mp.configStorage.redis.host:")
+ private String redisHost2;
+
+ @Bean
+ @ConditionalOnMissingBean(WxQidianConfigStorage.class)
+ public WxQidianConfigStorage wxQidianConfigStorage() {
+ StorageType type = wxQidianProperties.getConfigStorage().getType();
+ WxQidianConfigStorage config;
+ switch (type) {
+ case Jedis:
+ config = jedisConfigStorage();
+ break;
+ case RedisTemplate:
+ config = redisTemplateConfigStorage();
+ break;
+ default:
+ config = defaultConfigStorage();
+ break;
+ }
+ // wx host config
+ if (null != wxQidianProperties.getHosts() && StringUtils.isNotEmpty(wxQidianProperties.getHosts().getApiHost())) {
+ WxQidianHostConfig hostConfig = new WxQidianHostConfig();
+ hostConfig.setApiHost(wxQidianProperties.getHosts().getApiHost());
+ hostConfig.setQidianHost(wxQidianProperties.getHosts().getQidianHost());
+ hostConfig.setOpenHost(wxQidianProperties.getHosts().getOpenHost());
+ config.setHostConfig(hostConfig);
+ }
+ return config;
+ }
+
+ private WxQidianConfigStorage defaultConfigStorage() {
+ WxQidianDefaultConfigImpl config = new WxQidianDefaultConfigImpl();
+ setWxMpInfo(config);
+ return config;
+ }
+
+ private WxQidianConfigStorage jedisConfigStorage() {
+ Pool jedisPool;
+ if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) {
+ jedisPool = getJedisPool();
+ } else {
+ jedisPool = applicationContext.getBean(JedisPool.class);
+ }
+ WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+ WxQidianRedisConfigImpl wxQidianRedisConfig = new WxQidianRedisConfigImpl(redisOps,
+ wxQidianProperties.getConfigStorage().getKeyPrefix());
+ setWxMpInfo(wxQidianRedisConfig);
+ return wxQidianRedisConfig;
+ }
+
+ private WxQidianConfigStorage redisTemplateConfigStorage() {
+ StringRedisTemplate redisTemplate = null;
+ try {
+ redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ try {
+ if (null == redisTemplate) {
+ redisTemplate = (StringRedisTemplate) applicationContext.getBean("stringRedisTemplate");
+ }
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+
+ if (null == redisTemplate) {
+ redisTemplate = (StringRedisTemplate) applicationContext.getBean("redisTemplate");
+ }
+
+ WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+ WxQidianRedisConfigImpl wxMpRedisConfig = new WxQidianRedisConfigImpl(redisOps,
+ wxQidianProperties.getConfigStorage().getKeyPrefix());
+
+ setWxMpInfo(wxMpRedisConfig);
+ return wxMpRedisConfig;
+ }
+
+ private void setWxMpInfo(WxQidianDefaultConfigImpl config) {
+ WxQidianProperties properties = wxQidianProperties;
+ WxQidianProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+ config.setAppId(properties.getAppId());
+ config.setSecret(properties.getSecret());
+ config.setToken(properties.getToken());
+ config.setAesKey(properties.getAesKey());
+
+ config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+ config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+ config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+ if (configStorageProperties.getHttpProxyPort() != null) {
+ config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+ }
+ }
+
+ private Pool getJedisPool() {
+ WxQidianProperties.ConfigStorage storage = wxQidianProperties.getConfigStorage();
+ RedisProperties redis = storage.getRedis();
+
+ JedisPoolConfig config = new JedisPoolConfig();
+ if (redis.getMaxActive() != null) {
+ config.setMaxTotal(redis.getMaxActive());
+ }
+ if (redis.getMaxIdle() != null) {
+ config.setMaxIdle(redis.getMaxIdle());
+ }
+ if (redis.getMaxWaitMillis() != null) {
+ config.setMaxWaitMillis(redis.getMaxWaitMillis());
+ }
+ if (redis.getMinIdle() != null) {
+ config.setMinIdle(redis.getMinIdle());
+ }
+ config.setTestOnBorrow(true);
+ config.setTestWhileIdle(true);
+ if (StringUtils.isNotEmpty(redis.getSentinelIps())) {
+
+ Set sentinels = Sets.newHashSet(redis.getSentinelIps().split(","));
+ return new JedisSentinelPool(redis.getSentinelName(), sentinels,config);
+ }
+
+ return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+ redis.getDatabase());
+ }
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java
new file mode 100644
index 0000000000..04589a911b
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.qidian.enums;
+
+/**
+ * httpclient类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum HttpClientType {
+ /**
+ * HttpClient.
+ */
+ HttpClient,
+ /**
+ * OkHttp.
+ */
+ OkHttp,
+ /**
+ * JoddHttp.
+ */
+ JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java
new file mode 100644
index 0000000000..f4e26bc156
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.qidian.enums;
+
+/**
+ * storage类型.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+public enum StorageType {
+ /**
+ * 内存.
+ */
+ Memory,
+ /**
+ * redis(JedisClient).
+ */
+ Jedis,
+ /**
+ * redis(Redisson).
+ */
+ Redisson,
+ /**
+ * redis(RedisTemplate).
+ */
+ RedisTemplate
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java
new file mode 100644
index 0000000000..92ade849fa
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java
@@ -0,0 +1,18 @@
+package com.binarywang.spring.starter.wxjava.qidian.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class HostConfig implements Serializable {
+
+ private static final long serialVersionUID = -4172767630740346001L;
+
+ private String apiHost;
+
+ private String openHost;
+
+ private String qidianHost;
+
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java
new file mode 100644
index 0000000000..abfad572e7
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java
@@ -0,0 +1,56 @@
+package com.binarywang.spring.starter.wxjava.qidian.properties;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * redis 配置属性.
+ *
+ * @author Binary Wang
+ * created on 2020-08-30
+ */
+@Data
+public class RedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java
new file mode 100644
index 0000000000..bec5dfcce0
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java
@@ -0,0 +1,99 @@
+package com.binarywang.spring.starter.wxjava.qidian.properties;
+
+import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType;
+import com.binarywang.spring.starter.wxjava.qidian.enums.StorageType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.io.Serializable;
+
+import static com.binarywang.spring.starter.wxjava.qidian.enums.StorageType.Memory;
+import static com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties.PREFIX;
+
+/**
+ * 企点接入相关配置属性.
+ *
+ * @author someone
+ */
+@Data
+@ConfigurationProperties(PREFIX)
+public class WxQidianProperties {
+ public static final String PREFIX = "wx.qidian";
+
+ /**
+ * 设置腾讯企点的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置腾讯企点的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置腾讯企点的token.
+ */
+ private String token;
+
+ /**
+ * 设置腾讯企点的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 自定义host配置
+ */
+ private HostConfig hosts;
+
+ /**
+ * 存储策略
+ */
+ private ConfigStorage configStorage = new ConfigStorage();
+
+ @Data
+ public static class ConfigStorage implements Serializable {
+ private static final long serialVersionUID = 4815731027000065434L;
+
+ /**
+ * 存储类型.
+ */
+ private StorageType type = Memory;
+
+ /**
+ * 指定key前缀.
+ */
+ private String keyPrefix = "wx";
+
+ /**
+ * redis连接配置.
+ */
+ private RedisProperties redis = new RedisProperties();
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
+
+ /**
+ * http代理主机.
+ */
+ private String httpProxyHost;
+
+ /**
+ * http代理端口.
+ */
+ private Integer httpProxyPort;
+
+ /**
+ * http代理用户名.
+ */
+ private String httpProxyUsername;
+
+ /**
+ * http代理密码.
+ */
+ private String httpProxyPassword;
+
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..bfcb7bf919
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..6e7e511448
--- /dev/null
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration
diff --git a/weixin-graal/pom.xml b/weixin-graal/pom.xml
new file mode 100644
index 0000000000..881dc911fe
--- /dev/null
+++ b/weixin-graal/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.5.B
+
+
+ weixin-graal
+ WxJava - Graal
+ 微信开发Java内部配合graal以产生native-image配置的辅助工具, 可以通过项目的 native-image Profile 来启用: mvn -P native-image ...
+
+
+
+
+
+ org.projectlombok
+ lombok
+ compile
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+ none
+
+
+
+
+
+
diff --git a/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java b/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java
new file mode 100644
index 0000000000..a983a51897
--- /dev/null
+++ b/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java
@@ -0,0 +1,177 @@
+package com.github.binarywang.wx.graal;
+
+import lombok.Data;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * 目前仅仅处理@Data,且必须在lombok自己的processor之前执行,千万注意!!!!!
+ *
+ * @author outersky
+ */
+@SupportedAnnotationTypes("lombok.Data")
+@SupportedSourceVersion(SourceVersion.RELEASE_8)
+public class GraalProcessor extends AbstractProcessor {
+ private static final String REFLECTION_CONFIG_JSON = "reflection-config.json";
+ private static final String NATIVE_IMAGE_PROPERTIES = "native-image.properties";
+
+ private final SortedSet classSet = new TreeSet<>();
+ private String shortestPackageName = null;
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Data.class))) {
+ registerClass(annotatedClass.getQualifiedName().toString());
+ handleSuperClass(annotatedClass);
+ }
+
+ //只有最后一轮才可以写文件,否则文件会被重复打开,报错!
+ if (!roundEnv.processingOver()) {
+ return false;
+ }
+
+ // 如果没有文件要写,跳过
+ if (classSet.isEmpty()) {
+ return false;
+ }
+
+ writeFiles();
+
+ //必须返回false,以便让lombok能继续处理。
+ return false;
+ }
+
+ /**
+ * 设置当前最短的package名称
+ *
+ * @param packageName 包名
+ */
+ private void setShortestPackageName(String packageName) {
+ if (shortestPackageName == null) {
+ shortestPackageName = packageName;
+ } else if (packageName.length() < shortestPackageName.length()) {
+ shortestPackageName = packageName;
+ }
+ }
+
+ /**
+ * 更加完整的类名来获取package名称
+ *
+ * @param fullClassName 完整的类名
+ * @return package name
+ */
+ private String getPackageName(String fullClassName) {
+ int last = fullClassName.lastIndexOf('.');
+ if (last == -1) {
+ return fullClassName;
+ }
+ return fullClassName.substring(0, last);
+ }
+
+ /**
+ * 保存文件
+ * META-INF/native-image/.../reflection-config.json
+ * META-INF/native-image/.../native-image.properties
+ */
+ private void writeFiles() {
+ String basePackage = shortestPackageName;
+
+ String module;
+ if (basePackage.contains(".")) {
+ final int i = basePackage.lastIndexOf('.');
+ module = basePackage.substring(i + 1);
+ basePackage = basePackage.substring(0, i);
+ } else {
+ module = basePackage;
+ }
+
+ String path = "META-INF/native-image/" + basePackage + "/" + module + "/";
+ String reflectFile = path + REFLECTION_CONFIG_JSON;
+ String propsFile = path + NATIVE_IMAGE_PROPERTIES;
+ try {
+ FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", propsFile);
+ try (Writer writer = fileObject.openWriter();) {
+ writer.append("Args = -H:ReflectionConfigurationResources=${.}/" + REFLECTION_CONFIG_JSON);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ try {
+ FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", reflectFile);
+ try (Writer writer = fileObject.openWriter();) {
+ writer.write("[\n");
+ boolean first = true;
+ for (String name : classSet) {
+ if (first) {
+ first = false;
+ } else {
+ writer.write(",");
+ }
+ writer.write(assetGraalJsonElement(name));
+ writer.append('\n');
+ }
+ writer.write("]");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ private String assetGraalJsonElement(String className) {
+ return "{\n" +
+ " \"name\" : \"" + className + "\",\n" +
+ " \"allDeclaredFields\":true,\n" +
+ " \"allDeclaredMethods\":true,\n" +
+ " \"allDeclaredConstructors\":true,\n" +
+ " \"allPublicMethods\" : true\n" +
+ "}";
+ }
+
+ /**
+ * 登记一个class
+ *
+ * @param className 完整的类名
+ */
+ private void registerClass(String className) {
+ classSet.add(className);
+ setShortestPackageName(getPackageName(className));
+ }
+
+ /**
+ * 获取一个类型的所有的父类,并登记
+ *
+ * @param typeElement 类型元素
+ */
+ private void handleSuperClass(TypeElement typeElement) {
+ TypeMirror superclass = typeElement.getSuperclass();
+ if (superclass.getKind() == TypeKind.DECLARED) {
+ TypeElement s = (TypeElement) ((DeclaredType) superclass).asElement();
+ String sName = s.toString();
+ // ignore java.**/javax.**
+ if (sName.startsWith("java.") || sName.startsWith("javax.")) {
+ return;
+ }
+ registerClass(sName);
+ handleSuperClass(s);
+ }
+ }
+
+}
diff --git a/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000000..f358b92ef9
--- /dev/null
+++ b/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+com.github.binarywang.wx.graal.GraalProcessor
diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml
new file mode 100644
index 0000000000..6b5e68a1fa
--- /dev/null
+++ b/weixin-java-aispeech/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.5.B
+
+
+ weixin-java-aispeech
+ WxJava - Aispeech Java SDK
+ 微信智能对话 Java SDK
+
+
+
+ com.github.binarywang
+ weixin-java-common
+ ${project.version}
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ org.testng
+ testng
+ test
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ src/test/resources/testng.xml
+
+
+
+
+
+
+
+
+ native-image
+
+ false
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+
+ com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor
+
+
+
+ com.github.binarywang
+ weixin-graal
+ ${project.version}
+
+
+
+
+
+
+
+
+
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
new file mode 100644
index 0000000000..51d46562cb
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.util.List;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechDialogService {
+ String getAccessToken(String appid, String account) throws WxErrorException;
+
+ String importBotJson(int mode, List data) throws WxErrorException;
+
+ String publishBot() throws WxErrorException;
+
+ PublishProgress getPublishProgress(String env) throws WxErrorException;
+
+ AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException;
+
+ DialogResult query(DialogQueryRequest request) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
new file mode 100644
index 0000000000..fa27d48235
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechKnowledgeService {
+ KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) throws WxErrorException;
+
+ List listKnowledgeByIds(List knowledgeIds) throws WxErrorException;
+
+ KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException;
+
+ KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException;
+
+ KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ boolean deleteKnowledge(String knowledgeId) throws WxErrorException;
+
+ boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException;
+
+ List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException;
+
+ String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException;
+
+ KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException;
+
+ boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException;
+
+ boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) throws WxErrorException;
+
+ String postRaw(String path, Object requestBody) throws WxErrorException;
+
+ String getRaw(String path, Map queryParams) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
new file mode 100644
index 0000000000..08ccf837e4
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.api;
+
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+
+public interface WxAispeechService {
+ WxAispeechDialogService getDialogService();
+
+ WxAispeechKnowledgeService getKnowledgeService();
+
+ WxAispeechConfigStorage getConfigStorage();
+
+ void setConfigStorage(WxAispeechConfigStorage configStorage);
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
new file mode 100644
index 0000000000..9bd53b454e
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
@@ -0,0 +1,129 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.bean.dialog.AispeechApiResponse;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechDialogServiceImpl implements WxAispeechDialogService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechDialogServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public String getAccessToken(String appid, String account) throws WxErrorException {
+ Map request = new HashMap<>();
+ if (StringUtils.isNotBlank(account)) {
+ request.put("account", account);
+ }
+
+ String response = service.executeDialogPost("/v2/token", request, false, appid);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ String token = result.getData().get("access_token").getAsString();
+ service.getConfigStorage().setOpenAiToken(token);
+ return token;
+ }
+
+ @Override
+ public String importBotJson(int mode, List data) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("mode", mode);
+ request.put("data", data);
+
+ String response = service.executeDialogPost("/v2/bot/import/json", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData().get("task_id").getAsString();
+ }
+
+ @Override
+ public String publishBot() throws WxErrorException {
+ String response = service.executeDialogPost("/v2/bot/publish", "{}", true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getRequestId();
+ }
+
+ @Override
+ public PublishProgress getPublishProgress(String env) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("env", env);
+
+ String response = service.executeDialogPost("/v2/bot/effective_progress", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("task_id", taskId);
+
+ String response = service.executeDialogPost("/v2/async/fetch", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public DialogResult query(DialogQueryRequest request) throws WxErrorException {
+ String json = WxGsonBuilder.create().toJson(request);
+ String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(json, service.getConfigStorage().getAesKey());
+ String response = service.executeDialogPost("/v2/bot/query", encrypted, true, null);
+
+ String responseJson = response;
+ if (!looksLikeJson(response)) {
+ responseJson = WxAispeechSignUtil.decryptAesCbcFromBase64(response, service.getConfigStorage().getAesKey());
+ }
+
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(responseJson, type);
+ ensureSuccess(result);
+
+ DialogResult dialogResult = result.getData();
+ if (dialogResult != null && looksLikeJson(dialogResult.getAnswer())) {
+ dialogResult.setRawAnswer(WxGsonBuilder.create().fromJson(dialogResult.getAnswer(), JsonElement.class));
+ }
+ return dialogResult;
+ }
+
+ private boolean looksLikeJson(String value) {
+ return StringUtils.isNotBlank(value) && (value.startsWith("{") || value.startsWith("["));
+ }
+
+ private void ensureSuccess(AispeechApiResponse> response) throws WxErrorException {
+ if (response == null) {
+ throw new WxErrorException("响应为空");
+ }
+ if (response.getCode() == null || response.getCode() != 0) {
+ throw new WxErrorException(WxError.builder()
+ .errorCode(response.getCode() == null ? -1 : response.getCode())
+ .errorMsg(response.getMsg())
+ .build());
+ }
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
new file mode 100644
index 0000000000..708f12890d
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
@@ -0,0 +1,184 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.io.File;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeListResult;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechKnowledgeServiceImpl implements WxAispeechKnowledgeService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechKnowledgeServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ String response = service.executeKnowledgeMultipartPost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/file",
+ file, title, description, metadata);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/url", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/manual", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge", query);
+ KnowledgeListResult result = WxGsonBuilder.create().fromJson(response, KnowledgeListResult.class);
+ return result == null ? null : result.getData();
+ }
+
+ @Override
+ public List listKnowledgeByIds(List knowledgeIds) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty()) {
+ return null;
+ }
+ StringJoiner joiner = new StringJoiner(",");
+ for (String knowledgeId : knowledgeIds) {
+ if (StringUtils.isNotBlank(knowledgeId)) {
+ joiner.add(knowledgeId);
+ }
+ }
+ if (joiner.length() == 0) {
+ return null;
+ }
+
+ Map query = new HashMap<>();
+ query.put("ids", joiner.toString());
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/batch", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/" + knowledgeId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/manual/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public boolean deleteKnowledge(String knowledgeId) throws WxErrorException {
+ service.executeKnowledgeDelete("/api/v1/knowledge/" + knowledgeId);
+ return true;
+ }
+
+ @Override
+ public boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty() || tagId == null) {
+ return false;
+ }
+
+ Map request = new HashMap<>();
+ request.put("knowledge_ids", knowledgeIds);
+ request.put("tag_id", tagId);
+ String response = service.executeKnowledgePut("/api/v1/knowledge/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("keyword", keyword);
+ query.put("knowledge_base_id", knowledgeBaseId);
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/search", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException {
+ return service.executeKnowledgePost("/api/v1/knowledge/move", request);
+ }
+
+ @Override
+ public KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/move/progress/" + taskId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeMoveProgress.class);
+ }
+
+ @Override
+ public boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags/" + tagId, request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public String postRaw(String path, Object requestBody) throws WxErrorException {
+ return service.executeKnowledgePost(path, requestBody);
+ }
+
+ @Override
+ public String getRaw(String path, Map queryParams) throws WxErrorException {
+ return service.executeKnowledgeGet(path, queryParams);
+ }
+
+ private List parseKnowledgeInfoList(String response) {
+ if (StringUtils.isBlank(response)) {
+ return null;
+ }
+
+ JsonElement element = WxGsonBuilder.create().fromJson(response, JsonElement.class);
+ Type listType = new TypeToken>() { } .getType();
+ if (element != null && element.isJsonObject()) {
+ JsonObject object = element.getAsJsonObject();
+ if (object.has("data")) {
+ return WxGsonBuilder.create().fromJson(object.get("data"), listType);
+ }
+ }
+ return WxGsonBuilder.create().fromJson(element, listType);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
new file mode 100644
index 0000000000..e37d60e352
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpClientImpl extends WxAispeechServiceHttpComponentsImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
new file mode 100644
index 0000000000..ac91d98938
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpComponentsImpl extends WxAispeechServiceImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
new file mode 100644
index 0000000000..37a657cef2
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
@@ -0,0 +1,250 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.Gson;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.api.WxAispeechService;
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder;
+import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+
+public class WxAispeechServiceImpl implements WxAispeechService {
+ private static final Gson GSON = new Gson();
+
+ private final WxAispeechDialogService dialogService = new WxAispeechDialogServiceImpl(this);
+ private final WxAispeechKnowledgeService knowledgeService = new WxAispeechKnowledgeServiceImpl(this);
+
+ private WxAispeechConfigStorage configStorage;
+ private CloseableHttpClient httpClient;
+ private HttpHost proxy;
+
+ @Override
+ public WxAispeechDialogService getDialogService() {
+ return dialogService;
+ }
+
+ @Override
+ public WxAispeechKnowledgeService getKnowledgeService() {
+ return knowledgeService;
+ }
+
+ @Override
+ public WxAispeechConfigStorage getConfigStorage() {
+ return configStorage;
+ }
+
+ @Override
+ public void setConfigStorage(WxAispeechConfigStorage configStorage) {
+ this.configStorage = configStorage;
+ this.initHttp();
+ }
+
+ protected void initHttp() {
+ HttpComponentsClientBuilder builder = configStorage.getHttpComponentsClientBuilder();
+ if (builder == null) {
+ builder = DefaultHttpComponentsClientBuilder.get();
+ }
+
+ builder.httpProxyHost(configStorage.getHttpProxyHost())
+ .httpProxyPort(configStorage.getHttpProxyPort())
+ .httpProxyUsername(configStorage.getHttpProxyUsername())
+ .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null :
+ configStorage.getHttpProxyPassword().toCharArray());
+
+ if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
+ this.proxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
+ } else {
+ this.proxy = null;
+ }
+
+ this.httpClient = builder.build();
+ }
+
+ protected String executeDialogPost(String path, Object requestBody, boolean withOpenToken, String appid)
+ throws WxErrorException {
+ String body = toBody(requestBody);
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String sign = WxAispeechSignUtil.calcDialogSign(configStorage.getToken(), timestamp, nonce, body);
+ String resolvedAppid = StringUtils.defaultIfBlank(appid, configStorage.getAppid());
+
+ HttpPost request = new HttpPost(configStorage.getDialogApiBaseUrl() + path);
+ request.setHeader("request_id", requestId);
+ request.setHeader("timestamp", String.valueOf(timestamp));
+ request.setHeader("nonce", nonce);
+ request.setHeader("sign", sign);
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ if (withOpenToken) {
+ if (StringUtils.isBlank(configStorage.getOpenAiToken())) {
+ throw new WxErrorException("X-OPENAI-TOKEN不能为空,请先调用getAccessToken或手动设置");
+ }
+ request.setHeader("X-OPENAI-TOKEN", configStorage.getOpenAiToken());
+ } else {
+ if (StringUtils.isBlank(resolvedAppid)) {
+ throw new WxErrorException("X-APPID不能为空");
+ }
+ request.setHeader("X-APPID", resolvedAppid);
+ }
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException {
+ try {
+ URIBuilder builder = new URIBuilder(configStorage.getKnowledgeApiBaseUrl() + path);
+ if (queryParams != null) {
+ for (Map.Entry entry : queryParams.entrySet()) {
+ if (entry.getValue() != null) {
+ builder.addParameter(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ HttpGet request = new HttpGet(builder.build());
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ } catch (Exception e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPut request = new HttpPut(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+ builder.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName());
+ if (StringUtils.isNotBlank(title)) {
+ builder.addTextBody("title", title, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(description)) {
+ builder.addTextBody("description", description, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(metadata)) {
+ builder.addTextBody("metadata", metadata, ContentType.APPLICATION_JSON);
+ }
+ HttpEntity entity = builder.build();
+ request.setEntity(entity);
+ if (entity.getContentType() != null) {
+ request.setHeader("Content-Type", entity.getContentType());
+ }
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeDelete(String path) throws WxErrorException {
+ HttpUriRequestBase request = new HttpUriRequestBase("DELETE", URI.create(configStorage.getKnowledgeApiBaseUrl() + path));
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ private void enrichKnowledgeHeaders(HttpUriRequestBase request, String body) throws WxErrorException {
+ if (StringUtils.isBlank(configStorage.getAppid())) {
+ throw new WxErrorException("知识助理请求需要配置appid");
+ }
+ if (StringUtils.isBlank(configStorage.getSecretKey())) {
+ throw new WxErrorException("知识助理请求需要配置secretKey");
+ }
+
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String signature = WxAispeechSignUtil.calcKnowledgeSignature(configStorage.getSecretKey(), timestamp, nonce,
+ requestId, body);
+
+ request.setHeader("X-APPID", configStorage.getAppid());
+ request.setHeader("X-Request-ID", requestId);
+ request.setHeader("X-Timestamp", String.valueOf(timestamp));
+ request.setHeader("X-Nonce", nonce);
+ request.setHeader("X-Signature", signature);
+ if (!request.containsHeader("Content-Type")) {
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ }
+ }
+
+ private String executeRequest(HttpUriRequestBase request) throws WxErrorException {
+ if (this.proxy != null) {
+ RequestConfig requestConfig = RequestConfig.custom().setProxy(this.proxy).build();
+ request.setConfig(requestConfig);
+ }
+
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ int statusCode = response.getCode();
+ HttpEntity entity = response.getEntity();
+ String body = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
+ if (statusCode >= 200 && statusCode < 300) {
+ return body;
+ }
+
+ throw new WxErrorException(WxError.builder().errorCode(statusCode).errorMsg(body).build());
+ } catch (IOException | ParseException e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected T fromJson(String json, Class clazz) {
+ return GSON.fromJson(json, clazz);
+ }
+
+ private String toBody(Object requestBody) {
+ if (requestBody == null) {
+ return "{}";
+ }
+ if (requestBody instanceof String) {
+ return (String) requestBody;
+ }
+ return GSON.toJson(requestBody);
+ }
+
+ private WxErrorException toWxErrorException(Exception e) {
+ if (e instanceof WxErrorException) {
+ return (WxErrorException) e;
+ }
+ return new WxErrorException(e);
+ }
+
+ private String randomNonce() {
+ return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
new file mode 100644
index 0000000000..24595b8b46
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class AispeechApiResponse {
+ private Integer code;
+ private String msg;
+ @SerializedName("request_id")
+ private String requestId;
+ private T data;
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public T getData() {
+ return data;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
new file mode 100644
index 0000000000..a806fb368a
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
@@ -0,0 +1,33 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class AsyncTaskResult {
+ private Integer state;
+ private String msg;
+ private Integer progress;
+ private Long start;
+ private Long end;
+ private String url;
+ private Integer totalCount;
+ private Integer successCount;
+ private Integer failCount;
+ private JsonElement successSkillInfo;
+ private List successSkillInfoList;
+
+ @Data
+ public static class SkillInfo {
+ private Long id;
+ private String name;
+ private List intents;
+ }
+
+ @Data
+ public static class IntentInfo {
+ private Long id;
+ private String name;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
new file mode 100644
index 0000000000..3927461fc8
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class BotIntent {
+ private String skill;
+ private String intent;
+ private Boolean disable;
+ private List questions;
+ private List answers;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
new file mode 100644
index 0000000000..dd748957ff
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogQueryRequest {
+ private String query;
+ private String env;
+ @SerializedName("first_priority_skills")
+ private List firstPrioritySkills;
+ @SerializedName("second_priority_skills")
+ private List secondPrioritySkills;
+ @SerializedName("user_name")
+ private String userName;
+ private String avatar;
+ private String userid;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
new file mode 100644
index 0000000000..575628dc10
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogResult {
+ private String answer;
+ @SerializedName("answer_type")
+ private String answerType;
+ @SerializedName("skill_name")
+ private String skillName;
+ @SerializedName("intent_name")
+ private String intentName;
+ @SerializedName("msg_id")
+ private String msgId;
+ private List