mirror of
				https://gitee.com/binary/weixin-java-tools.git
				synced 2025-10-31 18:46:10 +08:00 
			
		
		
		
	🆕 #1952 增加腾讯企点子模块,用于对接企点开放平台。
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,6 @@ | ||||
| .bash | ||||
| .history | ||||
|  | ||||
| *.class | ||||
| test-output | ||||
|  | ||||
|  | ||||
							
								
								
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * 微信接入相关配置属性. | ||||
|  * | ||||
|  | ||||
| @ -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 | ||||
| @ -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> | ||||
| @ -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 { | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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()); | ||||
|   } | ||||
| } | ||||
| @ -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, | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration | ||||
							
								
								
									
										201
									
								
								weixin-java-qidian/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								weixin-java-qidian/LICENSE
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										134
									
								
								weixin-java-qidian/pom.xml
									
									
									
									
									
										Normal 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> | ||||
| @ -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; | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
| } | ||||
| @ -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(); | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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 { | ||||
| } | ||||
| @ -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(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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()); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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"); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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() { | ||||
|   } | ||||
| } | ||||
| @ -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()); | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										13
									
								
								weixin-java-qidian/src/test/resources/logback-test.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								weixin-java-qidian/src/test/resources/logback-test.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										16
									
								
								weixin-java-qidian/src/test/resources/test-config.sample.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								weixin-java-qidian/src/test/resources/test-config.sample.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										30
									
								
								weixin-java-qidian/src/test/resources/testng.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								weixin-java-qidian/src/test/resources/testng.xml
									
									
									
									
									
										Normal 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> | ||||
		Reference in New Issue
	
	Block a user
	 fanxiayang12
					fanxiayang12