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/starters/wx-java-pay-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
similarity index 100%
rename from starters/wx-java-pay-starter/src/main/resources/META-INF/spring.factories
rename to spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring.factories
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/starters/wx-java-mp-starter/README.md b/starters/wx-java-mp-starter/README.md
deleted file mode 100644
index d8e1010322..0000000000
--- a/starters/wx-java-mp-starter/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# wx-java-mp-starter
-## 快速开始
-1. 引入依赖
- ```xml
-
- com.github.binarywang
- wx-java-mp-starter
- ${version}
-
- ```
-2. 添加配置(application.properties)
- ```properties
- # 公众号配置(必填)
- wx.mp.appId = @appId
- wx.mp.secret = @secret
- wx.mp.token = @token
- wx.mp.aesKey = @aesKey
- # 存储配置redis(可选)
- wx.mp.config-storage.type = redis
- wx.mp.config-storage.redis.host = 127.0.0.1
- wx.mp.config-storage.redis.port = 6379
- ```
-3. 支持自动注入的类型
-
-`WxMpService`以及相关的服务类, 比如: `wxMpService.getXxxService`。
-
-
-
-
-
-
-
diff --git a/starters/wx-java-mp-starter/pom.xml b/starters/wx-java-mp-starter/pom.xml
deleted file mode 100644
index 639662b8fe..0000000000
--- a/starters/wx-java-mp-starter/pom.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
- 4.0.0
-
- com.github.binarywang
- wx-java
- 3.4.0
- ../../
-
-
- wx-java-mp-starter
- WxJava - Spring Boot Starter for MP
- 微信公众号开发的Spring Boot Starter
-
-
- 2.1.4.RELEASE
-
-
-
-
- org.springframework.boot
- spring-boot-autoconfigure
- ${spring.boot.version}
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- ${spring.boot.version}
- true
-
-
- com.github.binarywang
- weixin-java-mp
- ${project.version}
-
-
- redis.clients
- jedis
-
-
- org.projectlombok
- lombok
- 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/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/RedisProperties.java b/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/RedisProperties.java
deleted file mode 100644
index d609cc88c1..0000000000
--- a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/RedisProperties.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp;
-
-import lombok.Data;
-
-import java.io.Serializable;
-
-/**
- * Redis配置
- */
-@Data
-public class RedisProperties implements Serializable {
-
- /**
- * 主机地址
- */
- 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 maxWaitMillis;
- private Integer minIdle;
-}
\ No newline at end of file
diff --git a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpAutoConfiguration.java b/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpAutoConfiguration.java
deleted file mode 100644
index ba85d04482..0000000000
--- a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpAutoConfiguration.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp;
-
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
-
-@Configuration
-@EnableConfigurationProperties(WxMpProperties.class)
-@Import({WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class})
-public class WxMpAutoConfiguration {
-}
diff --git a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpProperties.java b/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpProperties.java
deleted file mode 100644
index af74f430ed..0000000000
--- a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpProperties.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-import java.io.Serializable;
-
-import static com.binarywang.spring.starter.wxjava.mp.WxMpProperties.PREFIX;
-import static com.binarywang.spring.starter.wxjava.mp.WxMpProperties.StorageType.memory;
-
-
-/**
- * 微信接入相关配置属性
- */
-@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;
-
- /**
- * 存储策略, memory, redis
- */
- private ConfigStorage configStorage = new ConfigStorage();
-
-
- @Data
- public static class ConfigStorage implements Serializable {
-
- private StorageType type = memory;
-
- private RedisProperties redis = new RedisProperties();
-
- }
-
- public enum StorageType {
- memory, redis
- }
-}
diff --git a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpServiceAutoConfiguration.java b/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpServiceAutoConfiguration.java
deleted file mode 100644
index 7a6cf920c4..0000000000
--- a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpServiceAutoConfiguration.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp;
-
-import me.chanjar.weixin.mp.api.WxMpConfigStorage;
-import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * 微信公众号相关服务自动注册
- */
-@Configuration
-public class WxMpServiceAutoConfiguration {
- @Autowired
- private ApplicationContext ctx;
-
- @Bean
- @ConditionalOnMissingBean
- public WxMpService wxMpService(WxMpConfigStorage configStorage) {
- WxMpService wxMpService = new WxMpServiceImpl();
- wxMpService.setWxMpConfigStorage(configStorage);
- registerWxMpSubService(wxMpService);
- return wxMpService;
- }
-
- @ConditionalOnBean(WxMpService.class)
- public Object registerWxMpSubService(WxMpService wxMpService) {
- ConfigurableListableBeanFactory factory = (ConfigurableListableBeanFactory) ctx.getAutowireCapableBeanFactory();
- factory.registerSingleton("wxMpKefuService", wxMpService.getKefuService());
- factory.registerSingleton("wxMpMaterialService", wxMpService.getMaterialService());
- factory.registerSingleton("wxMpMenuService", wxMpService.getMenuService());
- factory.registerSingleton("wxMpUserService", wxMpService.getUserService());
- factory.registerSingleton("wxMpUserTagService", wxMpService.getUserTagService());
- factory.registerSingleton("wxMpQrcodeService", wxMpService.getQrcodeService());
- factory.registerSingleton("wxMpCardService", wxMpService.getCardService());
- factory.registerSingleton("wxMpDataCubeService", wxMpService.getDataCubeService());
- factory.registerSingleton("wxMpUserBlacklistService", wxMpService.getBlackListService());
- factory.registerSingleton("wxMpStoreService", wxMpService.getStoreService());
- factory.registerSingleton("wxMpTemplateMsgService", wxMpService.getTemplateMsgService());
- factory.registerSingleton("wxMpSubscribeMsgService", wxMpService.getSubscribeMsgService());
- factory.registerSingleton("wxMpDeviceService", wxMpService.getDeviceService());
- factory.registerSingleton("wxMpShakeService", wxMpService.getShakeService());
- factory.registerSingleton("wxMpMemberCardService", wxMpService.getMemberCardService());
- factory.registerSingleton("wxMpMassMessageService", wxMpService.getMassMessageService());
- return Boolean.TRUE;
- }
-
-}
diff --git a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpStorageAutoConfiguration.java b/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpStorageAutoConfiguration.java
deleted file mode 100644
index a0fb7eb3d5..0000000000
--- a/starters/wx-java-mp-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/WxMpStorageAutoConfiguration.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp;
-
-import me.chanjar.weixin.mp.api.WxMpConfigStorage;
-import me.chanjar.weixin.mp.api.WxMpInMemoryConfigStorage;
-import me.chanjar.weixin.mp.api.WxMpInRedisConfigStorage;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import redis.clients.jedis.JedisPool;
-import redis.clients.jedis.JedisPoolConfig;
-
-/**
- * 微信公众号存储策略自动配置
- */
-@Configuration
-public class WxMpStorageAutoConfiguration {
-
- @Autowired
- private WxMpProperties properties;
-
- @Autowired(required = false)
- private JedisPool jedisPool;
-
- @Bean
- @ConditionalOnMissingBean(WxMpConfigStorage.class)
- public WxMpConfigStorage wxMpInMemoryConfigStorage() {
- WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
- WxMpProperties.StorageType type = storage.getType();
-
- if (type == WxMpProperties.StorageType.redis) {
- return getWxMpInRedisConfigStorage();
- }
- return getWxMpInMemoryConfigStorage();
- }
-
- private WxMpInMemoryConfigStorage getWxMpInMemoryConfigStorage() {
- WxMpInMemoryConfigStorage config = new WxMpInMemoryConfigStorage();
- setWxMpInfo(config);
- return config;
- }
-
- private WxMpInRedisConfigStorage getWxMpInRedisConfigStorage() {
- JedisPool poolToUse = jedisPool;
- if (poolToUse == null) {
- poolToUse = getJedisPool();
- }
- WxMpInRedisConfigStorage config = new WxMpInRedisConfigStorage(poolToUse);
- setWxMpInfo(config);
- return config;
- }
-
- private void setWxMpInfo(WxMpInMemoryConfigStorage config) {
- config.setAppId(properties.getAppId());
- config.setSecret(properties.getSecret());
- config.setToken(properties.getToken());
- config.setAesKey(properties.getAesKey());
- }
-
- private JedisPool getJedisPool() {
- 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);
-
- JedisPool pool = new JedisPool(config, redis.getHost(), redis.getPort(),
- redis.getTimeout(), redis.getPassword(), redis.getDatabase());
- return pool;
- }
-}
diff --git a/starters/wx-java-mp-starter/src/main/resources/META-INF/spring.factories b/starters/wx-java-mp-starter/src/main/resources/META-INF/spring.factories
deleted file mode 100644
index aaa17f7676..0000000000
--- a/starters/wx-java-mp-starter/src/main/resources/META-INF/spring.factories
+++ /dev/null
@@ -1 +0,0 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.mp.WxMpAutoConfiguration
\ No newline at end of file
diff --git a/starters/wx-java-pay-starter/README.md b/starters/wx-java-pay-starter/README.md
deleted file mode 100644
index c987216147..0000000000
--- a/starters/wx-java-pay-starter/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# 使用说明
-1. 在自己的Spring Boot项目里,引入maven依赖
-```xml
-
- com.github.binarywang
- wx-java-pay-starter
- ${version}
-
- ```
-2. 添加配置(application.yml)
-```yml
-wx:
- pay:
- appId: wx5b69c56ac01ed858
- mchId: 1462547202
- mchKey: OGL9fvig9y2HrXrQ86tM4jTwyv4ja6G5
- subAppId:
- subMchId:
- keyPath:
-```
-
-
-
-
-
-
-
diff --git a/starters/wx-java-pay-starter/pom.xml b/starters/wx-java-pay-starter/pom.xml
deleted file mode 100644
index eba398b454..0000000000
--- a/starters/wx-java-pay-starter/pom.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
- wx-java
- com.github.binarywang
- 3.4.0
- ../../
-
- 4.0.0
-
- wx-java-pay-starter
- WxJava - Spring Boot Starter for WxPay
- 微信支付开发的Spring Boot Starter
-
-
- 2.1.4.RELEASE
-
-
-
-
- org.springframework.boot
- spring-boot-autoconfigure
- ${spring.boot.version}
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- ${spring.boot.version}
- true
-
-
- org.projectlombok
- lombok
- provided
-
-
- 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/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
deleted file mode 100644
index 43b2114e6e..0000000000
--- a/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
+++ /dev/null
@@ -1,57 +0,0 @@
-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()));
-
- wxPayService.setConfig(payConfig);
- return wxPayService;
- }
-
-}
diff --git a/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
deleted file mode 100644
index fe8a215650..0000000000
--- a/starters/wx-java-pay-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
+++ /dev/null
@@ -1,46 +0,0 @@
-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;
-}
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 options;
+ private String status;
+ private List slots;
+ private JsonElement rawAnswer;
+
+ public String getAnswer() {
+ return answer;
+ }
+
+ public void setRawAnswer(JsonElement rawAnswer) {
+ this.rawAnswer = rawAnswer;
+ }
+
+ @Data
+ public static class Option {
+ @SerializedName("ans_node_name")
+ private String ansNodeName;
+ private String title;
+ private String answer;
+ private Float confidence;
+ }
+
+ @Data
+ public static class SlotDetail {
+ private String name;
+ private String value;
+ private String norm;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/PublishProgress.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/PublishProgress.java
new file mode 100644
index 0000000000..1e7a649f64
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/PublishProgress.java
@@ -0,0 +1,12 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class PublishProgress {
+ @SerializedName("end_time")
+ private String endTime;
+ private Integer progress;
+ private Integer status;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeInfo.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeInfo.java
new file mode 100644
index 0000000000..9b5d333766
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeInfo.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+import java.util.Map;
+import lombok.Data;
+
+@Data
+public class KnowledgeInfo {
+ private String id;
+ @SerializedName("tenant_id")
+ private Long tenantId;
+ @SerializedName("knowledge_base_id")
+ private String knowledgeBaseId;
+ private String type;
+ private String title;
+ private String description;
+ private String source;
+ @SerializedName("parse_status")
+ private String parseStatus;
+ @SerializedName("summary_status")
+ private String summaryStatus;
+ @SerializedName("enable_status")
+ private String enableStatus;
+ private Map metadata;
+ @SerializedName("created_at")
+ private String createdAt;
+ @SerializedName("updated_at")
+ private String updatedAt;
+ private JsonObject raw;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeListResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeListResult.java
new file mode 100644
index 0000000000..16757c92e2
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeListResult.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class KnowledgeListResult {
+ private List data;
+ private Integer page;
+ @SerializedName("page_size")
+ private Integer pageSize;
+ private Integer total;
+ private Boolean success;
+
+ public List getData() {
+ return data;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeManualCreateRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeManualCreateRequest.java
new file mode 100644
index 0000000000..ab1b7671c1
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeManualCreateRequest.java
@@ -0,0 +1,11 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import lombok.Data;
+
+@Data
+public class KnowledgeManualCreateRequest {
+ private String content;
+ private String title;
+ private String description;
+ private String status;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveProgress.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveProgress.java
new file mode 100644
index 0000000000..6087377d8c
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveProgress.java
@@ -0,0 +1,16 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class KnowledgeMoveProgress {
+ @SerializedName("task_id")
+ private String taskId;
+ private String status;
+ private Double progress;
+ private Integer total;
+ private Integer processed;
+ private String message;
+ private String error;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveRequest.java
new file mode 100644
index 0000000000..1590dfd4d0
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveRequest.java
@@ -0,0 +1,16 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class KnowledgeMoveRequest {
+ @SerializedName("knowledge_ids")
+ private List knowledgeIds;
+ @SerializedName("source_kb_id")
+ private String sourceKnowledgeBaseId;
+ @SerializedName("target_kb_id")
+ private String targetKnowledgeBaseId;
+ private String mode;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeTagRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeTagRequest.java
new file mode 100644
index 0000000000..c8be4520a5
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeTagRequest.java
@@ -0,0 +1,12 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class KnowledgeTagRequest {
+ private String name;
+ private String color;
+ @SerializedName("sort_order")
+ private Integer sortOrder;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUpdateRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUpdateRequest.java
new file mode 100644
index 0000000000..bb5e98c46f
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUpdateRequest.java
@@ -0,0 +1,12 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class KnowledgeUpdateRequest {
+ private String title;
+ private String description;
+ @SerializedName("enable_status")
+ private String enableStatus;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUrlCreateRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUrlCreateRequest.java
new file mode 100644
index 0000000000..690f20e5c6
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUrlCreateRequest.java
@@ -0,0 +1,10 @@
+package me.chanjar.weixin.aispeech.bean.knowledge;
+
+import lombok.Data;
+
+@Data
+public class KnowledgeUrlCreateRequest {
+ private String url;
+ private String title;
+ private String description;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/WxAispeechConfigStorage.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/WxAispeechConfigStorage.java
new file mode 100644
index 0000000000..6706d6643d
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/WxAispeechConfigStorage.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.aispeech.config;
+
+import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder;
+
+public interface WxAispeechConfigStorage {
+ String getAppid();
+
+ String getToken();
+
+ String getAesKey();
+
+ String getOpenAiToken();
+
+ void setOpenAiToken(String openAiToken);
+
+ String getSecretKey();
+
+ String getDialogApiBaseUrl();
+
+ String getKnowledgeApiBaseUrl();
+
+ String getHttpProxyHost();
+
+ int getHttpProxyPort();
+
+ String getHttpProxyUsername();
+
+ String getHttpProxyPassword();
+
+ HttpComponentsClientBuilder getHttpComponentsClientBuilder();
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/impl/WxAispeechDefaultConfigImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/impl/WxAispeechDefaultConfigImpl.java
new file mode 100644
index 0000000000..60634d452b
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/impl/WxAispeechDefaultConfigImpl.java
@@ -0,0 +1,88 @@
+package me.chanjar.weixin.aispeech.config.impl;
+
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder;
+
+@Getter
+@Setter
+public class WxAispeechDefaultConfigImpl implements WxAispeechConfigStorage {
+ private String appid;
+ private String token;
+ private String aesKey;
+ private String openAiToken;
+ private String secretKey;
+ private String dialogApiBaseUrl = "https://openaiapi.weixin.qq.com";
+ private String knowledgeApiBaseUrl = "https://weknora.weixin.qq.com";
+ private String httpProxyHost;
+ private int httpProxyPort;
+ private String httpProxyUsername;
+ private String httpProxyPassword;
+ private HttpComponentsClientBuilder httpComponentsClientBuilder;
+
+ @Override
+ public String getAppid() {
+ return appid;
+ }
+
+ @Override
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ public String getAesKey() {
+ return aesKey;
+ }
+
+ @Override
+ public String getOpenAiToken() {
+ return openAiToken;
+ }
+
+ @Override
+ public void setOpenAiToken(String openAiToken) {
+ this.openAiToken = openAiToken;
+ }
+
+ @Override
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ @Override
+ public String getDialogApiBaseUrl() {
+ return dialogApiBaseUrl;
+ }
+
+ @Override
+ public String getKnowledgeApiBaseUrl() {
+ return knowledgeApiBaseUrl;
+ }
+
+ @Override
+ public String getHttpProxyHost() {
+ return httpProxyHost;
+ }
+
+ @Override
+ public int getHttpProxyPort() {
+ return httpProxyPort;
+ }
+
+ @Override
+ public String getHttpProxyUsername() {
+ return httpProxyUsername;
+ }
+
+ @Override
+ public String getHttpProxyPassword() {
+ return httpProxyPassword;
+ }
+
+ @Override
+ public HttpComponentsClientBuilder getHttpComponentsClientBuilder() {
+ return httpComponentsClientBuilder;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtil.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtil.java
new file mode 100644
index 0000000000..273d865e1e
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtil.java
@@ -0,0 +1,74 @@
+package me.chanjar.weixin.aispeech.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+public final class WxAispeechSignUtil {
+ private WxAispeechSignUtil() {
+ }
+
+ public static String calcDialogSign(String token, long timestamp, String nonce, String body) {
+ String bodyMd5 = DigestUtils.md5Hex(defaultString(body));
+ return DigestUtils.md5Hex(defaultString(token) + timestamp + defaultString(nonce) + bodyMd5);
+ }
+
+ public static String calcKnowledgeSignature(String secretKey, long timestamp, String nonce, String requestId,
+ String requestBody) {
+ String payload = timestamp + "\n" + defaultString(nonce) + "\n" + defaultString(requestId) + "\n"
+ + defaultString(requestBody);
+ try {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(defaultString(secretKey).getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+ return bytesToHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("HmacSHA256 signature failed", e);
+ }
+ }
+
+ public static String encryptAesCbcToBase64(String plainText, String aesKey) {
+ try {
+ byte[] keyBytes = decodeAesKey(aesKey);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(Arrays.copyOf(keyBytes, 16)));
+ byte[] encrypted = cipher.doFinal(defaultString(plainText).getBytes(StandardCharsets.UTF_8));
+ return Base64.encodeBase64String(encrypted);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("AES CBC encrypt failed", e);
+ }
+ }
+
+ public static String decryptAesCbcFromBase64(String cipherTextBase64, String aesKey) {
+ try {
+ byte[] keyBytes = decodeAesKey(aesKey);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(Arrays.copyOf(keyBytes, 16)));
+ byte[] encrypted = Base64.decodeBase64(defaultString(cipherTextBase64));
+ return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("AES CBC decrypt failed", e);
+ }
+ }
+
+ private static byte[] decodeAesKey(String aesKey) {
+ return Base64.decodeBase64(defaultString(aesKey) + "=");
+ }
+
+ private static String defaultString(String value) {
+ return value == null ? "" : value;
+ }
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder builder = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) {
+ builder.append(String.format("%02x", b));
+ }
+ return builder.toString();
+ }
+}
diff --git a/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImplTest.java b/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImplTest.java
new file mode 100644
index 0000000000..eb1bf6a5a4
--- /dev/null
+++ b/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImplTest.java
@@ -0,0 +1,134 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+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.common.error.WxErrorException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class WxAispeechKnowledgeServiceImplTest {
+
+ @Test
+ public void testCreateKnowledgeByFile() throws IOException, WxErrorException {
+ MockWxAispeechService service = new MockWxAispeechService();
+ service.putResponse("POST_MULTIPART:/api/v1/knowledge-bases/kb1/knowledge/file", "{\"id\":\"k1\"}");
+ WxAispeechKnowledgeServiceImpl knowledgeService = new WxAispeechKnowledgeServiceImpl(service);
+
+ File file = File.createTempFile("wxjava-aispeech", ".txt");
+ file.deleteOnExit();
+ KnowledgeInfo result = knowledgeService.createKnowledgeByFile("kb1", file, "标题", "描述", "{\"source\":\"web\"}");
+
+ Assert.assertEquals(result.getId(), "k1");
+ Assert.assertEquals(service.lastPath, "/api/v1/knowledge-bases/kb1/knowledge/file");
+ Assert.assertEquals(service.lastTitle, "标题");
+ Assert.assertEquals(service.lastDescription, "描述");
+ Assert.assertEquals(service.lastMetadata, "{\"source\":\"web\"}");
+ Assert.assertEquals(service.lastFile, file);
+ }
+
+ @Test
+ public void testListKnowledgeByIdsAndMoveProgress() throws WxErrorException {
+ MockWxAispeechService service = new MockWxAispeechService();
+ service.putResponse("GET:/api/v1/knowledge/batch", "{\"data\":[{\"id\":\"k1\"},{\"id\":\"k2\"}]}");
+ service.putResponse("GET:/api/v1/knowledge/move/progress/task-1", "{\"task_id\":\"task-1\",\"status\":\"processing\",\"progress\":35.5}");
+ WxAispeechKnowledgeServiceImpl knowledgeService = new WxAispeechKnowledgeServiceImpl(service);
+
+ List result = knowledgeService.listKnowledgeByIds(Arrays.asList("k1", "k2"));
+ KnowledgeMoveProgress progress = knowledgeService.getMoveProgress("task-1");
+
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertEquals(result.get(0).getId(), "k1");
+ Assert.assertEquals(service.lastQueryParams.get("ids"), "k1,k2");
+ Assert.assertEquals(progress.getTaskId(), "task-1");
+ Assert.assertEquals(progress.getStatus(), "processing");
+ }
+
+ @Test
+ public void testUpdateManualMoveAndTagApis() throws WxErrorException {
+ MockWxAispeechService service = new MockWxAispeechService();
+ service.putResponse("PUT:/api/v1/knowledge/manual/k1", "{\"id\":\"k1\"}");
+ service.putResponse("POST:/api/v1/knowledge/move", "{\"task_id\":\"task-2\"}");
+ service.putResponse("PUT:/api/v1/knowledge/tags", "{\"success\":true}");
+ service.putResponse("POST:/api/v1/knowledge-bases/kb1/tags", "{\"id\":\"t1\"}");
+ service.putResponse("PUT:/api/v1/knowledge-bases/kb1/tags/t1", "{\"id\":\"t1\"}");
+ WxAispeechKnowledgeServiceImpl knowledgeService = new WxAispeechKnowledgeServiceImpl(service);
+
+ KnowledgeManualCreateRequest manualRequest = new KnowledgeManualCreateRequest();
+ manualRequest.setContent("# 内容");
+ KnowledgeInfo updated = knowledgeService.updateManualKnowledge("k1", manualRequest);
+
+ KnowledgeMoveRequest moveRequest = new KnowledgeMoveRequest();
+ moveRequest.setSourceKnowledgeBaseId("kb1");
+ moveRequest.setTargetKnowledgeBaseId("kb2");
+ moveRequest.setKnowledgeIds(Arrays.asList("k1"));
+ moveRequest.setMode("reuse_vectors");
+ String moveResult = knowledgeService.moveKnowledge(moveRequest);
+
+ boolean updateTagsResult = knowledgeService.updateKnowledgeTags(Arrays.asList("k1"), 1001L);
+ KnowledgeTagRequest tagRequest = new KnowledgeTagRequest();
+ tagRequest.setName("FAQ");
+ boolean createTagResult = knowledgeService.createKnowledgeBaseTag("kb1", tagRequest);
+ boolean updateTagResult = knowledgeService.updateKnowledgeBaseTag("kb1", "t1", tagRequest);
+
+ Assert.assertEquals(updated.getId(), "k1");
+ Assert.assertTrue(moveResult.contains("task-2"));
+ Assert.assertTrue(updateTagsResult);
+ Assert.assertTrue(createTagResult);
+ Assert.assertTrue(updateTagResult);
+ Assert.assertFalse(knowledgeService.updateKnowledgeTags(null, 1001L));
+ Assert.assertFalse(knowledgeService.updateKnowledgeTags(Arrays.asList("k1"), null));
+ }
+
+ private static class MockWxAispeechService extends WxAispeechServiceImpl {
+ private final Map responses = new HashMap<>();
+ private String lastPath;
+ private Map lastQueryParams;
+ private File lastFile;
+ private String lastTitle;
+ private String lastDescription;
+ private String lastMetadata;
+
+ void putResponse(String key, String response) {
+ this.responses.put(key, response);
+ }
+
+ @Override
+ protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException {
+ this.lastPath = path;
+ this.lastQueryParams = queryParams;
+ return responses.get("GET:" + path);
+ }
+
+ @Override
+ protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException {
+ this.lastPath = path;
+ return responses.get("POST:" + path);
+ }
+
+ @Override
+ protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException {
+ this.lastPath = path;
+ return responses.get("PUT:" + path);
+ }
+
+ @Override
+ protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ this.lastPath = path;
+ this.lastFile = file;
+ this.lastTitle = title;
+ this.lastDescription = description;
+ this.lastMetadata = metadata;
+ return responses.get("POST_MULTIPART:" + path);
+ }
+ }
+}
diff --git a/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtilTest.java b/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtilTest.java
new file mode 100644
index 0000000000..bee6d497b5
--- /dev/null
+++ b/weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtilTest.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.aispeech.util;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class WxAispeechSignUtilTest {
+
+ @Test
+ public void testCalcDialogSign() {
+ String sign = WxAispeechSignUtil.calcDialogSign("token123", 1711520394L, "abcdefghijklmn", "{\"env\":\"online\"}");
+ Assert.assertEquals(sign, "db3f57ece7f56fef3ac512f97ef1f624");
+ }
+
+ @Test
+ public void testCalcKnowledgeSignature() {
+ String signature = WxAispeechSignUtil.calcKnowledgeSignature("secret-key", 1677652288L,
+ "nonce-abc", "request-1", "{\"a\":1}");
+ Assert.assertEquals(signature, "bf31b89ef3008e1ef91f7057d8819d4c8d5f9e435f5657097cb8f7fbf69d4e73");
+ }
+
+ @Test
+ public void testAesEncryptAndDecrypt() {
+ String aesKey = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz";
+ String source = "{\"query\":\"你好\"}";
+
+ String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(source, aesKey);
+ String decrypted = WxAispeechSignUtil.decryptAesCbcFromBase64(encrypted, aesKey);
+
+ Assert.assertEquals(decrypted, source);
+ }
+}
diff --git a/weixin-java-aispeech/src/test/resources/testng.xml b/weixin-java-aispeech/src/test/resources/testng.xml
new file mode 100644
index 0000000000..74379872ed
--- /dev/null
+++ b/weixin-java-aispeech/src/test/resources/testng.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/weixin-java-channel/pom.xml b/weixin-java-channel/pom.xml
new file mode 100644
index 0000000000..86d398b173
--- /dev/null
+++ b/weixin-java-channel/pom.xml
@@ -0,0 +1,171 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.5.B
+
+
+ weixin-java-channel
+ WxJava - Channel Java SDK
+ 微信视频号/微信小店 Java SDK
+
+
+ 2.18.8
+
+
+
+
+ com.github.binarywang
+ weixin-java-common
+ ${project.version}
+
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpmime
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ ${jackson.version}
+ true
+
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+ org.projectlombok
+ lombok
+
+
+ org.redisson
+ redisson
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+ org.testng
+ testng
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ com.google.inject
+ guice
+ test
+
+
+ org.eclipse.jetty
+ jetty-server
+ test
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ test
+
+
+ org.assertj
+ assertj-guava
+ test
+
+
+ redis.clients
+ jedis
+
+
+
+ com.github.jedis-lock
+ jedis-lock
+ true
+
+
+
+
+
+
+
+ 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-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelMessageService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelMessageService.java
new file mode 100644
index 0000000000..a908da9479
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelMessageService.java
@@ -0,0 +1,540 @@
+package me.chanjar.weixin.channel.api;
+
+import java.util.Map;
+import me.chanjar.weixin.channel.bean.message.after.AfterSaleMessage;
+import me.chanjar.weixin.channel.bean.message.after.ComplaintMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.CouponActionMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.CouponReceiveMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.UserCouponExpireMessage;
+import me.chanjar.weixin.channel.bean.message.fund.AccountNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.fund.QrNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.fund.WithdrawNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderCancelMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderConfirmMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderDeliveryMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderExtMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderIdMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderPayMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderSettleMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderStatusMessage;
+import me.chanjar.weixin.channel.bean.message.product.BrandMessage;
+import me.chanjar.weixin.channel.bean.message.product.CategoryAuditMessage;
+import me.chanjar.weixin.channel.bean.message.product.SpuAuditMessage;
+import me.chanjar.weixin.channel.bean.message.product.SpuStockMessage;
+import me.chanjar.weixin.channel.bean.message.store.CloseStoreMessage;
+import me.chanjar.weixin.channel.bean.message.store.NicknameUpdateMessage;
+import me.chanjar.weixin.channel.bean.message.supplier.SupplierItemMessage;
+import me.chanjar.weixin.channel.bean.message.vip.ExchangeInfoMessage;
+import me.chanjar.weixin.channel.bean.message.vip.UserInfoMessage;
+import me.chanjar.weixin.channel.bean.message.voucher.VoucherMessage;
+import me.chanjar.weixin.channel.message.WxChannelMessage;
+import me.chanjar.weixin.channel.message.WxChannelMessageRouterRule;
+import me.chanjar.weixin.common.session.WxSessionManager;
+
+/**
+ * @author Zeyes
+ */
+public interface BaseWxChannelMessageService {
+
+ /**
+ * 路由微信消息
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param service 服务实例
+ * @return Object
+ */
+ Object route(final WxChannelMessage message, final String content, final String appId,
+ final WxChannelService service);
+
+ /**
+ * 添加一条规则进入路由器
+ *
+ * @param rule 规则
+ */
+ void addRule(WxChannelMessageRouterRule extends WxChannelMessage> rule);
+
+ /**
+ * 订单下单
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderNew(final OrderIdMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单取消
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderCancel(OrderCancelMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单支付成功
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderPay(OrderPayMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 订单待发货
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderWaitShipping(OrderIdMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 订单发货
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderDelivery(OrderDeliveryMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单确认收货
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderConfirm(OrderConfirmMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单结算成功
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderSettle(OrderSettleMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单其他信息更新
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderExtInfoUpdate(OrderExtMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 订单状态更新
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void orderStatusUpdate(OrderStatusMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 商品审核结果
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void spuAudit(SpuAuditMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 商品系统下架通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void spuStatusUpdate(SpuAuditMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 商品更新通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void spuUpdate(SpuAuditMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 商品库存不足通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void stockNoEnough(SpuStockMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 类目审核结果
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void categoryAudit(CategoryAuditMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 品牌更新
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void brandUpdate(BrandMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 售后单状态更新
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void afterSaleStatusUpdate(AfterSaleMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 纠纷回调
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void complaintNotify(ComplaintMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户领券通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponReceive(CouponReceiveMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 创建优惠券通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponCreate(CouponActionMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 优惠券删除通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponDelete(CouponActionMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 优惠券过期通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponExpire(CouponActionMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 更新优惠券信息通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponUpdate(CouponActionMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 优惠券作废通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void couponInvalid(CouponActionMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户优惠券过期通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void userCouponExpire(UserCouponExpireMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户优惠券使用通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void userCouponUse(UserCouponExpireMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户优惠券返还通知
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void userCouponUnuse(UserCouponExpireMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 发放团购优惠成功回调
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void voucherSendSucc(VoucherMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+ /**
+ * 结算账户变更回调
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void accountNotify(AccountNotifyMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 提现回调
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void withdrawNotify(WithdrawNotifyMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 提现二维码回调
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void qrNotify(QrNotifyMessage message, final String content, final String appId, final Map context,
+ final WxSessionManager sessionManager);
+
+ /**
+ * 团长商品变更
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void supplierItemUpdate(SupplierItemMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+
+ /**
+ * 用户加入会员.
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ public void vipJoin(UserInfoMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户注销会员.
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ void vipClose(UserInfoMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户等级更新.
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ void vipGradeUpdate(UserInfoMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户积分更新.
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ void vipScoreUpdate(UserInfoMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 用户积分兑换
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ void vipScoreExchange(ExchangeInfoMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 小店注销
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void closeStore(CloseStoreMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+
+ /**
+ * 小店修改名称
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ */
+ void updateNickname(NicknameUpdateMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+ /**
+ * 默认消息处理
+ *
+ * @param message 消息
+ * @param content 消息原始内容
+ * @param appId appId
+ * @param context 上下文
+ * @param sessionManager session管理器
+ * @return Object
+ */
+ Object defaultMessageHandler(WxChannelMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+
+
+ /**
+ * 分享员变更
+ *
+ * @param message the message
+ * @param content the content
+ * @param appId the app id
+ * @param context the context
+ * @param sessionManager the session manager
+ */
+ void sharerChange(WxChannelMessage message, final String content, final String appId,
+ final Map context, final WxSessionManager sessionManager);
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelService.java
new file mode 100644
index 0000000000..07278da7ef
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/BaseWxChannelService.java
@@ -0,0 +1,135 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.config.WxChannelConfig;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.service.WxService;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+
+/**
+ * The interface Wx Channel service
+ *
+ * @author Zeyes
+ */
+public interface BaseWxChannelService extends WxService {
+
+ /**
+ *
+ * 验证消息的确来自微信服务器.
+ * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
+ *
+ *
+ * @param timestamp the timestamp
+ * @param nonce the nonce
+ * @param signature the signature
+ * @return the boolean
+ */
+ boolean checkSignature(String timestamp, String nonce, String signature);
+
+ /**
+ * 获取access_token, 不强制刷新access_token.
+ *
+ * @return the access token
+ *
+ * @throws WxErrorException the wx error exception
+ * @see #getAccessToken(boolean) #getAccessToken(boolean)
+ */
+ String getAccessToken() throws WxErrorException;
+
+ /**
+ *
+ * 获取access_token,本方法线程安全.
+ * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+ * 使用【稳定版接口】获取access_token时,限制【20次/日】,连续使用该模式时,请保证调用时间隔至少为30s,否则不会刷新
+ *
+ * 程序员在非必要情况下尽量不要主动调用此方法
+ *
+ * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
+ *
+ *
+ * @param forceRefresh 强制刷新
+ * @return the access token
+ *
+ * @throws WxErrorException the wx error exception
+ */
+ String getAccessToken(boolean forceRefresh) throws WxErrorException;
+
+ /**
+ *
+ * Service没有实现某个API的时候,可以用这个,
+ * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+ * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+ *
+ *
+ * @param .
+ * @param .
+ * @param executor 执行器
+ * @param uri 接口请求地址
+ * @param data 参数或请求数据
+ * @return . t
+ *
+ * @throws WxErrorException the wx error exception
+ */
+ T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
+
+ /**
+ * 执行器
+ *
+ * @param .
+ * @param .
+ * @param executor 执行器
+ * @param uri 接口请求地址
+ * @param data 参数或请求数据
+ * @return T
+ *
+ * @throws WxErrorException the wx error exception
+ */
+ T executeWithoutLog(RequestExecutor executor, String uri, E data) throws WxErrorException;
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
+ * 默认:1000ms
+ *
+ *
+ * @param retrySleepMillis 重试等待毫秒数
+ */
+ void setRetrySleepMillis(int retrySleepMillis);
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,最大重试次数.
+ * 默认:5次
+ *
+ *
+ * @param maxRetryTimes 最大重试次数
+ */
+ void setMaxRetryTimes(int maxRetryTimes);
+
+ /**
+ * WxChannelConfig对象
+ *
+ * @return WxMaConfig wx channel config
+ */
+ WxChannelConfig getConfig();
+
+ /**
+ * 注入 {@link WxChannelConfig} 的实现.
+ *
+ * @param config config
+ */
+ void setConfig(WxChannelConfig config);
+
+ /**
+ * 初始化http请求对象.
+ */
+ void initHttp();
+
+ /**
+ * 请求http请求相关信息.
+ *
+ * @return . request http
+ */
+ RequestHttp, ?> getRequestHttp();
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxAssistantService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxAssistantService.java
new file mode 100644
index 0000000000..7adaf30a3e
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxAssistantService.java
@@ -0,0 +1,55 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.window.request.AddWindowProductRequest;
+import me.chanjar.weixin.channel.bean.window.request.GetWindowProductListRequest;
+import me.chanjar.weixin.channel.bean.window.request.WindowProductRequest;
+import me.chanjar.weixin.channel.bean.window.response.GetWindowProductListResponse;
+import me.chanjar.weixin.channel.bean.window.response.GetWindowProductResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号助手 橱窗管理服务
+ * 关于橱窗商品ID的说明:
+ * 不支持带货中心来源的商品,其余商品的橱窗商品ID与商品来源处的平台内部商品ID相同,对应关系如下
+ *
+ * 商品来源 橱窗ID说明
+ * 视频号小店 视频号小店商品的 product_id 字段
+ * 交易组件 组件商品的 product_id 字段
+ *
+ *
+ * @author imyzt
+ */
+public interface WxAssistantService {
+
+ /**
+ * 上架商品到橱窗
+ * @param req 商品信息
+ * @return 操作结果
+ */
+ WxChannelBaseResponse addWindowProduct(AddWindowProductRequest req) throws WxErrorException;
+
+ /**
+ * 获取橱窗商品详情
+ *
+ * @param req 商品信息
+ * @return 橱窗商品详情
+ */
+ GetWindowProductResponse getWindowProduct(WindowProductRequest req) throws WxErrorException;
+
+ /**
+ * 获取已添加到橱窗的商品列表
+ * 接口限制了 page_size × page_index ≤ 10000。命中限制时建议改用传last_buffer顺序翻页的请求方式
+ * @param req 商品信息
+ * @return 已添加到橱窗的商品列表
+ */
+ GetWindowProductListResponse getWindowProductList(GetWindowProductListRequest req) throws WxErrorException;
+
+ /**
+ * 下架橱窗商品
+ * @param req 商品信息
+ * @return 操作结果
+ */
+ WxChannelBaseResponse offWindowProduct(WindowProductRequest req) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAddressService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAddressService.java
new file mode 100644
index 0000000000..063dd53948
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAddressService.java
@@ -0,0 +1,68 @@
+package me.chanjar.weixin.channel.api;
+
+
+import me.chanjar.weixin.channel.bean.address.AddressDetail;
+import me.chanjar.weixin.channel.bean.address.AddressIdResponse;
+import me.chanjar.weixin.channel.bean.address.AddressInfoResponse;
+import me.chanjar.weixin.channel.bean.address.AddressListResponse;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 地址管理服务
+ *
+ * @author Zeyes
+ */
+public interface WxChannelAddressService {
+
+ /**
+ * 获取地址列表
+ *
+ * @param offset 起始位置
+ * @param limit 拉取个数
+ * @return 列表
+ *
+ * @throws WxErrorException 异常
+ */
+ AddressListResponse listAddress(Integer offset, Integer limit) throws WxErrorException;
+
+ /**
+ * 获取地址详情
+ *
+ * @param addressId 地址id
+ * @return 地址详情
+ *
+ * @throws WxErrorException 异常
+ */
+ AddressInfoResponse getAddress(String addressId) throws WxErrorException;
+
+ /**
+ * 添加地址
+ *
+ * @param addressDetail 地址
+ * @return AddressIdResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ AddressIdResponse addAddress(AddressDetail addressDetail) throws WxErrorException;
+
+ /**
+ * 更新地址
+ *
+ * @param addressDetail 地址
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateAddress(AddressDetail addressDetail) throws WxErrorException;
+
+ /**
+ * 删除地址
+ *
+ * @param addressId 地址id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteAddress(String addressId) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
new file mode 100644
index 0000000000..b8d1156b66
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
@@ -0,0 +1,202 @@
+package me.chanjar.weixin.channel.api;
+
+
+import java.util.List;
+
+import me.chanjar.weixin.channel.bean.after.*;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.complaint.ComplaintOrderResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 售后服务接口
+ *
+ * @author Zeyes
+ */
+public interface WxChannelAfterSaleService {
+
+ /**
+ * 获取售后单列表
+ *
+ * @param beginCreateTime 订单创建启始时间 unix时间戳
+ * @param endCreateTime 订单创建结束时间,end_create_time减去begin_create_time不得大于24小时
+ * @param nextKey 翻页参数,从第二页开始传,来源于上一页的返回值
+ * @return 售后单列表
+ *
+ * @throws WxErrorException 异常
+ * @deprecated 使用 {@link WxChannelAfterSaleService#listIds(AfterSaleListParam)}
+ */
+ @Deprecated
+ AfterSaleListResponse listIds(Long beginCreateTime, Long endCreateTime, String nextKey)
+ throws WxErrorException;
+
+ /**
+ * 获取售后单列表
+ *
+ * @param param 参数
+ * @return 售后单列表
+ *
+ * @throws WxErrorException 异常
+ */
+ AfterSaleListResponse listIds(AfterSaleListParam param) throws WxErrorException;
+
+ /**
+ * 获取售后单详情
+ *
+ * @param afterSaleOrderId 售后单号
+ * @return 售后单信息
+ *
+ * @throws WxErrorException 异常
+ */
+ AfterSaleInfoResponse get(String afterSaleOrderId) throws WxErrorException;
+
+ /**
+ * 同意售后
+ * 文档地址 https://developers.weixin.qq.com/doc/channels/API/aftersale/acceptapply.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param addressId 同意退货时传入地址id
+ * @param acceptType 1. 同意退货退款,并通知用户退货; 2. 确认收到货并退款给用户。 如果不填则将根据当前的售后单状态自动选择相应操作。对于仅退款的情况,由于只存在一种同意的场景,无需填写此字段。
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse accept(String afterSaleOrderId, String addressId, Integer acceptType) throws WxErrorException;
+
+ /**
+ * 拒绝售后
+ * 文档地址 https://developers.weixin.qq.com/doc/channels/API/aftersale/rejectapply.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param rejectReason 拒绝原因
+ * @param rejectReasonType 拒绝原因枚举值
+ * @see #getRejectReason()
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType) throws WxErrorException;
+
+ /**
+ * 拒绝售后(支持拒绝凭证)
+ * 文档地址 https://developers.weixin.qq.com/doc/channels/API/aftersale/rejectapply.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param rejectReason 拒绝原因
+ * @param rejectReasonType 拒绝原因枚举值
+ * @param rejectCertificates 拒绝凭证图片列表,可使用图片上传接口获取media_id
+ * @see #getRejectReason()
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) throws WxErrorException;
+
+ /**
+ * 上传退款凭证
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param desc 退款凭证描述
+ * @param certificates 退款凭证图片列表
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse uploadRefundEvidence(String afterSaleOrderId, String desc, List certificates)
+ throws WxErrorException;
+
+ /**
+ * 商家补充纠纷单留言
+ *
+ * @param complaintId 纠纷单号
+ * @param content 留言内容,最多500字
+ * @param mediaIds 图片media_id列表,所有留言总图片数量最多20张
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addComplaintMaterial(String complaintId, String content, List mediaIds)
+ throws WxErrorException;
+
+ /**
+ * 商家举证
+ *
+ * @param complaintId 纠纷单号
+ * @param content 举证内容,最多500字
+ * @param mediaIds 图片media_id列表,所有留言总图片数量最多20张
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addComplaintEvidence(String complaintId, String content, List mediaIds)
+ throws WxErrorException;
+
+ /**
+ * 获取纠纷单
+ *
+ * @param complaintId 纠纷单号
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ ComplaintOrderResponse getComplaint(String complaintId) throws WxErrorException;
+
+
+ /**
+ * 获取全量售后原因
+ * 文档地址:https://developers.weixin.qq.com/doc/channels/API/aftersale/getaftersalereason.html
+ *
+ * @return 售后原因
+ *
+ * @throws WxErrorException 异常
+ */
+ AfterSaleReasonResponse getAllReason() throws WxErrorException;
+
+ /**
+ * 获取拒绝售后原因
+ * 文档地址:https://developers.weixin.qq.com/doc/channels/API/aftersale/getrejectreason.html
+ *
+ * @return 拒绝售后原因
+ *
+ * @throws WxErrorException 异常
+ */
+ AfterSaleRejectReasonResponse getRejectReason() throws WxErrorException;
+
+ /**
+ * 换货发货
+ * 文档地址:https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_acceptexchangereship.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param waybillId 快递单号
+ * @param deliveryId 快递公司id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse acceptExchangeReship(String afterSaleOrderId, String waybillId, String deliveryId) throws WxErrorException;
+
+ /**
+ * 换货拒绝发货
+ * 文档地址:https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_rejectexchangereship.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param rejectReason 拒绝原因具体描述 ,可使用默认描述,也可以自定义描述
+ * @param rejectReasonType 拒绝原因枚举值
+ * @param rejectCertificates 退款凭证,可使用图片上传接口获取media_id(数据类型填0)
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse rejectExchangeReship(String afterSaleOrderId, String rejectReason, Integer rejectReasonType, List rejectCertificates) throws WxErrorException;
+
+ /**
+ * 商家协商
+ * 文档地址:https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_merchantupdateaftersale.html
+ * @param param 参数
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse merchantUpdateAfterSale(AfterSaleMerchantUpdateParam param) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java
new file mode 100644
index 0000000000..4d2133a56d
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java
@@ -0,0 +1,101 @@
+package me.chanjar.weixin.channel.api;
+
+import java.io.File;
+import me.chanjar.weixin.channel.bean.address.AddressCodeResponse;
+import me.chanjar.weixin.channel.bean.image.ChannelImageInfo;
+import me.chanjar.weixin.channel.bean.image.ChannelImageResponse;
+import me.chanjar.weixin.channel.bean.image.QualificationFileResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopH5UrlResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopQrCodeResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopTagLinkResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 基础接口
+ *
+ * @author Zeyes
+ */
+public interface WxChannelBasicService {
+
+ /**
+ * 获取店铺基本信息
+ *
+ * @return 店铺基本信息
+ */
+ ShopInfoResponse getShopInfo() throws WxErrorException;
+
+ /**
+ * 获取店铺H5链接
+ *
+ * @return 店铺H5链接
+ * @throws WxErrorException 异常
+ */
+ ShopH5UrlResponse getShopH5Url() throws WxErrorException;
+
+ /**
+ * 获取店铺二维码
+ *
+ * @param qrcodeType 二维码类型,1:二维码;2:标准物料;3:送礼物物料
+ * @return 店铺二维码
+ * @throws WxErrorException 异常
+ */
+ ShopQrCodeResponse getShopQrCode(int qrcodeType) throws WxErrorException;
+
+ /**
+ * 获取店铺口令
+ *
+ * @return 店铺口令
+ * @throws WxErrorException 异常
+ */
+ ShopTagLinkResponse getShopTagLink() throws WxErrorException;
+
+ /**
+ * 上传图片
+ *
+ * @param respType 0:media_id和pay_media_id;1:图片链接(商品信息相关图片请务必使用此参数得到链接)
+ * @param imgUrl 图片url
+ * @return 图片信息
+ *
+ * @throws WxErrorException 异常
+ */
+ ChannelImageInfo uploadImg(int respType, String imgUrl) throws WxErrorException;
+
+ /**
+ * 上传图片
+ *
+ * @param respType 0:media_id和pay_media_id;1:图片链接(商品信息相关图片请务必使用此参数得到链接)
+ * @param file 图片文件
+ * @param height 图片的高,单位:像素
+ * @param width 图片的宽,单位:像素
+ * @return 图片信息
+ *
+ * @throws WxErrorException 异常
+ */
+ ChannelImageInfo uploadImg(int respType, File file, int height, int width) throws WxErrorException;
+
+ /**
+ * 上传资质图片
+ *
+ * @param file 资质图片
+ * @return 结果
+ *
+ * @throws WxErrorException 异常
+ */
+ QualificationFileResponse uploadQualificationFile(File file) throws WxErrorException;
+
+ /**
+ * 根据media_id获取图片
+ *
+ * @param mediaId media_id
+ */
+ ChannelImageResponse getImg(String mediaId) throws WxErrorException;
+
+ /**
+ * 获取地址编码(最多获取4级)
+ *
+ * @param code 地址行政编码,不填或者填0时,拉取全国的省级行政编码
+ * @return AddressCodeResponse
+ */
+ AddressCodeResponse getAddressCode(Integer code) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBrandService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBrandService.java
new file mode 100644
index 0000000000..905d354955
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBrandService.java
@@ -0,0 +1,103 @@
+package me.chanjar.weixin.channel.api;
+
+
+import me.chanjar.weixin.channel.bean.audit.AuditApplyResponse;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.brand.Brand;
+import me.chanjar.weixin.channel.bean.brand.BrandApplyListResponse;
+import me.chanjar.weixin.channel.bean.brand.BrandInfoResponse;
+import me.chanjar.weixin.channel.bean.brand.BrandListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 品牌服务接口
+ *
+ * @author Zeyes
+ */
+public interface WxChannelBrandService {
+
+ /**
+ * 获取品牌库列表
+ *
+ * @param pageSize 每页数量(默认10, 不超过50)
+ * @param nextKey 由上次请求返回, 记录翻页的上下文, 传入时会从上次返回的结果往后翻一页, 不传默认拉取第一页数据
+ * @return 品牌库列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BrandListResponse listAllBrand(Integer pageSize, String nextKey) throws WxErrorException;
+
+ /**
+ * 新增品牌资质
+ *
+ * @param brand 品牌参数
+ * @return 审核id
+ *
+ * @throws WxErrorException 异常
+ */
+ AuditApplyResponse addBrandApply(Brand brand) throws WxErrorException;
+
+ /**
+ * 修改品牌资质
+ *
+ * @param brand 品牌参数
+ * @return 审核id
+ *
+ * @throws WxErrorException 异常
+ */
+ AuditApplyResponse updateBrandApply(Brand brand) throws WxErrorException;
+
+ /**
+ * 撤回品牌资质审核
+ *
+ * @param brandId 品牌id
+ * @param auditId 审核id
+ * @return 审核id
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelBrandApply(String brandId, String auditId) throws WxErrorException;
+
+ /**
+ * 删除品牌资质
+ *
+ * @param brandId 品牌id
+ * @return 结果
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteBrandApply(String brandId) throws WxErrorException;
+
+ /**
+ * 获取品牌资质申请详情
+ *
+ * @param brandId 品牌id
+ * @return 品牌信息
+ *
+ * @throws WxErrorException 异常
+ */
+ BrandInfoResponse getBrandApply(String brandId) throws WxErrorException;
+
+ /**
+ * 获取品牌资质申请列表
+ *
+ * @param pageSize 每页数量(默认10, 不超过50)
+ * @param nextKey 由上次请求返回, 记录翻页的上下文, 传入时会从上次返回的结果往后翻一页, 不传默认拉取第一页数据
+ * @param status 审核单状态, 不填默认拉全部商品
+ * @return 品牌列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BrandApplyListResponse listBrandApply(Integer pageSize, String nextKey, Integer status) throws WxErrorException;
+
+ /**
+ * 获取生效中的品牌资质列表
+ *
+ * @param pageSize 每页数量(默认10, 不超过50)
+ * @param nextKey 由上次请求返回, 记录翻页的上下文, 传入时会从上次返回的结果往后翻一页, 不传默认拉取第一页数据
+ * @return 品牌列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BrandApplyListResponse listValidBrandApply(Integer pageSize, String nextKey) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java
new file mode 100644
index 0000000000..ad86697614
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java
@@ -0,0 +1,132 @@
+package me.chanjar.weixin.channel.api;
+
+import java.io.File;
+import java.util.List;
+import me.chanjar.weixin.channel.bean.audit.AuditApplyResponse;
+import me.chanjar.weixin.channel.bean.audit.AuditResponse;
+import me.chanjar.weixin.channel.bean.audit.CategoryAuditInfo;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.category.*;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 商品类目相关接口
+ *
+ * @author Zeyes
+ * @see 新旧类目树差异
+ */
+public interface WxChannelCategoryService {
+
+ /**
+ * 获取所有的类目
+ *
+ * @return 所有类目以及资质信息
+ *
+ * @throws WxErrorException 异常
+ */
+ CategoryQualificationResponse listAllCategory() throws WxErrorException;
+
+ /**
+ * 获取商品类目列表(全量) 有频率限制
+ *
+ * @param fCatId 类目父id
+ * @return 类目列表
+ *
+ * @throws WxErrorException 异常
+ * @deprecated 接口返回更新,请使用 {@link #listAvailableCategories(String)}
+ */
+ @Deprecated
+ List listAvailableCategory(String fCatId) throws WxErrorException;
+
+ /**
+ * 获取可用的子类目详情
+ *
+ * 1.f_cat_id 为旧类目树中的非叶子类目,仅设置 cat_list 字段。
+ * 2.f_cat_id 为新类目树中的非叶子类目,仅设置 cat_list_v2 字段。
+ * 3.f_cat_id 为0,同时设置 cat_list 和 cat_list_v2 字段
+ *
+ * @param fCatId 父类目ID,可先填0获取根部类目
+ * @return 类目列表
+ * @throws WxErrorException 异常
+ */
+ ShopCategoryResponse listAvailableCategories(String fCatId) throws WxErrorException;
+
+ /**
+ * 获取类目信息
+ *
+ * @param id 三级类目id
+ * @return 类目信息
+ *
+ * @throws WxErrorException 异常
+ */
+ CategoryDetailResult getCategoryDetail(String id) throws WxErrorException;
+
+ /**
+ * 上传类目资质
+ *
+ * @param level1 一级类目ID
+ * @param level2 二级类目ID
+ * @param level3 三级类目ID
+ * @param certificate 资质材料,图片mediaid,图片类型,最多不超过10张
+ * @return 审核id
+ *
+ * @throws WxErrorException 异常
+ * @see WxChannelBasicService#uploadQualificationFile(File)
+ * @deprecated 请使用 {@link #addCategory(CategoryAuditInfo)}
+ */
+ @Deprecated
+ AuditApplyResponse addCategory(String level1, String level2, String level3, List certificate)
+ throws WxErrorException;
+
+ /**
+ * 上传类目资质
+ *
+ * @param info 类目资质信息
+ * @return 审核id
+ *
+ * @throws WxErrorException 异常
+ * @see WxChannelBasicService#uploadQualificationFile(File)
+ */
+ AuditApplyResponse addCategory(CategoryAuditInfo info) throws WxErrorException;
+
+ /**
+ * 取消类目提审
+ *
+ * @param auditId 提交审核时返回的id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelCategoryAudit(String auditId) throws WxErrorException;
+
+ /**
+ * 查询类目审核结果
+ *
+ * @param auditId 审核id
+ * @return 审核结果
+ *
+ * @throws WxErrorException 异常
+ */
+ AuditResponse getAudit(String auditId) throws WxErrorException;
+
+ /**
+ * 获取账号申请通过的类目和资质信息
+ *
+ * @return 类目和资质信息
+ *
+ * @throws WxErrorException 异常
+ */
+ PassCategoryResponse listPassCategory() throws WxErrorException;
+
+ /**
+ * 获取店铺的类目权限列表
+ *
+ * @param isFilterStatus 是否按状态筛选
+ * @param status 类目状态(当 isFilterStatus 为 true 时有效)
+ * @return 类目权限列表
+ *
+ * @throws WxErrorException 异常
+ */
+ RelationCategoryResponse listRelationCategory(Boolean isFilterStatus, Integer status) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassFinderService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassFinderService.java
new file mode 100644
index 0000000000..db123f61a4
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassFinderService.java
@@ -0,0 +1,58 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.compass.finder.OverallResponse;
+import me.chanjar.weixin.channel.bean.compass.finder.ProductDataResponse;
+import me.chanjar.weixin.channel.bean.compass.finder.ProductListResponse;
+import me.chanjar.weixin.channel.bean.compass.finder.SaleProfileDataResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号助手 罗盘达人版服务
+ *
+ * @author Winnie
+ */
+public interface WxChannelCompassFinderService {
+
+ /**
+ * 获取电商概览数据
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 电商概览数据
+ *
+ * @throws WxErrorException 异常
+ */
+ OverallResponse getOverall(String ds) throws WxErrorException;
+
+ /**
+ * 获取带货商品数据
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @param productId 商品id
+ * @return 带货商品数据
+ *
+ * @throws WxErrorException 异常
+ */
+ ProductDataResponse getProductData(String ds, String productId) throws WxErrorException;
+
+ /**
+ * 获取带货商品列表
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 带货商品列表
+ *
+ * @throws WxErrorException 异常
+ */
+ ProductListResponse getProductList(String ds) throws WxErrorException;
+
+ /**
+ * 获取带货人群数据
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @param type 用户类型,1=商品曝光用户, 2=商品点击用户, 3=购买用户, 4=首购用户, 5=复购用户, 6=直播观看用户
+ * @return 带货人群数据
+ *
+ * @throws WxErrorException 异常
+ */
+ SaleProfileDataResponse getSaleProfileData(String ds, Integer type) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassShopService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassShopService.java
new file mode 100644
index 0000000000..aa3a85fa74
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCompassShopService.java
@@ -0,0 +1,126 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.compass.shop.FinderAuthListResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.FinderListResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.FinderOverallResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.FinderProductListResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.FinderProductOverallResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.ShopLiveListResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.ShopOverallResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.ShopProductDataResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.ShopProductListResponse;
+import me.chanjar.weixin.channel.bean.compass.shop.ShopSaleProfileDataResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号/微信小店 罗盘商家版服务
+ *
+ * @author Zeyes
+ */
+public interface WxChannelCompassShopService {
+
+ /**
+ * 获取电商概览数据
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 电商概览数据
+ *
+ * @throws WxErrorException 异常
+ */
+ ShopOverallResponse getShopOverall(String ds) throws WxErrorException;
+
+ /**
+ * 获取授权视频号列表
+ *
+ * @return 获取授权视频号列表
+ *
+ * @throws WxErrorException 异常
+ */
+ FinderAuthListResponse getFinderAuthorizationList() throws WxErrorException;
+
+ /**
+ * 获取带货达人列表
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 带货达人列表
+ *
+ * @throws WxErrorException 异常
+ */
+ FinderListResponse getFinderList(String ds) throws WxErrorException;
+
+ /**
+ * 获取带货数据概览
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 带货数据概览
+ *
+ * @throws WxErrorException 异常
+ */
+ FinderOverallResponse getFinderOverall(String ds) throws WxErrorException;
+
+ /**
+ * 获取带货达人商品列表
+ *
+ * @param ds 日期,格式YYYYMMDD
+ * @param finderId 视频号ID
+ * @return 带货达人商品列表
+ *
+ * @throws WxErrorException 异常
+ */
+ FinderProductListResponse getFinderProductList(String ds, String finderId) throws WxErrorException;
+
+ /**
+ * 获取带货达人详情
+ *
+ * @param ds 日期,格式YYYYMMDD
+ * @param finderId 视频号ID
+ * @return 带货达人详情
+ *
+ * @throws WxErrorException 异常
+ */
+ FinderProductOverallResponse getFinderProductOverall(String ds, String finderId) throws WxErrorException;
+
+ /**
+ * 获取店铺开播列表
+ *
+ * @param ds 日期,格式YYYYMMDD
+ * @param finderId 视频号ID
+ * @return 店铺开播列表
+ *
+ * @throws WxErrorException 异常
+ */
+ ShopLiveListResponse getShopLiveList(String ds, String finderId) throws WxErrorException;
+
+ /**
+ * 获取商品详细信息
+ *
+ * @param ds 日期,格式YYYYMMDD
+ * @param productId 商品id
+ * @return 商品详细信息
+ *
+ * @throws WxErrorException 异常
+ */
+ ShopProductDataResponse getShopProductData(String ds, String productId) throws WxErrorException;
+
+ /**
+ * 获取商品列表
+ *
+ * @param ds 日期,格式YYYYMMDD
+ * @return 商品列表
+ *
+ * @throws WxErrorException 异常
+ */
+ ShopProductListResponse getShopProductList(String ds) throws WxErrorException;
+
+ /**
+ * 获取店铺人群数据
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @param type 用户类型,1商品曝光用户 2商品点击用户 3购买用户 4首购用户 5复购用户
+ * @return 店铺人群数据
+ *
+ * @throws WxErrorException 异常
+ */
+ ShopSaleProfileDataResponse getShopSaleProfileData(String ds, Integer type) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCouponService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCouponService.java
new file mode 100644
index 0000000000..df59fdc8b9
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCouponService.java
@@ -0,0 +1,92 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.coupon.CouponIdResponse;
+import me.chanjar.weixin.channel.bean.coupon.CouponInfoResponse;
+import me.chanjar.weixin.channel.bean.coupon.CouponListParam;
+import me.chanjar.weixin.channel.bean.coupon.CouponListResponse;
+import me.chanjar.weixin.channel.bean.coupon.CouponParam;
+import me.chanjar.weixin.channel.bean.coupon.UserCouponListParam;
+import me.chanjar.weixin.channel.bean.coupon.UserCouponListResponse;
+import me.chanjar.weixin.channel.bean.coupon.UserCouponResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 优惠券服务
+ *
+ * @author Zeyes
+ */
+public interface WxChannelCouponService {
+
+ /**
+ * 创建优惠券
+ *
+ * @param coupon 优惠券
+ * @return 优惠券ID
+ *
+ * @throws WxErrorException 异常
+ */
+ CouponIdResponse createCoupon(CouponParam coupon) throws WxErrorException;
+
+ /**
+ * 更新优惠券
+ *
+ * @param coupon 优惠券
+ * @return 优惠券ID
+ *
+ * @throws WxErrorException 异常
+ */
+ CouponIdResponse updateCoupon(CouponParam coupon) throws WxErrorException;
+
+ /**
+ * 更新优惠券状态
+ *
+ * @param couponId 优惠券ID
+ * @param status 状态 2生效 4已作废 5删除 {@link me.chanjar.weixin.channel.enums.WxCouponStatus}
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateCouponStatus(String couponId, Integer status) throws WxErrorException;
+
+ /**
+ * 获取优惠券详情
+ *
+ * @param couponId 优惠券ID
+ * @return CouponInfoResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ CouponInfoResponse getCoupon(String couponId) throws WxErrorException;
+
+ /**
+ * 获取优惠券ID列表
+ *
+ * @param param 条件参数
+ * @return 优惠券ID列表
+ *
+ * @throws WxErrorException 异常
+ */
+ CouponListResponse getCouponList(CouponListParam param) throws WxErrorException;
+
+ /**
+ * 获取用户优惠券
+ *
+ * @param openId 用户openid
+ * @param userCouponId 用户优惠券ID
+ * @return UserCouponResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ UserCouponResponse getUserCoupon(String openId, String userCouponId) throws WxErrorException;
+
+ /**
+ * 获取用户优惠券ID列表
+ *
+ * @param param 条件参数
+ * @return UserCouponListResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ UserCouponListResponse getUserCouponList(UserCouponListParam param) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java
new file mode 100644
index 0000000000..c1c03cc615
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java
@@ -0,0 +1,21 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.favorite.FavoriteCountResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 收藏管理接口
+ *
+ * @author GitHub Copilot
+ * @link 收藏管理接口文档
+ */
+public interface WxChannelFavoriteService {
+
+ /**
+ * 获取店铺收藏的人数
+ *
+ * @return 店铺收藏人数响应
+ * @throws WxErrorException 异常
+ */
+ FavoriteCountResponse getFavoriteCount() throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFreightTemplateService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFreightTemplateService.java
new file mode 100644
index 0000000000..188b33464b
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFreightTemplateService.java
@@ -0,0 +1,57 @@
+package me.chanjar.weixin.channel.api;
+
+
+import me.chanjar.weixin.channel.bean.freight.FreightTemplate;
+import me.chanjar.weixin.channel.bean.freight.TemplateIdResponse;
+import me.chanjar.weixin.channel.bean.freight.TemplateInfoResponse;
+import me.chanjar.weixin.channel.bean.freight.TemplateListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 运费模板服务接口
+ *
+ * @author Zeyes
+ */
+public interface WxChannelFreightTemplateService {
+
+ /**
+ * 获取运费模板列表
+ *
+ * @param offset 起始位置
+ * @param limit 拉取个数
+ * @return 列表
+ *
+ * @throws WxErrorException 异常
+ */
+ TemplateListResponse listTemplate(Integer offset, Integer limit) throws WxErrorException;
+
+ /**
+ * 获取运费模板
+ *
+ * @param templateId 模板id
+ * @return 运费模板
+ *
+ * @throws WxErrorException 异常
+ */
+ TemplateInfoResponse getTemplate(String templateId) throws WxErrorException;
+
+ /**
+ * 添加运费模板
+ *
+ * @param template 运费模板
+ * @return TemplateIdResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ TemplateIdResponse addTemplate(FreightTemplate template) throws WxErrorException;
+
+ /**
+ * 更新运费模板
+ *
+ * @param template 运费模板
+ * @return TemplateIdResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ TemplateIdResponse updateTemplate(FreightTemplate template) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFundService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFundService.java
new file mode 100644
index 0000000000..cb0f5aab79
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFundService.java
@@ -0,0 +1,189 @@
+package me.chanjar.weixin.channel.api;
+
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.fund.AccountInfo;
+import me.chanjar.weixin.channel.bean.fund.AccountInfoResponse;
+import me.chanjar.weixin.channel.bean.fund.BalanceInfoResponse;
+import me.chanjar.weixin.channel.bean.fund.FlowListResponse;
+import me.chanjar.weixin.channel.bean.fund.FundsFlowResponse;
+import me.chanjar.weixin.channel.bean.fund.FundsListParam;
+import me.chanjar.weixin.channel.bean.fund.WithdrawDetailResponse;
+import me.chanjar.weixin.channel.bean.fund.WithdrawListResponse;
+import me.chanjar.weixin.channel.bean.fund.WithdrawSubmitResponse;
+import me.chanjar.weixin.channel.bean.fund.bank.BankCityResponse;
+import me.chanjar.weixin.channel.bean.fund.bank.BankInfoResponse;
+import me.chanjar.weixin.channel.bean.fund.bank.BankListResponse;
+import me.chanjar.weixin.channel.bean.fund.bank.BankProvinceResponse;
+import me.chanjar.weixin.channel.bean.fund.bank.BranchInfoResponse;
+import me.chanjar.weixin.channel.bean.fund.qrcode.QrCheckResponse;
+import me.chanjar.weixin.channel.bean.fund.qrcode.QrCodeResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 资金相关服务
+ *
+ * @author Zeyes
+ */
+public interface WxChannelFundService {
+
+ /**
+ * 获取账户余额
+ *
+ * @return 账户余额
+ *
+ * @throws WxErrorException 异常
+ */
+ BalanceInfoResponse getBalance() throws WxErrorException;
+
+ /**
+ * 获取结算账户
+ *
+ * @return 结算账户
+ *
+ * @throws WxErrorException 异常
+ */
+ AccountInfoResponse getBankAccount() throws WxErrorException;
+
+ /**
+ * 获取资金流水详情
+ *
+ * @param flowId 资金流水号
+ * @return 资金流水详情
+ *
+ * @throws WxErrorException 异常
+ */
+ FundsFlowResponse getFundsFlowDetail(String flowId) throws WxErrorException;
+
+ /**
+ * 获取资金流水列表
+ *
+ * @param param 资金流水列表参数
+ * @return 资金流水列表
+ *
+ * @throws WxErrorException 异常
+ */
+ FlowListResponse listFundsFlow(FundsListParam param) throws WxErrorException;
+
+ /**
+ * 获取提现记录
+ *
+ * @param withdrawId 提现单号
+ * @return 提现记录
+ *
+ * @throws WxErrorException 异常
+ */
+ WithdrawDetailResponse getWithdrawDetail(String withdrawId) throws WxErrorException;
+
+ /**
+ * 获取提现记录列表
+ *
+ * @param pageNum 页码
+ * @param pageSize 每页大小
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ * @return 提现记录列表
+ *
+ * @throws WxErrorException 异常
+ */
+ WithdrawListResponse listWithdraw(Integer pageNum, Integer pageSize, Long startTime, Long endTime)
+ throws WxErrorException;
+
+ /**
+ * 修改结算账户
+ *
+ * @param accountInfo 结算账户信息
+ * @return 修改结果
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse setBankAccount(AccountInfo accountInfo) throws WxErrorException;
+
+ /***
+ * 商户提现
+ *
+ * @param amount 提现金额(单位:分)
+ * @param remark 提现备注
+ * @param bankMemo 银行附言
+ * @return 提现结果
+ * @throws WxErrorException 异常
+ */
+ WithdrawSubmitResponse submitWithdraw(Integer amount, String remark, String bankMemo) throws WxErrorException;
+
+ /**
+ * 根据卡号查银行信息
+ *
+ * @param accountNumber 卡号
+ * @return 银行信息
+ *
+ * @throws WxErrorException 异常
+ */
+ BankInfoResponse getBankInfoByCardNo(String accountNumber) throws WxErrorException;
+
+ /**
+ * 搜索银行列表
+ *
+ * @param offset 偏移量
+ * @param limit 每页数据大小
+ * @param keywords 银行关键字
+ * @param bankType 银行类型(1:对私银行,2:对公银行; 默认对公)
+ * @return 银行列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BankListResponse searchBankList(Integer offset, Integer limit, String keywords, Integer bankType)
+ throws WxErrorException;
+
+ /**
+ * 查询城市列表
+ *
+ * @param provinceCode 省份编码
+ * @return 城市列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BankCityResponse searchCityList(String provinceCode) throws WxErrorException;
+
+ /**
+ * 查询大陆银行省份列表
+ *
+ * @return 省份列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BankProvinceResponse getProvinceList() throws WxErrorException;
+
+ /**
+ * 查询支行列表
+ *
+ * @param bankCode 银行编码
+ * @param cityCode 城市编码
+ * @param offset 偏移量
+ * @param limit 每页数据大小
+ * @return 支行列表
+ *
+ * @throws WxErrorException 异常
+ */
+ BranchInfoResponse searchBranchList(String bankCode, String cityCode, Integer offset, Integer limit)
+ throws WxErrorException;
+
+ /**
+ * 获取二维码
+ *
+ * @param qrcodeTicket 二维码ticket
+ * @return 二维码响应
+ *
+ * @throws WxErrorException 异常
+ */
+ QrCodeResponse getQrCode(String qrcodeTicket) throws WxErrorException;
+
+ /**
+ * 查询扫码状态
+ *
+ * @param qrcodeTicket 二维码ticket
+ * @return 扫码状态
+ *
+ * @throws WxErrorException 异常
+ */
+ QrCheckResponse checkQrStatus(String qrcodeTicket) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelLiveDashboardService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelLiveDashboardService.java
new file mode 100644
index 0000000000..be93b06a97
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelLiveDashboardService.java
@@ -0,0 +1,34 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.live.dashboard.LiveDataResponse;
+import me.chanjar.weixin.channel.bean.live.dashboard.LiveListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号助手 直播大屏数据服务
+ *
+ * @author Winnie
+ */
+public interface WxChannelLiveDashboardService {
+
+ /**
+ * 获取直播大屏直播列表
+ *
+ * @param ds 日期,格式 yyyyMMdd
+ * @return 播大屏直播列表
+ *
+ * @throws WxErrorException 异常
+ */
+ LiveListResponse getLiveList(Long ds) throws WxErrorException;
+
+ /**
+ * 获取直播大屏数据
+ *
+ * @param exportId 直播唯一ID
+ * @return 播大屏数据
+ *
+ * @throws WxErrorException 异常
+ */
+ LiveDataResponse getLiveData(String exportId) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java
new file mode 100644
index 0000000000..e2da5c7a5b
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java
@@ -0,0 +1,326 @@
+package me.chanjar.weixin.channel.api;
+
+import java.util.List;
+import me.chanjar.weixin.channel.bean.base.AddressInfo;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.delivery.PackageAuditInfo;
+import me.chanjar.weixin.channel.bean.delivery.DeliveryCompanyResponse;
+import me.chanjar.weixin.channel.bean.delivery.DeliveryInfo;
+import me.chanjar.weixin.channel.bean.order.ChangeOrderInfo;
+import me.chanjar.weixin.channel.bean.order.DecodeSensitiveInfoResponse;
+import me.chanjar.weixin.channel.bean.order.DeliveryUpdateParam;
+import me.chanjar.weixin.channel.bean.order.OrderCompensationDeliveryParam;
+import me.chanjar.weixin.channel.bean.order.OrderInfoResponse;
+import me.chanjar.weixin.channel.bean.order.OrderListParam;
+import me.chanjar.weixin.channel.bean.order.OrderListResponse;
+import me.chanjar.weixin.channel.bean.order.OrderSearchParam;
+import me.chanjar.weixin.channel.bean.order.PreShipmentChangeSkuResponse;
+import me.chanjar.weixin.channel.bean.order.PresentSubOrderResponse;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberGetPhoneResponse;
+import me.chanjar.weixin.channel.bean.order.RealNumberViewAuditResponse;
+import me.chanjar.weixin.channel.bean.order.VirtualTelNumberResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 订单服务接口
+ *
+ * @author Zeyes
+ * @link 订单接口文档
+ */
+public interface WxChannelOrderService {
+
+ /**
+ * 获取订单
+ *
+ * @param orderId 订单id
+ * @return 订单详情
+ *
+ * @throws WxErrorException 异常
+ */
+ OrderInfoResponse getOrder(String orderId) throws WxErrorException;
+
+ /**
+ * 获取订单详情
+ *
+ * @param orderId 订单id
+ * @param encodeSensitiveInfo 是否编码敏感信息
+ * @return 订单详情
+ *
+ * @throws WxErrorException 异常
+ */
+ OrderInfoResponse getOrder(String orderId, Boolean encodeSensitiveInfo) throws WxErrorException;
+
+ /**
+ * 获取订单列表
+ *
+ * @param param 搜索条件
+ * @return 订单列表
+ *
+ * @throws WxErrorException 异常
+ */
+ OrderListResponse getOrders(OrderListParam param) throws WxErrorException;
+
+ /**
+ * 订单搜索
+ *
+ * @param param 搜索条件
+ * @return 订单列表
+ *
+ * @throws WxErrorException 异常
+ */
+ OrderListResponse searchOrder(OrderSearchParam param) throws WxErrorException;
+
+ /**
+ * 更改订单价格
+ *
+ * @param orderId 订单id
+ * @param expressFee 运费价格(以分为单位)(不填不改)
+ * @param changeOrderInfos 改价列表
+ * @return 结果
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updatePrice(String orderId, Integer expressFee, List changeOrderInfos)
+ throws WxErrorException;
+
+ /**
+ * 更改订单备注
+ *
+ * @param orderId 订单id
+ * @param merchantNotes 备注
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateRemark(String orderId, String merchantNotes) throws WxErrorException;
+
+ /**
+ * 更新订单地址
+ *
+ * @param orderId 订单id
+ * @param userAddress 用户地址
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateAddress(String orderId, AddressInfo userAddress) throws WxErrorException;
+
+ /**
+ * 修改物流信息 发货完成的订单可以修改,最多修改1次 拆包发货的订单暂不允许修改物流 虚拟商品订单暂不允许修改物流
+ *
+ * @param param 物流信息
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateDelivery(DeliveryUpdateParam param) throws WxErrorException;
+
+ /**
+ * 同意用户修改收货地址请求
+ *
+ * @param orderId 订单id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse acceptAddressModify(String orderId) throws WxErrorException;
+
+ /**
+ * 拒接用户修改收货地址请求
+ *
+ * @param orderId 订单id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse rejectAddressModify(String orderId) throws WxErrorException;
+
+ /**
+ * 关闭订单 (需要订单状态为未付款状态)
+ *
+ * @param orderId 订单id
+ * @return BaseResponse
+ */
+ WxChannelBaseResponse closeOrder(String orderId);
+
+ /**
+ * 获取快递公司列表-旧
+ *
+ * @return 快递公司列表
+ *
+ * @throws WxErrorException 异常
+ */
+ DeliveryCompanyResponse listDeliveryCompany() throws WxErrorException;
+
+ /**
+ * 获取快递公司列表
+ *
+ * @param ewaybillOnly 是否仅返回支持电子面单功能的快递公司
+ * @return 快递公司列表
+ *
+ * @throws WxErrorException 异常
+ */
+ DeliveryCompanyResponse listDeliveryCompany(Boolean ewaybillOnly) throws WxErrorException;
+
+ /**
+ * 订单发货
+ *
+ * @param orderId 订单id
+ * @param deliveryList 物流信息
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deliveryOrder(String orderId, List deliveryList) throws WxErrorException;
+
+ /**
+ * 上传生鲜质检信息
+ *
+ * 注意事项:
+ * 1. 非生鲜质检的订单不能进行上传
+ * 2. 图片url必须用图片上传接口获取 {@link WxChannelBasicService#uploadImg(int, String)}
+ *
+ * @param orderId 订单id
+ * @param items 商品打包信息
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse uploadFreshInspect(String orderId, List items) throws WxErrorException;
+
+ /**
+ * 兑换虚拟号
+ *
+ * @param orderId 订单id
+ * @return 虚拟号信息
+ * @throws WxErrorException 异常
+ */
+ VirtualTelNumberResponse getVirtualTelNumber(String orderId) throws WxErrorException;
+
+ /**
+ * 解码订单包含的敏感数据
+ *
+ * @param orderId 订单id
+ * @return 解码结果
+ * @throws WxErrorException 异常
+ */
+ DecodeSensitiveInfoResponse decodeSensitiveInfo(String orderId) throws WxErrorException;
+
+ /**
+ * 礼物订单新增备注信息
+ *
+ * @param orderId 礼物订单ID
+ * @param note 备注内容
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addPresentNote(String orderId, String note) throws WxErrorException;
+
+ /**
+ * 获取礼物单的子单列表
+ *
+ * @param orderId 礼物订单ID
+ * @return 子单列表
+ * @throws WxErrorException 异常
+ */
+ PresentSubOrderResponse getPresentSubOrders(String orderId) throws WxErrorException;
+
+ /**
+ * 获取待发货前更换SKU待处理请求
+ *
+ * @param orderId 订单ID
+ * @return 换SKU信息
+ * @throws WxErrorException 异常
+ */
+ PreShipmentChangeSkuResponse getPreShipmentChangeSku(String orderId) throws WxErrorException;
+
+ /**
+ * 同意待发货前更换SKU请求
+ *
+ * @param orderId 订单ID
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse approvePreShipmentChangeSku(String orderId) throws WxErrorException;
+
+ /**
+ * 拒绝待发货前更换SKU请求
+ *
+ * @param orderId 订单ID
+ * @param rejectReason 拒绝原因
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse rejectPreShipmentChangeSku(String orderId, String rejectReason) throws WxErrorException;
+
+ /**
+ * 申请查看订单真实号码
+ *
+ * @param orderId 订单ID
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse applyRealNumber(String orderId) throws WxErrorException;
+
+ /**
+ * 查看订单真实号审核状态
+ *
+ * @param orderId 订单ID
+ * @return 审核状态
+ * @throws WxErrorException 异常
+ */
+ RealNumberViewAuditResponse getRealNumberViewAudit(String orderId) throws WxErrorException;
+
+ /**
+ * 订单再次申请虚拟号
+ *
+ * @param orderId 订单ID
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse applyVirtualNumberAgain(String orderId) throws WxErrorException;
+
+ /**
+ * 订单虚拟号延期
+ *
+ * @param orderId 订单ID
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse delayVirtualNumber(String orderId) throws WxErrorException;
+
+ /**
+ * 添加待认证的手机号
+ *
+ * @param phone 手机号
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addPrivatePhone(String phone) throws WxErrorException;
+
+ /**
+ * 获取短信验证码
+ *
+ * @param phone 手机号
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse sendPrivatePhoneVerifyCode(String phone) throws WxErrorException;
+
+ /**
+ * 获取小店手机号认证状态
+ *
+ * @return 手机号认证状态
+ * @throws WxErrorException 异常
+ */
+ PrivateNumberGetPhoneResponse getPrivatePhone() throws WxErrorException;
+
+ /**
+ * 订单补发货
+ *
+ * @param param 补发货参数
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse compensationDelivery(OrderCompensationDeliveryParam param) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelProductService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelProductService.java
new file mode 100644
index 0000000000..ce85e9f0a4
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelProductService.java
@@ -0,0 +1,343 @@
+package me.chanjar.weixin.channel.api;
+
+
+import java.util.List;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.limit.LimitTaskAddResponse;
+import me.chanjar.weixin.channel.bean.limit.LimitTaskListResponse;
+import me.chanjar.weixin.channel.bean.limit.LimitTaskParam;
+import me.chanjar.weixin.channel.bean.product.GiftActivityAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftActivityInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductGetResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductListParam;
+import me.chanjar.weixin.channel.bean.product.GiftProductListResponse;
+import me.chanjar.weixin.channel.bean.product.SkuStockBatchResponse;
+import me.chanjar.weixin.channel.bean.product.SkuStockResponse;
+import me.chanjar.weixin.channel.bean.product.SpuFastInfo;
+import me.chanjar.weixin.channel.bean.product.SpuGetResponse;
+import me.chanjar.weixin.channel.bean.product.SpuInfo;
+import me.chanjar.weixin.channel.bean.product.SpuListResponse;
+import me.chanjar.weixin.channel.bean.product.SpuUpdateInfo;
+import me.chanjar.weixin.channel.bean.product.SpuUpdateResponse;
+import me.chanjar.weixin.channel.bean.product.link.ProductH5UrlResponse;
+import me.chanjar.weixin.channel.bean.product.link.ProductQrCodeResponse;
+import me.chanjar.weixin.channel.bean.product.link.ProductTagLinkResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 商品服务接口
+ *
+ * @author Zeyes
+ * @see 商品状态流转图
+ */
+public interface WxChannelProductService {
+
+ /**
+ * 添加商品
+ *
+ * @param info 商品信息
+ * @return 返回商品的状态和id
+ *
+ * @throws WxErrorException 异常
+ */
+ SpuUpdateResponse addProduct(SpuUpdateInfo info) throws WxErrorException;
+
+ /**
+ * 更新商品
+ *
+ * @param info 商品信息
+ * @return 返回商品的状态和id
+ *
+ * @throws WxErrorException 异常
+ */
+ SpuUpdateResponse updateProduct(SpuUpdateInfo info) throws WxErrorException;
+
+ /**
+ * 添加商品
+ *
+ * @param info 商品信息
+ * @return 返回商品的状态和id
+ *
+ * @throws WxErrorException 异常
+ * @deprecated 请使用 {@link #addProduct(SpuUpdateInfo)}
+ */
+ @Deprecated
+ SpuUpdateResponse addProduct(SpuInfo info) throws WxErrorException;
+
+ /**
+ * 更新商品
+ *
+ * @param info 商品信息
+ * @return 返回商品的状态和id
+ *
+ * @throws WxErrorException 异常
+ * @deprecated 请使用 {@link #updateProduct(SpuUpdateInfo)}
+ */
+ @Deprecated
+ SpuUpdateResponse updateProduct(SpuInfo info) throws WxErrorException;
+
+ /**
+ * 免审更新商品
+ *
+ * @param info 商品信息
+ * @return 返回商品的状态和id
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateProductAuditFree(SpuFastInfo info) throws WxErrorException;
+
+ /**
+ * 更新商品库存 (仅对edit_status != 2 的商品适用,其他状态的商品无法通过该接口修改库存)
+ *
+ * @param productId 内部商品ID
+ * @param skuId 内部sku_id
+ * @param diffType 修改类型 1增加 2减少 3设置
+ * 建议使用1或2,不建议使用3,因为使用3在高并发场景可能会出现预期外表现
+ * @param num 增加、减少或者设置的库存值
+ * @return WxChannelBaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateStock(String productId, String skuId, Integer diffType, Integer num)
+ throws WxErrorException;
+
+ /**
+ * 删除商品
+ *
+ * @param productId 商品ID
+ * @return 是否成功
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteProduct(String productId) throws WxErrorException;
+
+ /**
+ * 撤回商品审核
+ *
+ * @param productId 商品ID
+ * @return 是否成功
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelProductAudit(String productId) throws WxErrorException;
+
+ /**
+ * 获取商品
+ *
+ * @param productId 商品ID
+ * @param dataType 默认取1 1:获取线上数据 2:获取草稿数据 3:同时获取线上和草稿数据(注意:需成功上架后才有线上数据)
+ * @return 商品信息
+ *
+ * @throws WxErrorException 异常
+ */
+ SpuGetResponse getProduct(String productId, Integer dataType) throws WxErrorException;
+
+ /**
+ * 获取商品列表
+ *
+ * @param pageSize 每页数量(默认10,不超过30)
+ * @param nextKey 由上次请求返回,记录翻页的上下文。传入时会从上次返回的结果往后翻一页,不传默认拉取第一页数据。
+ * @param status 商品状态,不填默认拉全部商品(不包含回收站) {@link me.chanjar.weixin.channel.enums.SpuStatus}
+ * @return List
+ *
+ * @throws WxErrorException 异常
+ */
+ SpuListResponse listProduct(Integer pageSize, String nextKey, Integer status) throws WxErrorException;
+
+ /**
+ * 上架商品
+ *
+ * @param productId 商品ID
+ * @return 是否成功
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse upProduct(String productId) throws WxErrorException;
+
+ /**
+ * 下架商品
+ *
+ * @param productId 商品ID
+ * @return 是否成功
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse downProduct(String productId) throws WxErrorException;
+
+ /**
+ * 获取商品实时库存
+ *
+ * @param productId 商品ID
+ * @param skuId skuId
+ * @return SkuStockResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ SkuStockResponse getSkuStock(String productId, String skuId) throws WxErrorException;
+
+ /**
+ * 批量获取库存信息 (单次请求不能超过50个商品ID)
+ *
+ * @param productIds 商品ID列表
+ * @return 库存信息
+ * @throws WxErrorException 异常
+ */
+ SkuStockBatchResponse getSkuStockBatch(List productIds) throws WxErrorException;
+
+ /**
+ * 获取商品H5链接
+ *
+ * @param productId 商品ID
+ * @return 商品H5链接
+ * @throws WxErrorException 异常
+ */
+ ProductH5UrlResponse getProductH5Url(String productId) throws WxErrorException;
+
+ /**
+ * 获取商品二维码
+ *
+ * @param productId 商品ID
+ * @return 商品二维码
+ * @throws WxErrorException 异常
+ */
+ ProductQrCodeResponse getProductQrCode(String productId) throws WxErrorException;
+
+ /**
+ * 获取商品口令
+ *
+ * @param productId 商品ID
+ * @return 商品口令
+ * @throws WxErrorException 异常
+ */
+ ProductTagLinkResponse getProductTagLink(String productId) throws WxErrorException;
+
+ /**
+ * 添加非卖商品
+ *
+ * @param info 赠品信息
+ * @return 添加赠品响应
+ * @throws WxErrorException 异常
+ */
+ GiftProductAddResponse addGiftProduct(GiftProductInfo info) throws WxErrorException;
+
+ /**
+ * 更新非卖商品
+ *
+ * @param info 赠品信息
+ * @return 操作响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateGiftProduct(GiftProductInfo info) throws WxErrorException;
+
+ /**
+ * 在售商品转赠品
+ *
+ * @param productId 商品ID
+ * @return 操作响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse setProductAsGift(String productId) throws WxErrorException;
+
+ /**
+ * 获取赠品
+ *
+ * @param productId 赠品商品ID
+ * @return 赠品详情响应
+ * @throws WxErrorException 异常
+ */
+ GiftProductGetResponse getGiftProduct(String productId) throws WxErrorException;
+
+ /**
+ * 获取赠品列表
+ *
+ * @param param 查询参数
+ * @return 赠品列表
+ * @throws WxErrorException 异常
+ */
+ GiftProductListResponse listGiftProduct(GiftProductListParam param) throws WxErrorException;
+
+ /**
+ * 更新赠品库存
+ *
+ * @param productId 赠品商品ID
+ * @param skuId 赠品sku_id
+ * @param diffType 修改类型 1增加 2减少 3设置
+ * 建议使用1或2,不建议使用3,因为使用3在高并发场景可能会出现预期外表现
+ * @param num 增加、减少或者设置的库存值
+ * @return 操作响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateGiftStock(String productId, String skuId, Integer diffType, Integer num)
+ throws WxErrorException;
+
+ /**
+ * 创建赠品活动
+ *
+ * @param info 活动信息
+ * @return 创建赠品活动响应
+ * @throws WxErrorException 异常
+ */
+ GiftActivityAddResponse addGiftActivity(GiftActivityInfo info) throws WxErrorException;
+
+ /**
+ * 删除赠品活动
+ *
+ * @param activityId 活动ID
+ * @return 操作响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteGiftActivity(String activityId) throws WxErrorException;
+
+ /**
+ * 停止赠品活动
+ *
+ * @param activityId 活动ID
+ * @return 操作响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse stopGiftActivity(String activityId) throws WxErrorException;
+
+ /**
+ * 添加限时抢购任务
+ *
+ * @param param 限时抢购任务
+ * @return LimitTaskAddResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ LimitTaskAddResponse addLimitTask(LimitTaskParam param) throws WxErrorException;
+
+ /**
+ * 拉取限时抢购任务列表
+ *
+ * @param pageSize 每页数量(默认10,不超过50)
+ * @param nextKey 由上次请求返回,记录翻页的上下文。传入时会从上次返回的结果往后翻一页,不传默认拉取第一页数据
+ * @param status 抢购活动状态
+ * @return LimitTaskListResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ LimitTaskListResponse listLimitTask(Integer pageSize, String nextKey, Integer status) throws WxErrorException;
+
+ /**
+ * 停止限时抢购任务
+ *
+ * @param taskId 限时抢购任务ID
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse stopLimitTask(String taskId) throws WxErrorException;
+
+ /**
+ * 停止限时抢购任务
+ *
+ * @param taskId 限时抢购任务ID
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteLimitTask(String taskId) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelQicService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelQicService.java
new file mode 100644
index 0000000000..df783b68c7
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelQicService.java
@@ -0,0 +1,67 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectCodeResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.RegisterLogisticsRequest;
+import me.chanjar.weixin.channel.bean.qic.SubmitConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.SubmitInspectRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 质检管理接口.
+ */
+public interface WxChannelQicService {
+
+ /**
+ * 查询质检仓配置.
+ *
+ * @return 质检仓配置
+ * @throws WxErrorException 异常
+ */
+ InspectConfigResponse getInspectConfig() throws WxErrorException;
+
+ /**
+ * 查询送检配置模板信息.
+ *
+ * @param orderId 订单号(可选)
+ * @return 送检配置模板信息
+ * @throws WxErrorException 异常
+ */
+ SubmitConfigResponse getSubmitConfig(String orderId) throws WxErrorException;
+
+ /**
+ * 查询送检配置模板信息.
+ *
+ * @return 送检配置模板信息
+ * @throws WxErrorException 异常
+ */
+ SubmitConfigResponse getSubmitConfig() throws WxErrorException;
+
+ /**
+ * 打印质检码.
+ *
+ * @param orderId 订单号
+ * @return 质检码详情
+ * @throws WxErrorException 异常
+ */
+ InspectCodeResponse printInspectCode(String orderId) throws WxErrorException;
+
+ /**
+ * 绑定送检信息.
+ *
+ * @param request 送检信息请求
+ * @return 基础响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse submitInspectInfo(SubmitInspectRequest request) throws WxErrorException;
+
+ /**
+ * 自寄快递送检.
+ *
+ * @param request 自寄快递请求
+ * @return 基础响应
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse registerLogistics(RegisterLogisticsRequest request) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java
new file mode 100644
index 0000000000..ab5497e212
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java
@@ -0,0 +1,213 @@
+package me.chanjar.weixin.channel.api;
+
+/**
+ * The interface Wx Channel service
+ *
+ * @author Zeyes
+ */
+public interface WxChannelService extends BaseWxChannelService {
+
+ /**
+ * 基础接口服务
+ *
+ * @return 基础接口服务
+ */
+ WxChannelBasicService getBasicService();
+
+ /**
+ * 商品类目服务
+ *
+ * @return 商品类目服务
+ */
+ WxChannelCategoryService getCategoryService();
+
+ /**
+ * 品牌服务
+ *
+ * @return 品牌服务
+ */
+ WxChannelBrandService getBrandService();
+
+ /**
+ * 商品服务
+ *
+ * @return 商品服务
+ */
+ WxChannelProductService getProductService();
+
+ /**
+ * 仓库服务
+ *
+ * @return 仓库服务
+ */
+ WxChannelWarehouseService getWarehouseService();
+
+ /**
+ * 订单服务
+ *
+ * @return 订单服务
+ */
+ WxChannelOrderService getOrderService();
+
+ /**
+ * 售后服务
+ *
+ * @return 售后服务
+ */
+ WxChannelAfterSaleService getAfterSaleService();
+
+ /**
+ * 运费模板服务
+ *
+ * @return 运费模板服务
+ */
+ WxChannelFreightTemplateService getFreightTemplateService();
+
+ /**
+ * 地址服务
+ *
+ * @return 地址服务
+ */
+ WxChannelAddressService getAddressService();
+
+ /**
+ * 优惠券服务
+ *
+ * @return 优惠券服务
+ */
+ WxChannelCouponService getCouponService();
+
+ /**
+ * 分享员服务
+ *
+ * @return 分享员服务
+ */
+ WxChannelSharerService getSharerService();
+
+ /**
+ * 资金服务
+ *
+ * @return 资金服务
+ */
+ WxChannelFundService getFundService();
+
+ /**
+ * 主页管理服务
+ *
+ * @return 主页管理服务
+ */
+ WxStoreHomePageService getHomePageService();
+
+ /**
+ * 合作账号服务
+ *
+ * @return 团长合作服务
+ */
+ WxStoreCooperationService getCooperationService();
+
+ /**
+ * 视频号/微信小店 罗盘商家版服务
+ *
+ * @return 罗盘商家版服务
+ */
+ WxChannelCompassShopService getCompassShopService();
+
+ /**
+ * 优选联盟-团长合作达人管理服务
+ *
+ * @return 团长合作达人管理服务
+ */
+ WxLeagueWindowService getLeagueWindowService();
+
+ /**
+ * 代发管理服务
+ *
+ * @return 代发管理服务
+ */
+ WxChannelSupplierService getSupplierService();
+
+ /**
+ * 优选联盟-团长服务
+ *
+ * @return 团长服务
+ */
+ WxLeagueSupplierService getLeagueSupplierService();
+
+ /**
+ * 优选联盟-达人服务
+ *
+ * @return 达人服务
+ */
+ WxLeaguePromoterService getLeaguePromoterService();
+
+ /**
+ * 优选联盟-商品服务
+ *
+ * @return 商品服务
+ */
+ WxLeagueProductService getLeagueProductService();
+
+ /**
+ * 视频号助手 留资组件管理服务
+ *
+ * @return 留资组件管理服务
+ */
+ WxLeadComponentService getLeadComponentService();
+
+ /**
+ * 视频号助手 留资服务的直播数据服务
+ *
+ * @return 留资服务的直播数据服务
+ */
+ WxFinderLiveService getFinderLiveService();
+
+ /**
+ * 视频号助手 橱窗管理服务
+ *
+ * @return 橱窗管理服务
+ */
+ WxAssistantService getAssistantService();
+
+ /**
+ * 会员功能
+ *
+ * @return 会员服务
+ */
+ WxChannelVipService getVipService();
+
+ /**
+ * 视频号助手-罗盘达人版服务
+ *
+ * @return 罗盘达人版服务
+ */
+ WxChannelCompassFinderService getCompassFinderService();
+
+ /**
+ * 视频号助手-直播大屏数据服务
+ *
+ * @return 直播大屏数据服务
+ */
+ WxChannelLiveDashboardService getLiveDashboardService();
+
+ /**
+ * 质检管理服务.
+ *
+ * @return 质检管理服务
+ */
+ WxChannelQicService getQicService();
+
+ /**
+ * 微信小店-带货助手服务
+ *
+ * @return 带货助手服务
+ */
+ WxTalentService getTalentService();
+
+ /**
+ * 收藏管理服务
+ *
+ * @return 收藏管理服务
+ */
+ WxChannelFavoriteService getFavoriteService();
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSharerService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSharerService.java
new file mode 100644
index 0000000000..300493158b
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSharerService.java
@@ -0,0 +1,71 @@
+package me.chanjar.weixin.channel.api;
+
+import java.util.List;
+import me.chanjar.weixin.channel.bean.sharer.SharerBindResponse;
+import me.chanjar.weixin.channel.bean.sharer.SharerInfoResponse;
+import me.chanjar.weixin.channel.bean.sharer.SharerOrderParam;
+import me.chanjar.weixin.channel.bean.sharer.SharerOrderResponse;
+import me.chanjar.weixin.channel.bean.sharer.SharerSearchResponse;
+import me.chanjar.weixin.channel.bean.sharer.SharerUnbindResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 分享员服务接口
+ *
+ * @author Zeyes
+ */
+public interface WxChannelSharerService {
+
+ /**
+ * 邀请分享员
+ *
+ * @param username 邀请的用户微信号
+ * @return SharerBindResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ SharerBindResponse bindSharer(String username) throws WxErrorException;
+
+ /**
+ * 获取绑定的分享员
+ *
+ * @param openid 分享员openid
+ * @param username 分享员微信号(二选一)
+ * @return SharerSearchResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ SharerSearchResponse searchSharer(String openid, String username) throws WxErrorException;
+
+ /**
+ * 获取绑定的分享员列表
+ *
+ * @param page 分页参数,页数
+ * @param pageSize 分页参数,每页分享员数(不超过100
+ * @param sharerType 分享员类型
+ * @return 分享员列表
+ *
+ * @throws WxErrorException 异常
+ */
+ SharerInfoResponse listSharer(Integer page, Integer pageSize, Integer sharerType) throws WxErrorException;
+
+ /**
+ * 获取分享员订单列表
+ *
+ * @param param 参数
+ * @return 列表
+ *
+ * @throws WxErrorException 异常
+ */
+ SharerOrderResponse listSharerOrder(SharerOrderParam param) throws WxErrorException;
+
+ /**
+ * 解绑分享员
+ *
+ * @param openIds openid列表
+ * @return 状态
+ *
+ * @throws WxErrorException 异常
+ */
+ SharerUnbindResponse unbindSharer(List openIds) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSupplierService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSupplierService.java
new file mode 100644
index 0000000000..35e46b6dca
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelSupplierService.java
@@ -0,0 +1,70 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.supplier.DistributeTypeResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipAssignRequest;
+import me.chanjar.weixin.channel.bean.supplier.DropshipDetailResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipListRequest;
+import me.chanjar.weixin.channel.bean.supplier.DropshipListResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipSearchRequest;
+import me.chanjar.weixin.channel.bean.supplier.ProductDistributeRequest;
+import me.chanjar.weixin.channel.bean.supplier.ProductListResponse;
+import me.chanjar.weixin.channel.bean.supplier.SupplierInfoResponse;
+import me.chanjar.weixin.channel.bean.supplier.SupplierListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店代发管理服务。
+ *
+ * @author GitHub Copilot
+ * @see 代发管理接口文档
+ */
+public interface WxChannelSupplierService {
+
+ /** 获取供货商列表。 */
+ SupplierListResponse getSupplierList() throws WxErrorException;
+
+ /**
+ * 获取供货商列表。
+ *
+ * @param pageSize 每页数量
+ * @param nextKey 由上次请求返回,记录翻页的上下文。传入时会从上次返回的结果往后翻一页
+ * @return 供货商列表响应
+ * @throws WxErrorException 异常
+ */
+ SupplierListResponse getSupplierList(Integer pageSize, String nextKey) throws WxErrorException;
+
+ /** 获取分配方式。 */
+ DistributeTypeResponse getDistribute() throws WxErrorException;
+
+ /** 设置全店订单手动分配。 */
+ WxChannelBaseResponse setManuallyDistribute() throws WxErrorException;
+
+ /** 设置全店订单自动分配。 */
+ WxChannelBaseResponse setAllDistribute(String supplierId) throws WxErrorException;
+
+ /** 设置按商品自动分配。 */
+ WxChannelBaseResponse setProductDistribute(ProductDistributeRequest req) throws WxErrorException;
+
+ /** 获取商品对应的自动分配供货商。 */
+ SupplierInfoResponse getProductDefaultDistribute(String productId) throws WxErrorException;
+
+ /** 获取按商品自动分配的商品列表。 */
+ ProductListResponse getProductList(String supplierId) throws WxErrorException;
+
+ /** 分配订单代发。 */
+ DropshipResponse assignOrder(DropshipAssignRequest req) throws WxErrorException;
+
+ /** 取消分配代发单。 */
+ WxChannelBaseResponse cancelDropship(String orderId) throws WxErrorException;
+
+ /** 查询代发单详情。 */
+ DropshipDetailResponse getDropship(String orderId) throws WxErrorException;
+
+ /** 拉取代发单列表。 */
+ DropshipListResponse listDropship(DropshipListRequest req) throws WxErrorException;
+
+ /** 搜索代发单。 */
+ DropshipListResponse searchDropship(DropshipSearchRequest req) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelVipService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelVipService.java
new file mode 100644
index 0000000000..4100659200
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelVipService.java
@@ -0,0 +1,97 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.vip.VipInfoResponse;
+import me.chanjar.weixin.channel.bean.vip.VipListResponse;
+import me.chanjar.weixin.channel.bean.vip.VipScoreResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 会员功能接口
+ *
+ * @author aushiye
+ * @link 会员功能接口文档
+ */
+public interface WxChannelVipService {
+ /** 拉取用户详情 */
+ // String VIP_USER_INFO_URL = "https://api.weixin.qq.com/channels/ec/vip/user/info/get";
+ // /** 拉取用户列表 */
+ // String VIP_USER_LIST_URL = "https://api.weixin.qq.com/channels/ec/vip/user/list/get";
+ //
+ // /** 获取用户积分 */
+ // String VIP_SCORE_URL = "https://api.weixin.qq.com/channels/ec/vip/user/score/get";
+ // /** 增加用户积分 */
+ // String SCORE_INCREASE_URL = "https://api.weixin.qq.com/channels/ec/vip/user/score/increase";
+ // /** 减少用户积分 */
+ // String SCORE_DECREASE_URL = "https://api.weixin.qq.com/channels/ec/vip/user/score/decrease";
+ //
+ // /** 更新用户等级 */
+ // String GRADE_UPDATE_URL = "https://api.weixin.qq.com/channels/ec/vip/user/grade/update";
+
+
+ /**
+ * 获取用户详情
+ *
+ * @param openId the open id
+ * @param needPhoneNumber the need phone number
+ * @return the vip info
+ * @throws WxErrorException the wx error exception
+ */
+ VipInfoResponse getVipInfo(String openId, Boolean needPhoneNumber) throws WxErrorException;
+
+
+ /**
+ * 获取用户积分
+ *
+ * @param needPhoneNumber the need phone number
+ * @param pageNum the page num
+ * @param pageSize the page size
+ * @return the vip list
+ * @throws WxErrorException the wx error exception
+ */
+ VipListResponse getVipList(Boolean needPhoneNumber, Integer pageNum, Integer pageSize) throws WxErrorException;
+
+ /**
+ * 获取用户积分
+ *
+ * @param openId the open id
+ * @return the vip score
+ * @throws WxErrorException the wx error exception
+ */
+ VipScoreResponse getVipScore(String openId) throws WxErrorException;
+
+ /**
+ * 增加用户积分
+ *
+ * @param openId the open id
+ * @param score the score
+ * @param remark the remark
+ * @param requestId the request id
+ * @return the wx channel base response
+ * @throws WxErrorException the wx error exception
+ */
+ WxChannelBaseResponse increaseVipScore(String openId, String score, String remark, String requestId) throws WxErrorException;
+
+ /**
+ * 减少用户积分
+ *
+ * @param openId the open id
+ * @param score the score
+ * @param remark the remark
+ * @param requestId the request id
+ * @return the wx channel base response
+ * @throws WxErrorException the wx error exception
+ */
+ WxChannelBaseResponse decreaseVipScore(String openId, String score, String remark, String requestId) throws WxErrorException;
+
+ /**
+ * 更新用户等级
+ *
+ * @param openId the open id
+ * @param score the score
+ * @return the wx channel base response
+ * @throws WxErrorException the wx error exception
+ */
+ WxChannelBaseResponse updateVipGrade(String openId, Integer score) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelWarehouseService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelWarehouseService.java
new file mode 100644
index 0000000000..1bb00885f5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelWarehouseService.java
@@ -0,0 +1,137 @@
+package me.chanjar.weixin.channel.api;
+
+
+import java.util.List;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.warehouse.LocationPriorityResponse;
+import me.chanjar.weixin.channel.bean.warehouse.PriorityLocationParam;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseIdsResponse;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseLocation;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseParam;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseResponse;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseStockParam;
+import me.chanjar.weixin.channel.bean.warehouse.WarehouseStockResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+
+/**
+ * 视频号小店 区域仓库服务
+ *
+ * @author Zeyes
+ */
+public interface WxChannelWarehouseService {
+
+ /**
+ * 创建仓库
+ *
+ * @param param 仓库信息
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse createWarehouse(WarehouseParam param) throws WxErrorException;
+
+ /**
+ * 查询仓库列表
+ *
+ * @param pageSize 每页数量(最大不超过10)
+ * @param nextKey 由上次请求返回,记录翻页的上下文。传入时会从上次返回的结果往后翻一页,不传默认拉取第一页数据
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WarehouseIdsResponse listWarehouse(Integer pageSize, String nextKey) throws WxErrorException;
+
+ /**
+ * 获取仓库详情
+ *
+ * @param outWarehouseId 外部仓库ID
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WarehouseResponse getWarehouse(String outWarehouseId) throws WxErrorException;
+
+ /**
+ * 修改仓库详情
+ *
+ * @param outWarehouseId 外部仓库ID
+ * @param name 仓库名称
+ * @param intro 仓库介绍
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateWarehouse(String outWarehouseId, String name, String intro) throws WxErrorException;
+
+ /**
+ * 批量增加覆盖区域
+ *
+ * @param outWarehouseId 外部仓库ID
+ * @param coverLocations 覆盖区域
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addWarehouseArea(String outWarehouseId, List coverLocations)
+ throws WxErrorException;
+
+ /**
+ * 批量删除覆盖区域
+ *
+ * @param outWarehouseId 外部仓库ID
+ * @param coverLocations 覆盖区域
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse deleteWarehouseArea(String outWarehouseId, List coverLocations)
+ throws WxErrorException;
+
+ /**
+ * 设置指定地址下的仓的优先级
+ *
+ * @param param 参数
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse setWarehousePriority(PriorityLocationParam param) throws WxErrorException;
+
+ /**
+ * 获取指定地址下的仓的优先级
+ *
+ * @param addressId1 省份地址编码
+ * @param addressId2 市地址编码
+ * @param addressId3 区地址编码
+ * @param addressId4 街道地址编码
+ * @return 仓的优先级
+ *
+ * @throws WxErrorException 异常
+ */
+ LocationPriorityResponse getWarehousePriority(Integer addressId1, Integer addressId2, Integer addressId3,
+ Integer addressId4) throws WxErrorException;
+
+ /**
+ * 更新区域仓库存数量
+ *
+ * @param param 参数
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse updateWarehouseStock(WarehouseStockParam param) throws WxErrorException;
+
+ /**
+ * 获取区域仓库存数量
+ *
+ * @param productId 商品ID
+ * @param outWarehouseId 外部仓库ID
+ * @param skuId 商品skuId
+ * @return 响应
+ *
+ * @throws WxErrorException 异常
+ */
+ WarehouseStockResponse getWarehouseStock(String productId, String skuId, String outWarehouseId)
+ throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxFinderLiveService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxFinderLiveService.java
new file mode 100644
index 0000000000..6e98134bcb
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxFinderLiveService.java
@@ -0,0 +1,41 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.lead.component.request.GetFinderLiveDataListRequest;
+import me.chanjar.weixin.channel.bean.lead.component.request.GetFinderLiveLeadsDataRequest;
+import me.chanjar.weixin.channel.bean.lead.component.response.FinderAttrResponse;
+import me.chanjar.weixin.channel.bean.lead.component.response.GetFinderLiveDataListResponse;
+import me.chanjar.weixin.channel.bean.lead.component.response.GetFinderLiveLeadsDataResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号助手 留资服务的直播数据服务
+ *
+ * @author imyzt
+ */
+public interface WxFinderLiveService {
+
+ /**
+ * 获取视频号账号信息
+ *
+ * @return 视频号账号信息
+ */
+ FinderAttrResponse getFinderAttrByAppid() throws WxErrorException;
+
+ /**
+ * 获取留资直播间数据详情
+ *
+ * @param req 留资组件信息
+ * @return 留资信息详情
+ */
+ GetFinderLiveDataListResponse getFinderLiveDataList(GetFinderLiveDataListRequest req) throws WxErrorException;
+
+ /**
+ * 获取账号收集的留资数量
+ * 说明:该接口只统计2023.9.13号起的数据,所以start_time应大于等于1694534400
+ *
+ * @param req 留资组件信息
+ * @return 留资信息列表
+ */
+ GetFinderLiveLeadsDataResponse getFinderLiveLeadsData(GetFinderLiveLeadsDataRequest req) throws WxErrorException;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeadComponentService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeadComponentService.java
new file mode 100644
index 0000000000..36ae14bed3
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeadComponentService.java
@@ -0,0 +1,60 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.lead.component.request.GetLeadInfoByComponentRequest;
+import me.chanjar.weixin.channel.bean.lead.component.request.GetLeadsComponentIdRequest;
+import me.chanjar.weixin.channel.bean.lead.component.request.GetLeadsComponentPromoteRecordRequest;
+import me.chanjar.weixin.channel.bean.lead.component.request.GetLeadsInfoByRequestIdRequest;
+import me.chanjar.weixin.channel.bean.lead.component.request.GetLeadsRequestIdRequest;
+import me.chanjar.weixin.channel.bean.lead.component.response.GetLeadsComponentIdResponse;
+import me.chanjar.weixin.channel.bean.lead.component.response.GetLeadsComponentPromoteRecordResponse;
+import me.chanjar.weixin.channel.bean.lead.component.response.GetLeadsRequestIdResponse;
+import me.chanjar.weixin.channel.bean.lead.component.response.LeadInfoResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号助手 留资组件管理服务
+ *
+ * @author imyzt
+ */
+public interface WxLeadComponentService {
+
+ /**
+ * 按时间获取留资信息详情
+ *
+ * @param req 留资组件信息
+ * @return 留资信息详情
+ */
+ LeadInfoResponse getLeadsInfoByComponentId(GetLeadInfoByComponentRequest req) throws WxErrorException;
+
+ /**
+ * 按直播场次获取留资信息详情
+ *
+ * @param req 留资组件信息
+ * @return 留资信息详情
+ */
+ LeadInfoResponse getLeadsInfoByRequestId(GetLeadsInfoByRequestIdRequest req) throws WxErrorException;
+
+ /**
+ * 获取留资request_id列表详情
+ *
+ * @param req 留资组件信息
+ * @return 留资信息列表
+ */
+ GetLeadsRequestIdResponse getLeadsRequestId(GetLeadsRequestIdRequest req) throws WxErrorException;
+
+ /**
+ * 获取留资组件直播推广记录信息详情
+ *
+ * @param req 留资组件信息
+ * @return 留资组件直播推广记录信息详情
+ */
+ GetLeadsComponentPromoteRecordResponse getLeadsComponentPromoteRecord(GetLeadsComponentPromoteRecordRequest req) throws WxErrorException;
+
+ /**
+ * 获取留资组件Id列表详情
+ *
+ * @param req 留资组件信息
+ * @return 留资组件Id列表
+ */
+ GetLeadsComponentIdResponse getLeadsComponentId(GetLeadsComponentIdRequest req) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueProductService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueProductService.java
new file mode 100644
index 0000000000..d8d6781505
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueProductService.java
@@ -0,0 +1,62 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.league.product.BatchAddParam;
+import me.chanjar.weixin.channel.bean.league.product.BatchAddResponse;
+import me.chanjar.weixin.channel.bean.league.product.ProductDetailParam;
+import me.chanjar.weixin.channel.bean.league.product.ProductDetailResponse;
+import me.chanjar.weixin.channel.bean.league.product.ProductListParam;
+import me.chanjar.weixin.channel.bean.league.product.ProductListResponse;
+import me.chanjar.weixin.channel.bean.league.product.ProductUpdateParam;
+import me.chanjar.weixin.channel.bean.league.product.ProductUpdateResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 优选联盟 商品操作服务
+ *
+ * @author Zeyes
+ */
+public interface WxLeagueProductService {
+
+ /**
+ * 批量新增联盟商品
+ *
+ * @param param 参数
+ * @return 结果
+ */
+ BatchAddResponse batchAddProduct(BatchAddParam param) throws WxErrorException;
+
+ /**
+ * 更新联盟商品信息
+ *
+ * @param param 参数
+ * @return 结果
+ */
+ ProductUpdateResponse updateProduct(ProductUpdateParam param) throws WxErrorException;
+
+ /**
+ * 删除联盟商品
+ *
+ * @param type 1普通推广商品 2定向推广商品 3专属推广商品
+ * @param productId 商品id type为普通推广商品时必填
+ * @param infoId 特殊推广商品计划id type为特殊推广商品时必填
+ * @return
+ */
+ WxChannelBaseResponse deleteProduct(Integer type, String productId, String infoId) throws WxErrorException;
+
+ /**
+ * 拉取联盟商品详情
+ *
+ * @param param 参数
+ * @return 结果
+ */
+ ProductDetailResponse getProductDetail(ProductDetailParam param) throws WxErrorException;
+
+ /**
+ * 拉取联盟商品推广列表
+ *
+ * @param param 参数
+ * @return 结果
+ */
+ ProductListResponse listProduct(ProductListParam param) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeaguePromoterService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeaguePromoterService.java
new file mode 100644
index 0000000000..60cf112271
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeaguePromoterService.java
@@ -0,0 +1,98 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.league.promoter.PromoterInfoResponse;
+import me.chanjar.weixin.channel.bean.league.promoter.PromoterListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 优选联盟 达人服务
+ *
+ * @author Zeyes
+ */
+public interface WxLeaguePromoterService {
+
+ /**
+ * 新增达人
+ *
+ * @param finderId 视频号finder_id,待废除
+ * @return 结果
+ * @deprecated 使用 {@link #addPromoterV2(String)}
+ */
+ @Deprecated
+ WxChannelBaseResponse addPromoter(String finderId) throws WxErrorException;
+
+ /**
+ * 编辑达人
+ *
+ * @param finderId 视频号finder_id,待废除
+ * @param type 操作 1取消邀请 2结束合作
+ * @return 结果
+ * @deprecated 使用 {@link #updatePromoterV2(String, int)}
+ */
+ @Deprecated
+ WxChannelBaseResponse updatePromoter(String finderId, int type) throws WxErrorException;
+
+ /**
+ * 删除达人
+ *
+ * @param finderId 视频号finder_id,待废除
+ * @return 结果
+ * @deprecated 使用 {@link #deletePromoterV2(String)}
+ */
+ @Deprecated
+ WxChannelBaseResponse deletePromoter(String finderId) throws WxErrorException;
+
+ /**
+ * 获取达人详情信息
+ *
+ * @param finderId 视频号finder_id,待废除
+ * @return 结果
+ * @deprecated 使用 {@link #getPromoterInfoV2(String)}
+ */
+ @Deprecated
+ PromoterInfoResponse getPromoterInfo(String finderId) throws WxErrorException;
+
+ /**
+ * 新增达人
+ *
+ * @param promoterId 达人带货id
+ * @return 结果
+ */
+ WxChannelBaseResponse addPromoterV2(String promoterId) throws WxErrorException;
+
+ /**
+ * 编辑达人
+ *
+ * @param promoterId 达人带货id
+ * @param type 操作 1取消邀请 2结束合作
+ * @return 结果
+ */
+ WxChannelBaseResponse updatePromoterV2(String promoterId, int type) throws WxErrorException;
+
+ /**
+ * 删除达人
+ *
+ * @param promoterId 达人带货id
+ * @return 结果
+ */
+ WxChannelBaseResponse deletePromoterV2(String promoterId) throws WxErrorException;
+
+ /**
+ * 获取达人详情信息
+ *
+ * @param promoterId 达人带货id
+ * @return 结果
+ */
+ PromoterInfoResponse getPromoterInfoV2(String promoterId) throws WxErrorException;
+
+ /**
+ * 获取达人列表
+ *
+ * @param pageIndex 页面下标,下标从1开始,默认为1
+ * @param pageSize 单页达人数(不超过200)
+ * @param status 拉取该状态下的达人列表
+ * @return 结果
+ */
+ PromoterListResponse listPromoter(Integer pageIndex, Integer pageSize, Integer status) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueSupplierService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueSupplierService.java
new file mode 100644
index 0000000000..cde96843f1
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueSupplierService.java
@@ -0,0 +1,98 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.league.supplier.CommissionOrderListParam;
+import me.chanjar.weixin.channel.bean.league.supplier.CommissionOrderListResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.CommissionOrderResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.CoopProductListResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.CoopProductResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.FlowListParam;
+import me.chanjar.weixin.channel.bean.league.supplier.ShopDetailResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.ShopListResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.SupplierBalanceResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.SupplierFlowDetailResponse;
+import me.chanjar.weixin.channel.bean.league.supplier.SupplierFlowListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 优选联盟 团长数据服务
+ *
+ * @author Zeyes
+ */
+public interface WxLeagueSupplierService {
+
+ /**
+ * 获取团长账户余额
+ *
+ * @return 余额
+ */
+ SupplierBalanceResponse getBalanceInfo() throws WxErrorException;
+
+ /**
+ * 获取资金流水详情
+ *
+ * @param flowId 流水ID
+ * @return 流水详情
+ */
+ SupplierFlowDetailResponse getFlowDetail(String flowId) throws WxErrorException;
+
+ /**
+ * 获取团长资金流水列表
+ *
+ * @param param 查询参数
+ * @return 流水列表
+ */
+ SupplierFlowListResponse getFlowList(FlowListParam param) throws WxErrorException;
+
+ /**
+ * 获取合作商品详情
+ *
+ * @param productId 商品ID
+ * @param appId 团长商品 所属小店appid
+ * @return 商品详情
+ */
+ CoopProductResponse getProductDetail(String productId, String appId) throws WxErrorException;
+
+ /**
+ * 获取合作商品列表
+ *
+ * @param appid 团长商品 所属小店appid
+ * @param pageSize 单页商品数(不超过30)
+ * @param nextKey 由上次请求返回,顺序翻页时需要传入, 会从上次返回的结果往后翻一页
+ * @return 商品列表
+ */
+ CoopProductListResponse getProductList(String appid, Integer pageSize, String nextKey) throws WxErrorException;
+
+ /**
+ * 获取佣金单详情
+ *
+ * @param orderId 订单号,可从获取佣金单列表中获得
+ * @param skuId 商品skuId
+ * @return 订单详情
+ */
+ CommissionOrderResponse getCommissionOrder(String orderId, String skuId) throws WxErrorException;
+
+ /**
+ * 获取佣金单列表
+ *
+ * @param param 查询参数
+ * @return 佣金单列表
+ */
+ CommissionOrderListResponse getCommissionOrderList(CommissionOrderListParam param) throws WxErrorException;
+
+ /**
+ * 获取合作小店详情
+ *
+ * @param appid 小店appid
+ * @return 小店详情
+ */
+ ShopDetailResponse getShopDetail(String appid) throws WxErrorException;
+
+ /**
+ * 获取合作小店列表
+ *
+ * @param pageSize 单页小店数(不超过30)
+ * @param nextKey 由上次请求返回,顺序翻页时需要传入, 会从上次返回的结果往后翻一页
+ * @return 小店列表
+ */
+ ShopListResponse getShopList(Integer pageSize, String nextKey) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueWindowService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueWindowService.java
new file mode 100644
index 0000000000..c4af1571d0
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxLeagueWindowService.java
@@ -0,0 +1,72 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.league.window.AuthInfoResponse;
+import me.chanjar.weixin.channel.bean.league.window.AuthStatusResponse;
+import me.chanjar.weixin.channel.bean.league.window.ProductSearchParam;
+import me.chanjar.weixin.channel.bean.league.window.WindowProductListResponse;
+import me.chanjar.weixin.channel.bean.league.window.WindowProductResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 优选联盟 团长合作达人管理服务
+ *
+ * @author Zeyes
+ */
+public interface WxLeagueWindowService {
+
+ /**
+ * 添加团长商品到橱窗
+ *
+ * @param appid 团长appid
+ * @param openfinderid 视频号openfinderid
+ * @param productId 团长商品ID
+ * @return 结果
+ */
+ WxChannelBaseResponse addProduct(String appid, String openfinderid, String productId) throws WxErrorException;
+
+ /**
+ * 查询橱窗上团长商品列表
+ *
+ * @param param 查询参数
+ * @return 团长商品列表
+ */
+ WindowProductListResponse listProduct(ProductSearchParam param) throws WxErrorException;
+
+ /**
+ * 从橱窗移除团长商品
+ *
+ * @param appid 团长appid
+ * @param openfinderid 视频号openfinderid
+ * @param productId 团长商品ID
+ * @return 结果
+ */
+ WxChannelBaseResponse removeProduct(String appid, String openfinderid, String productId) throws WxErrorException;
+
+ /**
+ * 查询橱窗上团长商品详情
+ *
+ * @param appid 团长appid
+ * @param openfinderid 视频号openfinderid
+ * @param productId 团长商品ID
+ * @return 结果
+ */
+ WindowProductResponse getProductDetail(String appid, String openfinderid, String productId)
+ throws WxErrorException;
+
+ /**
+ * 获取达人橱窗授权链接
+ *
+ * @param finderId 视频号finder_id
+ * @return 授权链接
+ */
+ AuthInfoResponse getWindowAuthInfo(String finderId) throws WxErrorException;
+
+ /**
+ * 获取达人橱窗授权状态
+ *
+ * @param finderId 视频号finder_id
+ * @return 授权链接
+ */
+ AuthStatusResponse getWindowAuthStatus(String finderId) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreCooperationService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreCooperationService.java
new file mode 100644
index 0000000000..96d2ff5f8d
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreCooperationService.java
@@ -0,0 +1,70 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.cooperation.CooperationListResponse;
+import me.chanjar.weixin.channel.bean.cooperation.CooperationQrCodeResponse;
+import me.chanjar.weixin.channel.bean.cooperation.CooperationStatusResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信小店 合作账号相关接口
+ *
+ * @author Zeyes
+ * @see 合作账号状态机
+ */
+public interface WxStoreCooperationService {
+
+ /**
+ * 获取合作账号列表
+ *
+ * @param sharerType 合作账号类型 2公众号 3小程序
+ * @return 合作账号列表
+ *
+ * @throws WxErrorException 异常
+ */
+ CooperationListResponse listCooperation(Integer sharerType) throws WxErrorException;
+
+ /**
+ * 获取合作账号状态
+ *
+ * @param sharerId 合作账号id 公众号: gh_开头id 小程序: appid
+ * @param sharerType 合作账号类型 2公众号 3小程序
+ * @return 合作账号状态
+ *
+ * @throws WxErrorException 异常
+ */
+ CooperationStatusResponse getCooperationStatus(String sharerId, Integer sharerType) throws WxErrorException;
+
+ /**
+ * 生成合作账号邀请二维码
+ *
+ * @param sharerId 合作账号id 公众号: gh_开头id 小程序: appid
+ * @param sharerType 合作账号类型 2公众号 3小程序
+ * @return 二维码
+ *
+ * @throws WxErrorException 异常
+ */
+ CooperationQrCodeResponse generateQrCode(String sharerId, Integer sharerType) throws WxErrorException;
+
+ /**
+ * 取消合作账号邀请
+ *
+ * @param sharerId 合作账号id 公众号: gh_开头id 小程序: appid
+ * @param sharerType 合作账号类型 2公众号 3小程序
+ * @return WxChannelBaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelInvitation(String sharerId, Integer sharerType) throws WxErrorException;
+
+ /**
+ * 解绑合作账号
+ *
+ * @param sharerId 合作账号id 公众号: gh_开头id 小程序: appid
+ * @param sharerType 合作账号类型 2公众号 3小程序
+ * @return WxChannelBaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse unbind(String sharerId, Integer sharerType) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreHomePageService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreHomePageService.java
new file mode 100644
index 0000000000..bd11e471b3
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxStoreHomePageService.java
@@ -0,0 +1,188 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.home.background.BackgroundApplyResponse;
+import me.chanjar.weixin.channel.bean.home.background.BackgroundGetResponse;
+import me.chanjar.weixin.channel.bean.home.banner.BannerApplyResponse;
+import me.chanjar.weixin.channel.bean.home.banner.BannerGetResponse;
+import me.chanjar.weixin.channel.bean.home.banner.BannerInfo;
+import me.chanjar.weixin.channel.bean.home.tree.TreeProductEditInfo;
+import me.chanjar.weixin.channel.bean.home.tree.TreeProductListInfo;
+import me.chanjar.weixin.channel.bean.home.tree.TreeProductListResponse;
+import me.chanjar.weixin.channel.bean.home.tree.TreeShowGetResponse;
+import me.chanjar.weixin.channel.bean.home.tree.TreeShowInfo;
+import me.chanjar.weixin.channel.bean.home.tree.TreeShowSetResponse;
+import me.chanjar.weixin.channel.bean.home.window.WindowProductSettingResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信小店 主页管理相关接口
+ *
+ * @author Zeyes
+ */
+public interface WxStoreHomePageService {
+
+ /**
+ * 添加分类关联的商品
+ *
+ * @param info 商品分类以及商品id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse addTreeProduct(TreeProductEditInfo info) throws WxErrorException;
+
+ /**
+ * 删除分类关联的商品
+ *
+ * @param info 商品分类以及商品id
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse delTreeProduct(TreeProductEditInfo info) throws WxErrorException;
+
+ /**
+ * 获取分类关联的商品ID列表
+ *
+ * @param info 分类id、分页大小、分页上下文
+ * @return 商品id、分页上下文
+ *
+ * @throws WxErrorException 异常
+ */
+ TreeProductListResponse getTreeProductList(TreeProductListInfo info) throws WxErrorException;
+
+ /**
+ * 设置展示在店铺主页的商品分类
+ *
+ * @param info 分类id
+ * @return 商品分类审核结果
+ *
+ * @throws WxErrorException 异常
+ */
+ TreeShowSetResponse setShowTree(TreeShowInfo info) throws WxErrorException;
+
+ /**
+ * 获取展示在店铺主页的商品分类
+ *
+ * @return 商品分类信息
+ *
+ * @throws WxErrorException 异常
+ */
+ TreeShowGetResponse getShowTree() throws WxErrorException;
+
+ /**
+ * 获取主页展示商品列表
+ *
+ * @param pageSize 分页大小
+ * @param nextKey 分页上下文
+ * @return WindowProductSettingResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WindowProductSettingResponse listWindowProduct(Integer pageSize, String nextKey) throws WxErrorException;
+
+ /**
+ * 删除主页展示商品
+ *
+ * @param productId 商品id
+ * @param indexNum 商品重新排序后的新序号,最大移动步长为500(即新序号与当前序号的距离小于500)
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse reorderWindowProduct(String productId, Integer indexNum) throws WxErrorException;
+
+ /**
+ * 隐藏小店主页商品
+ *
+ * @param productId 商品id
+ * @param setHide 是否隐藏。1-隐藏,0-取消隐藏
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse hideWindowProduct(String productId, Integer setHide) throws WxErrorException;
+
+ /**
+ * 置顶小店主页商品
+ *
+ * @param productId 商品id
+ * @param setTop 是否顶置。1-置顶,0-取消置顶
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse topWindowProduct(String productId, Integer setTop) throws WxErrorException;
+
+ /**
+ * 提交背景图申请
+ *
+ * @param imgUrl 图片链接。请务必使用接口上传图片(参数resp_type=1),并将返回的img_url填入此处,不接受其他任何格式的图片url。
+ * 若url曾经做过转换(url前缀为mmecimage.cn/p/),则可以直接提交。
+ * @return 申请编号
+ *
+ * @throws WxErrorException 异常
+ * @see WxChannelBasicService#uploadImg(int, String)
+ */
+ BackgroundApplyResponse applyBackground(String imgUrl) throws WxErrorException;
+
+ /**
+ * 查询背景图
+ *
+ * @return 背景图信息
+ * @throws WxErrorException 异常
+ */
+ BackgroundGetResponse getBackground() throws WxErrorException;
+
+ /**
+ * 撤销主页背景图申请
+ *
+ * @param applyId 申请编号
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelBackground(Integer applyId) throws WxErrorException;
+
+ /**
+ * 清空主页背景图并撤销流程中的申请
+ *
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse removeBackground() throws WxErrorException;
+
+ /**
+ * 提交精选展示位申请
+ *
+ * @param info 展示位信息
+ * @return 申请编号
+ * @throws WxErrorException 异常
+ */
+ BannerApplyResponse applyBanner(BannerInfo info) throws WxErrorException;
+
+ /**
+ * 查询精选展示位
+ *
+ * @return 展示位信息
+ * @throws WxErrorException 异常
+ */
+ BannerGetResponse getBanner() throws WxErrorException;
+
+ /**
+ * 撤销精选展示位申请
+ *
+ * @param applyId 申请编号
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse cancelBanner(Integer applyId) throws WxErrorException;
+
+ /**
+ * 清空精选展示位并撤销流程中的申请
+ *
+ * @return BaseResponse
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse removeBanner() throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java
new file mode 100644
index 0000000000..e03cdbf502
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java
@@ -0,0 +1,56 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信小店-带货助手服务接口
+ *
+ * @author GitHub Copilot
+ */
+public interface WxTalentService {
+
+ /**
+ * 获取佣金单列表
+ *
+ * @param param 查询参数
+ * @return 佣金单列表
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentOrderListResponse getOrderList(TalentOrderListParam param) throws WxErrorException;
+
+ /**
+ * 获取佣金单详情
+ *
+ * @param param 查询参数
+ * @return 佣金单详情
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentOrderDetailResponse getOrderDetail(TalentOrderDetailParam param) throws WxErrorException;
+
+ /**
+ * 获取达人橱窗商品列表
+ *
+ * @param param 查询参数
+ * @return 橱窗商品列表
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentWindowProductListResponse getWindowProductList(TalentWindowProductListParam param) throws WxErrorException;
+
+ /**
+ * 获取达人橱窗商品详情
+ *
+ * @param param 查询参数
+ * @return 橱窗商品详情
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentWindowProductDetailResponse getWindowProductDetail(TalentWindowProductDetailParam param)
+ throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelMessageServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelMessageServiceImpl.java
new file mode 100644
index 0000000000..0aeabdd7c6
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelMessageServiceImpl.java
@@ -0,0 +1,419 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.channel.api.BaseWxChannelMessageService;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.bean.message.after.AfterSaleMessage;
+import me.chanjar.weixin.channel.bean.message.after.ComplaintMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.CouponActionMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.CouponReceiveMessage;
+import me.chanjar.weixin.channel.bean.message.coupon.UserCouponExpireMessage;
+import me.chanjar.weixin.channel.bean.message.fund.AccountNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.fund.QrNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.fund.WithdrawNotifyMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderCancelMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderConfirmMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderDeliveryMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderExtMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderIdMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderPayMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderSettleMessage;
+import me.chanjar.weixin.channel.bean.message.order.OrderStatusMessage;
+import me.chanjar.weixin.channel.bean.message.product.BrandMessage;
+import me.chanjar.weixin.channel.bean.message.product.CategoryAuditMessage;
+import me.chanjar.weixin.channel.bean.message.product.SpuAuditMessage;
+import me.chanjar.weixin.channel.bean.message.product.SpuStockMessage;
+import me.chanjar.weixin.channel.bean.message.sharer.SharerChangeMessage;
+import me.chanjar.weixin.channel.bean.message.store.CloseStoreMessage;
+import me.chanjar.weixin.channel.bean.message.store.NicknameUpdateMessage;
+import me.chanjar.weixin.channel.bean.message.supplier.SupplierItemMessage;
+import me.chanjar.weixin.channel.bean.message.vip.ExchangeInfoMessage;
+import me.chanjar.weixin.channel.bean.message.vip.UserInfoMessage;
+import me.chanjar.weixin.channel.bean.message.voucher.VoucherMessage;
+import me.chanjar.weixin.channel.message.WxChannelMessage;
+import me.chanjar.weixin.channel.message.WxChannelMessageRouter;
+import me.chanjar.weixin.channel.message.WxChannelMessageRouterRule;
+import me.chanjar.weixin.channel.message.rule.HandlerConsumer;
+import me.chanjar.weixin.channel.util.JsonUtils;
+import me.chanjar.weixin.common.session.WxSessionManager;
+
+import static me.chanjar.weixin.channel.constant.MessageEventConstants.*;
+
+/**
+ * @author Zeyes
+ */
+@Slf4j
+public abstract class BaseWxChannelMessageServiceImpl implements BaseWxChannelMessageService {
+
+ /** 消息路由器 */
+ protected WxChannelMessageRouter router;
+
+ public BaseWxChannelMessageServiceImpl(WxChannelMessageRouter router) {
+ this.router = router;
+ this.addDefaultRule();
+ }
+
+ /**
+ * 添加默认的回调规则
+ */
+ protected void addDefaultRule() {
+ /* 品牌资质事件回调 */
+ this.addRule(BrandMessage.class, BRAND, this::brandUpdate);
+ /* 商品审核结果 */
+ this.addRule(SpuAuditMessage.class, PRODUCT_SPU_AUDIT, this::spuAudit);
+ /* 商品上下架 */
+ this.addRule(SpuAuditMessage.class, PRODUCT_SPU_STATUS_UPDATE, this::spuStatusUpdate);
+ /* 商品更新 */
+ this.addRule(SpuAuditMessage.class, PRODUCT_SPU_UPDATE, this::spuUpdate);
+ /* 商品库存不足 */
+ this.addRule(SpuStockMessage.class, PRODUCT_STOCK_NO_ENOUGH, this::stockNoEnough);
+ /* 类目审核结果 */
+ this.addRule(CategoryAuditMessage.class, PRODUCT_CATEGORY_AUDIT, this::categoryAudit);
+ /* 订单下单 */
+ this.addRule(OrderIdMessage.class, ORDER_NEW, this::orderNew);
+ /* 订单取消 */
+ this.addRule(OrderCancelMessage.class, ORDER_CANCEL, this::orderCancel);
+ /* 订单支付成功 */
+ this.addRule(OrderPayMessage.class, ORDER_PAY, this::orderPay);
+ /* 订单待发货 */
+ this.addRule(OrderIdMessage.class, ORDER_WAIT_SHIPPING, this::orderWaitShipping);
+ /* 订单发货 */
+ this.addRule(OrderDeliveryMessage.class, ORDER_DELIVER, this::orderDelivery);
+ /* 订单确认收货 */
+ this.addRule(OrderConfirmMessage.class, ORDER_CONFIRM, this::orderConfirm);
+ /* 订单结算成功 */
+ this.addRule(OrderSettleMessage.class, ORDER_SETTLE, this::orderSettle);
+ /* 订单其他信息更新 */
+ this.addRule(OrderExtMessage.class, ORDER_EXT_INFO_UPDATE, this::orderExtInfoUpdate);
+ /* 订单状态更新 */
+ this.addRule(OrderStatusMessage.class, ORDER_STATUS_UPDATE, this::orderStatusUpdate);
+ /* 售后单更新通知 */
+ this.addRule(AfterSaleMessage.class, AFTER_SALE_UPDATE, this::afterSaleStatusUpdate);
+ /* 纠纷更新通知 */
+ this.addRule(ComplaintMessage.class, COMPLAINT_NOTIFY, this::complaintNotify);
+ /* 优惠券领取通知 */
+ this.addRule(CouponReceiveMessage.class, RECEIVE_COUPON, this::couponReceive);
+ /* 优惠券使用通知 */
+ this.addRule(CouponActionMessage.class, CREATE_COUPON, this::couponCreate);
+ /* 优惠券删除通知 */
+ this.addRule(CouponActionMessage.class, DELETE_COUPON, this::couponDelete);
+ /* 优惠券过期通知 */
+ this.addRule(CouponActionMessage.class, EXPIRE_COUPON, this::couponExpire);
+ /* 更新优惠券信息通知 */
+ this.addRule(CouponActionMessage.class, UPDATE_COUPON_INFO, this::couponUpdate);
+ /* 更新优惠券信息通知 */
+ this.addRule(CouponActionMessage.class, INVALID_COUPON, this::couponInvalid);
+ /* 用户优惠券过期通知 */
+ this.addRule(UserCouponExpireMessage.class, USER_COUPON_EXPIRE, this::userCouponExpire);
+ /* 用户优惠券过期通知 */
+ this.addRule(UserCouponExpireMessage.class, USER_COUPON_UNUSE, this::userCouponUnuse);
+ /* 优惠券返还通知 */
+ this.addRule(UserCouponExpireMessage.class, USER_COUPON_USE, this::userCouponUse);
+ /* 发放团购优惠成功通知 */
+ this.addRule(VoucherMessage.class, VOUCHER_SEND_SUCC, this::voucherSendSucc);
+ /* 结算账户变更回调 */
+ this.addRule(AccountNotifyMessage.class, ACCOUNT_NOTIFY, this::accountNotify);
+ /* 提现回调 */
+ this.addRule(WithdrawNotifyMessage.class, WITHDRAW_NOTIFY, this::withdrawNotify);
+ /* 提现二维码回调 */
+ this.addRule(QrNotifyMessage.class, QRCODE_STATUS, this::qrNotify);
+ /* 团长 */
+ this.addRule(SupplierItemMessage.class, SUPPLIER_ITEM_UPDATE, this::supplierItemUpdate);
+
+ /* 用户加入会员 */
+ this.addRule(UserInfoMessage.class, USER_VIP_JOIN, false, this::vipJoin);
+ /* 用户注销会员 */
+ this.addRule(UserInfoMessage.class, USER_VIP_CLOSE,false, this::vipClose);
+ /* 用户等级信息更新 */
+ this.addRule(UserInfoMessage.class, USER_VIP_GRADE_INFO_UPDATE, false, this::vipGradeUpdate);
+ /* 用户积分更新 */
+ this.addRule(UserInfoMessage.class, USER_VIP_SCORE_UPDATE, false, this::vipScoreUpdate);
+ /* 用户积分兑换 */
+ this.addRule(ExchangeInfoMessage.class, USER_VIP_SCORE_EXCHANGE, false, this::vipScoreExchange);
+
+ /* 分享员变更 */
+ this.addRule(SharerChangeMessage.class,SHARER_CHANGE,false,this::sharerChange);
+
+ /* 小店注销 */
+ this.addRule(CloseStoreMessage.class, CLOSE_STORE, this::closeStore);
+ /* 小店修改名称 */
+ this.addRule(NicknameUpdateMessage.class, SET_SHOP_NICKNAME, this::updateNickname);
+ }
+
+ /**
+ * 添加一条规则进入路由器
+ *
+ * @param clazz 消息类型
+ * @param event 事件类型
+ * @param consumer 处理器
+ * @param 消息类型
+ */
+ protected void addRule(Class clazz, String event, Boolean async,
+ HandlerConsumer, WxSessionManager> consumer) {
+ WxChannelMessageRouterRule rule = new WxChannelMessageRouterRule<>();
+ rule.setMessageClass(clazz).setEvent(event).setAsync(async);
+ rule.getHandlers().add((message, content, appId, context, sessionManager) -> {
+ consumer.accept(message, content, appId, context, sessionManager);
+ return "success";
+ });
+ rule.setNext(true);
+ this.addRule(rule);
+ }
+
+ protected void addRule(Class clazz, String event,
+ HandlerConsumer, WxSessionManager> consumer) {
+ this.addRule(clazz, event, true, consumer);
+ }
+
+ @Override
+ public void addRule(WxChannelMessageRouterRule extends WxChannelMessage> rule) {
+ router.getRules().add(rule);
+ }
+
+ @Override
+ public Object route(WxChannelMessage message, String content, String appId, final WxChannelService service) {
+ return router.route(message, content, appId, service);
+ }
+
+
+ @Override
+ public void orderNew(OrderIdMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单下单:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderCancel(OrderCancelMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单取消:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderPay(OrderPayMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单支付成功:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderWaitShipping(OrderIdMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单待发货:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderDelivery(OrderDeliveryMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单发货:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderConfirm(OrderConfirmMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单确认收货:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderSettle(OrderSettleMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单结算:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderExtInfoUpdate(OrderExtMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单其他信息更新:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void orderStatusUpdate(OrderStatusMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("订单状态更新:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void spuAudit(SpuAuditMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("商品审核:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void spuStatusUpdate(SpuAuditMessage message, String content, String appId,
+ Map context, WxSessionManager sessionManager) {
+ log.info("商品状态更新:{}", JsonUtils.encode(message));
+ }
+
+ @Override
+ public void spuUpdate(SpuAuditMessage message, String content, String appId,
+ Map