🆕 #1952 增加腾讯企点子模块,用于对接企点开放平台。

This commit is contained in:
fanxiayang12 2020-12-30 09:17:35 +08:00 committed by GitHub
parent a8232f6c91
commit e7f2bd62f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 3928 additions and 12 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
.bash
.history
*.class
test-output

View File

@ -1,8 +1,5 @@
<?xml version="1.0"?>
<project
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"
xmlns="http://maven.apache.org/POM/4.0.0">
<project 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" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
@ -111,6 +108,7 @@
<module>weixin-java-pay</module>
<module>weixin-java-miniapp</module>
<module>weixin-java-open</module>
<module>weixin-java-qidian</module>
<module>spring-boot-starters</module>
<!--module>weixin-java-osgi</module-->
</modules>

View File

@ -1,7 +1,5 @@
<?xml version="1.0"?>
<project 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"
xmlns="http://maven.apache.org/POM/4.0.0">
<project 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" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.binarywang</groupId>
@ -22,6 +20,7 @@
<module>wx-java-mp-spring-boot-starter</module>
<module>wx-java-pay-spring-boot-starter</module>
<module>wx-java-open-spring-boot-starter</module>
<module>wx-java-qidian-spring-boot-starter</module>
</modules>
<dependencies>

View File

@ -12,6 +12,6 @@ import org.springframework.context.annotation.Import;
*/
@Configuration
@EnableConfigurationProperties(WxMpProperties.class)
@Import({WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class})
@Import({ WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class })
public class WxMpAutoConfiguration {
}

View File

@ -88,7 +88,7 @@ public class WxMpStorageAutoConfiguration {
}
WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps,
wxMpProperties.getConfigStorage().getKeyPrefix());
wxMpProperties.getConfigStorage().getKeyPrefix());
setWxMpInfo(wxMpRedisConfig);
return wxMpRedisConfig;
}
@ -114,7 +114,7 @@ public class WxMpStorageAutoConfiguration {
WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps,
wxMpProperties.getConfigStorage().getKeyPrefix());
wxMpProperties.getConfigStorage().getKeyPrefix());
setWxMpInfo(wxMpRedisConfig);
return wxMpRedisConfig;
@ -160,6 +160,6 @@ public class WxMpStorageAutoConfiguration {
}
return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
redis.getDatabase());
redis.getDatabase());
}
}

View File

@ -11,7 +11,6 @@ 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;
/**
* 微信接入相关配置属性.
*

View File

@ -0,0 +1,45 @@
# wx-java-qidian-spring-boot-starter
## 快速开始
1. 引入依赖
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-qidian-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
```
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 = Jedis # 配置类型: 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

View File

@ -0,0 +1,66 @@
<?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.0.1.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wx-java-qidian-spring-boot-starter</artifactId>
<name>WxJava - Spring Boot Starter for QiDian</name>
<description>腾讯企点的 Spring Boot Starter</description>
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-qidian</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>${spring.boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jodd</groupId>
<artifactId>jodd-http</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</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,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 {
}

View File

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

View File

@ -0,0 +1,166 @@
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.JedisPool;
import redis.clients.jedis.JedisPoolAbstract;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
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() {
JedisPoolAbstract 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 JedisPoolAbstract 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<String> sentinels = Sets.newHashSet(redis.getSentinelIps().split(","));
return new JedisSentinelPool(redis.getSentinelName(), sentinels);
}
return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
redis.getDatabase());
}
}

View File

@ -0,0 +1,22 @@
package com.binarywang.spring.starter.wxjava.qidian.enums;
/**
* httpclient类型.
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
* @date 2020-08-30
*/
public enum HttpClientType {
/**
* HttpClient.
*/
HttpClient,
/**
* OkHttp.
*/
OkHttp,
/**
* JoddHttp.
*/
JoddHttp,
}

View File

@ -0,0 +1,22 @@
package com.binarywang.spring.starter.wxjava.qidian.enums;
/**
* storage类型.
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
* @date 2020-08-30
*/
public enum StorageType {
/**
* 内存.
*/
Memory,
/**
* redis(JedisClient).
*/
Jedis,
/**
* redis(RedisTemplate).
*/
RedisTemplate
}

View File

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

View File

@ -0,0 +1,56 @@
package com.binarywang.spring.starter.wxjava.qidian.properties;
import lombok.Data;
import java.io.Serializable;
/**
* redis 配置属性.
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
* @date 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;
}

View File

@ -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.HttpClient;
/**
* http代理主机.
*/
private String httpProxyHost;
/**
* http代理端口.
*/
private Integer httpProxyPort;
/**
* http代理用户名.
*/
private String httpProxyUsername;
/**
* http代理密码.
*/
private String httpProxyPassword;
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration

201
weixin-java-qidian/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

134
weixin-java-qidian/pom.xml Normal file
View File

@ -0,0 +1,134 @@
<?xml version="1.0"?>
<project
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"
xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.0.1.B</version>
</parent>
<artifactId>weixin-java-qidian</artifactId>
<name>WxJava - 企点 Java SDK</name>
<description>腾讯企点Java SDK</description>
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jodd</groupId>
<artifactId>jodd-http</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-guava</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<annotationProcessors>
com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor
</annotationProcessors>
<annotationProcessorPaths>
<path>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-graal</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,13 @@
package me.chanjar.weixin.qidian.api;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.bean.call.GetSwitchBoardListResponse;
/**
* 通话数据相关操作接口.
*
* @author alegria
*/
public interface WxQidianCallDataService {
public GetSwitchBoardListResponse getSwitchBoardList() throws WxErrorException;
}

View File

@ -0,0 +1,18 @@
package me.chanjar.weixin.qidian.api;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.bean.dial.IVRDialRequest;
import me.chanjar.weixin.qidian.bean.dial.IVRDialResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRListResponse;
/**
* 基础话务相关操作接口.
*
* @author alegria
*/
public interface WxQidianDialService {
IVRDialResponse ivrDial(IVRDialRequest ivrDial) throws WxErrorException;
IVRListResponse getIVRList() throws WxErrorException;
}

View File

@ -0,0 +1,348 @@
package me.chanjar.weixin.qidian.api;
import java.util.Map;
import com.google.gson.JsonObject;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.enums.TicketType;
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;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import me.chanjar.weixin.qidian.enums.WxQidianApiUrl;
/**
* 腾讯企点API的Service.
*
* @author alegria
*/
public interface WxQidianService extends WxService {
/**
* <pre>
* 验证消息的确来自微信服务器.
* 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
* </pre>
*
* @param timestamp 时间戳
* @param nonce 随机串
* @param signature 签名
* @return 是否验证通过 boolean
*/
boolean checkSignature(String timestamp, String nonce, String signature);
/**
* 获取access_token, 不强制刷新access_token.
*
* @return token access token
* @throws WxErrorException .
* @see #getAccessToken(boolean) #getAccessToken(boolean)
*/
String getAccessToken() throws WxErrorException;
/**
* <pre>
* 获取access_token本方法线程安全.
* 且在多线程同时刷新时只刷新一次避免超出2000次/日的调用次数上限
*
* 本service的所有方法都会在access_token过期时调用此方法
*
* 程序员在非必要情况下尽量不要主动调用此方法
*
* 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
* </pre>
*
* @param forceRefresh 是否强制刷新
* @return token access token
* @throws WxErrorException .
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
/**
* 获得ticket,不强制刷新ticket.
*
* @param type ticket 类型
* @return ticket ticket
* @throws WxErrorException .
* @see #getTicket(TicketType, boolean) #getTicket(TicketType, boolean)
*/
String getTicket(TicketType type) throws WxErrorException;
/**
* <pre>
* 获得ticket.
* 获得时会检查 Token是否过期如果过期了那么就刷新一下否则就什么都不干
* </pre>
*
* @param type ticket类型
* @param forceRefresh 强制刷新
* @return ticket ticket
* @throws WxErrorException .
*/
String getTicket(TicketType type, boolean forceRefresh) throws WxErrorException;
/**
* 获得jsapi_ticket,不强制刷新jsapi_ticket.
*
* @return jsapi ticket
* @throws WxErrorException .
* @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)
*/
String getJsapiTicket() throws WxErrorException;
/**
* <pre>
* 获得jsapi_ticket.
* 获得时会检查jsapiToken是否过期如果过期了那么就刷新一下否则就什么都不干
*
* 详情请见http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
* </pre>
*
* @param forceRefresh 强制刷新
* @return jsapi ticket
* @throws WxErrorException .
*/
String getJsapiTicket(boolean forceRefresh) throws WxErrorException;
/**
* <pre>
* 创建调用jsapi时所需要的签名.
*
* 详情请见http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
* </pre>
*
* @param url 地址
* @return 生成的签名对象 wx jsapi signature
* @throws WxErrorException .
*/
WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
/**
* <pre>
* 长链接转短链接接口.
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=长链接转短链接接口
* </pre>
*
* @param longUrl 长url
* @return 生成的短地址 string
* @throws WxErrorException .
*/
String shortUrl(String longUrl) throws WxErrorException;
/**
* <pre>
* 构造第三方使用网站应用授权登录的url.
* 详情请见: <a href=
"https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN">网站应用微信登录开发指南</a>
* URL格式为https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
* </pre>
*
* @param redirectUri 用户授权完成后的重定向链接无需urlencode, 方法内会进行encode
* @param scope 应用授权作用域拥有多个作用域用逗号,分隔网页应用目前仅填写snsapi_login即可
* @param state 非必填用于保持请求和回调的状态授权请求后原样带回给第三方该参数可用于防止csrf攻击跨站请求伪造攻击建议第三方带上该参数可设置为简单的随机数加session进行校验
* @return url string
*/
String buildQrConnectUrl(String redirectUri, String scope, String state);
/**
* <pre>
* 获取微信服务器IP地址
* http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html
* </pre>
*
* @return 微信服务器ip地址数组 string [ ]
* @throws WxErrorException .
*/
String[] getCallbackIP() throws WxErrorException;
/**
* <pre>
* 网络检测
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21541575776DtsuT
* 为了帮助开发者排查回调连接失败的问题提供这个网络检测的API它可以对开发者URL做域名解析然后对所有IP进行一次ping操作得到丢包率和耗时
* </pre>
*
* @param action 执行的检测动作
* @param operator 指定平台从某个运营商进行检测
* @return 检测结果 wx net check result
* @throws WxErrorException .
*/
WxNetCheckResult netCheck(String action, String operator) throws WxErrorException;
/**
* <pre>
* 公众号调用或第三方平台帮公众号调用对公众号的所有api调用包括第三方帮其调用次数进行清零
* HTTP调用https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=ACCESS_TOKEN
* 接口文档地址https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433744592
*
* </pre>
*
* @param appid 公众号的APPID
* @throws WxErrorException the wx error exception
*/
void clearQuota(String appid) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候可以用这个
* {@link #get}{@link #post}方法更灵活可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型
* 可以参考{@link MediaUploadRequestExecutor}的实现方法
* </pre>
*
* @param <T> the type parameter
* @param <E> the type parameter
* @param executor 执行器
* @param url 接口地址
* @param data 参数数据
* @return 结果 t
* @throws WxErrorException 异常
*/
<T, E> T execute(RequestExecutor<T, E> executor, String url, E data) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的GET请求.
*
* @param url 请求接口地址
* @param queryParam 参数
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String get(WxQidianApiUrl url, String queryParam) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的POST请求.
*
* @param url 请求接口地址
* @param postData 请求参数json值
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String post(WxQidianApiUrl url, String postData) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的POST请求.
*
* @param url 请求接口地址
* @param jsonObject 请求参数json对象
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String post(WxQidianApiUrl url, JsonObject jsonObject) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候可以用这个
* {@link #get}{@link #post}方法更灵活可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型
* 可以参考{@link MediaUploadRequestExecutor}的实现方法
* </pre>
*
* @param <T> the type parameter
* @param <E> the type parameter
* @param executor 执行器
* @param url 接口地址
* @param data 参数数据
* @return 结果 t
* @throws WxErrorException 异常
*/
<T, E> T execute(RequestExecutor<T, E> executor, WxQidianApiUrl url, E data) throws WxErrorException;
/**
* 设置当微信系统响应系统繁忙时要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
*
* @param retrySleepMillis 默认1000ms
*/
void setRetrySleepMillis(int retrySleepMillis);
/**
* <pre>
* 设置当微信系统响应系统繁忙时最大重试次数.
* 默认5次
* </pre>
*
* @param maxRetryTimes 最大重试次数
*/
void setMaxRetryTimes(int maxRetryTimes);
/**
* 获取WxMpConfigStorage 对象.
*
* @return WxMpConfigStorage wx mp config storage
*/
WxQidianConfigStorage getWxMpConfigStorage();
/**
* 设置 {@link WxQidianConfigStorage} 的实现. 兼容老版本
*
* @param wxConfigProvider .
*/
void setWxMpConfigStorage(WxQidianConfigStorage wxConfigProvider);
/**
* Map里 加入新的 {@link WxQidianConfigStorage}适用于动态添加新的微信公众号配置.
*
* @param mpId 公众号id
* @param configStorage 新的微信配置
*/
void addConfigStorage(String mpId, WxQidianConfigStorage configStorage);
/**
* Map中 移除 {@link String mpId} 所对应的
* {@link WxQidianConfigStorage}适用于动态移除微信公众号配置.
*
* @param mpId 对应公众号的标识
*/
void removeConfigStorage(String mpId);
/**
* 注入多个 {@link WxQidianConfigStorage} 的实现. 并为每个 {@link WxQidianConfigStorage}
* 赋予不同的 {@link String mpId} 随机采用一个{@link String mpId}进行Http初始化操作
*
* @param configStorages WxMpConfigStorage map
*/
void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages);
/**
* 注入多个 {@link WxQidianConfigStorage} 的实现. 并为每个 {@link WxQidianConfigStorage}
* 赋予不同的 {@link String label}
*
* @param configStorages WxMpConfigStorage map
* @param defaultMpId 设置一个{@link WxQidianConfigStorage} 所对应的{@link String
* mpId}进行Http初始化
*/
void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages, String defaultMpId);
/**
* 进行相应的公众号切换.
*
* @param mpId 公众号标识
* @return 切换是否成功 boolean
*/
boolean switchover(String mpId);
/**
* 进行相应的公众号切换.
*
* @param mpId 公众号标识
* @return 切换成功 则返回当前对象方便链式调用否则抛出异常
*/
WxQidianService switchoverTo(String mpId);
/**
* 初始化http请求对象.
*/
void initHttp();
/**
* 获取RequestHttp对象.
*
* @return RequestHttp对象 request http
*/
RequestHttp getRequestHttp();
WxQidianDialService getDialService();
WxQidianCallDataService getCallDataService();
}

View File

@ -0,0 +1,420 @@
package me.chanjar.weixin.qidian.api.impl;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.CLEAR_QUOTA_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_CALLBACK_IP_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_CURRENT_AUTOREPLY_INFO_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_TICKET_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.NETCHECK_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.QRCONNECT_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.SHORTURL_API_URL;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.ToJson;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.enums.TicketType;
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.DataUtils;
import me.chanjar.weixin.common.util.RandomUtils;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
import me.chanjar.weixin.common.util.http.URIUtil;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.qidian.api.WxQidianCallDataService;
import me.chanjar.weixin.qidian.api.WxQidianDialService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import me.chanjar.weixin.qidian.enums.WxQidianApiUrl;
import me.chanjar.weixin.qidian.util.WxQidianConfigStorageHolder;
/**
* 基础实现类.
*
* @author someone
*/
@Slf4j
public abstract class BaseWxQidianServiceImpl<H, P> implements WxQidianService, RequestHttp<H, P> {
@Getter
private WxQidianDialService dialService = new WxQidianDialServiceImpl(this);
@Getter
private WxQidianCallDataService callDataService = new WxQidianCallDataServiceImpl(this);
private Map<String, WxQidianConfigStorage> configStorageMap;
private int retrySleepMillis = 1000;
private int maxRetryTimes = 5;
@Override
public boolean checkSignature(String timestamp, String nonce, String signature) {
try {
return SHA1.gen(this.getWxMpConfigStorage().getToken(), timestamp, nonce).equals(signature);
} catch (Exception e) {
log.error("Checking signature failed, and the reason is :" + e.getMessage());
return false;
}
}
@Override
public String getTicket(TicketType type) throws WxErrorException {
return this.getTicket(type, false);
}
@Override
public String getTicket(TicketType type, boolean forceRefresh) throws WxErrorException {
if (forceRefresh) {
this.getWxMpConfigStorage().expireTicket(type);
}
if (this.getWxMpConfigStorage().isTicketExpired(type)) {
Lock lock = this.getWxMpConfigStorage().getTicketLock(type);
lock.lock();
try {
if (this.getWxMpConfigStorage().isTicketExpired(type)) {
String responseContent = execute(SimpleGetRequestExecutor.create(this),
GET_TICKET_URL.getUrl(this.getWxMpConfigStorage()) + type.getCode(), null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
this.getWxMpConfigStorage().updateTicket(type, jsapiTicket, expiresInSeconds);
}
} finally {
lock.unlock();
}
}
return this.getWxMpConfigStorage().getTicket(type);
}
@Override
public String getJsapiTicket() throws WxErrorException {
return this.getJsapiTicket(false);
}
@Override
public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
return this.getTicket(TicketType.JSAPI, forceRefresh);
}
@Override
public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
long timestamp = System.currentTimeMillis() / 1000;
String randomStr = RandomUtils.getRandomStr();
String jsapiTicket = getJsapiTicket(false);
String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + randomStr,
"timestamp=" + timestamp, "url=" + url);
WxJsapiSignature jsapiSignature = new WxJsapiSignature();
jsapiSignature.setAppId(this.getWxMpConfigStorage().getAppId());
jsapiSignature.setTimestamp(timestamp);
jsapiSignature.setNonceStr(randomStr);
jsapiSignature.setUrl(url);
jsapiSignature.setSignature(signature);
return jsapiSignature;
}
@Override
public String getAccessToken() throws WxErrorException {
return getAccessToken(false);
}
@Override
public String shortUrl(String longUrl) throws WxErrorException {
if (longUrl.contains("&access_token=")) {
throw new WxErrorException("要转换的网址中存在非法字符{&access_token=" + "会导致微信接口报错属于微信bug请调整地址否则不建议使用此方法");
}
JsonObject o = new JsonObject();
o.addProperty("action", "long2short");
o.addProperty("long_url", longUrl);
String responseContent = this.post(SHORTURL_API_URL, o.toString());
return GsonParser.parse(responseContent).get("short_url").getAsString();
}
@Override
public String buildQrConnectUrl(String redirectUri, String scope, String state) {
return String.format(QRCONNECT_URL.getUrl(this.getWxMpConfigStorage()), this.getWxMpConfigStorage().getAppId(),
URIUtil.encodeURIComponent(redirectUri), scope, StringUtils.trimToEmpty(state));
}
@Override
public String[] getCallbackIP() throws WxErrorException {
String responseContent = this.get(GET_CALLBACK_IP_URL, null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
JsonArray ipList = tmpJsonObject.get("ip_list").getAsJsonArray();
String[] ipArray = new String[ipList.size()];
for (int i = 0; i < ipList.size(); i++) {
ipArray[i] = ipList.get(i).getAsString();
}
return ipArray;
}
@Override
public WxNetCheckResult netCheck(String action, String operator) throws WxErrorException {
JsonObject o = new JsonObject();
o.addProperty("action", action);
o.addProperty("check_operator", operator);
String responseContent = this.post(NETCHECK_URL, o.toString());
return WxNetCheckResult.fromJson(responseContent);
}
@Override
public void clearQuota(String appid) throws WxErrorException {
JsonObject o = new JsonObject();
o.addProperty("appid", appid);
this.post(CLEAR_QUOTA_URL, o.toString());
}
@Override
public String get(String url, String queryParam) throws WxErrorException {
return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
}
@Override
public String get(WxQidianApiUrl url, String queryParam) throws WxErrorException {
return this.get(url.getUrl(this.getWxMpConfigStorage()), queryParam);
}
@Override
public String post(String url, String postData) throws WxErrorException {
return execute(SimplePostRequestExecutor.create(this), url, postData);
}
@Override
public String post(WxQidianApiUrl url, String postData) throws WxErrorException {
return this.post(url.getUrl(this.getWxMpConfigStorage()), postData);
}
@Override
public String post(WxQidianApiUrl url, JsonObject jsonObject) throws WxErrorException {
return this.post(url.getUrl(this.getWxMpConfigStorage()), jsonObject.toString());
}
@Override
public String post(String url, ToJson obj) throws WxErrorException {
return this.post(url, obj.toJson());
}
@Override
public String post(String url, JsonObject jsonObject) throws WxErrorException {
return this.post(url, jsonObject.toString());
}
@Override
public String post(String url, Object obj) throws WxErrorException {
return this.execute(SimplePostRequestExecutor.create(this), url, WxGsonBuilder.create().toJson(obj));
}
@Override
public <T, E> T execute(RequestExecutor<T, E> executor, WxQidianApiUrl url, E data) throws WxErrorException {
return this.execute(executor, url.getUrl(this.getWxMpConfigStorage()), data);
}
/**
* 向微信端发送请求在这里执行的策略是当发生access_token过期时才去刷新然后重新执行请求而不是全局定时请求.
*/
@Override
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
int retryTimes = 0;
do {
try {
return this.executeInternal(executor, uri, data);
} catch (WxErrorException e) {
if (retryTimes + 1 > this.maxRetryTimes) {
log.warn("重试达到最大次数【{}】", maxRetryTimes);
// 最后一次重试失败后直接抛出异常不再等待
throw new WxRuntimeException("微信服务端异常,超出重试次数");
}
WxError error = e.getError();
// -1 系统繁忙, 1000ms后重试
if (error.getErrorCode() == -1) {
int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
try {
log.warn("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
Thread.sleep(sleepMillis);
} catch (InterruptedException e1) {
throw new WxRuntimeException(e1);
}
} else {
throw e;
}
}
} while (retryTimes++ < this.maxRetryTimes);
log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
throw new WxRuntimeException("微信服务端异常,超出重试次数");
}
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
E dataForLog = DataUtils.handleDataWithSecret(data);
if (uri.contains("access_token=")) {
throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
}
String accessToken = getAccessToken(false);
String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
try {
T result = executor.execute(uriWithAccessToken, data, WxType.MP);
log.debug("\n【请求地址】: {}\n【请求参数】{}\n【响应数据】{}", uriWithAccessToken, dataForLog, result);
return result;
} catch (WxErrorException e) {
WxError error = e.getError();
if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
// 强制设置wxMpConfigStorage它的access token过期了这样在下一次请求里就会刷新access token
Lock lock = this.getWxMpConfigStorage().getAccessTokenLock();
lock.lock();
try {
if (StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)) {
this.getWxMpConfigStorage().expireAccessToken();
}
} catch (Exception ex) {
this.getWxMpConfigStorage().expireAccessToken();
} finally {
lock.unlock();
}
if (this.getWxMpConfigStorage().autoRefreshToken()) {
log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
return this.execute(executor, uri, data);
}
}
if (error.getErrorCode() != 0) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【错误信息】{}", uriWithAccessToken, dataForLog, error);
throw new WxErrorException(error, e);
}
return null;
} catch (IOException e) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【异常信息】{}", uriWithAccessToken, dataForLog, e.getMessage());
throw new WxErrorException(e);
}
}
@Override
public WxQidianConfigStorage getWxMpConfigStorage() {
if (this.configStorageMap.size() == 1) {
// 只有一个公众号直接返回其配置即可
return this.configStorageMap.values().iterator().next();
}
return this.configStorageMap.get(WxQidianConfigStorageHolder.get());
}
protected String extractAccessToken(String resultContent) throws WxErrorException {
WxQidianConfigStorage config = this.getWxMpConfigStorage();
WxError error = WxError.fromJson(resultContent, WxType.MP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
config.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
return config.getAccessToken();
}
@Override
public void setWxMpConfigStorage(WxQidianConfigStorage wxConfigProvider) {
final String defaultMpId = wxConfigProvider.getAppId();
this.setMultiConfigStorages(ImmutableMap.of(defaultMpId, wxConfigProvider), defaultMpId);
}
@Override
public void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages) {
this.setMultiConfigStorages(configStorages, configStorages.keySet().iterator().next());
}
@Override
public void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages, String defaultMpId) {
this.configStorageMap = Maps.newHashMap(configStorages);
WxQidianConfigStorageHolder.set(defaultMpId);
this.initHttp();
}
@Override
public void addConfigStorage(String mpId, WxQidianConfigStorage configStorages) {
synchronized (this) {
if (this.configStorageMap == null) {
this.setWxMpConfigStorage(configStorages);
} else {
this.configStorageMap.put(mpId, configStorages);
}
}
}
@Override
public void removeConfigStorage(String mpId) {
synchronized (this) {
if (this.configStorageMap.size() == 1) {
this.configStorageMap.remove(mpId);
log.warn("已删除最后一个公众号配置:{}须立即使用setWxMpConfigStorage或setMultiConfigStorages添加配置", mpId);
return;
}
if (WxQidianConfigStorageHolder.get().equals(mpId)) {
this.configStorageMap.remove(mpId);
final String defaultMpId = this.configStorageMap.keySet().iterator().next();
WxQidianConfigStorageHolder.set(defaultMpId);
log.warn("已删除默认公众号配置,公众号【{}】被设为默认配置", defaultMpId);
return;
}
this.configStorageMap.remove(mpId);
}
}
@Override
public WxQidianService switchoverTo(String mpId) {
if (this.configStorageMap.containsKey(mpId)) {
WxQidianConfigStorageHolder.set(mpId);
return this;
}
throw new WxRuntimeException(String.format("无法找到对应【%s】的公众号配置信息请核实", mpId));
}
@Override
public boolean switchover(String mpId) {
if (this.configStorageMap.containsKey(mpId)) {
WxQidianConfigStorageHolder.set(mpId);
return true;
}
log.error("无法找到对应【{}】的公众号配置信息,请核实!", mpId);
return false;
}
@Override
public void setRetrySleepMillis(int retrySleepMillis) {
this.retrySleepMillis = retrySleepMillis;
}
@Override
public void setMaxRetryTimes(int maxRetryTimes) {
this.maxRetryTimes = maxRetryTimes;
}
@Override
public RequestHttp getRequestHttp() {
return this;
}
}

View File

@ -0,0 +1,23 @@
package me.chanjar.weixin.qidian.api.impl;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.CallData.GET_SWITCH_BOARD_LIST;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianCallDataService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.bean.call.GetSwitchBoardListResponse;
@Slf4j
@RequiredArgsConstructor
public class WxQidianCallDataServiceImpl implements WxQidianCallDataService {
private final WxQidianService wxQidianService;
@Override
public GetSwitchBoardListResponse getSwitchBoardList() throws WxErrorException {
String result = this.wxQidianService.get(GET_SWITCH_BOARD_LIST, null);
return GetSwitchBoardListResponse.fromJson(result);
}
}

View File

@ -0,0 +1,43 @@
package me.chanjar.weixin.qidian.api.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianDialService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.bean.dial.IVRDialRequest;
import me.chanjar.weixin.qidian.bean.dial.IVRDialResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRListResponse;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Dial.GET_IVR_LIST;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Dial.IVR_DIAL;
/**
* Created by Binary Wang on 2016/7/21.
*
* @author Binary Wang
*/
@Slf4j
@RequiredArgsConstructor
public class WxQidianDialServiceImpl implements WxQidianDialService {
private final WxQidianService wxQidianService;
@Override
public IVRDialResponse ivrDial(IVRDialRequest ivrDial) throws WxErrorException {
String json = ivrDial.toJson();
log.debug("IVR外呼{}", json);
String result = this.wxQidianService.post(IVR_DIAL, json);
log.debug("创建菜单:{},结果:{}", json, result);
return IVRDialResponse.fromJson(result);
}
@Override
public IVRListResponse getIVRList() throws WxErrorException {
String result = this.wxQidianService.get(GET_IVR_LIST, null);
return IVRListResponse.fromJson(result);
}
}

View File

@ -0,0 +1,106 @@
package me.chanjar.weixin.qidian.api.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* apache http client方式实现.
*
* @author someone
*/
public class WxQidianServiceHttpClientImpl extends BaseWxQidianServiceImpl<CloseableHttpClient, HttpHost> {
private CloseableHttpClient httpClient;
private HttpHost httpProxy;
@Override
public CloseableHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public HttpHost getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.APACHE_HTTP;
}
@Override
public void initHttp() {
WxQidianConfigStorage configStorage = this.getWxMpConfigStorage();
ApacheHttpClientBuilder apacheHttpClientBuilder = configStorage.getApacheHttpClientBuilder();
if (null == apacheHttpClientBuilder) {
apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
}
apacheHttpClientBuilder.httpProxyHost(configStorage.getHttpProxyHost())
.httpProxyPort(configStorage.getHttpProxyPort()).httpProxyUsername(configStorage.getHttpProxyUsername())
.httpProxyPassword(configStorage.getHttpProxyPassword());
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
this.httpProxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
}
this.httpClient = apacheHttpClientBuilder.build();
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
try {
HttpGet httpGet = new HttpGet(url);
if (this.getRequestHttpProxy() != null) {
RequestConfig requestConfig = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
httpGet.setConfig(requestConfig);
}
try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
return this.extractAccessToken(new BasicResponseHandler().handleResponse(response));
} finally {
httpGet.releaseConnection();
}
} catch (IOException e) {
throw new WxRuntimeException(e);
}
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
}

View File

@ -0,0 +1,12 @@
package me.chanjar.weixin.qidian.api.impl;
/**
* <pre>
* 默认接口实现类使用apache httpclient实现
* Created by Binary Wang on 2017-5-27.
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public class WxQidianServiceImpl extends WxQidianServiceHttpClientImpl {
}

View File

@ -0,0 +1,90 @@
package me.chanjar.weixin.qidian.api.impl;
import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.ProxyInfo;
import jodd.http.net.SocketHttpConnectionProvider;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* jodd-http方式实现.
*
* @author someone
*/
public class WxQidianServiceJoddHttpImpl extends BaseWxQidianServiceImpl<HttpConnectionProvider, ProxyInfo> {
private HttpConnectionProvider httpClient;
private ProxyInfo httpProxy;
@Override
public HttpConnectionProvider getRequestHttpClient() {
return httpClient;
}
@Override
public ProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.JODD_HTTP;
}
@Override
public void initHttp() {
WxQidianConfigStorage configStorage = this.getWxMpConfigStorage();
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}
httpClient = new SocketHttpConnectionProvider();
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
HttpRequest request = HttpRequest.get(url);
if (this.getRequestHttpProxy() != null) {
SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider();
provider.useProxy(getRequestHttpProxy());
request.withConnectionProvider(provider);
}
return this.extractAccessToken(request.send().bodyText());
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
}

View File

@ -0,0 +1,98 @@
package me.chanjar.weixin.qidian.api.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import okhttp3.*;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* okhttp实现.
*
* @author someone
*/
public class WxQidianServiceOkHttpImpl extends BaseWxQidianServiceImpl<OkHttpClient, OkHttpProxyInfo> {
private OkHttpClient httpClient;
private OkHttpProxyInfo httpProxy;
@Override
public OkHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public OkHttpProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.OK_HTTP;
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
Request request = new Request.Builder().url(url).get().build();
Response response = getRequestHttpClient().newCall(request).execute();
return this.extractAccessToken(Objects.requireNonNull(response.body()).string());
} catch (IOException e) {
throw new WxRuntimeException(e);
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
@Override
public void initHttp() {
WxQidianConfigStorage wxMpConfigStorage = getWxMpConfigStorage();
// 设置代理
if (wxMpConfigStorage.getHttpProxyHost() != null && wxMpConfigStorage.getHttpProxyPort() > 0) {
httpProxy = OkHttpProxyInfo.httpProxy(wxMpConfigStorage.getHttpProxyHost(), wxMpConfigStorage.getHttpProxyPort(),
wxMpConfigStorage.getHttpProxyUsername(), wxMpConfigStorage.getHttpProxyPassword());
}
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
if (httpProxy != null) {
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();
}
}

View File

@ -0,0 +1,56 @@
package me.chanjar.weixin.qidian.bean;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 企点接口地址域名部分的自定义设置信息.
*
* @author alegria
* @date 2020-12-24
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WxQidianHostConfig {
public static final String API_DEFAULT_HOST_URL = "https://api.weixin.qq.com";
public static final String OPEN_DEFAULT_HOST_URL = "https://open.weixin.qq.com";
public static final String QIDIAN_DEFAULT_HOST_URL = "https://api.qidian.qq.com";
/**
* 对应于https://api.weixin.qq.com
*/
private String apiHost;
/**
* 对应于https://open.weixin.qq.com
*/
private String openHost;
/**
* 对应于https://api.qidian.qq.com
*/
private String qidianHost;
public static String buildUrl(WxQidianHostConfig hostConfig, String prefix, String path) {
if (hostConfig == null) {
return prefix + path;
}
if (hostConfig.getApiHost() != null && prefix.equals(API_DEFAULT_HOST_URL)) {
return hostConfig.getApiHost() + path;
}
if (hostConfig.getQidianHost() != null && prefix.equals(QIDIAN_DEFAULT_HOST_URL)) {
return hostConfig.getQidianHost() + path;
}
if (hostConfig.getOpenHost() != null && prefix.equals(OPEN_DEFAULT_HOST_URL)) {
return hostConfig.getOpenHost() + path;
}
return prefix + path;
}
}

View File

@ -0,0 +1,14 @@
package me.chanjar.weixin.qidian.bean.call;
import lombok.Data;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
@Data
public class GetSwitchBoardListResponse extends QidianResponse {
private SwitchBoardList data;
public static GetSwitchBoardListResponse fromJson(String result) {
return WxGsonBuilder.create().fromJson(result, GetSwitchBoardListResponse.class);
}
}

View File

@ -0,0 +1,13 @@
package me.chanjar.weixin.qidian.bean.call;
import lombok.Data;
@Data
public class SwitchBoard {
private String switchboard;
private String createTime;
private Boolean callinStatus;
private Boolean calloutStatus;
private String spName;
private String cityName;
}

View File

@ -0,0 +1,15 @@
package me.chanjar.weixin.qidian.bean.call;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Data;
@Data
public class SwitchBoardList {
private List<SwitchBoard> records;
public List<String> switchBoards() {
return records.stream().map(SwitchBoard::getSwitchboard).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,109 @@
package me.chanjar.weixin.qidian.bean.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class QidianResponse {
private static Map<Integer, String> errorCodesMap = new HashMap<Integer, String>() {
private static final long serialVersionUID = 1125349909878104934L;
{
put(-1, "系统繁忙");
put(0, "请求成功");
put(40001, "获取access_token时AppSecret错误或者access_token无效");
put(40002, "不合法的凭证类型");
put(40003, "不合法的OpenID");
put(40004, "不合法的媒体文件类型");
put(40005, "不合法的文件类型");
put(40006, "不合法的文件大小");
put(40007, "不合法的媒体文件id");
put(40008, "不合法的消息类型");
put(40009, "不合法的图片文件大小");
put(40010, "不合法的语音文件大小");
put(40011, "不合法的视频文件大小");
put(40012, "不合法的缩略图文件大小");
put(40013, "不合法的APPID");
put(40014, "不合法的access_token");
put(40015, "不合法的菜单类型");
put(40016, "不合法的按钮个数");
put(40017, "不合法的按钮个数");
put(40018, "不合法的按钮名字长度");
put(40019, "不合法的按钮KEY长度");
put(40020, "不合法的按钮URL长度");
put(40021, "不合法的菜单版本号");
put(40022, "不合法的子菜单级数");
put(40023, "不合法的子菜单按钮个数");
put(40024, "不合法的子菜单按钮类型");
put(40025, "不合法的子菜单按钮名字长度");
put(40026, "不合法的子菜单按钮KEY长度");
put(40027, "不合法的子菜单按钮URL长度");
put(40028, "不合法的自定义菜单使用用户");
put(40029, "不合法的oauth_code");
put(40030, "不合法的refresh_token");
put(40031, "不合法的openid列表");
put(40032, "不合法的openid列表长度");
put(40033, "不合法的请求字符,不能包含\\uxxxx格式的字符");
put(40035, "不合法的参数");
put(40038, "不合法的请求格式");
put(40039, "不合法的URL长度");
put(40050, "不合法的分组id");
put(40051, "分组名字不合法");
put(41001, "缺少access_token参数");
put(41002, "缺少appid参数");
put(41003, "缺少refresh_token参数");
put(41004, "缺少secret参数");
put(41005, "缺少多媒体文件数据");
put(41006, "缺少media_id参数");
put(41007, "缺少子菜单数据");
put(41008, "缺少oauth code");
put(41009, "缺少openid");
put(42001, "access_token超时");
put(42002, "refresh_token超时");
put(42003, "oauth_code超时");
put(43001, "需要GET请求");
put(43002, "需要POST请求");
put(43003, "需要HTTPS请求");
put(43004, "需要接收者关注");
put(43005, "需要好友关系");
put(44001, "多媒体文件为空");
put(44002, "POST的数据包为空");
put(44003, "图文消息内容为空");
put(44004, "文本消息内容为空");
put(45001, "多媒体文件大小超过限制");
put(45002, "消息内容超过限制");
put(45003, "标题字段超过限制");
put(45004, "描述字段超过限制");
put(45005, "链接字段超过限制");
put(45006, "图片链接字段超过限制");
put(45007, "语音播放时间超过限制");
put(45008, "图文消息超过限制");
put(45009, "接口调用超过限制");
put(45010, "创建菜单个数超过限制");
put(45015, "回复时间超过限制");
put(45016, "系统分组,不允许修改");
put(45017, "分组名字过长");
put(45018, "分组数量超过上限");
put(46001, "不存在媒体数据");
put(46002, "不存在的菜单版本");
put(46003, "不存在的菜单数据");
put(46004, "不存在的用户");
put(47001, "解析JSON/XML内容错误");
put(48001, "api功能未授权");
put(50001, "用户未授权该api");
}
};
private Integer code = 0;
private String msg;
private Integer errcode = 0;
private String errmsg = "ok";
private String errmsgChinese;
public String getErrmsgChinese() {
if (errcode != null && errmsgChinese == null) {
errmsgChinese = errorCodesMap.get(errcode);
}
return errmsgChinese;
}
}

View File

@ -0,0 +1,28 @@
package me.chanjar.weixin.qidian.bean.dial;
import lombok.Data;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import java.io.Serializable;
import java.util.List;
@Data
public class IVRDialRequest implements Serializable {
private static final long serialVersionUID = -5552935329136465927L;
private String phone_number;
private String ivr_id;
private List<String> corp_phone_list;
private Integer loc_pref_on = 1;
private List<String> backup_corp_phone_list;
private Boolean skip_restrict = false;
@Override
public String toString() {
return this.toJson();
}
public String toJson() {
return WxGsonBuilder.create().toJson(this);
}
}

View File

@ -0,0 +1,20 @@
package me.chanjar.weixin.qidian.bean.dial;
import lombok.Data;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
import me.chanjar.weixin.qidian.util.json.WxQidianGsonBuilder;
@Data
public class IVRDialResponse extends QidianResponse {
private String callid;
public static IVRDialResponse fromJson(String json) {
return WxGsonBuilder.create().fromJson(json, IVRDialResponse.class);
}
@Override
public String toString() {
return WxQidianGsonBuilder.create().toJson(this);
}
}

View File

@ -0,0 +1,16 @@
package me.chanjar.weixin.qidian.bean.dial;
import lombok.Data;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
import java.util.List;
@Data
public class IVRListResponse extends QidianResponse {
private List<Ivr> node;
public static IVRListResponse fromJson(String json) {
return WxGsonBuilder.create().fromJson(json, IVRListResponse.class);
}
}

View File

@ -0,0 +1,9 @@
package me.chanjar.weixin.qidian.bean.dial;
import lombok.Data;
@Data
public class Ivr {
private String ivr_id;
private String ivr_name;
}

View File

@ -0,0 +1,210 @@
package me.chanjar.weixin.qidian.config;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
import java.io.File;
import java.util.concurrent.locks.Lock;
/**
* 微信客户端配置存储.
*
* @author chanjarster
*/
public interface WxQidianConfigStorage {
/**
* Gets access token.
*
* @return the access token
*/
String getAccessToken();
/**
* Gets access token lock.
*
* @return the access token lock
*/
Lock getAccessTokenLock();
/**
* Is access token expired boolean.
*
* @return the boolean
*/
boolean isAccessTokenExpired();
/**
* 强制将access token过期掉.
*/
void expireAccessToken();
/**
* 应该是线程安全的.
*
* @param accessToken 要更新的WxAccessToken对象
*/
void updateAccessToken(WxAccessToken accessToken);
/**
* 应该是线程安全的.
*
* @param accessToken 新的accessToken值
* @param expiresInSeconds 过期时间以秒为单位
*/
void updateAccessToken(String accessToken, int expiresInSeconds);
/**
* Gets ticket.
*
* @param type the type
* @return the ticket
*/
String getTicket(TicketType type);
/**
* Gets ticket lock.
*
* @param type the type
* @return the ticket lock
*/
Lock getTicketLock(TicketType type);
/**
* Is ticket expired boolean.
*
* @param type the type
* @return the boolean
*/
boolean isTicketExpired(TicketType type);
/**
* 强制将ticket过期掉.
*
* @param type the type
*/
void expireTicket(TicketType type);
/**
* 更新ticket.
* 应该是线程安全的
*
* @param type ticket类型
* @param ticket 新的ticket值
* @param expiresInSeconds 过期时间以秒为单位
*/
void updateTicket(TicketType type, String ticket, int expiresInSeconds);
/**
* Gets app id.
*
* @return the app id
*/
String getAppId();
/**
* Gets secret.
*
* @return the secret
*/
String getSecret();
/**
* Gets token.
*
* @return the token
*/
String getToken();
/**
* Gets aes key.
*
* @return the aes key
*/
String getAesKey();
/**
* Gets template id.
*
* @return the template id
*/
String getTemplateId();
/**
* Gets expires time.
*
* @return the expires time
*/
long getExpiresTime();
/**
* Gets oauth 2 redirect uri.
*
* @return the oauth 2 redirect uri
*/
String getOauth2redirectUri();
/**
* Gets http proxy host.
*
* @return the http proxy host
*/
String getHttpProxyHost();
/**
* Gets http proxy port.
*
* @return the http proxy port
*/
int getHttpProxyPort();
/**
* Gets http proxy username.
*
* @return the http proxy username
*/
String getHttpProxyUsername();
/**
* Gets http proxy password.
*
* @return the http proxy password
*/
String getHttpProxyPassword();
/**
* Gets tmp dir file.
*
* @return the tmp dir file
*/
File getTmpDirFile();
/**
* http client builder.
*
* @return ApacheHttpClientBuilder apache http client builder
*/
ApacheHttpClientBuilder getApacheHttpClientBuilder();
/**
* 是否自动刷新token.
*
* @return the boolean
*/
boolean autoRefreshToken();
/**
* 得到微信接口地址域名部分的自定义设置信息.
*
* @return the host config
*/
WxQidianHostConfig getHostConfig();
/**
* 设置微信接口地址域名部分的自定义设置信息.
*
* @param hostConfig host config
*/
void setHostConfig(WxQidianHostConfig hostConfig);
}

View File

@ -0,0 +1,196 @@
package me.chanjar.weixin.qidian.config.impl;
import lombok.Data;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import me.chanjar.weixin.qidian.util.json.WxQidianGsonBuilder;
import java.io.File;
import java.io.Serializable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 基于内存的微信配置provider在实际生产环境中应该将这些配置持久化.
*
* @author chanjarster
*/
@Data
public class WxQidianDefaultConfigImpl implements WxQidianConfigStorage, Serializable {
private static final long serialVersionUID = -6646519023303395185L;
protected volatile String appId;
protected volatile String secret;
protected volatile String token;
protected volatile String templateId;
protected volatile String accessToken;
protected volatile String aesKey;
protected volatile long expiresTime;
protected volatile String oauth2redirectUri;
protected volatile String httpProxyHost;
protected volatile int httpProxyPort;
protected volatile String httpProxyUsername;
protected volatile String httpProxyPassword;
protected volatile String jsapiTicket;
protected volatile long jsapiTicketExpiresTime;
protected volatile String sdkTicket;
protected volatile long sdkTicketExpiresTime;
protected volatile String cardApiTicket;
protected volatile long cardApiTicketExpiresTime;
protected volatile Lock accessTokenLock = new ReentrantLock();
protected volatile Lock jsapiTicketLock = new ReentrantLock();
protected volatile Lock sdkTicketLock = new ReentrantLock();
protected volatile Lock cardApiTicketLock = new ReentrantLock();
protected volatile File tmpDirFile;
protected volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private WxQidianHostConfig hostConfig = null;
@Override
public boolean isAccessTokenExpired() {
return System.currentTimeMillis() > this.expiresTime;
}
@Override
public synchronized void updateAccessToken(WxAccessToken accessToken) {
updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
}
@Override
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
this.accessToken = accessToken;
this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
}
@Override
public void expireAccessToken() {
this.expiresTime = 0;
}
@Override
public String getTicket(TicketType type) {
switch (type) {
case SDK:
return this.sdkTicket;
case JSAPI:
return this.jsapiTicket;
case WX_CARD:
return this.cardApiTicket;
default:
return null;
}
}
public void setTicket(TicketType type, String ticket) {
switch (type) {
case JSAPI:
this.jsapiTicket = ticket;
break;
case WX_CARD:
this.cardApiTicket = ticket;
break;
case SDK:
this.sdkTicket = ticket;
break;
default:
}
}
@Override
public Lock getTicketLock(TicketType type) {
switch (type) {
case SDK:
return this.sdkTicketLock;
case JSAPI:
return this.jsapiTicketLock;
case WX_CARD:
return this.cardApiTicketLock;
default:
return null;
}
}
@Override
public boolean isTicketExpired(TicketType type) {
switch (type) {
case SDK:
return System.currentTimeMillis() > this.sdkTicketExpiresTime;
case JSAPI:
return System.currentTimeMillis() > this.jsapiTicketExpiresTime;
case WX_CARD:
return System.currentTimeMillis() > this.cardApiTicketExpiresTime;
default:
return false;
}
}
@Override
public synchronized void updateTicket(TicketType type, String ticket, int expiresInSeconds) {
switch (type) {
case JSAPI:
this.jsapiTicket = ticket;
// 预留200秒的时间
this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
break;
case WX_CARD:
this.cardApiTicket = ticket;
// 预留200秒的时间
this.cardApiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
break;
case SDK:
this.sdkTicket = ticket;
// 预留200秒的时间
this.sdkTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
break;
default:
}
}
@Override
public void expireTicket(TicketType type) {
switch (type) {
case JSAPI:
this.jsapiTicketExpiresTime = 0;
break;
case WX_CARD:
this.cardApiTicketExpiresTime = 0;
break;
case SDK:
this.sdkTicketExpiresTime = 0;
break;
default:
}
}
@Override
public String toString() {
return WxQidianGsonBuilder.create().toJson(this);
}
@Override
public boolean autoRefreshToken() {
return true;
}
@Override
public WxQidianHostConfig getHostConfig() {
return this.hostConfig;
}
@Override
public void setHostConfig(WxQidianHostConfig hostConfig) {
this.hostConfig = hostConfig;
}
}

View File

@ -0,0 +1,99 @@
package me.chanjar.weixin.qidian.config.impl;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.redis.WxRedisOps;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis的微信配置provider.
*
* <pre>
* 使用说明本实现仅供参考并不完整
* 比如为减少项目依赖未加入redis分布式锁的实现如有需要请自行实现
* </pre>
*
* @author nickwong
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class WxQidianRedisConfigImpl extends WxQidianDefaultConfigImpl {
private static final long serialVersionUID = -988502871997239733L;
private static final String ACCESS_TOKEN_KEY_TPL = "%s:access_token:%s";
private static final String TICKET_KEY_TPL = "%s:ticket:key:%s:%s";
private static final String LOCK_KEY_TPL = "%s:lock:%s:";
private final WxRedisOps redisOps;
private final String keyPrefix;
private String accessTokenKey;
private String lockKey;
public WxQidianRedisConfigImpl(WxRedisOps redisOps, String keyPrefix) {
this.redisOps = redisOps;
this.keyPrefix = keyPrefix;
}
/**
* 每个公众号生成独有的存储key.
*/
@Override
public void setAppId(String appId) {
super.setAppId(appId);
this.accessTokenKey = String.format(ACCESS_TOKEN_KEY_TPL, this.keyPrefix, appId);
this.lockKey = String.format(LOCK_KEY_TPL, this.keyPrefix, appId);
accessTokenLock = this.redisOps.getLock(lockKey.concat("accessTokenLock"));
jsapiTicketLock = this.redisOps.getLock(lockKey.concat("jsapiTicketLock"));
sdkTicketLock = this.redisOps.getLock(lockKey.concat("sdkTicketLock"));
cardApiTicketLock = this.redisOps.getLock(lockKey.concat("cardApiTicketLock"));
}
private String getTicketRedisKey(TicketType type) {
return String.format(TICKET_KEY_TPL, this.keyPrefix, appId, type.getCode());
}
@Override
public String getAccessToken() {
return redisOps.getValue(this.accessTokenKey);
}
@Override
public boolean isAccessTokenExpired() {
Long expire = redisOps.getExpire(this.accessTokenKey);
return expire == null || expire < 2;
}
@Override
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireAccessToken() {
redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public String getTicket(TicketType type) {
return redisOps.getValue(this.getTicketRedisKey(type));
}
@Override
public boolean isTicketExpired(TicketType type) {
return redisOps.getExpire(this.getTicketRedisKey(type)) < 2;
}
@Override
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
redisOps.setValue(this.getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireTicket(TicketType type) {
redisOps.expire(this.getTicketRedisKey(type), 0, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,101 @@
package me.chanjar.weixin.qidian.config.impl;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import me.chanjar.weixin.common.redis.WxRedisOps;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/**
* @author wuxingye
* @date 2020/6/12
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class WxQidianRedissonConfigImpl extends WxQidianDefaultConfigImpl {
private static final long serialVersionUID = -5139855123878455556L;
private static final String ACCESS_TOKEN_KEY_TPL = "%s:access_token:%s";
private static final String TICKET_KEY_TPL = "%s:ticket:key:%s:%s";
private static final String LOCK_KEY_TPL = "%s:lock:%s:";
private final WxRedisOps redisOps;
private final String keyPrefix;
private String accessTokenKey;
private String lockKey;
public WxQidianRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
this(new RedissonWxRedisOps(redissonClient), keyPrefix);
}
public WxQidianRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
this(redissonClient, null);
}
private WxQidianRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) {
this.redisOps = redisOps;
this.keyPrefix = keyPrefix;
}
/**
* 每个公众号生成独有的存储key.
*/
@Override
public void setAppId(String appId) {
super.setAppId(appId);
this.accessTokenKey = String.format(ACCESS_TOKEN_KEY_TPL, this.keyPrefix, appId);
this.lockKey = String.format(LOCK_KEY_TPL, this.keyPrefix, appId);
accessTokenLock = this.redisOps.getLock(lockKey.concat("accessTokenLock"));
jsapiTicketLock = this.redisOps.getLock(lockKey.concat("jsapiTicketLock"));
sdkTicketLock = this.redisOps.getLock(lockKey.concat("sdkTicketLock"));
cardApiTicketLock = this.redisOps.getLock(lockKey.concat("cardApiTicketLock"));
}
private String getTicketRedisKey(TicketType type) {
return String.format(TICKET_KEY_TPL, this.keyPrefix, appId, type.getCode());
}
@Override
public String getAccessToken() {
return redisOps.getValue(this.accessTokenKey);
}
@Override
public boolean isAccessTokenExpired() {
Long expire = redisOps.getExpire(this.accessTokenKey);
return expire == null || expire < 2;
}
@Override
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireAccessToken() {
redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public String getTicket(TicketType type) {
return redisOps.getValue(this.getTicketRedisKey(type));
}
@Override
public boolean isTicketExpired(TicketType type) {
return redisOps.getExpire(this.getTicketRedisKey(type)) < 2;
}
@Override
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
redisOps.setValue(this.getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireTicket(TicketType type) {
redisOps.expire(this.getTicketRedisKey(type), 0, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,155 @@
package me.chanjar.weixin.qidian.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import static me.chanjar.weixin.qidian.bean.WxQidianHostConfig.*;
/**
* <pre>
* 腾讯企点接口api地址
* Created by alegria on 2020年12月26日.
* </pre>
*/
public interface WxQidianApiUrl {
/**
* 得到api完整地址.
*
* @param config 微信公众号配置
* @return api地址
*/
default String getUrl(WxQidianConfigStorage config) {
WxQidianHostConfig hostConfig = null;
if (config != null) {
hostConfig = config.getHostConfig();
}
return buildUrl(hostConfig, this.getPrefix(), this.getPath());
}
/**
* the path
*
* @return path
*/
String getPath();
/**
* the prefix
*
* @return prefix
*/
String getPrefix();
@AllArgsConstructor
@Getter
enum OAuth2 implements WxQidianApiUrl {
/**
* 用code换取oauth2的access token.
*/
OAUTH2_ACCESS_TOKEN_URL(API_DEFAULT_HOST_URL,
"/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"),
/**
* 刷新oauth2的access token.
*/
OAUTH2_REFRESH_TOKEN_URL(API_DEFAULT_HOST_URL,
"/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"),
/**
* 用oauth2获取用户信息.
*/
OAUTH2_USERINFO_URL(API_DEFAULT_HOST_URL, "/sns/userinfo?access_token=%s&openid=%s&lang=%s"),
/**
* 验证oauth2的access token是否有效.
*/
OAUTH2_VALIDATE_TOKEN_URL(API_DEFAULT_HOST_URL, "/sns/auth?access_token=%s&openid=%s"),
/**
* oauth2授权的url连接.
*/
CONNECT_OAUTH2_AUTHORIZE_URL(OPEN_DEFAULT_HOST_URL,
"/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s&connect_redirect=1#wechat_redirect");
private final String prefix;
private final String path;
}
@AllArgsConstructor
@Getter
enum Other implements WxQidianApiUrl {
/**
* 获取access_token.
*/
GET_ACCESS_TOKEN_URL(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"),
/**
* 获得各种类型的ticket.
*/
GET_TICKET_URL(API_DEFAULT_HOST_URL, "/cgi-bin/ticket/getticket?type="),
/**
* 长链接转短链接接口.
*/
SHORTURL_API_URL(API_DEFAULT_HOST_URL, "/cgi-bin/shorturl"),
/**
* 语义查询接口.
*/
SEMANTIC_SEMPROXY_SEARCH_URL(API_DEFAULT_HOST_URL, "/semantic/semproxy/search"),
/**
* 获取微信服务器IP地址.
*/
GET_CALLBACK_IP_URL(API_DEFAULT_HOST_URL, "/cgi-bin/getcallbackip"),
/**
* 网络检测.
*/
NETCHECK_URL(API_DEFAULT_HOST_URL, "/cgi-bin/callback/check"),
/**
* 第三方使用网站应用授权登录的url.
*/
QRCONNECT_URL(OPEN_DEFAULT_HOST_URL,
"/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"),
/**
* 获取公众号的自动回复规则.
*/
GET_CURRENT_AUTOREPLY_INFO_URL(API_DEFAULT_HOST_URL, "/cgi-bin/get_current_autoreply_info"),
/**
* 公众号调用或第三方平台帮公众号调用对公众号的所有api调用包括第三方帮其调用次数进行清零.
*/
CLEAR_QUOTA_URL(API_DEFAULT_HOST_URL, "/cgi-bin/clear_quota");
private final String prefix;
private final String path;
}
@AllArgsConstructor
@Getter
enum Dial implements WxQidianApiUrl {
/**
* IVR外呼.
*/
IVR_DIAL(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/dial/ivrdial"),
/**
* 拉取IVR列表.
*/
GET_IVR_LIST(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/dial/getivrlist");
private final String prefix;
private final String path;
}
@AllArgsConstructor
@Getter
enum CallData implements WxQidianApiUrl {
/**
* 总机号列表拉取.
*/
GET_SWITCH_BOARD_LIST(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/callData/getswitchboardlist");
private final String prefix;
private final String path;
}
}

View File

@ -0,0 +1,29 @@
package me.chanjar.weixin.qidian.util;
/**
* @author alegria
* @date 2020年12月26日
*/
public class WxQidianConfigStorageHolder {
private final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "default";
}
};
public static String get() {
return THREAD_LOCAL.get();
}
public static void set(String label) {
THREAD_LOCAL.set(label);
}
/**
* 此方法需要用户根据自己程序代码在适当位置手动触发调用本SDK里无法判断调用时机
*/
public static void remove() {
THREAD_LOCAL.remove();
}
}

View File

@ -0,0 +1,21 @@
package me.chanjar.weixin.qidian.util.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* @author someone
*/
public class WxQidianGsonBuilder {
private static final GsonBuilder INSTANCE = new GsonBuilder();
static {
INSTANCE.disableHtmlEscaping();
}
public static Gson create() {
return INSTANCE.create();
}
}

View File

@ -0,0 +1,64 @@
package me.chanjar.weixin.qidian.api;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl;
import org.testng.annotations.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Test
@Slf4j
public class WxMpBusyRetryTest {
@DataProvider(name = "getService")
public Object[][] getService() {
WxQidianService service = new WxQidianServiceHttpClientImpl() {
@Override
public synchronized <T, E> T executeInternal(
RequestExecutor<T, E> executor, String uri, E data)
throws WxErrorException {
log.info("Executed");
throw new WxErrorException("something");
}
};
service.setMaxRetryTimes(3);
service.setRetrySleepMillis(500);
return new Object[][]{{service}};
}
@Test(dataProvider = "getService", expectedExceptions = RuntimeException.class)
public void testRetry(WxQidianService service) throws WxErrorException {
service.execute(null, (String)null, null);
}
@Test(dataProvider = "getService")
public void testRetryInThreadPool(final WxQidianService service) throws InterruptedException, ExecutionException {
// 当线程池中的线程复用的时候还是能保证相同的重试次数
ExecutorService executorService = Executors.newFixedThreadPool(1);
Runnable runnable = () -> {
try {
System.out.println("=====================");
System.out.println(Thread.currentThread().getName() + ": testRetry");
service.execute(null, (String)null, null);
} catch (WxErrorException e) {
throw new WxRuntimeException(e);
} catch (RuntimeException e) {
// OK
}
};
Future<?> submit1 = executorService.submit(runnable);
Future<?> submit2 = executorService.submit(runnable);
submit1.get();
submit2.get();
}
}

View File

@ -0,0 +1,37 @@
package me.chanjar.weixin.qidian.api;
import com.google.inject.Inject;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.qidian.api.test.ApiTestModule;
import org.testng.Assert;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
/**
* 测试jsapi ticket接口
*
* @author chanjarster
*/
@Test
@Guice(modules = ApiTestModule.class)
public class WxMpJsAPITest {
@Inject
protected WxQidianService wxService;
public void test() {
long timestamp = 1419835025L;
String url = "http://omstest.vmall.com:23568/thirdparty/wechat/vcode/gotoshare?quantity=1&batchName=MATE7";
String noncestr = "82693e11-b9bc-448e-892f-f5289f46cd0f";
String jsapiTicket = "bxLdikRXVbTPdHSM05e5u4RbEYQn7pNQMPrfzl8lJNb1foLDa3HIwI3BRMkQmSO_5F64VFa75uURcq6Uz7QHgA";
String result = SHA1.genWithAmple(
"jsapi_ticket=" + jsapiTicket,
"noncestr=" + noncestr,
"timestamp=" + timestamp,
"url=" + url
);
Assert.assertEquals(result, "c6f04b64d6351d197b71bd23fb7dd2d44c0db486");
}
}

View File

@ -0,0 +1,407 @@
package me.chanjar.weixin.qidian.api.impl;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.api.test.ApiTestModule;
import me.chanjar.weixin.qidian.util.WxQidianConfigStorageHolder;
import org.testng.Assert;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
/**
* <pre>
* Created by BinaryWang on 2019/3/29.
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Test
@Guice(modules = ApiTestModule.class)
public class BaseWxQidianServiceImplTest {
@Inject
private WxQidianService wxService;
@Test
public void testSwitchover() {
assertTrue(this.wxService.switchover("another"));
assertThat(WxQidianConfigStorageHolder.get()).isEqualTo("another");
assertFalse(this.wxService.switchover("whatever"));
assertFalse(this.wxService.switchover("default"));
}
@Test
public void testSwitchoverTo() throws WxErrorException {
assertThat(this.wxService.switchoverTo("another").getAccessToken()).isNotEmpty();
assertThat(WxQidianConfigStorageHolder.get()).isEqualTo("another");
}
@Test
public void testNetCheck() throws WxErrorException {
WxNetCheckResult result = this.wxService.netCheck(WxConsts.NetCheckArgs.ACTIONALL, WxConsts.NetCheckArgs.OPERATORDEFAULT);
Assert.assertNotNull(result);
}
@Test
public void testGetCallbackIP() throws WxErrorException {
String[] ipArray = this.wxService.getCallbackIP();
System.out.println(Arrays.toString(ipArray));
Assert.assertNotNull(ipArray);
Assert.assertNotEquals(ipArray.length, 0);
}
public void testShortUrl() throws WxErrorException {
String shortUrl = this.wxService.shortUrl("http://www.baidu.com/test?access_token=123");
assertThat(shortUrl).isNotEmpty();
System.out.println(shortUrl);
}
@Test(expectedExceptions = WxErrorException.class)
public void testShortUrl_with_exceptional_url() throws WxErrorException {
this.wxService.shortUrl("http://www.baidu.com/test?redirect_count=1&access_token=123");
}
@Test
public void refreshAccessTokenDuplicatelyTest() throws InterruptedException {
// 测试多线程刷新accessToken时是否重复刷新
wxService.getWxMpConfigStorage().expireAccessToken();
final Set<String> set = Sets.newConcurrentHashSet();
Runnable r = () -> {
try {
String accessToken = wxService.getAccessToken();
set.add(accessToken);
} catch (WxErrorException e) {
e.printStackTrace();
}
};
final int threadNumber = 10;
ExecutorService executorService = Executors.newFixedThreadPool(threadNumber);
for ( int i = 0; i < threadNumber; i++ ) {
executorService.submit(r);
}
executorService.shutdown();
boolean isTerminated = executorService.awaitTermination(15, TimeUnit.SECONDS);
System.out.println("isTerminated: " + isTerminated);
System.out.println("times of refreshing accessToken: " + set.size());
assertEquals(set.size(), 1);
}
@Test
public void testCheckSignature() {
}
@Test
public void testGetTicket() {
}
@Test
public void testTestGetTicket() {
}
@Test
public void testGetJsapiTicket() {
}
@Test
public void testTestGetJsapiTicket() {
}
@Test
public void testCreateJsapiSignature() throws WxErrorException {
final WxJsapiSignature jsapiSignature = this.wxService.createJsapiSignature("http://www.baidu.com");
assertThat(jsapiSignature).isNotNull();
assertThat(jsapiSignature.getSignature()).isNotNull();
System.out.println(jsapiSignature);
}
@Test
public void testGetAccessToken() {
}
@Test
public void testSemanticQuery() {
}
@Test
public void testOauth2buildAuthorizationUrl() {
}
@Test
public void testBuildQrConnectUrl() {
}
@Test
public void testOauth2getAccessToken() {
}
@Test
public void testOauth2refreshAccessToken() {
}
@Test
public void testOauth2getUserInfo() {
}
@Test
public void testOauth2validateAccessToken() {
}
@Test
public void testGetCurrentAutoReplyInfo() {
}
@Test
public void testClearQuota() {
}
@Test
public void testGet() {
}
@Test
public void testTestGet() {
}
@Test
public void testPost() {
}
@Test
public void testTestPost() {
}
@Test
public void testExecute() {
}
@Test
public void testTestExecute() {
}
@Test
public void testExecuteInternal() {
}
@Test
public void testGetWxMpConfigStorage() {
}
@Test
public void testSetWxMpConfigStorage() {
}
@Test
public void testSetMultiConfigStorages() {
}
@Test
public void testTestSetMultiConfigStorages() {
}
@Test
public void testAddConfigStorage() {
}
@Test
public void testRemoveConfigStorage() {
}
@Test
public void testSetRetrySleepMillis() {
}
@Test
public void testSetMaxRetryTimes() {
}
@Test
public void testGetKefuService() {
}
@Test
public void testGetMaterialService() {
}
@Test
public void testGetMenuService() {
}
@Test
public void testGetUserService() {
}
@Test
public void testGetUserTagService() {
}
@Test
public void testGetQrcodeService() {
}
@Test
public void testGetCardService() {
}
@Test
public void testGetDataCubeService() {
}
@Test
public void testGetBlackListService() {
}
@Test
public void testGetStoreService() {
}
@Test
public void testGetTemplateMsgService() {
}
@Test
public void testGetSubscribeMsgService() {
}
@Test
public void testGetDeviceService() {
}
@Test
public void testGetShakeService() {
}
@Test
public void testGetMemberCardService() {
}
@Test
public void testGetRequestHttp() {
}
@Test
public void testGetMassMessageService() {
}
@Test
public void testSetKefuService() {
}
@Test
public void testSetMaterialService() {
}
@Test
public void testSetMenuService() {
}
@Test
public void testSetUserService() {
}
@Test
public void testSetTagService() {
}
@Test
public void testSetQrCodeService() {
}
@Test
public void testSetCardService() {
}
@Test
public void testSetStoreService() {
}
@Test
public void testSetDataCubeService() {
}
@Test
public void testSetBlackListService() {
}
@Test
public void testSetTemplateMsgService() {
}
@Test
public void testSetDeviceService() {
}
@Test
public void testSetShakeService() {
}
@Test
public void testSetMemberCardService() {
}
@Test
public void testSetMassMessageService() {
}
@Test
public void testGetAiOpenService() {
}
@Test
public void testSetAiOpenService() {
}
@Test
public void testGetWifiService() {
}
@Test
public void testGetOcrService() {
}
@Test
public void testGetMarketingService() {
}
@Test
public void testSetMarketingService() {
}
@Test
public void testSetOcrService() {
}
@Test
public void testGetCommentService() {
}
@Test
public void testSetCommentService() {
}
@Test
public void testGetImgProcService() {
}
@Test
public void testSetImgProcService() {
}
}

View File

@ -0,0 +1,58 @@
package me.chanjar.weixin.qidian.api.impl;
import java.util.List;
import java.util.Optional;
import com.google.inject.Inject;
import org.testng.Assert;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.api.test.ApiTestModule;
import me.chanjar.weixin.qidian.bean.call.GetSwitchBoardListResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRDialRequest;
import me.chanjar.weixin.qidian.bean.dial.IVRDialResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRListResponse;
import me.chanjar.weixin.qidian.bean.dial.Ivr;
@Test
@Guice(modules = ApiTestModule.class)
@Slf4j
public class WxQidianDialServiceImplTest {
@Inject
private WxQidianService wxService;
@Test
public void dial() throws WxErrorException {
// ivr
IVRListResponse iVRListResponse = this.wxService.getDialService().getIVRList();
Assert.assertEquals(iVRListResponse.getErrcode(), new Integer(0));
log.info("ivr size:" + iVRListResponse.getNode().size());
Optional<Ivr> optional = iVRListResponse.getNode().stream().filter((o) -> o.getIvr_name().equals("自动接听需求测试"))
.findFirst();
Assert.assertTrue(optional.isPresent());
Ivr ivr = optional.get();
String ivr_id = ivr.getIvr_id();
// ivr_id = "433";
// switch
GetSwitchBoardListResponse getSwitchBoardListResponse = this.wxService.getCallDataService().getSwitchBoardList();
Assert.assertEquals(getSwitchBoardListResponse.getErrcode(), new Integer(0));
log.info("switch size:" + getSwitchBoardListResponse.getData().switchBoards().size());
List<String> switchBoards = getSwitchBoardListResponse.getData().switchBoards();
// ivrdial
IVRDialRequest ivrDial = new IVRDialRequest();
ivrDial.setPhone_number("18434399105");
// ivrDial.setPhone_number("13811768266");
ivrDial.setIvr_id(ivr_id);
ivrDial.setCorp_phone_list(switchBoards);
IVRDialResponse ivrDialResponse = this.wxService.getDialService().ivrDial(ivrDial);
Assert.assertEquals(ivrDialResponse.getCode(), new Integer(0));
log.info(ivrDialResponse.getCallid());
}
}

View File

@ -0,0 +1,51 @@
package me.chanjar.weixin.qidian.api.test;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.locks.ReentrantLock;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.thoughtworks.xstream.XStream;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.xml.XStreamInitializer;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
@Slf4j
public class ApiTestModule implements Module {
private static final String TEST_CONFIG_XML = "test-config.xml";
@Override
public void configure(Binder binder) {
try (InputStream inputStream = ClassLoader.getSystemResourceAsStream(TEST_CONFIG_XML)) {
if (inputStream == null) {
throw new WxRuntimeException("测试配置文件【" + TEST_CONFIG_XML + "】未找到请参照test-config-sample.xml文件生成");
}
TestConfigStorage config = this.fromXml(TestConfigStorage.class, inputStream);
config.setAccessTokenLock(new ReentrantLock());
WxQidianService mpService = new WxQidianServiceHttpClientImpl();
mpService.setWxMpConfigStorage(config);
mpService.addConfigStorage("another", config);
binder.bind(WxQidianConfigStorage.class).toInstance(config);
binder.bind(WxQidianService.class).toInstance(mpService);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
private <T> T fromXml(Class<T> clazz, InputStream is) {
XStream xstream = XStreamInitializer.getInstance();
xstream.alias("xml", clazz);
xstream.processAnnotations(clazz);
return (T) xstream.fromXML(is);
}
}

View File

@ -0,0 +1,69 @@
package me.chanjar.weixin.qidian.api.test;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import me.chanjar.weixin.qidian.config.impl.WxQidianDefaultConfigImpl;
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.util.concurrent.locks.Lock;
@XStreamAlias("xml")
public class TestConfigStorage extends WxQidianDefaultConfigImpl {
private String openid;
private String kfAccount;
private String qrconnectRedirectUrl;
private String templateId;
private String keyPath;
public String getKeyPath() {
return keyPath;
}
public void setKeyPath(String keyPath) {
this.keyPath = keyPath;
}
public String getOpenid() {
return this.openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
public String getKfAccount() {
return this.kfAccount;
}
public void setKfAccount(String kfAccount) {
this.kfAccount = kfAccount;
}
public String getQrconnectRedirectUrl() {
return this.qrconnectRedirectUrl;
}
public void setQrconnectRedirectUrl(String qrconnectRedirectUrl) {
this.qrconnectRedirectUrl = qrconnectRedirectUrl;
}
@Override
public String getTemplateId() {
return this.templateId;
}
@Override
public void setTemplateId(String templateId) {
this.templateId = templateId;
}
public void setAccessTokenLock(Lock lock) {
super.accessTokenLock = lock;
}
}

View File

@ -0,0 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'Caller', ''} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -0,0 +1,16 @@
<xml>
<appId>公众号appID</appId>
<secret>公众号appsecret</secret>
<token>公众号Token</token>
<aesKey>公众号EncodingAESKey</aesKey>
<accessToken>可以不填写</accessToken>
<expiresTime>可以不填写</expiresTime>
<openid>某个加你公众号的用户的openId</openid>
<partnerId>微信商户平台ID</partnerId>
<partnerKey>商户平台设置的API密钥</partnerKey>
<keyPath>商户平台的证书文件地址</keyPath>
<templateId>模版消息的模版ID</templateId>
<oauth2redirectUri>网页授权获取用户信息回调地址</oauth2redirectUri>
<qrconnectRedirectUrl>网页应用授权登陆回调地址</qrconnectRedirectUrl>
<kfAccount>完整客服账号,格式为:账号前缀@公众号微信号</kfAccount>
</xml>

View File

@ -0,0 +1,30 @@
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Weixin-java-tool-suite" verbose="1">
<test name="API_Test">
<classes>
<class name="me.chanjar.weixin.qidian.api.WxMpBusyRetryTest"/>
<class name="me.chanjar.weixin.qidian.api.WxMpBaseAPITest"/>
<class name="me.chanjar.weixin.qidian.api.impl.WxMpMassMessageServiceImplTest"/>
<class name="me.chanjar.weixin.qidian.api.impl.WxMpUserServiceImplTest"/>
<class name="me.chanjar.weixin.qidian.api.impl.WxMpQrcodeServiceImplTest"/>
<class name="me.chanjar.weixin.qidian.api.WxMpShortUrlAPITest"/>
<class name="me.chanjar.weixin.qidian.api.WxMpMessageRouterTest"/>
<class name="me.chanjar.weixin.qidian.api.WxMpJsAPITest"/>
<class name="me.chanjar.weixin.qidian.api.WxMpMiscAPITest"/>
</classes>
</test>
<test name="Bean_Test">
<classes>
<class name="me.chanjar.weixin.qidian.bean.kefu.WxMpKefuMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutImageMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutMusicMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutNewsMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutVideoMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutVoiceMessageTest"/>
<class name="me.chanjar.weixin.qidian.bean.message.WxMpXmlOutTextMessageTest"/>
</classes>
</test>
</suite>