From e7f2bd62f841f604abb889fd5ed6ce0ac5cdd195 Mon Sep 17 00:00:00 2001 From: fanxiayang12 <751510087@qq.com> Date: Wed, 30 Dec 2020 09:17:35 +0800 Subject: [PATCH] =?UTF-8?q?:new:=20#1952=20=E5=A2=9E=E5=8A=A0=E8=85=BE?= =?UTF-8?q?=E8=AE=AF=E4=BC=81=E7=82=B9=E5=AD=90=E6=A8=A1=E5=9D=97=EF=BC=8C?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E5=AF=B9=E6=8E=A5=E4=BC=81=E7=82=B9=E5=BC=80?= =?UTF-8?q?=E6=94=BE=E5=B9=B3=E5=8F=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + pom.xml | 6 +- spring-boot-starters/pom.xml | 5 +- .../mp/config/WxMpAutoConfiguration.java | 2 +- .../config/WxMpStorageAutoConfiguration.java | 6 +- .../wxjava/mp/properties/WxMpProperties.java | 1 - .../README.md | 45 ++ .../pom.xml | 66 +++ .../config/WxQidianAutoConfiguration.java | 17 + .../WxQidianServiceAutoConfiguration.java | 63 +++ .../WxQidianStorageAutoConfiguration.java | 166 +++++++ .../wxjava/qidian/enums/HttpClientType.java | 22 + .../wxjava/qidian/enums/StorageType.java | 22 + .../wxjava/qidian/properties/HostConfig.java | 18 + .../qidian/properties/RedisProperties.java | 56 +++ .../qidian/properties/WxQidianProperties.java | 99 +++++ .../main/resources/META-INF/spring.factories | 1 + weixin-java-qidian/LICENSE | 201 +++++++++ weixin-java-qidian/pom.xml | 134 ++++++ .../qidian/api/WxQidianCallDataService.java | 13 + .../qidian/api/WxQidianDialService.java | 18 + .../weixin/qidian/api/WxQidianService.java | 348 +++++++++++++++ .../api/impl/BaseWxQidianServiceImpl.java | 420 ++++++++++++++++++ .../api/impl/WxQidianCallDataServiceImpl.java | 23 + .../api/impl/WxQidianDialServiceImpl.java | 43 ++ .../impl/WxQidianServiceHttpClientImpl.java | 106 +++++ .../qidian/api/impl/WxQidianServiceImpl.java | 12 + .../api/impl/WxQidianServiceJoddHttpImpl.java | 90 ++++ .../api/impl/WxQidianServiceOkHttpImpl.java | 98 ++++ .../qidian/bean/WxQidianHostConfig.java | 56 +++ .../bean/call/GetSwitchBoardListResponse.java | 14 + .../weixin/qidian/bean/call/SwitchBoard.java | 13 + .../qidian/bean/call/SwitchBoardList.java | 15 + .../qidian/bean/common/QidianResponse.java | 109 +++++ .../qidian/bean/dial/IVRDialRequest.java | 28 ++ .../qidian/bean/dial/IVRDialResponse.java | 20 + .../qidian/bean/dial/IVRListResponse.java | 16 + .../chanjar/weixin/qidian/bean/dial/Ivr.java | 9 + .../qidian/config/WxQidianConfigStorage.java | 210 +++++++++ .../impl/WxQidianDefaultConfigImpl.java | 196 ++++++++ .../config/impl/WxQidianRedisConfigImpl.java | 99 +++++ .../impl/WxQidianRedissonConfigImpl.java | 101 +++++ .../weixin/qidian/enums/WxQidianApiUrl.java | 155 +++++++ .../util/WxQidianConfigStorageHolder.java | 29 ++ .../qidian/util/json/WxQidianGsonBuilder.java | 21 + .../weixin/qidian/api/WxMpBusyRetryTest.java | 64 +++ .../weixin/qidian/api/WxMpJsAPITest.java | 37 ++ .../api/impl/BaseWxQidianServiceImplTest.java | 407 +++++++++++++++++ .../api/impl/WxQidianDialServiceImplTest.java | 58 +++ .../weixin/qidian/api/test/ApiTestModule.java | 51 +++ .../qidian/api/test/TestConfigStorage.java | 69 +++ .../src/test/resources/logback-test.xml | 13 + .../src/test/resources/test-config.sample.xml | 16 + .../src/test/resources/testng.xml | 30 ++ 54 files changed, 3928 insertions(+), 12 deletions(-) create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java create mode 100644 spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 weixin-java-qidian/LICENSE create mode 100644 weixin-java-qidian/pom.xml create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianCallDataService.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianDialService.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianService.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianCallDataServiceImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceHttpClientImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceJoddHttpImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceOkHttpImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/WxQidianHostConfig.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/GetSwitchBoardListResponse.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoard.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoardList.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/common/QidianResponse.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialRequest.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialResponse.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRListResponse.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/Ivr.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/WxQidianConfigStorage.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianDefaultConfigImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedisConfigImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedissonConfigImpl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/enums/WxQidianApiUrl.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/WxQidianConfigStorageHolder.java create mode 100644 weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/json/WxQidianGsonBuilder.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpBusyRetryTest.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpJsAPITest.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImplTest.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImplTest.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/ApiTestModule.java create mode 100644 weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/TestConfigStorage.java create mode 100644 weixin-java-qidian/src/test/resources/logback-test.xml create mode 100644 weixin-java-qidian/src/test/resources/test-config.sample.xml create mode 100644 weixin-java-qidian/src/test/resources/testng.xml diff --git a/.gitignore b/.gitignore index 2a629437b..6a5b5f751 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.bash +.history + *.class test-output diff --git a/pom.xml b/pom.xml index 638bb04b7..7dab92d4c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,5 @@ - + 4.0.0 com.github.binarywang wx-java @@ -111,6 +108,7 @@ weixin-java-pay weixin-java-miniapp weixin-java-open + weixin-java-qidian spring-boot-starters diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index 05ddf3fdb..14be0e385 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 com.github.binarywang @@ -22,6 +20,7 @@ wx-java-mp-spring-boot-starter wx-java-pay-spring-boot-starter wx-java-open-spring-boot-starter + wx-java-qidian-spring-boot-starter diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java index e2b0a60d2..b2e3848ab 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java @@ -12,6 +12,6 @@ import org.springframework.context.annotation.Import; */ @Configuration @EnableConfigurationProperties(WxMpProperties.class) -@Import({WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class}) +@Import({ WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class }) public class WxMpAutoConfiguration { } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java index c47272714..ef5cdb25f 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java @@ -88,7 +88,7 @@ public class WxMpStorageAutoConfiguration { } WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, - wxMpProperties.getConfigStorage().getKeyPrefix()); + wxMpProperties.getConfigStorage().getKeyPrefix()); setWxMpInfo(wxMpRedisConfig); return wxMpRedisConfig; } @@ -114,7 +114,7 @@ public class WxMpStorageAutoConfiguration { WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate); WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, - wxMpProperties.getConfigStorage().getKeyPrefix()); + wxMpProperties.getConfigStorage().getKeyPrefix()); setWxMpInfo(wxMpRedisConfig); return wxMpRedisConfig; @@ -160,6 +160,6 @@ public class WxMpStorageAutoConfiguration { } return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), - redis.getDatabase()); + redis.getDatabase()); } } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java index 3d3518bfd..89d0e6629 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java @@ -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; - /** * 微信接入相关配置属性. * diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md new file mode 100644 index 000000000..d676616de --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md @@ -0,0 +1,45 @@ +# wx-java-qidian-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-qidian-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 公众号配置(必填) + wx.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 diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml new file mode 100644 index 000000000..85a944ebb --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml @@ -0,0 +1,66 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.0.1.B + + 4.0.0 + + wx-java-qidian-spring-boot-starter + WxJava - Spring Boot Starter for QiDian + 腾讯企点的 Spring Boot Starter + + + + com.github.binarywang + weixin-java-qidian + ${project.version} + + + redis.clients + jedis + compile + + + org.springframework.data + spring-data-redis + ${spring.boot.version} + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java new file mode 100644 index 000000000..bb66fde26 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java @@ -0,0 +1,17 @@ +package com.binarywang.spring.starter.wxjava.qidian.config; + +import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * . + * + * @author someone + */ +@Configuration +@EnableConfigurationProperties(WxQidianProperties.class) +@Import({ WxQidianStorageAutoConfiguration.class, WxQidianServiceAutoConfiguration.class }) +public class WxQidianAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java new file mode 100644 index 000000000..3af628d01 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java @@ -0,0 +1,63 @@ +package com.binarywang.spring.starter.wxjava.qidian.config; + +import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties; +import me.chanjar.weixin.qidian.api.WxQidianService; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceJoddHttpImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceOkHttpImpl; +import me.chanjar.weixin.qidian.config.WxQidianConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 腾讯企点相关服务自动注册. + * + * @author alegria + */ +@Configuration +public class WxQidianServiceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQidianProperties wxQidianProperties) { + HttpClientType httpClientType = wxQidianProperties.getConfigStorage().getHttpClientType(); + WxQidianService wxQidianService; + switch (httpClientType) { + case OkHttp: + wxQidianService = newWxQidianServiceOkHttpImpl(); + break; + case JoddHttp: + wxQidianService = newWxQidianServiceJoddHttpImpl(); + break; + case HttpClient: + wxQidianService = newWxQidianServiceHttpClientImpl(); + break; + default: + wxQidianService = newWxQidianServiceImpl(); + break; + } + + wxQidianService.setWxMpConfigStorage(configStorage); + return wxQidianService; + } + + private WxQidianService newWxQidianServiceImpl() { + return new WxQidianServiceImpl(); + } + + private WxQidianService newWxQidianServiceHttpClientImpl() { + return new WxQidianServiceHttpClientImpl(); + } + + private WxQidianService newWxQidianServiceOkHttpImpl() { + return new WxQidianServiceOkHttpImpl(); + } + + private WxQidianService newWxQidianServiceJoddHttpImpl() { + return new WxQidianServiceJoddHttpImpl(); + } + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java new file mode 100644 index 000000000..84163b005 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java @@ -0,0 +1,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 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()); + } +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java new file mode 100644 index 000000000..9418a8bec --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java @@ -0,0 +1,22 @@ +package com.binarywang.spring.starter.wxjava.qidian.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * @date 2020-08-30 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java new file mode 100644 index 000000000..0a7a6b85d --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java @@ -0,0 +1,22 @@ +package com.binarywang.spring.starter.wxjava.qidian.enums; + +/** + * storage类型. + * + * @author Binary Wang + * @date 2020-08-30 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java new file mode 100644 index 000000000..92ade849f --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java @@ -0,0 +1,18 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class HostConfig implements Serializable { + + private static final long serialVersionUID = -4172767630740346001L; + + private String apiHost; + + private String openHost; + + private String qidianHost; + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java new file mode 100644 index 000000000..b055b63fe --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * redis 配置属性. + * + * @author Binary Wang + * @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; +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java new file mode 100644 index 000000000..ddecefb7e --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java @@ -0,0 +1,99 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.qidian.enums.StorageType; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; + +import static com.binarywang.spring.starter.wxjava.qidian.enums.StorageType.Memory; +import static com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties.PREFIX; + +/** + * 企点接入相关配置属性. + * + * @author someone + */ +@Data +@ConfigurationProperties(PREFIX) +public class WxQidianProperties { + public static final String PREFIX = "wx.qidian"; + + /** + * 设置腾讯企点的appid. + */ + private String appId; + + /** + * 设置腾讯企点的app secret. + */ + private String secret; + + /** + * 设置腾讯企点的token. + */ + private String token; + + /** + * 设置腾讯企点的EncodingAESKey. + */ + private String aesKey; + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx"; + + /** + * redis连接配置. + */ + private RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpClient; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + } + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..bfcb7bf91 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration diff --git a/weixin-java-qidian/LICENSE b/weixin-java-qidian/LICENSE new file mode 100644 index 000000000..5c304d1a4 --- /dev/null +++ b/weixin-java-qidian/LICENSE @@ -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. diff --git a/weixin-java-qidian/pom.xml b/weixin-java-qidian/pom.xml new file mode 100644 index 000000000..710f6d559 --- /dev/null +++ b/weixin-java-qidian/pom.xml @@ -0,0 +1,134 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.0.1.B + + + weixin-java-qidian + WxJava - 企点 Java SDK + 腾讯企点Java SDK + + + + com.github.binarywang + weixin-java-common + ${project.version} + + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + org.testng + testng + test + + + org.mockito + mockito-all + test + + + com.google.inject + guice + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + + + joda-time + joda-time + test + + + redis.clients + jedis + + + ch.qos.logback + logback-classic + test + + + org.assertj + assertj-guava + test + + + org.projectlombok + lombok + + + org.redisson + redisson + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + src/test/resources/testng.xml + + + + + + + + + native-image + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor + + + + com.github.binarywang + weixin-graal + ${project.version} + + + + + + + + + + diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianCallDataService.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianCallDataService.java new file mode 100644 index 000000000..835102aed --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianCallDataService.java @@ -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; +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianDialService.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianDialService.java new file mode 100644 index 000000000..eebf777f6 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianDialService.java @@ -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; + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianService.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianService.java new file mode 100644 index 000000000..aeea34e82 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/WxQidianService.java @@ -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 { + /** + *
+   * 验证消息的确来自微信服务器.
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
+   * 
+ * + * @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; + + /** + *
+   * 获取access_token,本方法线程安全.
+   * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+   *
+   * 另:本service的所有方法都会在access_token过期时调用此方法
+   *
+   * 程序员在非必要情况下尽量不要主动调用此方法
+   *
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
+   * 
+ * + * @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; + + /** + *
+   * 获得ticket.
+   * 获得时会检查 Token是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+   * 
+ * + * @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; + + /** + *
+   * 获得jsapi_ticket.
+   * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+   *
+   * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
+   * 
+ * + * @param forceRefresh 强制刷新 + * @return jsapi ticket + * @throws WxErrorException . + */ + String getJsapiTicket(boolean forceRefresh) throws WxErrorException; + + /** + *
+   * 创建调用jsapi时所需要的签名.
+   *
+   * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
+   * 
+ * + * @param url 地址 + * @return 生成的签名对象 wx jsapi signature + * @throws WxErrorException . + */ + WxJsapiSignature createJsapiSignature(String url) throws WxErrorException; + + /** + *
+   * 长链接转短链接接口.
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=长链接转短链接接口
+   * 
+ * + * @param longUrl 长url + * @return 生成的短地址 string + * @throws WxErrorException . + */ + String shortUrl(String longUrl) throws WxErrorException; + + /** + *
+   * 构造第三方使用网站应用授权登录的url.
+   * 详情请见: 网站应用微信登录开发指南
+   * URL格式为:https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
+   * 
+ * + * @param redirectUri 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode + * @param scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可 + * @param state 非必填,用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验 + * @return url string + */ + String buildQrConnectUrl(String redirectUri, String scope, String state); + + /** + *
+   * 获取微信服务器IP地址
+   * http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html
+   * 
+ * + * @return 微信服务器ip地址数组 string [ ] + * @throws WxErrorException . + */ + String[] getCallbackIP() throws WxErrorException; + + /** + *
+   *  网络检测
+   *  https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21541575776DtsuT
+   *  为了帮助开发者排查回调连接失败的问题,提供这个网络检测的API。它可以对开发者URL做域名解析,然后对所有IP进行一次ping操作,得到丢包率和耗时。
+   * 
+ * + * @param action 执行的检测动作 + * @param operator 指定平台从某个运营商进行检测 + * @return 检测结果 wx net check result + * @throws WxErrorException . + */ + WxNetCheckResult netCheck(String action, String operator) throws WxErrorException; + + /** + *
+   *  公众号调用或第三方平台帮公众号调用对公众号的所有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
+   *
+   * 
+ * + * @param appid 公众号的APPID + * @throws WxErrorException the wx error exception + */ + void clearQuota(String appid) throws WxErrorException; + + /** + *
+   * Service没有实现某个API的时候,可以用这个,
+   * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+   * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+   * 
+ * + * @param the type parameter + * @param the type parameter + * @param executor 执行器 + * @param url 接口地址 + * @param data 参数数据 + * @return 结果 t + * @throws WxErrorException 异常 + */ + T execute(RequestExecutor 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; + + /** + *
+   * Service没有实现某个API的时候,可以用这个,
+   * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+   * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+   * 
+ * + * @param the type parameter + * @param the type parameter + * @param executor 执行器 + * @param url 接口地址 + * @param data 参数数据 + * @return 结果 t + * @throws WxErrorException 异常 + */ + T execute(RequestExecutor executor, WxQidianApiUrl url, E data) throws WxErrorException; + + /** + * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试. + * + * @param retrySleepMillis 默认:1000ms + */ + void setRetrySleepMillis(int retrySleepMillis); + + /** + *
+   * 设置当微信系统响应系统繁忙时,最大重试次数.
+   * 默认:5次
+   * 
+ * + * @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 configStorages); + + /** + * 注入多个 {@link WxQidianConfigStorage} 的实现. 并为每个 {@link WxQidianConfigStorage} + * 赋予不同的 {@link String label} 值 + * + * @param configStorages WxMpConfigStorage map + * @param defaultMpId 设置一个{@link WxQidianConfigStorage} 所对应的{@link String + * mpId}进行Http初始化 + */ + void setMultiConfigStorages(Map 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(); +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImpl.java new file mode 100644 index 000000000..0bc089608 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImpl.java @@ -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 implements WxQidianService, RequestHttp { + @Getter + private WxQidianDialService dialService = new WxQidianDialServiceImpl(this); + @Getter + private WxQidianCallDataService callDataService = new WxQidianCallDataServiceImpl(this); + + private Map 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 execute(RequestExecutor executor, WxQidianApiUrl url, E data) throws WxErrorException { + return this.execute(executor, url.getUrl(this.getWxMpConfigStorage()), data); + } + + /** + * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求. + */ + @Override + public T execute(RequestExecutor 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 executeInternal(RequestExecutor 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 configStorages) { + this.setMultiConfigStorages(configStorages, configStorages.keySet().iterator().next()); + } + + @Override + public void setMultiConfigStorages(Map 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; + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianCallDataServiceImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianCallDataServiceImpl.java new file mode 100644 index 000000000..344245eba --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianCallDataServiceImpl.java @@ -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); + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImpl.java new file mode 100644 index 000000000..c314c4a5c --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImpl.java @@ -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); + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceHttpClientImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceHttpClientImpl.java new file mode 100644 index 000000000..cd39f1a68 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceHttpClientImpl.java @@ -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 { + 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(); + } + } + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceImpl.java new file mode 100644 index 000000000..45e87204c --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceImpl.java @@ -0,0 +1,12 @@ +package me.chanjar.weixin.qidian.api.impl; + +/** + *
+ * 默认接口实现类,使用apache httpclient实现
+ * Created by Binary Wang on 2017-5-27.
+ * 
+ * + * @author Binary Wang + */ +public class WxQidianServiceImpl extends WxQidianServiceHttpClientImpl { +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceJoddHttpImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceJoddHttpImpl.java new file mode 100644 index 000000000..41ec6d9f3 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceJoddHttpImpl.java @@ -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 { + 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(); + } + } + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceOkHttpImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceOkHttpImpl.java new file mode 100644 index 000000000..2399399de --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/api/impl/WxQidianServiceOkHttpImpl.java @@ -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 { + 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(); + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/WxQidianHostConfig.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/WxQidianHostConfig.java new file mode 100644 index 000000000..677348863 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/WxQidianHostConfig.java @@ -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; + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/GetSwitchBoardListResponse.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/GetSwitchBoardListResponse.java new file mode 100644 index 000000000..aed74609b --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/GetSwitchBoardListResponse.java @@ -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); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoard.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoard.java new file mode 100644 index 000000000..440a26c16 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoard.java @@ -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; +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoardList.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoardList.java new file mode 100644 index 000000000..d4eba3386 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/call/SwitchBoardList.java @@ -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 records; + + public List switchBoards() { + return records.stream().map(SwitchBoard::getSwitchboard).collect(Collectors.toList()); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/common/QidianResponse.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/common/QidianResponse.java new file mode 100644 index 000000000..6089c5528 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/common/QidianResponse.java @@ -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 errorCodesMap = new HashMap() { + 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; + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialRequest.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialRequest.java new file mode 100644 index 000000000..35c2e805e --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialRequest.java @@ -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 corp_phone_list; + private Integer loc_pref_on = 1; + private List backup_corp_phone_list; + private Boolean skip_restrict = false; + + @Override + public String toString() { + return this.toJson(); + } + + public String toJson() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialResponse.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialResponse.java new file mode 100644 index 000000000..2d4edab70 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRDialResponse.java @@ -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); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRListResponse.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRListResponse.java new file mode 100644 index 000000000..c8fd08fd4 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/IVRListResponse.java @@ -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 node; + + public static IVRListResponse fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, IVRListResponse.class); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/Ivr.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/Ivr.java new file mode 100644 index 000000000..07c0c1c2b --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/bean/dial/Ivr.java @@ -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; +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/WxQidianConfigStorage.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/WxQidianConfigStorage.java new file mode 100644 index 000000000..3dc42cc31 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/WxQidianConfigStorage.java @@ -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); +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianDefaultConfigImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianDefaultConfigImpl.java new file mode 100644 index 000000000..a5851aadf --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianDefaultConfigImpl.java @@ -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; + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedisConfigImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedisConfigImpl.java new file mode 100644 index 000000000..436238153 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedisConfigImpl.java @@ -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. + * + *
+ *    使用说明:本实现仅供参考,并不完整,
+ *    比如为减少项目依赖,未加入redis分布式锁的实现,如有需要请自行实现。
+ * 
+ * + * @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); + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedissonConfigImpl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedissonConfigImpl.java new file mode 100644 index 000000000..876922e06 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/config/impl/WxQidianRedissonConfigImpl.java @@ -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); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/enums/WxQidianApiUrl.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/enums/WxQidianApiUrl.java new file mode 100644 index 000000000..54f80dee0 --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/enums/WxQidianApiUrl.java @@ -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.*; + +/** + *
+ *  腾讯企点接口api地址
+ *  Created by alegria on 2020年12月26日.
+ * 
+ */ +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; + + } + +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/WxQidianConfigStorageHolder.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/WxQidianConfigStorageHolder.java new file mode 100644 index 000000000..1177ce4ac --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/WxQidianConfigStorageHolder.java @@ -0,0 +1,29 @@ +package me.chanjar.weixin.qidian.util; + +/** + * @author alegria + * @date 2020年12月26日 + */ +public class WxQidianConfigStorageHolder { + private final static ThreadLocal THREAD_LOCAL = new ThreadLocal() { + @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(); + } +} diff --git a/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/json/WxQidianGsonBuilder.java b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/json/WxQidianGsonBuilder.java new file mode 100644 index 000000000..bdce6bbed --- /dev/null +++ b/weixin-java-qidian/src/main/java/me/chanjar/weixin/qidian/util/json/WxQidianGsonBuilder.java @@ -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(); + } + +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpBusyRetryTest.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpBusyRetryTest.java new file mode 100644 index 000000000..090dd893d --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpBusyRetryTest.java @@ -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 executeInternal( + RequestExecutor 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(); + } + +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpJsAPITest.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpJsAPITest.java new file mode 100644 index 000000000..7c84e7833 --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/WxMpJsAPITest.java @@ -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"); + } + +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImplTest.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImplTest.java new file mode 100644 index 000000000..409adf49c --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/BaseWxQidianServiceImplTest.java @@ -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; + +/** + *
+ *  Created by BinaryWang on 2019/3/29.
+ * 
+ * + * @author Binary Wang + */ +@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 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() { + } +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImplTest.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImplTest.java new file mode 100644 index 000000000..e91d471c1 --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/impl/WxQidianDialServiceImplTest.java @@ -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 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 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()); + } +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/ApiTestModule.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/ApiTestModule.java new file mode 100644 index 000000000..ddc1eb020 --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/ApiTestModule.java @@ -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 fromXml(Class clazz, InputStream is) { + XStream xstream = XStreamInitializer.getInstance(); + xstream.alias("xml", clazz); + xstream.processAnnotations(clazz); + return (T) xstream.fromXML(is); + } + +} diff --git a/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/TestConfigStorage.java b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/TestConfigStorage.java new file mode 100644 index 000000000..539136610 --- /dev/null +++ b/weixin-java-qidian/src/test/java/me/chanjar/weixin/qidian/api/test/TestConfigStorage.java @@ -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; + } + +} diff --git a/weixin-java-qidian/src/test/resources/logback-test.xml b/weixin-java-qidian/src/test/resources/logback-test.xml new file mode 100644 index 000000000..e4a33acd8 --- /dev/null +++ b/weixin-java-qidian/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'Caller', ''} - %msg%n + + + + + + + + diff --git a/weixin-java-qidian/src/test/resources/test-config.sample.xml b/weixin-java-qidian/src/test/resources/test-config.sample.xml new file mode 100644 index 000000000..3df1de9d5 --- /dev/null +++ b/weixin-java-qidian/src/test/resources/test-config.sample.xml @@ -0,0 +1,16 @@ + + 公众号appID + 公众号appsecret + 公众号Token + 公众号EncodingAESKey + 可以不填写 + 可以不填写 + 某个加你公众号的用户的openId + 微信商户平台ID + 商户平台设置的API密钥 + 商户平台的证书文件地址 + 模版消息的模版ID + 网页授权获取用户信息回调地址 + 网页应用授权登陆回调地址 + 完整客服账号,格式为:账号前缀@公众号微信号 + diff --git a/weixin-java-qidian/src/test/resources/testng.xml b/weixin-java-qidian/src/test/resources/testng.xml new file mode 100644 index 000000000..4690a4cad --- /dev/null +++ b/weixin-java-qidian/src/test/resources/testng.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +