🆕 #3703 【企业微信】第三方应用基础接口实现

This commit is contained in:
graalvm-samples
2025-09-23 00:25:39 +08:00
committed by GitHub
parent 213cf6fd67
commit 88bdd4a36e
35 changed files with 1730 additions and 492 deletions

15
pom.xml
View File

@@ -468,6 +468,21 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version> <!-- 建议使用最新版本 -->
<executions>
<execution>
<id>attach-sources</id>
<!-- 绑定到 verify 阶段,在 install 之前执行 -->
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal> <!-- 使用 jar-no-fork 效率更高 -->
</goals>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>

View File

@@ -26,6 +26,7 @@
<module>wx-java-open-spring-boot-starter</module> <module>wx-java-open-spring-boot-starter</module>
<module>wx-java-qidian-spring-boot-starter</module> <module>wx-java-qidian-spring-boot-starter</module>
<module>wx-java-cp-multi-spring-boot-starter</module> <module>wx-java-cp-multi-spring-boot-starter</module>
<module>wx-java-cp-tp-multi-spring-boot-starter</module>
<module>wx-java-cp-spring-boot-starter</module> <module>wx-java-cp-spring-boot-starter</module>
<module>wx-java-channel-spring-boot-starter</module> <module>wx-java-channel-spring-boot-starter</module>
<module>wx-java-channel-multi-spring-boot-starter</module> <module>wx-java-channel-multi-spring-boot-starter</module>

View File

@@ -0,0 +1,97 @@
# wx-java-cp-multi-spring-boot-starter
企业微信多账号配置
- 实现多 WxCpService 初始化。
- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
## 快速开始
1. 引入依赖
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-cp-multi-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
```
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 ...
}
}
```

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.7.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wx-java-cp-tp-multi-spring-boot-starter</artifactId>
<name>WxJava - Spring Boot Starter for WxCp::支持多账号配置</name>
<description>微信企业号开发的 Spring Boot Starter::支持多账号配置</description>
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-cp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<String, WxCpTpSingleProperties> corps = wxCpTpMultiProperties.getCorps();
if (corps == null || corps.isEmpty()) {
log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpTpService(\"tenantId\")获取实例将返回空");
return new WxCpTpMultiServicesImpl();
}
if (services == null) {
services = new WxCpTpMultiServicesImpl();
}
Set<Map.Entry<String, WxCpTpSingleProperties>> entries = corps.entrySet();
for (Map.Entry<String, WxCpTpSingleProperties> 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);
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, WxCpTpSingleProperties> 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 请求最大重试次数
* <pre>
* {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
* {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
* </pre>
*/
private int maxRetryTimes = 5;
/**
* http 请求重试间隔
* <pre>
* {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
* {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
* </pre>
*/
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
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<String, WxCpTpService> 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);
}
}

View File

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

View File

@@ -0,0 +1 @@
com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration

View File

@@ -793,4 +793,6 @@ public class WxCpTpXmlMessage implements Serializable {
log.debug("解密后的原始xml消息内容{}", plainText); log.debug("解密后的原始xml消息内容{}", plainText);
return fromXml(plainText); return fromXml(plainText);
} }
} }

View File

@@ -130,7 +130,7 @@ public interface WxCpTpConfigStorage {
* @return the aes key * @return the aes key
*/ */
//第三方应用的EncodingAESKey用来检查签名 //第三方应用的EncodingAESKey用来检查签名
String getAesKey(); String getEncodingAESKey();
/** /**
* 企微服务商企业ID & 企业secret * 企微服务商企业ID & 企业secret

View File

@@ -0,0 +1,431 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.Builder;
import lombok.NonNull;
import lombok.Setter;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import me.chanjar.weixin.common.redis.WxRedisOps;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.bean.WxCpProviderToken;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RedissonClient;
import java.io.File;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 企业微信各种固定、授权配置的Redisson存储实现
*/
public abstract class AbstractWxCpTpInRedisConfigImpl extends WxCpTpDefaultConfigImpl implements Serializable {
private static final long serialVersionUID = -5385639031981770319L;
public AbstractWxCpTpInRedisConfigImpl(@NonNull WxRedisOps wxRedisOps) {
this(wxRedisOps, null);
}
public AbstractWxCpTpInRedisConfigImpl(@NonNull WxRedisOps wxRedisOps, String keyPrefix) {
this.wxRedisOps = wxRedisOps;
this.keyPrefix = keyPrefix;
}
/**
* The constant LOCK_KEY.
*/
// lock key
protected static final String LOCK_KEY = "wechat_tp_lock:";
/**
* The constant LOCKER_PROVIDER_ACCESS_TOKEN.
*/
protected static final String LOCKER_PROVIDER_ACCESS_TOKEN = "providerAccessTokenLock";
/**
* The constant LOCKER_SUITE_ACCESS_TOKEN.
*/
protected static final String LOCKER_SUITE_ACCESS_TOKEN = "suiteAccessTokenLock";
/**
* The constant LOCKER_ACCESS_TOKEN.
*/
protected static final String LOCKER_ACCESS_TOKEN = "accessTokenLock";
/**
* The constant LOCKER_CORP_JSAPI_TICKET.
*/
protected static final String LOCKER_CORP_JSAPI_TICKET = "corpJsapiTicketLock";
/**
* The constant LOCKER_SUITE_JSAPI_TICKET.
*/
protected static final String LOCKER_SUITE_JSAPI_TICKET = "suiteJsapiTicketLock";
@NonNull
private final WxRedisOps wxRedisOps;
private final String suiteAccessTokenKey = ":suiteAccessTokenKey:";
private final String suiteTicketKey = ":suiteTicketKey:";
private final String accessTokenKey = ":accessTokenKey:";
private final String authCorpJsApiTicketKey = ":authCorpJsApiTicketKey:";
private final String authSuiteJsApiTicketKey = ":authSuiteJsApiTicketKey:";
private final String providerTokenKey = ":providerTokenKey:";
/**
* redis里面key的统一前缀
*/
@Setter
private String keyPrefix = "";
private volatile String baseApiUrl;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
private volatile String httpProxyUsername;
private volatile String httpProxyPassword;
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private volatile File tmpDirFile;
@Override
public void setBaseApiUrl(String baseUrl) {
this.baseApiUrl = baseUrl;
}
@Override
public String getApiUrl(String path) {
if (baseApiUrl == null) {
baseApiUrl = "https://qyapi.weixin.qq.com";
}
return baseApiUrl + path;
}
/**
* 第三方应用的suite access token相关
*/
@Override
public String getSuiteAccessToken() {
return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
}
@Override
public WxAccessToken getSuiteAccessTokenEntity() {
String suiteAccessToken = wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
Long expireIn = wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey));
if (StringUtils.isBlank(suiteAccessToken) || expireIn == null || expireIn == 0 || expireIn == -2) {
return new WxAccessToken();
}
WxAccessToken suiteAccessTokenEntity = new WxAccessToken();
suiteAccessTokenEntity.setAccessToken(suiteAccessToken);
suiteAccessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expireIn), 0));
return suiteAccessTokenEntity;
}
@Override
public boolean isSuiteAccessTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == -2;
}
@Override
public void expireSuiteAccessToken() {
wxRedisOps.expire(keyWithPrefix(suiteAccessTokenKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteAccessToken(WxAccessToken suiteAccessToken) {
updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn());
}
@Override
public void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteAccessTokenKey), suiteAccessToken, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的suite ticket相关
*/
@Override
public String getSuiteTicket() {
return wxRedisOps.getValue(keyWithPrefix(suiteTicketKey));
}
@Override
public boolean isSuiteTicketExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == -2;
}
@Override
public void expireSuiteTicket() {
wxRedisOps.expire(keyWithPrefix(suiteTicketKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteTicket(String suiteTicket, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteTicketKey), suiteTicket, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的其他配置,来自于企微配置
*/
@Override
public String getSuiteId() {
return suiteId;
}
@Override
public String getSuiteSecret() {
return suiteSecret;
}
// 第三方应用的token用来检查应用的签名
@Override
public String getToken() {
return token;
}
//第三方应用的EncodingAESKey用来检查签名
@Override
public String getEncodingAESKey() {
return encodingAESKey;
}
/**
* 企微服务商企业ID & 企业secret, 来自于企微配置
*/
@Override
public String getCorpId() {
return corpId;
}
@Override
public String getProviderSecret() {
return providerSecret;
}
@Override
public void setProviderSecret(String providerSecret) {
this.providerSecret = providerSecret;
}
/**
* 授权企业的access token相关
*/
@Override
public String getAccessToken(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
}
@Override
public WxAccessToken getAccessTokenEntity(String authCorpId) {
String accessToken = wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
Long expire = wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey);
if (StringUtils.isBlank(accessToken) || expire == null || expire == 0 || expire == -2) {
return new WxAccessToken();
}
WxAccessToken accessTokenEntity = new WxAccessToken();
accessTokenEntity.setAccessToken(accessToken);
accessTokenEntity.setExpiresIn((int) ((expire - System.currentTimeMillis()) / 1000 + 200));
return accessTokenEntity;
}
@Override
public boolean isAccessTokenExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2;
}
@Override
public void expireAccessToken(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS);
}
/**
* 授权企业的js api ticket相关
*/
@Override
public String getAuthCorpJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey);
}
@Override
public boolean isAuthCorpJsApiTicketExpired(String authCorpId) {
//没有设置或TTL为0,都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2;
}
@Override
public void expireAuthCorpJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
/**
* 授权企业的第三方应用js api ticket相关
*/
@Override
public String getAuthSuiteJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey);
}
@Override
public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2;
}
@Override
public void expireAuthSuiteJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
@Override
public boolean isProviderTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == 0L || wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == -2;
}
@Override
public void updateProviderToken(String providerToken, int expiredInSeconds) {
wxRedisOps.setValue(providerKeyWithPrefix(providerTokenKey), providerToken, expiredInSeconds, TimeUnit.SECONDS);
}
@Override
public String getProviderToken() {
return wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
}
@Override
public WxCpProviderToken getProviderTokenEntity() {
String providerToken = wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
Long expire = wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey));
if (StringUtils.isBlank(providerToken) || expire == null || expire == 0 || expire == -2) {
return new WxCpProviderToken();
}
WxCpProviderToken wxCpProviderToken = new WxCpProviderToken();
wxCpProviderToken.setProviderAccessToken(providerToken);
wxCpProviderToken.setExpiresIn(Math.max(Math.toIntExact(expire), 0));
return wxCpProviderToken;
}
@Override
public void expireProviderToken() {
wxRedisOps.expire(providerKeyWithPrefix(providerTokenKey), 0, TimeUnit.SECONDS);
}
/**
* 网络代理相关
*/
@Override
public String getHttpProxyHost() {
return this.httpProxyHost;
}
@Override
public int getHttpProxyPort() {
return this.httpProxyPort;
}
@Override
public String getHttpProxyUsername() {
return this.httpProxyUsername;
}
@Override
public String getHttpProxyPassword() {
return this.httpProxyPassword;
}
@Override
public File getTmpDirFile() {
return tmpDirFile;
}
@Override
public Lock getProviderAccessTokenLock() {
return getProviderLockByKey(String.join(":", this.corpId, LOCKER_PROVIDER_ACCESS_TOKEN));
}
@Override
public Lock getSuiteAccessTokenLock() {
return getLockByKey(LOCKER_SUITE_ACCESS_TOKEN);
}
@Override
public Lock getAccessTokenLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_ACCESS_TOKEN));
}
@Override
public Lock getAuthCorpJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_CORP_JSAPI_TICKET));
}
@Override
public Lock getSuiteJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_SUITE_JSAPI_TICKET));
}
private Lock getLockByKey(String key) {
// 最终key的模式(keyPrefix:)wechat_tp_lock:suiteId:(authCorpId):lockKey
// 其中keyPrefix目前不支持外部配置authCorpId只有涉及到corpAccessToken, suiteJsapiTicket, authCorpJsapiTicket时才会拼上
return this.wxRedisOps.getLock(String.join(":", keyWithPrefix(LOCK_KEY + this.suiteId), key));
}
/**
* 单独处理provider,且不应和suite 有关系
*/
private Lock getProviderLockByKey(String key) {
return this.wxRedisOps.getLock(String.join(":", providerKeyWithPrefix(LOCK_KEY), key));
}
@Override
public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
return this.apacheHttpClientBuilder;
}
@Override
public boolean autoRefreshToken() {
return false;
}
@Override
public String toString() {
return WxCpGsonBuilder.create().toJson(this);
}
/**
* 一个provider 会有多个suite,需要唯一标识作为前缀
*/
private String keyWithPrefix(String key) {
return keyPrefix + ":" + suiteId + ":" + key;
}
/**
* provider 应该独享一个key,且不和任何suite关联
* 一个provider 会有多个suite,不同的suite 都应该指向同一个provider 的数据
*/
private String providerKeyWithPrefix(String key) {
return keyPrefix + ":" + corpId + ":" + key;
}
}

View File

@@ -28,20 +28,32 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
private final transient Map<String, Lock> accessTokenLocker = new ConcurrentHashMap<>(); private final transient Map<String, Lock> accessTokenLocker = new ConcurrentHashMap<>();
private final transient Map<String, Lock> authCorpJsapiTicketLocker = new ConcurrentHashMap<>(); private final transient Map<String, Lock> authCorpJsapiTicketLocker = new ConcurrentHashMap<>();
private final transient Map<String, Lock> authSuiteJsapiTicketLocker = new ConcurrentHashMap<>(); private final transient Map<String, Lock> authSuiteJsapiTicketLocker = new ConcurrentHashMap<>();
private volatile String corpId; /**
private volatile String corpSecret; * 企微服务商企业ID & 企业secret来自于企微配置
*/
protected volatile String corpId;
/** /**
* 服务商secret * 服务商secret
*/ */
private volatile String providerSecret; protected volatile String providerSecret;
private volatile String providerToken; private volatile String providerToken;
private volatile long providerTokenExpiresTime; private volatile long providerTokenExpiresTime;
private volatile String suiteId; /**
private volatile String suiteSecret; * 第三方应用的其他配置,来自于企微配置
private volatile String token; */
protected volatile String suiteId;
protected volatile String suiteSecret;
/**
* 第三方应用的token用来检查应用的签名
*/
protected volatile String token;
private volatile String suiteAccessToken; private volatile String suiteAccessToken;
private volatile long suiteAccessTokenExpiresTime; private volatile long suiteAccessTokenExpiresTime;
private volatile String aesKey; /**
* 第三方应用的EncodingAESKey用来检查签名
*/
protected volatile String encodingAESKey;
private volatile String suiteTicket; private volatile String suiteTicket;
private volatile long suiteTicketExpiresTime; private volatile long suiteTicketExpiresTime;
private volatile String oauth2redirectUri; private volatile String oauth2redirectUri;
@@ -186,11 +198,10 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
/** /**
* Sets suite id. * Sets suite id.
* *
* @param corpId the corp id * @param suiteId
*/ */
@Deprecated public void setSuiteId(String suiteId) {
public void setSuiteId(String corpId) { this.suiteId = suiteId;
this.suiteId = corpId;
} }
@Override @Override
@@ -200,10 +211,7 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
/** /**
* Sets suite secret. * Sets suite secret.
*
* @param corpSecret the corp secret
*/ */
@Deprecated
public void setSuiteSecret(String corpSecret) { public void setSuiteSecret(String corpSecret) {
this.suiteSecret = corpSecret; this.suiteSecret = corpSecret;
} }
@@ -218,24 +226,22 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
* *
* @param token the token * @param token the token
*/ */
@Deprecated
public void setToken(String token) { public void setToken(String token) {
this.token = token; this.token = token;
} }
@Override @Override
public String getAesKey() { public String getEncodingAESKey() {
return this.aesKey; return this.encodingAESKey;
} }
/** /**
* Sets aes key. * Sets aes key. encodingAESKey
* *
* @param aesKey the aes key * @param encodingAESKey the aes key
*/ */
@Deprecated public void setEncodingAESKey(String encodingAESKey) {
public void setAesKey(String aesKey) { this.encodingAESKey = encodingAESKey;
this.aesKey = aesKey;
} }
@@ -249,25 +255,15 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
* *
* @param corpId the corp id * @param corpId the corp id
*/ */
@Deprecated
public void setCorpId(String corpId) { public void setCorpId(String corpId) {
this.corpId = corpId; this.corpId = corpId;
} }
@Override @Override
public String getCorpSecret() { public String getCorpSecret() {
return this.corpSecret; return this.providerSecret;
} }
/**
* Sets corp secret.
*
* @param corpSecret the corp secret
*/
@Deprecated
public void setCorpSecret(String corpSecret) {
this.corpSecret = corpSecret;
}
@Override @Override
public void setProviderSecret(String providerSecret) { public void setProviderSecret(String providerSecret) {

View File

@@ -0,0 +1,24 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.NonNull;
import me.chanjar.weixin.common.redis.JedisWxRedisOps;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.util.Pool;
/**
* 基于 jedis 的实现
*
* @author yl
* created on 2023/04/23
*/
public class WxCpTpJedisConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
private static final long serialVersionUID = -1869372247414407433L;
public WxCpTpJedisConfigImpl(Pool<Jedis> jedisPool) {
this(jedisPool, null);
}
public WxCpTpJedisConfigImpl(@NonNull Pool<Jedis> jedisPool, String keyPrefix) {
super(new JedisWxRedisOps(jedisPool), keyPrefix);
}
}

View File

@@ -0,0 +1,24 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.Builder;
import lombok.NonNull;
import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 基于 RedisTemplate 的实现
*
* @author yl
* created on 2023/04/23
*/
public class WxCpTpRedisTemplateConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
private static final long serialVersionUID = -1660004125413310620L;
public WxCpTpRedisTemplateConfigImpl(@NonNull StringRedisTemplate stringRedisTemplate) {
this(stringRedisTemplate, null);
}
public WxCpTpRedisTemplateConfigImpl(@NonNull StringRedisTemplate stringRedisTemplate, String keyPrefix) {
super(new RedisTemplateWxRedisOps(stringRedisTemplate), keyPrefix);
}
}

View File

@@ -1,449 +1,25 @@
package me.chanjar.weixin.cp.config.impl; package me.chanjar.weixin.cp.config.impl;
import lombok.Builder; import lombok.Builder;
import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import me.chanjar.weixin.common.bean.WxAccessToken; import org.redisson.api.RedissonClient;
import me.chanjar.weixin.common.redis.WxRedisOps;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.bean.WxCpProviderToken;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/** /**
* 企业微信各种固定、授权配置的Redisson存储实现 * 基于Redisson实现
*
* @author yuanqixun created on 2020 /5/13
* @author yl
*/ */
@Builder public class WxCpTpRedissonConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializable { private static final long serialVersionUID = -5674792341070783967L;
private static final long serialVersionUID = -5385639031981770319L;
/** public WxCpTpRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
* The constant LOCK_KEY. this(redissonClient, null);
*/
// lock key
protected static final String LOCK_KEY = "wechat_tp_lock:";
/**
* The constant LOCKER_PROVIDER_ACCESS_TOKEN.
*/
protected static final String LOCKER_PROVIDER_ACCESS_TOKEN = "providerAccessTokenLock";
/**
* The constant LOCKER_SUITE_ACCESS_TOKEN.
*/
protected static final String LOCKER_SUITE_ACCESS_TOKEN = "suiteAccessTokenLock";
/**
* The constant LOCKER_ACCESS_TOKEN.
*/
protected static final String LOCKER_ACCESS_TOKEN = "accessTokenLock";
/**
* The constant LOCKER_CORP_JSAPI_TICKET.
*/
protected static final String LOCKER_CORP_JSAPI_TICKET = "corpJsapiTicketLock";
/**
* The constant LOCKER_SUITE_JSAPI_TICKET.
*/
protected static final String LOCKER_SUITE_JSAPI_TICKET = "suiteJsapiTicketLock";
@NonNull
private final WxRedisOps wxRedisOps;
private final String suiteAccessTokenKey = ":suiteAccessTokenKey:";
private final String suiteTicketKey = ":suiteTicketKey:";
private final String accessTokenKey = ":accessTokenKey:";
private final String authCorpJsApiTicketKey = ":authCorpJsApiTicketKey:";
private final String authSuiteJsApiTicketKey = ":authSuiteJsApiTicketKey:";
private final String providerTokenKey = ":providerTokenKey:";
/**
* redis里面key的统一前缀
*/
@Setter
private String keyPrefix = "";
private volatile String baseApiUrl;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
private volatile String httpProxyUsername;
private volatile String httpProxyPassword;
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private volatile File tmpDirFile;
/**
* 第三方应用的其他配置,来自于企微配置
*/
private volatile String suiteId;
private volatile String suiteSecret;
/**
* 第三方应用的token用来检查应用的签名
*/
private volatile String token;
/**
* 第三方应用的EncodingAESKey用来检查签名
*/
private volatile String aesKey;
/**
* 企微服务商企业ID & 企业secret来自于企微配置
*/
private volatile String corpId;
private volatile String corpSecret;
/**
* 服务商secret
*/
private volatile String providerSecret;
@Override
public void setBaseApiUrl(String baseUrl) {
this.baseApiUrl = baseUrl;
} }
@Override public WxCpTpRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
public String getApiUrl(String path) { super(new RedissonWxRedisOps(redissonClient), keyPrefix);
if (baseApiUrl == null) {
baseApiUrl = "https://qyapi.weixin.qq.com";
}
return baseApiUrl + path;
}
/**
* 第三方应用的suite access token相关
*/
@Override
public String getSuiteAccessToken() {
return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
}
@Override
public WxAccessToken getSuiteAccessTokenEntity() {
String suiteAccessToken = wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
Long expireIn = wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey));
if (StringUtils.isBlank(suiteAccessToken) || expireIn == null || expireIn == 0 || expireIn == -2) {
return new WxAccessToken();
}
WxAccessToken suiteAccessTokenEntity = new WxAccessToken();
suiteAccessTokenEntity.setAccessToken(suiteAccessToken);
suiteAccessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expireIn), 0));
return suiteAccessTokenEntity;
}
@Override
public boolean isSuiteAccessTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == -2;
}
@Override
public void expireSuiteAccessToken() {
wxRedisOps.expire(keyWithPrefix(suiteAccessTokenKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteAccessToken(WxAccessToken suiteAccessToken) {
updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn());
}
@Override
public void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteAccessTokenKey), suiteAccessToken, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的suite ticket相关
*/
@Override
public String getSuiteTicket() {
return wxRedisOps.getValue(keyWithPrefix(suiteTicketKey));
}
@Override
public boolean isSuiteTicketExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == -2;
}
@Override
public void expireSuiteTicket() {
wxRedisOps.expire(keyWithPrefix(suiteTicketKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteTicket(String suiteTicket, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteTicketKey), suiteTicket, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的其他配置,来自于企微配置
*/
@Override
public String getSuiteId() {
return suiteId;
}
@Override
public String getSuiteSecret() {
return suiteSecret;
}
// 第三方应用的token用来检查应用的签名
@Override
public String getToken() {
return token;
}
//第三方应用的EncodingAESKey用来检查签名
@Override
public String getAesKey() {
return aesKey;
}
/**
* 企微服务商企业ID & 企业secret, 来自于企微配置
*/
@Override
public String getCorpId() {
return corpId;
}
@Override
public String getCorpSecret() {
return corpSecret;
}
@Override
public void setProviderSecret(String providerSecret) {
this.providerSecret = providerSecret;
}
@Override
public String getProviderSecret() {
return providerSecret;
}
/**
* 授权企业的access token相关
*/
@Override
public String getAccessToken(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
}
@Override
public WxAccessToken getAccessTokenEntity(String authCorpId) {
String accessToken = wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
Long expire = wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey);
if (StringUtils.isBlank(accessToken) || expire == null || expire == 0 || expire == -2) {
return new WxAccessToken();
}
WxAccessToken accessTokenEntity = new WxAccessToken();
accessTokenEntity.setAccessToken(accessToken);
accessTokenEntity.setExpiresIn((int) ((expire - System.currentTimeMillis()) / 1000 + 200));
return accessTokenEntity;
}
@Override
public boolean isAccessTokenExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2;
}
@Override
public void expireAccessToken(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS);
}
/**
* 授权企业的js api ticket相关
*/
@Override
public String getAuthCorpJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey);
}
@Override
public boolean isAuthCorpJsApiTicketExpired(String authCorpId) {
//没有设置或TTL为0,都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2;
}
@Override
public void expireAuthCorpJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
/**
* 授权企业的第三方应用js api ticket相关
*/
@Override
public String getAuthSuiteJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey);
}
@Override
public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2;
}
@Override
public void expireAuthSuiteJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
@Override
public boolean isProviderTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == 0L || wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == -2;
}
@Override
public void updateProviderToken(String providerToken, int expiredInSeconds) {
wxRedisOps.setValue(providerKeyWithPrefix(providerTokenKey), providerToken, expiredInSeconds, TimeUnit.SECONDS);
}
@Override
public String getProviderToken() {
return wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
}
@Override
public WxCpProviderToken getProviderTokenEntity() {
String providerToken = wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
Long expire = wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey));
if (StringUtils.isBlank(providerToken) || expire == null || expire == 0 || expire == -2) {
return new WxCpProviderToken();
}
WxCpProviderToken wxCpProviderToken = new WxCpProviderToken();
wxCpProviderToken.setProviderAccessToken(providerToken);
wxCpProviderToken.setExpiresIn(Math.max(Math.toIntExact(expire), 0));
return wxCpProviderToken;
}
@Override
public void expireProviderToken() {
wxRedisOps.expire(providerKeyWithPrefix(providerTokenKey), 0, TimeUnit.SECONDS);
}
/**
* 网络代理相关
*/
@Override
public String getHttpProxyHost() {
return this.httpProxyHost;
}
@Override
public int getHttpProxyPort() {
return this.httpProxyPort;
}
@Override
public String getHttpProxyUsername() {
return this.httpProxyUsername;
}
@Override
public String getHttpProxyPassword() {
return this.httpProxyPassword;
}
@Override
public File getTmpDirFile() {
return tmpDirFile;
}
@Override
public Lock getProviderAccessTokenLock() {
return getProviderLockByKey(String.join(":", this.corpId, LOCKER_PROVIDER_ACCESS_TOKEN));
}
@Override
public Lock getSuiteAccessTokenLock() {
return getLockByKey(LOCKER_SUITE_ACCESS_TOKEN);
}
@Override
public Lock getAccessTokenLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_ACCESS_TOKEN));
}
@Override
public Lock getAuthCorpJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_CORP_JSAPI_TICKET));
}
@Override
public Lock getSuiteJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_SUITE_JSAPI_TICKET));
}
private Lock getLockByKey(String key) {
// 最终key的模式(keyPrefix:)wechat_tp_lock:suiteId:(authCorpId):lockKey
// 其中keyPrefix目前不支持外部配置authCorpId只有涉及到corpAccessToken, suiteJsapiTicket, authCorpJsapiTicket时才会拼上
return this.wxRedisOps.getLock(String.join(":", keyWithPrefix(LOCK_KEY + this.suiteId), key));
}
/**
* 单独处理provider,且不应和suite 有关系
*/
private Lock getProviderLockByKey(String key) {
return this.wxRedisOps.getLock(String.join(":", providerKeyWithPrefix(LOCK_KEY), key));
}
@Override
public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
return this.apacheHttpClientBuilder;
}
@Override
public boolean autoRefreshToken() {
return false;
}
@Override
public String toString() {
return WxCpGsonBuilder.create().toJson(this);
}
/**
* 一个provider 会有多个suite,需要唯一标识作为前缀
*/
private String keyWithPrefix(String key) {
return keyPrefix + ":" + suiteId + ":" + key;
}
/**
* provider 应该独享一个key,且不和任何suite关联
* 一个provider 会有多个suite,不同的suite 都应该指向同一个provider 的数据
*/
private String providerKeyWithPrefix(String key) {
return keyPrefix + ":" + corpId + ":" + key;
} }
} }

View File

@@ -855,6 +855,10 @@ public interface WxCpApiPathConsts {
* The constant GET_PERMANENT_CODE. * The constant GET_PERMANENT_CODE.
*/ */
String GET_PERMANENT_CODE = "/cgi-bin/service/get_permanent_code"; String GET_PERMANENT_CODE = "/cgi-bin/service/get_permanent_code";
/**
* The constant GET_V2_PERMANENT_CODE.
*/
String GET_V2_PERMANENT_CODE = "/cgi-bin/service/v2/get_permanent_code";
/** /**
* The constant GET_SUITE_TOKEN. * The constant GET_SUITE_TOKEN.
*/ */

View File

@@ -8,6 +8,7 @@ import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
import me.chanjar.weixin.common.util.http.RequestExecutor; import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.cp.bean.*; import me.chanjar.weixin.cp.bean.*;
import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import java.util.List; import java.util.List;
@@ -186,6 +187,8 @@ public interface WxCpTpService {
@Deprecated @Deprecated
WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException; WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException;
WxCpTpCorp getV2PermanentCode(String authCode) throws WxErrorException;
/** /**
* 获取企业永久授权码信息 * 获取企业永久授权码信息
* <pre> * <pre>
@@ -200,6 +203,8 @@ public interface WxCpTpService {
*/ */
WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException; WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
WxCpTpPermanentCodeInfo getV2PermanentCodeInfo(String authCode) throws WxErrorException;
/** /**
* <pre> * <pre>
* 获取预授权链接 * 获取预授权链接
@@ -343,9 +348,7 @@ public interface WxCpTpService {
* 获取WxCpTpConfigStorage 对象. * 获取WxCpTpConfigStorage 对象.
* *
* @return WxCpTpConfigStorage wx cp tp config storage * @return WxCpTpConfigStorage wx cp tp config storage
* @deprecated storage应该在service内部使用 ,提供这个接口,容易破坏这个封装
*/ */
@Deprecated
WxCpTpConfigStorage getWxCpTpConfigStorage(); WxCpTpConfigStorage getWxCpTpConfigStorage();
/** /**
@@ -527,6 +530,11 @@ public interface WxCpTpService {
*/ */
WxCpTpLicenseService getWxCpTpLicenseService(); WxCpTpLicenseService getWxCpTpLicenseService();
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature);
String getVerifyDecrypt(String sVerifyEchoStr);
/** /**
* 获取应用的管理员列表 * 获取应用的管理员列表
* *

View File

@@ -25,8 +25,10 @@ import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.common.util.json.WxGsonBuilder; import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.cp.bean.*; import me.chanjar.weixin.cp.bean.*;
import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.tp.service.*; import me.chanjar.weixin.cp.tp.service.*;
import me.chanjar.weixin.cp.util.crypto.WxCpTpCryptUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.File; import java.io.File;
@@ -91,6 +93,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
private final WxSessionManager sessionManager = new StandardSessionManager(); private final WxSessionManager sessionManager = new StandardSessionManager();
/** /**
* 临时文件目录. * 临时文件目录.
*/ */
@@ -259,6 +262,18 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
return wxCpTpCorp; return wxCpTpCorp;
} }
@Override
public WxCpTpCorp getV2PermanentCode(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_V2_PERMANENT_CODE), jsonObject.toString());
jsonObject = GsonParser.parse(result);
WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString());
wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString());
return wxCpTpCorp;
}
@Override @Override
public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException { public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject(); JsonObject jsonObject = new JsonObject();
@@ -267,6 +282,14 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
return WxCpTpPermanentCodeInfo.fromJson(result); return WxCpTpPermanentCodeInfo.fromJson(result);
} }
@Override
public WxCpTpPermanentCodeInfo getV2PermanentCodeInfo(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_V2_PERMANENT_CODE), jsonObject.toString());
return WxCpTpPermanentCodeInfo.fromJson(result);
}
@Override @Override
@SneakyThrows @SneakyThrows
public String getPreAuthUrl(String redirectUri, String state) throws WxErrorException { public String getPreAuthUrl(String redirectUri, String state) throws WxErrorException {
@@ -452,7 +475,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) { if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) {
// 强制设置wxCpTpConfigStorage它的suite access token过期了这样在下一次请求里就会刷新suite access token // 强制设置wxCpTpConfigStorage它的suite access token过期了这样在下一次请求里就会刷新suite access token
this.configStorage.expireSuiteAccessToken(); this.configStorage.expireSuiteAccessToken();
if (this.getWxCpTpConfigStorage().autoRefreshToken()) { if (this.configStorage.autoRefreshToken()) {
log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg()); log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
return this.execute(executor, uri, data); return this.execute(executor, uri, data);
} }
@@ -646,6 +669,27 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
this.wxCpTpUserService = wxCpTpUserService; this.wxCpTpUserService = wxCpTpUserService;
} }
/**
*
* @param encryptedXml the encrypted xml
* @param timestamp the timestamp
* @param nonce the nonce
* @param msgSignature the msg signature
* @return the wx cp tp xml message
*/
@Override
public WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature) {
return WxCpTpXmlMessage.fromEncryptedXml(encryptedXml,this.configStorage,timestamp,nonce,msgSignature);
}
@Override
public String getVerifyDecrypt(String sVerifyEchoStr) {
WxCpTpCryptUtil cryptUtil = new WxCpTpCryptUtil(this.configStorage);
return cryptUtil.decrypt(sVerifyEchoStr);
}
@Override @Override
public WxCpTpAdmin getAdminList(String authCorpId, Integer agentId) throws WxErrorException { public WxCpTpAdmin getAdminList(String authCorpId, Integer agentId) throws WxErrorException {
JsonObject jsonObject = new JsonObject(); JsonObject jsonObject = new JsonObject();
@@ -764,4 +808,9 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
public void setWxCpTpOAuth2Service(WxCpTpOAuth2Service wxCpTpOAuth2Service) { public void setWxCpTpOAuth2Service(WxCpTpOAuth2Service wxCpTpOAuth2Service) {
this.wxCpTpOAuth2Service = wxCpTpOAuth2Service; this.wxCpTpOAuth2Service = wxCpTpOAuth2Service;
} }
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
}
} }

View File

@@ -101,9 +101,9 @@ public class WxCpTpServiceApacheHttpClientImpl extends BaseWxCpTpServiceImpl<Clo
this.httpClient = apacheHttpClientBuilder.build(); this.httpClient = apacheHttpClientBuilder.build();
} }
@Override // @Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() { // public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage; // return this.configStorage;
} // }
} }

View File

@@ -98,9 +98,9 @@ public class WxCpTpServiceHttpComponentsImpl extends BaseWxCpTpServiceImpl<Close
this.httpClient = apacheHttpClientBuilder.build(); this.httpClient = apacheHttpClientBuilder.build();
} }
@Override // @Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() { // public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage; // return this.configStorage;
} // }
} }

View File

@@ -0,0 +1,104 @@
package me.chanjar.weixin.cp.tp.service.impl;
import com.google.gson.JsonObject;
import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.http.net.SocketHttpConnectionProvider;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.http.HttpClientType;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
/**
* The type Wx cp service jodd http.
*
* @author someone
*/
public class WxCpTpServiceJoddHttpImpl extends BaseWxCpTpServiceImpl<HttpConnectionProvider, ProxyInfo> {
private HttpConnectionProvider httpClient;
private ProxyInfo httpProxy;
@Override
public HttpConnectionProvider getRequestHttpClient() {
return httpClient;
}
@Override
public ProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpClientType getRequestType() {
return HttpClientType.JODD_HTTP;
}
@Override
public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getSuiteAccessToken();
}
synchronized (this.globalSuiteAccessTokenRefreshLock) {
// 构建请求 URL
String url = this.configStorage.getApiUrl(WxCpApiPathConsts.Tp.GET_SUITE_TOKEN);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("suite_id", this.configStorage.getSuiteId());
jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret());
jsonObject.addProperty("suite_ticket", this.getSuiteTicket());
String jsonBody = jsonObject.toString();
if (this.httpProxy != null) {
httpClient.useProxy(this.httpProxy);
}
// 创建 POST 请求
HttpRequest request = HttpRequest
.post(url)
.contentType("application/json")
.body(jsonBody); // 使用 .body() 设置请求体
request.withConnectionProvider(httpClient);
// 发送请求
HttpResponse response = request.send();
// 解析响应
String resultContent = response.bodyText();
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
// 更新 access token
jsonObject = GsonParser.parse(resultContent);
String suiteAccussToken = jsonObject.get("suite_access_token").getAsString();
int expiresIn = jsonObject.get("expires_in").getAsInt();
this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn);
}
return this.configStorage.getSuiteAccessToken();
}
@Override
public void initHttp() {
if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}
httpClient = new SocketHttpConnectionProvider();
}
//
// @Override
// public WxCpConfigStorage getWxCpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -0,0 +1,130 @@
package me.chanjar.weixin.cp.tp.service.impl;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpClientType;
import me.chanjar.weixin.common.util.http.okhttp.DefaultOkHttpClientBuilder;
import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
import okhttp3.*;
import java.io.IOException;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN;
/**
* The type Wx cp service ok http.
*
* @author someone
*/
@Slf4j
public class WxCpTpServiceOkHttpImpl extends BaseWxCpTpServiceImpl<OkHttpClient, OkHttpProxyInfo> {
private OkHttpClient httpClient;
private OkHttpProxyInfo httpProxy;
@Override
public OkHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public OkHttpProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpClientType getRequestType() {
return HttpClientType.OK_HTTP;
}
@Override
public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getSuiteAccessToken();
}
synchronized (this.globalSuiteAccessTokenRefreshLock) {
// 得到 httpClient
OkHttpClient client = getRequestHttpClient();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("suite_id", this.configStorage.getSuiteId());
jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret());
jsonObject.addProperty("suite_ticket", this.getSuiteTicket());
String jsonBody = jsonObject.toString();
RequestBody requestBody = RequestBody.create(
MediaType.get("application/json; charset=utf-8"),
jsonBody
);
// 构建 POST 请求
Request request = new Request.Builder()
.url(this.configStorage.getApiUrl(WxCpApiPathConsts.Tp.GET_SUITE_TOKEN)) // URL 不包含查询参数
.post(requestBody) // 使用 POST 方法
.build();
String resultContent = null;
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected response code: " + response);
}
resultContent = response.body().string();
} catch (IOException e) {
log.error("获取 suite token 失败: {}", e.getMessage(), e);
throw new WxRuntimeException("获取 suite token 失败", e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
jsonObject = GsonParser.parse(resultContent);
String suiteAccussToken = jsonObject.get("suite_access_token").getAsString();
int expiresIn = jsonObject.get("expires_in").getAsInt();
this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn);
}
return this.configStorage.getSuiteAccessToken();
}
@Override
public void initHttp() {
log.debug("WxCpServiceOkHttpImpl initHttp");
//设置代理
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
httpProxy = OkHttpProxyInfo.httpProxy(configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(),
configStorage.getHttpProxyUsername(),
configStorage.getHttpProxyPassword());
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
clientBuilder.proxy(getRequestHttpProxy().getProxy());
//设置授权
clientBuilder.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(httpProxy.getProxyUsername(), httpProxy.getProxyPassword());
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
});
httpClient = clientBuilder.build();
} else {
httpClient = DefaultOkHttpClientBuilder.get().build();
}
}
// @Override
// public WxCpConfigStorage getWxCpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -23,7 +23,7 @@ public class WxCpTpCryptUtil extends WxCryptUtil {
* @param encodingAesKey 公众平台上开发者设置的EncodingAESKey * @param encodingAesKey 公众平台上开发者设置的EncodingAESKey
* @param appidOrCorpid 公众平台corpId * @param appidOrCorpid 公众平台corpId
*/ */
String encodingAesKey = wxCpTpConfigStorage.getAesKey(); String encodingAesKey = wxCpTpConfigStorage.getEncodingAESKey();
String token = wxCpTpConfigStorage.getToken(); String token = wxCpTpConfigStorage.getToken();
String corpId = wxCpTpConfigStorage.getCorpId(); String corpId = wxCpTpConfigStorage.getCorpId();

View File

@@ -9,6 +9,8 @@ import me.chanjar.weixin.cp.bean.WxCpTpPermanentCodeInfo;
import me.chanjar.weixin.cp.bean.WxTpCustomizedAuthUrl; import me.chanjar.weixin.cp.bean.WxTpCustomizedAuthUrl;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.AbstractWxCpTpInRedisConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedisTemplateConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl; import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl;
import me.chanjar.weixin.cp.tp.service.WxCpTpService; import me.chanjar.weixin.cp.tp.service.WxCpTpService;
import org.mockito.Mockito; import org.mockito.Mockito;
@@ -69,7 +71,10 @@ public class BaseWxCpTpServiceImplTest {
* @return the wx cp tp config storage * @return the wx cp tp config storage
*/ */
public WxCpTpConfigStorage wxCpTpConfigStorage() { public WxCpTpConfigStorage wxCpTpConfigStorage() {
return WxCpTpRedissonConfigImpl.builder().corpId(PROVIDER_CORP_ID).providerSecret(PROVIDER_SECRET).wxRedisOps(new RedissonWxRedisOps(redissonClient())).build(); WxCpTpRedissonConfigImpl wxCpTpRedissonConfig=new WxCpTpRedissonConfigImpl(redissonClient(),"");
wxCpTpRedissonConfig.setCorpId(PROVIDER_CORP_ID);
wxCpTpRedissonConfig.setProviderSecret(PROVIDER_SECRET);
return wxCpTpRedissonConfig;
} }
/** /**

View File

@@ -6,6 +6,8 @@ import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import me.chanjar.weixin.cp.bean.WxCpProviderToken; import me.chanjar.weixin.cp.bean.WxCpProviderToken;
import me.chanjar.weixin.cp.bean.WxCpTpCorpId2OpenCorpId; import me.chanjar.weixin.cp.bean.WxCpTpCorpId2OpenCorpId;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.config.impl.AbstractWxCpTpInRedisConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedisTemplateConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl; import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl;
import me.chanjar.weixin.cp.tp.service.WxCpTpService; import me.chanjar.weixin.cp.tp.service.WxCpTpService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -48,14 +50,10 @@ public class WxCpTpServiceApacheHttpClientImplTest {
* The constant PROVIDER_CORP_ID. * The constant PROVIDER_CORP_ID.
*/ */
public static final String PROVIDER_CORP_ID = "xxxxxx"; public static final String PROVIDER_CORP_ID = "xxxxxx";
/**
* The constant CORP_SECRET.
*/
public static final String CORP_SECRET = "xxxxxx";
/** /**
* The constant PROVIDER_SECRET. * The constant PROVIDER_SECRET.
*/ */
public static final String PROVIDER_SECRET = CORP_SECRET; public static final String PROVIDER_SECRET = "xxxxxx";
/** /**
* The constant REDIS_ADDR. * The constant REDIS_ADDR.
*/ */
@@ -85,9 +83,15 @@ public class WxCpTpServiceApacheHttpClientImplTest {
* @return the wx cp tp config storage * @return the wx cp tp config storage
*/ */
public WxCpTpConfigStorage wxCpTpConfigStorage() { public WxCpTpConfigStorage wxCpTpConfigStorage() {
return WxCpTpRedissonConfigImpl.builder().baseApiUrl(API_URL).suiteId(SUITE_ID).suiteSecret(SUITE_SECRET) WxCpTpRedissonConfigImpl wxCpTpRedissonConfig=new WxCpTpRedissonConfigImpl(redissonClient(),"");
.token(TOKEN).aesKey(AES_KEY).corpId(PROVIDER_CORP_ID).corpSecret(CORP_SECRET).providerSecret(PROVIDER_SECRET) wxCpTpRedissonConfig.setBaseApiUrl(API_URL);
.wxRedisOps(new RedissonWxRedisOps(redissonClient())).build(); wxCpTpRedissonConfig.setSuiteId(SUITE_ID);
wxCpTpRedissonConfig.setSuiteSecret(SUITE_SECRET);
wxCpTpRedissonConfig.setToken(TOKEN);
wxCpTpRedissonConfig.setEncodingAESKey(AES_KEY);
wxCpTpRedissonConfig.setCorpId(PROVIDER_CORP_ID);
wxCpTpRedissonConfig.setProviderSecret(PROVIDER_SECRET);
return wxCpTpRedissonConfig;
} }
/** /**