Browse Source

Merge pull request #1577 from Wechat-Group/develop

合并Develop分支,发布正式版本
Binary Wang 5 years ago
parent
commit
9ab2cfb2a8
100 changed files with 3790 additions and 678 deletions
  1. 24 10
      pom.xml
  2. 1 1
      spring-boot-starters/pom.xml
  3. 29 20
      spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md
  4. 12 1
      spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
  5. 91 10
      spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java
  6. 110 0
      spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java
  7. 19 12
      spring-boot-starters/wx-java-mp-spring-boot-starter/README.md
  8. 5 4
      spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
  9. 54 2
      spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java
  10. 57 28
      spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java
  11. 0 45
      spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java
  12. 98 4
      spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java
  13. 18 17
      spring-boot-starters/wx-java-open-spring-boot-starter/README.md
  14. 9 3
      spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
  15. 48 33
      spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java
  16. 1 1
      spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java
  17. 48 3
      spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java
  18. 1 1
      spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
  19. 7 0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
  20. 31 0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
  21. 1 1
      weixin-graal/pom.xml
  22. 33 22
      weixin-graal/src/main/java/cn/binarywang/wx/graal/GraalProcessor.java
  23. 17 1
      weixin-java-common/pom.xml
  24. 6 6
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/TicketType.java
  25. 317 1
      weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java
  26. 2 2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java
  27. 37 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java
  28. 52 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
  29. 43 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
  30. 47 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
  31. 27 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java
  32. 41 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java
  33. 9 13
      weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java
  34. 2 3
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java
  35. 16 11
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java
  36. 11 2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java
  37. 19 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java
  38. 6 1
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java
  39. 3 7
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientSimpleGetRequestExecutor.java
  40. 3 14
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java
  41. 46 91
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
  42. 6 1
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java
  43. 1 7
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java
  44. 1 16
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java
  45. 6 3
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java
  46. 1 6
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java
  47. 3 9
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java
  48. 73 0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java
  49. 31 6
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
  50. 51 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
  51. 21 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
  52. 26 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
  53. 27 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
  54. 13 2
      weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java
  55. 6 2
      weixin-java-cp/pom.xml
  56. 79 2
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java
  57. 40 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java
  58. 33 2
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java
  59. 117 12
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java
  60. 113 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
  61. 2 2
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
  62. 28 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java
  63. 3 2
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java
  64. 202 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java
  65. 44 42
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java
  66. 219 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java
  67. 26 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPreauthCode.java
  68. 72 68
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java
  69. 18 3
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java
  70. 75 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatInfo.java
  71. 46 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatList.java
  72. 90 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatStatistic.java
  73. 52 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalUnassignList.java
  74. 78 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalUserBehaviorStatistic.java
  75. 8 28
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserWithExternalPermission.java
  76. 66 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpXmlMessage.java
  77. 4 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/article/MpnewsArticle.java
  78. 0 1
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/WxCpApprovalComment.java
  79. 4 4
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/WxCpApprovalDetail.java
  80. 29 13
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/applydata/ContentValue.java
  81. 3 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/TemplateOptions.java
  82. 2 4
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/TemplateProperty.java
  83. 3 1
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/control/TemplateSelector.java
  84. 4 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/taskcard/TaskCardButton.java
  85. 7 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
  86. 20 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
  87. 17 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
  88. 154 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java
  89. 8 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
  90. 7 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpDepartGsonAdapter.java
  91. 60 25
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java
  92. 139 0
      weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImplTest.java
  93. 1 1
      weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImplTest.java
  94. 6 6
      weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/WxCpXmlMessageTest.java
  95. 42 12
      weixin-java-cp/src/test/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapterTest.java
  96. 6 2
      weixin-java-miniapp/pom.xml
  97. 84 9
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaCloudService.java
  98. 62 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaLiveService.java
  99. 50 17
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
  100. 0 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaCloudServiceImpl.java

+ 24 - 10
pom.xml

@@ -6,7 +6,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.github.binarywang</groupId>
   <artifactId>wx-java</artifactId>
-  <version>3.7.0</version>
+  <version>3.8.0</version>
   <packaging>pom</packaging>
   <name>WxJava - Weixin/Wechat Java SDK</name>
   <description>微信开发Java SDK</description>
@@ -116,7 +116,7 @@
 
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <httpclient.version>4.5</httpclient.version>
-    <jetty.version>9.4.17.v20190418</jetty.version>
+    <jetty.version>9.4.28.v20200408</jetty.version>
   </properties>
 
   <dependencyManagement>
@@ -130,13 +130,13 @@
       <dependency>
         <groupId>org.jodd</groupId>
         <artifactId>jodd-http</artifactId>
-        <version>3.7.1</version>
+        <version>5.1.4</version>
         <scope>provided</scope>
       </dependency>
       <dependency>
         <groupId>com.squareup.okhttp3</groupId>
         <artifactId>okhttp</artifactId>
-        <version>3.7.0</version>
+        <version>4.5.0</version>
         <scope>provided</scope>
       </dependency>
 
@@ -163,7 +163,7 @@
       <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
-        <version>3.5</version>
+        <version>3.10</version>
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
@@ -191,31 +191,31 @@
       <dependency>
         <groupId>joda-time</groupId>
         <artifactId>joda-time</artifactId>
-        <version>2.9.7</version>
+        <version>2.10.6</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>ch.qos.logback</groupId>
         <artifactId>logback-classic</artifactId>
-        <version>1.1.11</version>
+        <version>1.2.3</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>com.google.inject</groupId>
         <artifactId>guice</artifactId>
-        <version>3.0</version>
+        <version>4.2.3</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>org.testng</groupId>
         <artifactId>testng</artifactId>
-        <version>6.10</version>
+        <version>7.1.0</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>org.mockito</groupId>
         <artifactId>mockito-all</artifactId>
-        <version>1.9.5</version>
+        <version>1.10.19</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -244,9 +244,23 @@
         <scope>provided</scope>
       </dependency>
       <dependency>
+        <groupId>com.github.jedis-lock</groupId>
+        <artifactId>jedis-lock</artifactId>
+        <version>1.0.0</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
         <groupId>org.redisson</groupId>
         <artifactId>redisson</artifactId>
         <version>3.12.0</version>
+        <optional>true</optional>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework.data</groupId>
+        <artifactId>spring-data-redis</artifactId>
+        <version>1.8.23.RELEASE</version>
+        <optional>true</optional>
         <scope>provided</scope>
       </dependency>
       <dependency>

+ 1 - 1
spring-boot-starters/pom.xml

@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.github.binarywang</groupId>
     <artifactId>wx-java</artifactId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
   <packaging>pom</packaging>
   <artifactId>wx-java-spring-boot-starters</artifactId>

+ 29 - 20
spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md

@@ -1,26 +1,35 @@
-# 使用说明
-1. 在自己的Spring Boot项目里,引入maven依赖
-```xml
+# wx-java-miniapp-spring-boot-starter
+## 快速开始
+1. 引入依赖
+    ```xml
     <dependency>
         <groupId>com.github.binarywang</groupId>
         <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
         <version>${version}</version>
     </dependency>
- ```
-2. 添加配置(application.yml)
-```yml
-wx:
-  miniapp:
-    appid: 111
-    secret: 111
-    token: 111
-    aesKey: 111
-    msgDataFormat: JSON
-```
-
-
-
-
-
-
+    ```
+2. 添加配置(application.properties)
+    ```properties
+    # 公众号配置(必填)
+    wx.miniapp.appid = appId
+    wx.miniapp.secret = @secret
+    wx.miniapp.token = @token
+    wx.miniapp.aesKey = @aesKey
+    wx.miniapp.msgDataFormat = @msgDataFormat                  # 消息格式,XML或者JSON.
+    # 存储配置redis(可选)
+    # 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool)
+    wx.miniapp.config-storage.type = jedis                     # 配置类型: memory(默认), jedis, redistemplate
+    wx.miniapp.config-storage.key-prefix = wa                  # 相关redis前缀配置: wa(默认)
+    wx.miniapp.config-storage.redis.host = 127.0.0.1
+    wx.miniapp.config-storage.redis.port = 6379
+    # http客户端配置
+    wx.miniapp.config-storage.http-client-type=httpclient      # http客户端类型: httpclient(默认)
+    wx.miniapp.config-storage.http-proxy-host=
+    wx.miniapp.config-storage.http-proxy-port=
+    wx.miniapp.config-storage.http-proxy-username=
+    wx.miniapp.config-storage.http-proxy-password=
+    ```
+3. 自动注入的类型
+- `WxMaService`
+- `WxMaConfig`
 

+ 12 - 1
spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml

@@ -5,7 +5,7 @@
   <parent>
     <artifactId>wx-java-spring-boot-starters</artifactId>
     <groupId>com.github.binarywang</groupId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -19,6 +19,17 @@
       <artifactId>weixin-java-miniapp</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>redis.clients</groupId>
+      <artifactId>jedis</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.data</groupId>
+      <artifactId>spring-data-redis</artifactId>
+      <version>${spring.boot.version}</version>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 
   <build>

+ 91 - 10
spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java

@@ -4,15 +4,23 @@ import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
 import cn.binarywang.wx.miniapp.config.WxMaConfig;
 import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
 import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
 import lombok.AllArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+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.JedisPoolConfig;
 
 /**
  * 自动配置.
@@ -26,7 +34,9 @@ import org.springframework.context.annotation.Configuration;
 @EnableConfigurationProperties(WxMaProperties.class)
 @ConditionalOnProperty(prefix = "wx.miniapp", value = "enabled", matchIfMissing = true)
 public class WxMaAutoConfiguration {
-  private WxMaProperties properties;
+
+  private final WxMaProperties wxMaProperties;
+  private final ApplicationContext applicationContext;
 
   /**
    * 小程序service.
@@ -35,16 +45,87 @@ public class WxMaAutoConfiguration {
    */
   @Bean
   @ConditionalOnMissingBean(WxMaService.class)
-  public WxMaService service() {
-    WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
-    config.setAppid(StringUtils.trimToNull(this.properties.getAppid()));
-    config.setSecret(StringUtils.trimToNull(this.properties.getSecret()));
-    config.setToken(StringUtils.trimToNull(this.properties.getToken()));
-    config.setAesKey(StringUtils.trimToNull(this.properties.getAesKey()));
-    config.setMsgDataFormat(StringUtils.trimToNull(this.properties.getMsgDataFormat()));
-
+  public WxMaService service(WxMaConfig wxMaConfig) {
     final WxMaServiceImpl service = new WxMaServiceImpl();
-    service.setWxMaConfig(config);
+    service.setWxMaConfig(wxMaConfig);
     return service;
   }
+
+  @Bean
+  @ConditionalOnMissingBean(WxMaConfig.class)
+  public WxMaConfig wxMaConfig() {
+    WxMaProperties.StorageType type = wxMaProperties.getConfigStorage().getType();
+    WxMaDefaultConfigImpl config;
+    if (type == WxMaProperties.StorageType.jedis) {
+      config = wxMaInJedisConfigStorage();
+    } else if (type == WxMaProperties.StorageType.redistemplate) {
+      config = wxMaInRedisTemplateConfigStorage();
+    } else {
+      config = wxMaInMemoryConfigStorage();
+    }
+
+    config.setAppid(StringUtils.trimToNull(this.wxMaProperties.getAppid()));
+    config.setSecret(StringUtils.trimToNull(this.wxMaProperties.getSecret()));
+    config.setToken(StringUtils.trimToNull(this.wxMaProperties.getToken()));
+    config.setAesKey(StringUtils.trimToNull(this.wxMaProperties.getAesKey()));
+    config.setMsgDataFormat(StringUtils.trimToNull(this.wxMaProperties.getMsgDataFormat()));
+
+    WxMaProperties.ConfigStorage configStorageProperties = wxMaProperties.getConfigStorage();
+    config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+    config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+    config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+    if (configStorageProperties.getHttpProxyPort() != null) {
+      config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
+    }
+    return config;
+  }
+
+  private WxMaDefaultConfigImpl wxMaInMemoryConfigStorage() {
+    WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
+    return config;
+  }
+
+  private WxMaDefaultConfigImpl wxMaInJedisConfigStorage() {
+    WxMaProperties.RedisProperties redisProperties = wxMaProperties.getConfigStorage().getRedis();
+    JedisPool jedisPool;
+    if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+      jedisPool = getJedisPool();
+    } else {
+      jedisPool = applicationContext.getBean(JedisPool.class);
+    }
+    WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+    WxMaRedisBetterConfigImpl wxMaRedisConfig = new WxMaRedisBetterConfigImpl(redisOps, wxMaProperties.getConfigStorage().getKeyPrefix());
+    return wxMaRedisConfig;
+  }
+
+  private WxMaDefaultConfigImpl wxMaInRedisTemplateConfigStorage() {
+    StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+    WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+    WxMaRedisBetterConfigImpl wxMaRedisConfig = new WxMaRedisBetterConfigImpl(redisOps, wxMaProperties.getConfigStorage().getKeyPrefix());
+    return wxMaRedisConfig;
+  }
+
+  private JedisPool getJedisPool() {
+    WxMaProperties.ConfigStorage storage = wxMaProperties.getConfigStorage();
+    WxMaProperties.RedisProperties redis = storage.getRedis();
+
+    JedisPoolConfig config = new JedisPoolConfig();
+    if (redis.getMaxActive() != null) {
+      config.setMaxTotal(redis.getMaxActive());
+    }
+    if (redis.getMaxIdle() != null) {
+      config.setMaxIdle(redis.getMaxIdle());
+    }
+    if (redis.getMaxWaitMillis() != null) {
+      config.setMaxWaitMillis(redis.getMaxWaitMillis());
+    }
+    if (redis.getMinIdle() != null) {
+      config.setMinIdle(redis.getMinIdle());
+    }
+    config.setTestOnBorrow(true);
+    config.setTestWhileIdle(true);
+
+    return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+      redis.getDatabase());
+  }
 }

+ 110 - 0
spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java

@@ -3,6 +3,8 @@ package com.binarywang.spring.starter.wxjava.miniapp.properties;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
+import java.io.Serializable;
+
 /**
  * 属性配置类.
  *
@@ -36,4 +38,112 @@ public class WxMaProperties {
    * 消息格式,XML或者JSON.
    */
   private String msgDataFormat;
+
+  /**
+   * 存储策略
+   */
+  private ConfigStorage configStorage = new ConfigStorage();
+
+  @Data
+  public static class ConfigStorage implements Serializable {
+    private static final long serialVersionUID = 4815731027000065434L;
+
+    /**
+     * 存储类型.
+     */
+    private StorageType type = StorageType.memory;
+
+    /**
+     * 指定key前缀.
+     */
+    private String keyPrefix = "wa";
+
+    /**
+     * redis连接配置.
+     */
+    private RedisProperties redis;
+
+    /**
+     * http客户端类型.
+     */
+    private HttpClientType httpClientType = HttpClientType.httpclient;
+
+    /**
+     * http代理主机.
+     */
+    private String httpProxyHost;
+
+    /**
+     * http代理端口.
+     */
+    private Integer httpProxyPort;
+
+    /**
+     * http代理用户名.
+     */
+    private String httpProxyUsername;
+
+    /**
+     * http代理密码.
+     */
+    private String httpProxyPassword;
+
+  }
+
+  public enum StorageType {
+    /**
+     * 内存.
+     */
+    memory,
+    /**
+     * redis(JedisClient).
+     */
+    jedis,
+    /**
+     * redis(RedisTemplate).
+     */
+    redistemplate
+  }
+
+  public enum HttpClientType {
+    /**
+     * HttpClient.
+     */
+    httpclient
+  }
+
+  @Data
+  public static class RedisProperties implements Serializable {
+    private static final long serialVersionUID = -5924815351660074401L;
+
+    /**
+     * 主机地址.
+     */
+    private String host;
+
+    /**
+     * 端口号.
+     */
+    private int port = 6379;
+
+    /**
+     * 密码.
+     */
+    private String password;
+
+    /**
+     * 超时.
+     */
+    private int timeout = 2000;
+
+    /**
+     * 数据库.
+     */
+    private int database = 0;
+
+    private Integer maxActive;
+    private Integer maxIdle;
+    private Integer maxWaitMillis;
+    private Integer minIdle;
+  }
 }

+ 19 - 12
spring-boot-starters/wx-java-mp-spring-boot-starter/README.md

@@ -1,4 +1,4 @@
-# wx-java-mp-starter
+# wx-java-mp-spring-boot-starter
 ## 快速开始
 1. 引入依赖
     ```xml
@@ -11,18 +11,25 @@
 2. 添加配置(application.properties)
     ```properties
     # 公众号配置(必填)
-    wx.mp.appId = @appId
-	wx.mp.secret = @secret
-	wx.mp.token = @token
-	wx.mp.aesKey = @aesKey
-	# 存储配置redis(可选)
-	wx.mp.config-storage.type = redis
-	wx.mp.config-storage.redis.host = 127.0.0.1
-	wx.mp.config-storage.redis.port = 6379
+    wx.mp.appId = appId
+    wx.mp.secret = @secret
+    wx.mp.token = @token
+    wx.mp.aesKey = @aesKey
+    # 存储配置redis(可选)
+    wx.mp.config-storage.type = redis                     # 配置类型: memory(默认), redis, 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
+    # 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=
     ```
-3. 支持自动注入的类型
-
-`WxMpService`以及相关的服务类, 比如: `wxMpService.getXxxService`。
+3. 自动注入的类型
+- `WxMpService`以及~~相关的服务类, 比如: `wxMpService.getXxxService`。~~
+- `WxMpConfigStorage`
 
 
 

+ 5 - 4
spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml

@@ -5,7 +5,7 @@
   <parent>
     <artifactId>wx-java-spring-boot-starters</artifactId>
     <groupId>com.github.binarywang</groupId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -25,9 +25,10 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
-      <groupId>org.redisson</groupId>
-      <artifactId>redisson</artifactId>
-      <scope>compile</scope>
+      <groupId>org.springframework.data</groupId>
+      <artifactId>spring-data-redis</artifactId>
+      <version>${spring.boot.version}</version>
+      <scope>provided</scope>
     </dependency>
   </dependencies>
 

+ 54 - 2
spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java

@@ -1,7 +1,11 @@
 package com.binarywang.spring.starter.wxjava.mp.config;
 
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
 import me.chanjar.weixin.mp.api.*;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl;
 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl;
 import me.chanjar.weixin.mp.config.WxMpConfigStorage;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -17,113 +21,161 @@ public class WxMpServiceAutoConfiguration {
 
   @Bean
   @ConditionalOnMissingBean
-  public WxMpService wxMpService(WxMpConfigStorage configStorage) {
-    WxMpService wxMpService = new WxMpServiceImpl();
+  public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties wxMpProperties) {
+    WxMpProperties.HttpClientType httpClientType = wxMpProperties.getConfigStorage().getHttpClientType();
+    WxMpService wxMpService;
+    if (httpClientType == WxMpProperties.HttpClientType.okhttp) {
+      wxMpService = newWxMpServiceJoddHttpImpl();
+    } else if (httpClientType == WxMpProperties.HttpClientType.joddhttp) {
+      wxMpService = newWxMpServiceOkHttpImpl();
+    } else if (httpClientType == WxMpProperties.HttpClientType.httpclient) {
+      wxMpService = newWxMpServiceHttpClientImpl();
+    } else {
+      wxMpService = newWxMpServiceImpl();
+    }
+
     wxMpService.setWxMpConfigStorage(configStorage);
     return wxMpService;
   }
 
+  private WxMpService newWxMpServiceImpl() {
+    return new WxMpServiceImpl();
+  }
+
+  private WxMpService newWxMpServiceHttpClientImpl() {
+    return new WxMpServiceHttpClientImpl();
+  }
+
+  private WxMpService newWxMpServiceOkHttpImpl() {
+    return new WxMpServiceOkHttpImpl();
+  }
+
+  private WxMpService newWxMpServiceJoddHttpImpl() {
+    return new WxMpServiceJoddHttpImpl();
+  }
+
   @Bean
+  @Deprecated
   public WxMpKefuService wxMpKefuService(WxMpService wxMpService) {
     return wxMpService.getKefuService();
   }
 
   @Bean
+  @Deprecated
   public WxMpMaterialService wxMpMaterialService(WxMpService wxMpService) {
     return wxMpService.getMaterialService();
   }
 
   @Bean
+  @Deprecated
   public WxMpMenuService wxMpMenuService(WxMpService wxMpService) {
     return wxMpService.getMenuService();
   }
 
   @Bean
+  @Deprecated
   public WxMpUserService wxMpUserService(WxMpService wxMpService) {
     return wxMpService.getUserService();
   }
 
   @Bean
+  @Deprecated
   public WxMpUserTagService wxMpUserTagService(WxMpService wxMpService) {
     return wxMpService.getUserTagService();
   }
 
   @Bean
+  @Deprecated
   public WxMpQrcodeService wxMpQrcodeService(WxMpService wxMpService) {
     return wxMpService.getQrcodeService();
   }
 
   @Bean
+  @Deprecated
   public WxMpCardService wxMpCardService(WxMpService wxMpService) {
     return wxMpService.getCardService();
   }
 
   @Bean
+  @Deprecated
   public WxMpDataCubeService wxMpDataCubeService(WxMpService wxMpService) {
     return wxMpService.getDataCubeService();
   }
 
   @Bean
+  @Deprecated
   public WxMpUserBlacklistService wxMpUserBlacklistService(WxMpService wxMpService) {
     return wxMpService.getBlackListService();
   }
 
   @Bean
+  @Deprecated
   public WxMpStoreService wxMpStoreService(WxMpService wxMpService) {
     return wxMpService.getStoreService();
   }
 
   @Bean
+  @Deprecated
   public WxMpTemplateMsgService wxMpTemplateMsgService(WxMpService wxMpService) {
     return wxMpService.getTemplateMsgService();
   }
 
   @Bean
+  @Deprecated
   public WxMpSubscribeMsgService wxMpSubscribeMsgService(WxMpService wxMpService) {
     return wxMpService.getSubscribeMsgService();
   }
 
   @Bean
+  @Deprecated
   public WxMpDeviceService wxMpDeviceService(WxMpService wxMpService) {
     return wxMpService.getDeviceService();
   }
 
   @Bean
+  @Deprecated
   public WxMpShakeService wxMpShakeService(WxMpService wxMpService) {
     return wxMpService.getShakeService();
   }
 
   @Bean
+  @Deprecated
   public WxMpMemberCardService wxMpMemberCardService(WxMpService wxMpService) {
     return wxMpService.getMemberCardService();
   }
 
   @Bean
+  @Deprecated
   public WxMpMassMessageService wxMpMassMessageService(WxMpService wxMpService) {
     return wxMpService.getMassMessageService();
   }
 
   @Bean
+  @Deprecated
   public WxMpAiOpenService wxMpAiOpenService(WxMpService wxMpService) {
     return wxMpService.getAiOpenService();
   }
 
   @Bean
+  @Deprecated
   public WxMpWifiService wxMpWifiService(WxMpService wxMpService) {
     return wxMpService.getWifiService();
   }
 
   @Bean
+  @Deprecated
   public WxMpMarketingService wxMpMarketingService(WxMpService wxMpService) {
     return wxMpService.getMarketingService();
   }
 
   @Bean
+  @Deprecated
   public WxMpCommentService wxMpCommentService(WxMpService wxMpService) {
     return wxMpService.getCommentService();
   }
 
   @Bean
+  @Deprecated
   public WxMpOcrService wxMpOcrService(WxMpService wxMpService) {
     return wxMpService.getOcrService();
   }

+ 57 - 28
spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java

@@ -1,16 +1,20 @@
 package com.binarywang.spring.starter.wxjava.mp.config;
 
-import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties;
 import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
 import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
 import me.chanjar.weixin.mp.config.WxMpConfigStorage;
 import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
 import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
-import org.redisson.api.RedissonClient;
-import org.springframework.beans.factory.annotation.Autowired;
+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.JedisPoolConfig;
 
@@ -22,52 +26,78 @@ import redis.clients.jedis.JedisPoolConfig;
 @Configuration
 @RequiredArgsConstructor
 public class WxMpStorageAutoConfiguration {
-  private final WxMpProperties properties;
 
-  @Autowired(required = false)
-  private JedisPool jedisPool;
+  private final ApplicationContext applicationContext;
 
-  @Autowired(required = false)
-  private RedissonClient redissonClient;
+  private final WxMpProperties wxMpProperties;
+
+  @Value("${wx.mp.config-storage.redis.host:")
+  private String redisHost;
+
+  @Value("${wx.mp.configStorage.redis.host:")
+  private String redisHost2;
 
   @Bean
   @ConditionalOnMissingBean(WxMpConfigStorage.class)
-  public WxMpConfigStorage wxMpInMemoryConfigStorage() {
-    WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
-    WxMpProperties.StorageType type = storage.getType();
-
-    if (type == WxMpProperties.StorageType.redis) {
-      return getWxMpInRedisConfigStorage();
+  public WxMpConfigStorage wxMpConfigStorage() {
+    WxMpProperties.StorageType type = wxMpProperties.getConfigStorage().getType();
+    WxMpConfigStorage config;
+    if (type == WxMpProperties.StorageType.redis || type == WxMpProperties.StorageType.jedis) {
+      config = wxMpInJedisConfigStorage();
+    } else if (type == WxMpProperties.StorageType.redistemplate) {
+      config = wxMpInRedisTemplateConfigStorage();
+    } else {
+      config = wxMpInMemoryConfigStorage();
     }
-    return getWxMpInMemoryConfigStorage();
+    return config;
   }
 
-  private WxMpDefaultConfigImpl getWxMpInMemoryConfigStorage() {
+  private WxMpConfigStorage wxMpInMemoryConfigStorage() {
     WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
     setWxMpInfo(config);
     return config;
   }
 
-  private WxMpRedisConfigImpl getWxMpInRedisConfigStorage() {
-    JedisPool poolToUse = jedisPool;
-    if (poolToUse == null) {
-      poolToUse = getJedisPool();
+  private WxMpConfigStorage wxMpInJedisConfigStorage() {
+    JedisPool jedisPool;
+    if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) {
+      jedisPool = getJedisPool();
+    } else {
+      jedisPool = applicationContext.getBean(JedisPool.class);
     }
-    WxMpRedisConfigImpl config = new WxMpRedisConfigImpl(poolToUse);
-    setWxMpInfo(config);
-    return config;
+    WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+    WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, wxMpProperties.getConfigStorage().getKeyPrefix());
+    setWxMpInfo(wxMpRedisConfig);
+    return wxMpRedisConfig;
+  }
+
+  private WxMpConfigStorage wxMpInRedisTemplateConfigStorage() {
+    StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+    WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+    WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, wxMpProperties.getConfigStorage().getKeyPrefix());
+    setWxMpInfo(wxMpRedisConfig);
+    return wxMpRedisConfig;
   }
 
   private void setWxMpInfo(WxMpDefaultConfigImpl config) {
+    WxMpProperties properties = wxMpProperties;
+    WxMpProperties.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 JedisPool getJedisPool() {
-    WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
-    RedisProperties redis = storage.getRedis();
+    WxMpProperties.ConfigStorage storage = wxMpProperties.getConfigStorage();
+    WxMpProperties.RedisProperties redis = storage.getRedis();
 
     JedisPoolConfig config = new JedisPoolConfig();
     if (redis.getMaxActive() != null) {
@@ -85,8 +115,7 @@ public class WxMpStorageAutoConfiguration {
     config.setTestOnBorrow(true);
     config.setTestWhileIdle(true);
 
-    JedisPool pool = new JedisPool(config, redis.getHost(), redis.getPort(),
-      redis.getTimeout(), redis.getPassword(), redis.getDatabase());
-    return pool;
+    return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(),
+      redis.getDatabase());
   }
 }

+ 0 - 45
spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java

@@ -1,45 +0,0 @@
-package com.binarywang.spring.starter.wxjava.mp.properties;
-
-import lombok.Data;
-
-import java.io.Serializable;
-
-/**
- * Redis配置.
- *
- * @author someone
- */
-@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;
-
-  private Integer maxActive;
-  private Integer maxIdle;
-  private Integer maxWaitMillis;
-  private Integer minIdle;
-}

+ 98 - 4
spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java

@@ -40,19 +40,54 @@ public class WxMpProperties {
   private String aesKey;
 
   /**
-   * 存储策略, memory, redis.
+   * 存储策略
    */
   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;
+
   }
 
   public enum StorageType {
@@ -61,8 +96,67 @@ public class WxMpProperties {
      */
     memory,
     /**
-     * redis.
+     * jedis.
+     */
+    redis,
+    /**
+     * redis(JedisClient).
      */
-    redis
+    jedis,
+    /**
+     * redis(RedisTemplate).
+     */
+    redistemplate
   }
+
+  public enum HttpClientType {
+    /**
+     * HttpClient.
+     */
+    httpclient,
+    /**
+     * OkHttp.
+     */
+    okhttp,
+    /**
+     * JoddHttp.
+     */
+    joddhttp
+  }
+
+  @Data
+  public static 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;
+
+    private Integer maxActive;
+    private Integer maxIdle;
+    private Integer maxWaitMillis;
+    private Integer minIdle;
+  }
+
 }

+ 18 - 17
spring-boot-starters/wx-java-open-spring-boot-starter/README.md

@@ -9,26 +9,27 @@
     </dependency>
     ```
 2. 添加配置(application.properties)
-    ```
-    # 开放平台配置(必填)
-    wx.open.appId = @appId
-	  wx.open.secret = @secret
-	  wx.open.token = @token
-	  wx.open.aesKey = @aesKey
-	  # 存储配置redis(可选), 优先使用(wx.open.config-storage.redis)配置的redis, 支持自定注入的JedisPool
-	  wx.open.config-storage.type = redis             # 可选值, memory(默认), redis
-	  wx.open.config-storage.redis.host = 127.0.0.1
-	  wx.open.config-storage.redis.port = 6379
+    ```properties
+    # 公众号配置(必填)
+    wx.open.appId = appId
+    wx.open.secret = @secret
+    wx.open.token = @token
+    wx.open.aesKey = @aesKey
+    # 存储配置redis(可选)
+    # 优先注入容器的(JedisPool, RedissonClient), 当配置了wx.open.config-storage.redis.host, 不会使用容器注入redis连接配置
+    wx.open.config-storage.type = redis                     # 配置类型: memory(默认), redis(jedis), jedis, redisson, redistemplate
+    wx.open.config-storage.key-prefix = wx                  # 相关redis前缀配置: wx(默认)
+    wx.open.config-storage.redis.host = 127.0.0.1
+    wx.open.config-storage.redis.port = 6379
+    # http客户端配置
+    wx.open.config-storage.http-client-type=httpclient      # http客户端类型: httpclient(默认)
+    wx.open.config-storage.http-proxy-host=
+    wx.open.config-storage.http-proxy-port=
+    wx.open.config-storage.http-proxy-username=
+    wx.open.config-storage.http-proxy-password=
     ```
 3. 支持自动注入的类型: `WxOpenService, WxOpenMessageRouter, WxOpenComponentService`
 
 4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的
   - WxOpenConfigStorage
   - WxOpenService
-
-
-
-
-
-
-

+ 9 - 3
spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml

@@ -5,7 +5,7 @@
   <parent>
     <artifactId>wx-java-spring-boot-starters</artifactId>
     <groupId>com.github.binarywang</groupId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -22,12 +22,18 @@
     <dependency>
       <groupId>redis.clients</groupId>
       <artifactId>jedis</artifactId>
-      <scope>compile</scope>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson</artifactId>
-      <scope>compile</scope>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.data</groupId>
+      <artifactId>spring-data-redis</artifactId>
+      <version>${spring.boot.version}</version>
+      <scope>provided</scope>
     </dependency>
   </dependencies>
 

+ 48 - 33
spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java

@@ -3,20 +3,23 @@ package com.binarywang.spring.starter.wxjava.open.config;
 import com.binarywang.spring.starter.wxjava.open.properties.RedisProperties;
 import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
 import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.redis.JedisWxRedisOps;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
 import me.chanjar.weixin.open.api.WxOpenConfigStorage;
 import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
 import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage;
-import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage;
 import org.apache.commons.lang3.StringUtils;
 import org.redisson.Redisson;
 import org.redisson.api.RedissonClient;
 import org.redisson.config.Config;
 import org.redisson.config.TransportMode;
-import org.springframework.beans.factory.annotation.Autowired;
-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.JedisPoolConfig;
 
@@ -29,15 +32,7 @@ import redis.clients.jedis.JedisPoolConfig;
 @RequiredArgsConstructor
 public class WxOpenStorageAutoConfiguration {
   private final WxOpenProperties properties;
-
-  @Autowired(required = false)
-  private JedisPool jedisPool;
-
-  @Autowired(required = false)
-  private RedissonClient redissonClient;
-
-  @Value("${wx.open.config-storage.redis.host:}")
-  private String redisHost;
+  private final ApplicationContext applicationContext;
 
   @Bean
   @ConditionalOnMissingBean(WxOpenConfigStorage.class)
@@ -45,43 +40,63 @@ public class WxOpenStorageAutoConfiguration {
     WxOpenProperties.ConfigStorage storage = properties.getConfigStorage();
     WxOpenProperties.StorageType type = storage.getType();
 
-    if (type == WxOpenProperties.StorageType.redis) {
-      return getWxOpenInRedisConfigStorage();
+    WxOpenInMemoryConfigStorage config;
+    if (type == WxOpenProperties.StorageType.redis || type == WxOpenProperties.StorageType.jedis) {
+      config = getWxOpenInRedisConfigStorage();
+    } else if (type == WxOpenProperties.StorageType.redisson) {
+      config = getWxOpenInRedissonConfigStorage();
+    } else if (type == WxOpenProperties.StorageType.redistemplate) {
+      config = getWxOpenInRedisTemplateConfigStorage();
+    } else {
+      config = getWxOpenInMemoryConfigStorage();
     }
 
-    if (type == WxOpenProperties.StorageType.jedis) {
-      return getWxOpenInRedisConfigStorage();
-    }
-
-    if (type == WxOpenProperties.StorageType.redisson) {
-      return getWxOpenInRedissonConfigStorage();
+    WxOpenProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
+    config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
+    config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
+    config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
+    config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
+    if (configStorageProperties.getHttpProxyPort() != null) {
+      config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
     }
-    return getWxOpenInMemoryConfigStorage();
+    return config;
   }
 
   private WxOpenInMemoryConfigStorage getWxOpenInMemoryConfigStorage() {
     WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage();
-    config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
     return config;
   }
 
   private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() {
-    JedisPool poolToUse = jedisPool;
-    if (jedisPool == null || StringUtils.isNotEmpty(redisHost)) {
-      poolToUse = getJedisPool();
+    RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+    JedisPool jedisPool;
+    if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+      jedisPool = getJedisPool();
+    } else {
+      jedisPool = applicationContext.getBean(JedisPool.class);
     }
-    WxOpenInRedisConfigStorage config = new WxOpenInRedisConfigStorage(poolToUse, properties.getConfigStorage().getKeyPrefix());
-    config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
+    WxRedisOps redisOps = new JedisWxRedisOps(jedisPool);
+    WxOpenInRedisConfigStorage config = new WxOpenInRedisConfigStorage(redisOps, properties.getConfigStorage().getKeyPrefix());
     return config;
   }
 
-  private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() {
-    RedissonClient redissonClientToUse = this.redissonClient;
-    if (redissonClient == null) {
-      redissonClientToUse = getRedissonClient();
+  private WxOpenInRedisConfigStorage getWxOpenInRedissonConfigStorage() {
+    RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+    RedissonClient redissonClient;
+    if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+      redissonClient = getRedissonClient();
+    } else {
+      redissonClient = applicationContext.getBean(RedissonClient.class);
     }
-    WxOpenInRedissonConfigStorage config = new WxOpenInRedissonConfigStorage(redissonClientToUse, properties.getConfigStorage().getKeyPrefix());
-    config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey());
+    WxRedisOps redisOps = new RedissonWxRedisOps(redissonClient);
+    WxOpenInRedisConfigStorage config = new WxOpenInRedisConfigStorage(redisOps, properties.getConfigStorage().getKeyPrefix());
+    return config;
+  }
+
+  private WxOpenInRedisConfigStorage getWxOpenInRedisTemplateConfigStorage() {
+    StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+    WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate);
+    WxOpenInRedisConfigStorage config = new WxOpenInRedisConfigStorage(redisOps, properties.getConfigStorage().getKeyPrefix());
     return config;
   }
 

+ 1 - 1
spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java

@@ -16,7 +16,7 @@ public class RedisProperties implements Serializable {
   /**
    * 主机地址.
    */
-  private String host = "127.0.0.1";
+  private String host;
 
   /**
    * 端口号.

+ 48 - 3
spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java

@@ -40,7 +40,7 @@ public class WxOpenProperties {
   private String aesKey;
 
   /**
-   * 存储策略, memory, redis.
+   * 存储策略.
    */
   private ConfigStorage configStorage = new ConfigStorage();
 
@@ -49,11 +49,45 @@ public class WxOpenProperties {
   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();
 
-    private String keyPrefix;
+    /**
+     * http客户端类型.
+     */
+    private HttpClientType httpClientType = HttpClientType.httpclient;
+
+    /**
+     * http代理主机.
+     */
+    private String httpProxyHost;
+
+    /**
+     * http代理端口.
+     */
+    private Integer httpProxyPort;
+
+    /**
+     * http代理用户名.
+     */
+    private String httpProxyUsername;
+
+    /**
+     * http代理密码.
+     */
+    private String httpProxyPassword;
 
   }
 
@@ -73,6 +107,17 @@ public class WxOpenProperties {
     /**
      * redisson.
      */
-    redisson
+    redisson,
+    /**
+     * redistemplate
+     */
+    redistemplate
+  }
+
+  public enum HttpClientType {
+    /**
+     * HttpClient.
+     */
+    httpclient
   }
 }

+ 1 - 1
spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml

@@ -5,7 +5,7 @@
   <parent>
     <artifactId>wx-java-spring-boot-starters</artifactId>
     <groupId>com.github.binarywang</groupId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 

+ 7 - 0
spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java

@@ -49,6 +49,13 @@ public class WxPayAutoConfiguration {
     payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
     payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
     payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
+    //以下是apiv3以及支付分相关
+    payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
+    payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
+    payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
+    payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
+    payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
+    payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiv3Key()));
 
     wxPayService.setConfig(payConfig);
     return wxPayService;

+ 31 - 0
spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java

@@ -43,4 +43,35 @@ public class WxPayProperties {
    * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定.
    */
   private String keyPath;
+
+  /**
+   * 微信支付分serviceId
+   */
+  private String serviceId;
+
+  /**
+   * 证书序列号
+   */
+  private String certSerialNo;
+
+  /**
+   * apiV3秘钥
+   */
+  private String apiv3Key;
+
+  /**
+   * 微信支付分回调地址
+   */
+  private String payScoreNotifyUrl;
+
+  /**
+   * apiv3 商户apiclient_key.pem
+   */
+  private String privateKeyPath;
+
+  /**
+   * apiv3 商户apiclient_cert.pem
+   */
+  private String privateCertPath;
+
 }

+ 1 - 1
weixin-graal/pom.xml

@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.github.binarywang</groupId>
     <artifactId>wx-java</artifactId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
 
   <artifactId>weixin-graal</artifactId>

+ 33 - 22
weixin-graal/src/main/java/cn/binarywang/wx/graal/GraalProcessor.java

@@ -1,6 +1,7 @@
 package cn.binarywang.wx.graal;
 
 import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.RoundEnvironment;
@@ -20,11 +21,14 @@ import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
-// 目前仅仅处理@Data,且必须在lombok自己的processor之前执行,千万注意!!!!!
+/**
+ * 目前仅仅处理@Data,且必须在lombok自己的processor之前执行,千万注意!!!!!
+ *
+ * @author outersky
+ */
 @SupportedAnnotationTypes("lombok.Data")
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 public class GraalProcessor extends AbstractProcessor {
-
   private static final String REFLECTION_CONFIG_JSON = "reflection-config.json";
   private static final String NATIVE_IMAGE_PROPERTIES = "native-image.properties";
 
@@ -34,16 +38,19 @@ public class GraalProcessor extends AbstractProcessor {
   @Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
     for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Data.class))) {
-
       registerClass(annotatedClass.getQualifiedName().toString());
       handleSuperClass(annotatedClass);
     }
 
     //只有最后一轮才可以写文件,否则文件会被重复打开,报错!
-    if (!roundEnv.processingOver()) return false;
+    if (!roundEnv.processingOver()) {
+      return false;
+    }
 
     // 如果没有文件要写,跳过
-    if (classSet.isEmpty()) return false;
+    if (classSet.isEmpty()) {
+      return false;
+    }
 
     writeFiles();
 
@@ -72,7 +79,9 @@ public class GraalProcessor extends AbstractProcessor {
    */
   private String getPackageName(String fullClassName) {
     int last = fullClassName.lastIndexOf('.');
-    if (last == -1) return fullClassName;
+    if (last == -1) {
+      return fullClassName;
+    }
     return fullClassName.substring(0, last);
   }
 
@@ -98,29 +107,29 @@ public class GraalProcessor extends AbstractProcessor {
     String propsFile = path + NATIVE_IMAGE_PROPERTIES;
     try {
       FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", propsFile);
-      Writer writer = fileObject.openWriter();
-      writer.append("Args = -H:ReflectionConfigurationResources=${.}/" + REFLECTION_CONFIG_JSON);
-      writer.close();
+      try (Writer writer = fileObject.openWriter();) {
+        writer.append("Args = -H:ReflectionConfigurationResources=${.}/" + REFLECTION_CONFIG_JSON);
+      }
     } catch (IOException e) {
       e.printStackTrace();
     }
 
     try {
       FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", reflectFile);
-      Writer writer = fileObject.openWriter();
-      writer.write("[\n");
-      boolean first = true;
-      for (String name : classSet) {
-        if (first) {
-          first = false;
-        } else {
-          writer.write(",");
+      try (Writer writer = fileObject.openWriter();) {
+        writer.write("[\n");
+        boolean first = true;
+        for (String name : classSet) {
+          if (first) {
+            first = false;
+          } else {
+            writer.write(",");
+          }
+          writer.write(assetGraalJsonElement(name));
+          writer.append('\n');
         }
-        writer.write(assetGraalJsonElement(name));
-        writer.append('\n');
+        writer.write("]");
       }
-      writer.write("]");
-      writer.close();
     } catch (IOException e) {
       e.printStackTrace();
     }
@@ -158,7 +167,9 @@ public class GraalProcessor extends AbstractProcessor {
       TypeElement s = (TypeElement) ((DeclaredType) superclass).asElement();
       String sName = s.toString();
       // ignore java.**/javax.**
-      if (sName.startsWith("java.") || sName.startsWith("javax.")) return;
+      if (sName.startsWith("java.") || sName.startsWith("javax.")) {
+        return;
+      }
       registerClass(sName);
       handleSuperClass(s);
     }

+ 17 - 1
weixin-java-common/pom.xml

@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.github.binarywang</groupId>
     <artifactId>wx-java</artifactId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
 
   <artifactId>weixin-java-common</artifactId>
@@ -118,6 +118,22 @@
       <artifactId>dom4j</artifactId>
       <version>2.1.1</version>
     </dependency>
+    <dependency>
+      <groupId>redis.clients</groupId>
+      <artifactId>jedis</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.github.jedis-lock</groupId>
+      <artifactId>jedis-lock</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.redisson</groupId>
+      <artifactId>redisson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.data</groupId>
+      <artifactId>spring-data-redis</artifactId>
+    </dependency>
   </dependencies>
 
   <build>

+ 6 - 6
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/TicketType.java

@@ -1,6 +1,7 @@
-package me.chanjar.weixin.mp.enums;
+package me.chanjar.weixin.common.enums;
 
 import lombok.Getter;
+import lombok.RequiredArgsConstructor;
 
 /**
  * <pre>
@@ -11,6 +12,7 @@ import lombok.Getter;
  * @author <a href="https://github.com/binarywang">Binary Wang</a>
  */
 @Getter
+@RequiredArgsConstructor
 public enum TicketType {
   /**
    * jsapi
@@ -19,17 +21,15 @@ public enum TicketType {
   /**
    * sdk
    */
-  SDK("2"),
+  SDK("sdk"),
   /**
    * 微信卡券
    */
   WX_CARD("wx_card");
+
   /**
    * type代码
    */
-  private String code;
+  private final String code;
 
-  TicketType(String code) {
-    this.code = code;
-  }
 }

+ 317 - 1
weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java

@@ -5,7 +5,7 @@ import lombok.Getter;
 /**
  * <pre>
  * 企业微信全局错误码.
- * 参考文档:<a href="https://work.weixin.qq.com/api/doc#10649">企业微信全局错误码</a>
+ * 参考文档:<a href="https://work.weixin.qq.com/api/doc/90000/90139/90313">企业微信全局错误码</a>
  * Created by Binary Wang on 2018/5/13.
  * </pre>
  *
@@ -610,6 +610,322 @@ public enum WxCpErrorMsgEnum {
    */
   CODE_84025(84025, "不符合的state参数;必须是[a-zA-Z0-9]的参数值,长度不可超过128个字节"),
   /**
+   * 缺少caller参数.
+   */
+  CODE_84052(84052, "缺少caller参数"),
+  /**
+   * 缺少callee参数.
+   */
+  CODE_84053(84053, "缺少callee参数"),
+  /**
+   * 缺少auth_corpid参数.
+   */
+  CODE_84054(84054, "缺少auth_corpid参数"),
+  /**
+   * 超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话
+   */
+  CODE_84055(84055, "超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话"),
+  /**
+   * 被拨打用户安装应用时未授权拨打公费电话权限.
+   */
+  CODE_84056(84056, "被拨打用户安装应用时未授权拨打公费电话权限"),
+  /**
+   * 公费电话余额不足.
+   */
+  CODE_84057(84057, "公费电话余额不足"),
+  /**
+   * caller
+   */
+  CODE_84058(84058, "caller 呼叫号码不支持"),
+  /**
+   * 号码非法.
+   */
+  CODE_84059(84059, "号码非法"),
+  /**
+   * callee
+   */
+  CODE_84060(84060, "callee 呼叫号码不支持"),
+  /**
+   * 不存在外部联系人的关系.
+   */
+  CODE_84061(84061, "不存在外部联系人的关系"),
+  /**
+   * 未开启公费电话应用.
+   */
+  CODE_84062(84062, "未开启公费电话应用"),
+  /**
+   * caller不存在.
+   */
+  CODE_84063(84063, "caller不存在"),
+  /**
+   * callee不存在.
+   */
+  CODE_84064(84064, "callee不存在"),
+  /**
+   * caller跟callee电话号码一致。排查方法:不允许自己拨打给自己
+   */
+  CODE_84065(84065, "caller跟callee电话号码一致。排查方法:不允许自己拨打给自己"),
+  /**
+   * 服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10
+   */
+  CODE_84066(84066, "服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,对应单个服务商,只能被呼叫【4】次。"),
+  /**
+   * 管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10
+   */
+  CODE_84067(84067, "管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,一共只能被【3】个服务商成功呼叫。"),
+  /**
+   * 拨打方被限制拨打公费电话.
+   */
+  CODE_84069(84069, "拨打方被限制拨打公费电话"),
+  /**
+   * 不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持
+   */
+  CODE_84070(84070, "不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持"),
+  /**
+   * 不合法的外部联系人授权码。排查方法:非法或者已经消费过
+   */
+  CODE_84071(84071, "不合法的外部联系人授权码。排查方法:非法或者已经消费过"),
+  /**
+   * 应用未配置客服.
+   */
+  CODE_84072(84072, "应用未配置客服"),
+  /**
+   * 客服userid不在应用配置的客服列表中.
+   */
+  CODE_84073(84073, "客服userid不在应用配置的客服列表中"),
+  /**
+   * 没有外部联系人权限.
+   */
+  CODE_84074(84074, "没有外部联系人权限"),
+  /**
+   * 不合法或过期的authcode.
+   */
+  CODE_84075(84075, "不合法或过期的authcode"),
+  /**
+   * 缺失authcode.
+   */
+  CODE_84076(84076, "缺失authcode"),
+  /**
+   * 订单价格过高,无法受理.
+   */
+  CODE_84077(84077, "订单价格过高,无法受理"),
+  /**
+   * 购买人数不正确.
+   */
+  CODE_84078(84078, "购买人数不正确"),
+  /**
+   * 价格策略不存在.
+   */
+  CODE_84079(84079, "价格策略不存在"),
+  /**
+   * 订单不存在.
+   */
+  CODE_84080(84080, "订单不存在"),
+  /**
+   * 存在未支付订单.
+   */
+  CODE_84081(84081, "存在未支付订单"),
+  /**
+   * 存在申请退款中的订单.
+   */
+  CODE_84082(84082, "存在申请退款中的订单"),
+  /**
+   * 非服务人员.
+   */
+  CODE_84083(84083, "非服务人员"),
+  /**
+   * 非跟进用户.
+   */
+  CODE_84084(84084, "非跟进用户"),
+  /**
+   * 应用已下架.
+   */
+  CODE_84085(84085, "应用已下架"),
+  /**
+   * 订单人数超过可购买最大人数.
+   */
+  CODE_84086(84086, "订单人数超过可购买最大人数"),
+  /**
+   * 打开订单支付前禁止关闭订单.
+   */
+  CODE_84087(84087, "打开订单支付前禁止关闭订单"),
+  /**
+   * 禁止关闭已支付的订单.
+   */
+  CODE_84088(84088, "禁止关闭已支付的订单"),
+  /**
+   * 订单已支付.
+   */
+  CODE_84089(84089, "订单已支付"),
+  /**
+   * 缺失user_ticket.
+   */
+  CODE_84090(84090, "缺失user_ticket"),
+  /**
+   * 订单价格不可低于下限.
+   */
+  CODE_84091(84091, "订单价格不可低于下限"),
+  /**
+   * 无法发起代下单操作.
+   */
+  CODE_84092(84092, "无法发起代下单操作"),
+  /**
+   * 代理关系已占用,无法代下单.
+   */
+  CODE_84093(84093, "代理关系已占用,无法代下单"),
+  /**
+   * 该应用未配置代理分润规则,请先联系应用服务商处理.
+   */
+  CODE_84094(84094, "该应用未配置代理分润规则,请先联系应用服务商处理"),
+  /**
+   * 免费试用版,无法扩容.
+   */
+  CODE_84095(84095, "免费试用版,无法扩容"),
+  /**
+   * 免费试用版,无法续期.
+   */
+  CODE_84096(84096, "免费试用版,无法续期"),
+  /**
+   * 当前企业有未处理订单.
+   */
+  CODE_84097(84097, "当前企业有未处理订单"),
+  /**
+   * 固定总量,无法扩容.
+   */
+  CODE_84098(84098, "固定总量,无法扩容"),
+  /**
+   * 非购买状态,无法扩容.
+   */
+  CODE_84099(84099, "非购买状态,无法扩容"),
+  /**
+   * 未购买过此应用,无法续期.
+   */
+  CODE_84100(84100, "未购买过此应用,无法续期"),
+  /**
+   * 企业已试用付费版本,无法全新购买.
+   */
+  CODE_84101(84101, "企业已试用付费版本,无法全新购买"),
+  /**
+   * 企业当前应用状态已过期,无法扩容.
+   */
+  CODE_84102(84102, "企业当前应用状态已过期,无法扩容"),
+  /**
+   * 仅可修改未支付订单.
+   */
+  CODE_84103(84103, "仅可修改未支付订单"),
+  /**
+   * 订单已支付,无法修改.
+   */
+  CODE_84104(84104, "订单已支付,无法修改"),
+  /**
+   * 订单已被取消,无法修改.
+   */
+  CODE_84105(84105, "订单已被取消,无法修改"),
+  /**
+   * 企业含有该应用的待支付订单,无法代下单.
+   */
+  CODE_84106(84106, "企业含有该应用的待支付订单,无法代下单"),
+  /**
+   * 企业含有该应用的退款中订单,无法代下单.
+   */
+  CODE_84107(84107, "企业含有该应用的退款中订单,无法代下单"),
+  /**
+   * 企业含有该应用的待生效订单,无法代下单.
+   */
+  CODE_84108(84108, "企业含有该应用的待生效订单,无法代下单"),
+  /**
+   * 订单定价不能未0.
+   */
+  CODE_84109(84109, "订单定价不能未0"),
+  /**
+   * 新安装应用不在试用状态,无法升级为付费版.
+   */
+  CODE_84110(84110, "新安装应用不在试用状态,无法升级为付费版"),
+  /**
+   * 无足够可用优惠券.
+   */
+  CODE_84111(84111, "无足够可用优惠券"),
+  /**
+   * 无法关闭未支付订单.
+   */
+  CODE_84112(84112, "无法关闭未支付订单"),
+  /**
+   * 无付费信息.
+   */
+  CODE_84113(84113, "无付费信息"),
+  /**
+   * 虚拟版本不支持下单.
+   */
+  CODE_84114(84114, "虚拟版本不支持下单"),
+  /**
+   * 虚拟版本不支持扩容.
+   */
+  CODE_84115(84115, "虚拟版本不支持扩容"),
+  /**
+   * 虚拟版本不支持续期.
+   */
+  CODE_84116(84116, "虚拟版本不支持续期"),
+  /**
+   * 在虚拟正式版期内不能扩容.
+   */
+  CODE_84117(84117, "在虚拟正式版期内不能扩容"),
+  /**
+   * 虚拟正式版期内不能变更版本.
+   */
+  CODE_84118(84118, "虚拟正式版期内不能变更版本"),
+  /**
+   * 当前企业未报备,无法进行代下单.
+   */
+  CODE_84119(84119, "当前企业未报备,无法进行代下单"),
+  /**
+   * 当前应用版本已删除.
+   */
+  CODE_84120(84120, "当前应用版本已删除"),
+  /**
+   * 应用版本已删除,无法扩容.
+   */
+  CODE_84121(84121, "应用版本已删除,无法扩容"),
+  /**
+   * 应用版本已删除,无法续期.
+   */
+  CODE_84122(84122, "应用版本已删除,无法续期"),
+  /**
+   * 非虚拟版本,无法升级.
+   */
+  CODE_84123(84123, "非虚拟版本,无法升级"),
+  /**
+   * 非行业方案订单,不能添加部分应用版本的订单.
+   */
+  CODE_84124(84124, "非行业方案订单,不能添加部分应用版本的订单"),
+  /**
+   * 购买人数不能少于最少购买人数.
+   */
+  CODE_84125(84125, "购买人数不能少于最少购买人数"),
+  /**
+   * 购买人数不能多于最大购买人数.
+   */
+  CODE_84126(84126, "购买人数不能多于最大购买人数"),
+  /**
+   * 无应用管理权限.
+   */
+  CODE_84127(84127, "无应用管理权限"),
+  /**
+   * 无该行业方案下全部应用的管理权限.
+   */
+  CODE_84128(84128, "无该行业方案下全部应用的管理权限"),
+  /**
+   * 付费策略已被删除,无法下单.
+   */
+  CODE_84129(84129, "付费策略已被删除,无法下单"),
+  /**
+   * 订单生效时间不合法.
+   */
+  CODE_84130(84130, "订单生效时间不合法"),
+  /**
+   * 文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式
+   */
+  CODE_84200(84200, "文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式"),
+  /**
    * 包含不合法的词语.
    */
   CODE_85002(85002, "包含不合法的词语"),

+ 2 - 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java

@@ -86,9 +86,9 @@ public enum WxMpErrorMsgEnum {
    */
   CODE_40016(40016, "不合法的按钮个数"),
   /**
-   * 不合法的按钮个数.
+   * 不合法的按钮类型.
    */
-  CODE_40017(40017, "不合法的按钮个数"),
+  CODE_40017(40017, "不合法的按钮类型"),
   /**
    * 不合法的按钮名字长度.
    */

+ 37 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java

@@ -0,0 +1,37 @@
+package me.chanjar.weixin.common.redis;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 微信redis操作基本类
+ * <p>
+ * 非内置实现redis相关操作, 请实现该类
+ */
+public abstract class BaseWxRedisOps implements WxRedisOps {
+
+  @Override
+  public String getValue(String key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Long getExpire(String key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void expire(String key, int expire, TimeUnit timeUnit) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Lock getLock(String key) {
+    throw new UnsupportedOperationException();
+  }
+}

+ 52 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java

@@ -0,0 +1,52 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.util.locks.JedisDistributedLock;
+import redis.clients.jedis.Jedis;
+import redis.clients.util.Pool;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+@RequiredArgsConstructor
+public class JedisWxRedisOps implements WxRedisOps {
+
+  private final Pool<Jedis> jedisPool;
+
+  @Override
+  public String getValue(String key) {
+    try (Jedis jedis = this.jedisPool.getResource()) {
+      return jedis.get(key);
+    }
+  }
+
+  @Override
+  public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+    try (Jedis jedis = this.jedisPool.getResource()) {
+      if (expire <= 0) {
+        jedis.set(key, value);
+      } else {
+        jedis.psetex(key, timeUnit.toMillis(expire), value);
+      }
+    }
+  }
+
+  @Override
+  public Long getExpire(String key) {
+    try (Jedis jedis = this.jedisPool.getResource()) {
+      return jedis.ttl(key);
+    }
+  }
+
+  @Override
+  public void expire(String key, int expire, TimeUnit timeUnit) {
+    try (Jedis jedis = this.jedisPool.getResource()) {
+      jedis.pexpire(key, timeUnit.toMillis(expire));
+    }
+  }
+
+  @Override
+  public Lock getLock(String key) {
+    return new JedisDistributedLock(jedisPool, key);
+  }
+}

+ 43 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java

@@ -0,0 +1,43 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@RequiredArgsConstructor
+public class RedisTemplateWxRedisOps implements WxRedisOps {
+
+  private final StringRedisTemplate redisTemplate;
+
+  @Override
+  public String getValue(String key) {
+    return redisTemplate.opsForValue().get(key);
+  }
+
+  @Override
+  public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+    if (expire <= 0) {
+      redisTemplate.opsForValue().set(key, value);
+    } else {
+      redisTemplate.opsForValue().set(key, value, expire, timeUnit);
+    }
+  }
+
+  @Override
+  public Long getExpire(String key) {
+    return redisTemplate.getExpire(key);
+  }
+
+  @Override
+  public void expire(String key, int expire, TimeUnit timeUnit) {
+    redisTemplate.expire(key, expire, timeUnit);
+  }
+
+  @Override
+  public Lock getLock(String key) {
+    return new ReentrantLock();
+  }
+}

+ 47 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java

@@ -0,0 +1,47 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.RequiredArgsConstructor;
+import org.redisson.api.RedissonClient;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+@RequiredArgsConstructor
+public class RedissonWxRedisOps implements WxRedisOps {
+
+  private final RedissonClient redissonClient;
+
+  @Override
+  public String getValue(String key) {
+    Object value = redissonClient.getBucket(key).get();
+    return value == null ? null : value.toString();
+  }
+
+  @Override
+  public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+    if (expire <= 0) {
+      redissonClient.getBucket(key).set(value);
+    } else {
+      redissonClient.getBucket(key).set(value, expire, timeUnit);
+    }
+  }
+
+  @Override
+  public Long getExpire(String key) {
+    long expire = redissonClient.getBucket(key).remainTimeToLive();
+    if (expire > 0) {
+      expire = expire / 1000;
+    }
+    return expire;
+  }
+
+  @Override
+  public void expire(String key, int expire, TimeUnit timeUnit) {
+    redissonClient.getBucket(key).expire(expire, timeUnit);
+  }
+
+  @Override
+  public Lock getLock(String key) {
+    return redissonClient.getLock(key);
+  }
+}

+ 27 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java

@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.redis;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 微信Redis相关操作
+ * <p>
+ * 该接口不承诺稳定, 外部实现请继承{@link BaseWxRedisOps}
+ *
+ * @see BaseWxRedisOps 实现需要继承该类
+ * @see JedisWxRedisOps jedis实现
+ * @see RedissonWxRedisOps redisson实现
+ * @see RedisTemplateWxRedisOps redisTemplate实现
+ */
+public interface WxRedisOps {
+
+  String getValue(String key);
+
+  void setValue(String key, String value, int expire, TimeUnit timeUnit);
+
+  Long getExpire(String key);
+
+  void expire(String key, int expire, TimeUnit timeUnit);
+
+  Lock getLock(String key);
+}

+ 41 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java

@@ -0,0 +1,41 @@
+package me.chanjar.weixin.common.service;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信服务接口.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-04-25
+ */
+public interface WxService {
+  /**
+   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求.
+   *
+   * @param queryParam 参数
+   * @param url        请求接口地址
+   * @return 接口响应字符串
+   * @throws WxErrorException 异常
+   */
+  String get(String url, String queryParam) throws WxErrorException;
+
+  /**
+   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+   *
+   * @param postData 请求参数json值
+   * @param url      请求接口地址
+   * @return 接口响应字符串
+   * @throws WxErrorException 异常
+   */
+  String post(String url, String postData) throws WxErrorException;
+
+  /**
+   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+   *
+   * @param url 请求接口地址
+   * @param obj 请求对象
+   * @return 接口响应字符串
+   * @throws WxErrorException 异常
+   */
+  String post(String url, Object obj) throws WxErrorException;
+}

+ 9 - 13
weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java

@@ -1,13 +1,12 @@
 package me.chanjar.weixin.common.session;
 
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import me.chanjar.weixin.common.util.res.StringManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import me.chanjar.weixin.common.util.res.StringManager;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * 基于内存的session manager.
@@ -15,7 +14,6 @@ import me.chanjar.weixin.common.util.res.StringManager;
  * @author Daniel Qian
  */
 public class StandardSessionManager implements WxSessionManager, InternalSessionManager {
-
   protected static final StringManager SM = StringManager.getManager(Constants.PACKAGE);
   /**
    * The descriptive name of this Manager implementation (for logging).
@@ -51,7 +49,9 @@ public class StandardSessionManager implements WxSessionManager, InternalSession
    */
   protected int maxInactiveInterval = 30 * 60;
 
-  // Number of sessions created by this manager
+  /**
+   * Number of sessions created by this manager
+   */
   protected long sessionCounter = 0;
 
   protected volatile int maxActive = 0;
@@ -154,12 +154,10 @@ public class StandardSessionManager implements WxSessionManager, InternalSession
     session.setValid(true);
     session.setCreationTime(System.currentTimeMillis());
     session.setMaxInactiveInterval(this.maxInactiveInterval);
-    String id = sessionId;
-    session.setId(id);
+    session.setId(sessionId);
     this.sessionCounter++;
 
-    return (session);
-
+    return session;
   }
 
 
@@ -181,10 +179,8 @@ public class StandardSessionManager implements WxSessionManager, InternalSession
     return new StandardSession(this);
   }
 
-
   @Override
   public void add(InternalSession session) {
-
     // 当第一次有session创建的时候,开启session清理线程
     if (!this.backgroundProcessStarted.getAndSet(true)) {
       Thread t = new Thread(new Runnable() {

+ 2 - 3
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java

@@ -50,14 +50,13 @@ public class BeanUtils {
           }
         }
         field.setAccessible(isAccessible);
-      } catch (SecurityException | IllegalArgumentException
-        | IllegalAccessException e) {
+      } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
         log.error(e.getMessage(), e);
       }
     }
 
     if (!requiredFields.isEmpty()) {
-      String msg = "必填字段 " + requiredFields + " 必须提供值";
+      String msg = String.format("必填字段【%s】必须提供值!", requiredFields);
       log.debug(msg);
       throw new WxErrorException(WxError.builder().errorMsg(msg).build());
     }

+ 16 - 11
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java

@@ -1,21 +1,21 @@
 package me.chanjar.weixin.common.util;
 
-import java.io.StringReader;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import org.dom4j.Document;
 import org.dom4j.DocumentException;
 import org.dom4j.Element;
 import org.dom4j.Node;
 import org.dom4j.io.SAXReader;
 import org.dom4j.tree.DefaultText;
+import org.xml.sax.SAXException;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * <pre>
@@ -31,13 +31,18 @@ public class XmlUtils {
     Map<String, Object> map = new HashMap<>(16);
     try {
       SAXReader saxReader = new SAXReader();
+      saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+      saxReader.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
+      saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+      saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
+      saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
       Document doc = saxReader.read(new StringReader(xmlString));
       Element root = doc.getRootElement();
       List<Element> elements = root.elements();
       for (Element element : elements) {
-          map.put(element.getName(), element2MapOrString(element));
+        map.put(element.getName(), element2MapOrString(element));
       }
-    } catch (DocumentException e) {
+    } catch (DocumentException | SAXException e) {
       throw new RuntimeException(e);
     }
 

+ 11 - 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java

@@ -3,8 +3,9 @@ package me.chanjar.weixin.common.util.http;
 import java.io.IOException;
 
 import me.chanjar.weixin.common.WxType;
+import me.chanjar.weixin.common.error.WxError;
 import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientSimpleGetRequestExecutor;
+import me.chanjar.weixin.common.util.http.apache.ApacheSimpleGetRequestExecutor;
 import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimpleGetRequestExecutor;
 import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimpleGetRequestExecutor;
 
@@ -29,7 +30,7 @@ public abstract class SimpleGetRequestExecutor<H, P> implements RequestExecutor<
   public static RequestExecutor<String, String> create(RequestHttp requestHttp) {
     switch (requestHttp.getRequestType()) {
       case APACHE_HTTP:
-        return new ApacheHttpClientSimpleGetRequestExecutor(requestHttp);
+        return new ApacheSimpleGetRequestExecutor(requestHttp);
       case JODD_HTTP:
         return new JoddHttpSimpleGetRequestExecutor(requestHttp);
       case OK_HTTP:
@@ -39,4 +40,12 @@ public abstract class SimpleGetRequestExecutor<H, P> implements RequestExecutor<
     }
   }
 
+  protected String handleResponse(WxType wxType, String responseContent) throws WxErrorException {
+    WxError error = WxError.fromJson(responseContent, wxType);
+    if (error.getErrorCode() != 0) {
+      throw new WxErrorException(error);
+    }
+
+    return responseContent;
+  }
 }

+ 19 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java

@@ -1,10 +1,12 @@
 package me.chanjar.weixin.common.util.http;
 
 import me.chanjar.weixin.common.WxType;
+import me.chanjar.weixin.common.error.WxError;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.util.http.apache.ApacheSimplePostRequestExecutor;
 import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimplePostRequestExecutor;
 import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimplePostRequestExecutor;
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 
@@ -39,4 +41,21 @@ public abstract class SimplePostRequestExecutor<H, P> implements RequestExecutor
     }
   }
 
+  @NotNull
+  public String handleResponse(WxType wxType, String responseContent) throws WxErrorException {
+    if (responseContent.isEmpty()) {
+      throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build());
+    }
+
+    if (responseContent.startsWith("<xml>")) {
+      //xml格式输出直接返回
+      return responseContent;
+    }
+
+    WxError error = WxError.fromJson(responseContent, wxType);
+    if (error.getErrorCode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return responseContent;
+  }
 }

+ 6 - 1
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java

@@ -63,7 +63,12 @@ public class ApacheMediaDownloadRequestExecutor extends BaseMediaDownloadRequest
         fileName = String.valueOf(System.currentTimeMillis());
       }
 
-      return FileUtils.createTmpFile(inputStream, FilenameUtils.getBaseName(fileName), FilenameUtils.getExtension(fileName),
+      String baseName = FilenameUtils.getBaseName(fileName);
+      if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+        baseName = String.valueOf(System.currentTimeMillis());
+      }
+
+      return FileUtils.createTmpFile(inputStream, baseName, FilenameUtils.getExtension(fileName),
         super.tmpDirFile);
 
     } finally {

+ 3 - 7
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientSimpleGetRequestExecutor.java

@@ -19,8 +19,8 @@ import java.io.IOException;
  * @author ecoolper
  * @date 2017/5/4
  */
-public class ApacheHttpClientSimpleGetRequestExecutor extends SimpleGetRequestExecutor<CloseableHttpClient, HttpHost> {
-  public ApacheHttpClientSimpleGetRequestExecutor(RequestHttp requestHttp) {
+public class ApacheSimpleGetRequestExecutor extends SimpleGetRequestExecutor<CloseableHttpClient, HttpHost> {
+  public ApacheSimpleGetRequestExecutor(RequestHttp requestHttp) {
     super(requestHttp);
   }
 
@@ -40,11 +40,7 @@ public class ApacheHttpClientSimpleGetRequestExecutor extends SimpleGetRequestEx
 
     try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet)) {
       String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
-      WxError error = WxError.fromJson(responseContent, wxType);
-      if (error.getErrorCode() != 0) {
-        throw new WxErrorException(error);
-      }
-      return responseContent;
+      return handleResponse(wxType, responseContent);
     } finally {
       httpGet.releaseConnection();
     }

+ 3 - 14
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java

@@ -12,6 +12,7 @@ import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 
@@ -42,22 +43,10 @@ public class ApacheSimplePostRequestExecutor extends SimplePostRequestExecutor<C
 
     try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) {
       String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
-      if (responseContent.isEmpty()) {
-        throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build());
-      }
-
-      if (responseContent.startsWith("<xml>")) {
-        //xml格式输出直接返回
-        return responseContent;
-      }
-
-      WxError error = WxError.fromJson(responseContent, wxType);
-      if (error.getErrorCode() != 0) {
-        throw new WxErrorException(error);
-      }
-      return responseContent;
+      return this.handleResponse(wxType, responseContent);
     } finally {
       httpPost.releaseConnection();
     }
   }
+
 }

+ 46 - 91
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java

@@ -1,5 +1,7 @@
 package me.chanjar.weixin.common.util.http.apache;
 
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.HttpHost;
 import org.apache.http.annotation.NotThreadSafe;
@@ -23,8 +25,6 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.http.protocol.HttpContext;
 import org.apache.http.ssl.SSLContexts;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.net.ssl.SSLContext;
 import java.io.IOException;
@@ -41,26 +41,65 @@ import java.util.concurrent.atomic.AtomicBoolean;
  *
  * @author kakotor
  */
+@Slf4j
+@Data
 @NotThreadSafe
 public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
-  protected final Logger log = LoggerFactory.getLogger(DefaultApacheHttpClientBuilder.class);
   private final AtomicBoolean prepared = new AtomicBoolean(false);
-  private int connectionRequestTimeout = 3000;
+
+  /**
+   * 获取链接的超时时间设置
+   * <p>
+   * 设置为零时不超时,一直等待.
+   * 设置为负数是使用系统默认设置(非3000ms的默认值,而是httpClient的默认设置).
+   * </p>
+   */
+  private int connectionRequestTimeout = -1;
+
+  /**
+   * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用
+   * <p>
+   * 设置为零时不超时,一直等待.
+   * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置).
+   * </p>
+   */
   private int connectionTimeout = 5000;
+  /**
+   * 默认NIO的socket超时设置,默认5000ms.
+   */
   private int soTimeout = 5000;
+  /**
+   * 空闲链接的超时时间,默认60000ms.
+   * <p>
+   * 超时的链接将在下一次空闲链接检查是被销毁
+   * </p>
+   */
   private int idleConnTimeout = 60000;
+  /**
+   * 检查空间链接的间隔周期,默认60000ms.
+   */
   private int checkWaitTime = 60000;
+  /**
+   * 每路的最大链接数,默认10
+   */
   private int maxConnPerHost = 10;
+  /**
+   * 最大总连接数,默认50
+   */
   private int maxTotalConn = 50;
+  /**
+   * 自定义httpclient的User Agent
+   */
   private String userAgent;
-  private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
+
+  private final HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
     @Override
     public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
       return false;
     }
   };
   private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
-  private PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
+  private final PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
   private String httpProxyHost;
   private int httpProxyPort;
   private String httpProxyUsername;
@@ -111,90 +150,6 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
     return this;
   }
 
-  /**
-   * 获取链接的超时时间设置,默认3000ms
-   * <p>
-   * 设置为零时不超时,一直等待.
-   * 设置为负数是使用系统默认设置(非上述的3000ms的默认值,而是httpclient的默认设置).
-   * </p>
-   *
-   * @param connectionRequestTimeout 获取链接的超时时间设置(单位毫秒),默认3000ms
-   */
-  public void setConnectionRequestTimeout(int connectionRequestTimeout) {
-    this.connectionRequestTimeout = connectionRequestTimeout;
-  }
-
-  /**
-   * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用
-   * <p>
-   * 设置为零时不超时,一直等待.
-   * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置).
-   * </p>
-   *
-   * @param connectionTimeout 建立链接的超时时间设置(单位毫秒),默认5000ms
-   */
-  public void setConnectionTimeout(int connectionTimeout) {
-    this.connectionTimeout = connectionTimeout;
-  }
-
-  /**
-   * 默认NIO的socket超时设置,默认5000ms.
-   *
-   * @param soTimeout 默认NIO的socket超时设置,默认5000ms.
-   * @see java.net.SocketOptions#SO_TIMEOUT
-   */
-  public void setSoTimeout(int soTimeout) {
-    this.soTimeout = soTimeout;
-  }
-
-  /**
-   * 空闲链接的超时时间,默认60000ms.
-   * <p>
-   * 超时的链接将在下一次空闲链接检查是被销毁
-   * </p>
-   *
-   * @param idleConnTimeout 空闲链接的超时时间,默认60000ms.
-   */
-  public void setIdleConnTimeout(int idleConnTimeout) {
-    this.idleConnTimeout = idleConnTimeout;
-  }
-
-  /**
-   * 检查空间链接的间隔周期,默认60000ms.
-   *
-   * @param checkWaitTime 检查空间链接的间隔周期,默认60000ms.
-   */
-  public void setCheckWaitTime(int checkWaitTime) {
-    this.checkWaitTime = checkWaitTime;
-  }
-
-  /**
-   * 每路的最大链接数,默认10
-   *
-   * @param maxConnPerHost 每路的最大链接数,默认10
-   */
-  public void setMaxConnPerHost(int maxConnPerHost) {
-    this.maxConnPerHost = maxConnPerHost;
-  }
-
-  /**
-   * 最大总连接数,默认50
-   *
-   * @param maxTotalConn 最大总连接数,默认50
-   */
-  public void setMaxTotalConn(int maxTotalConn) {
-    this.maxTotalConn = maxTotalConn;
-  }
-
-  /**
-   * 自定义httpclient的User Agent
-   *
-   * @param userAgent User Agent
-   */
-  public void setUserAgent(String userAgent) {
-    this.userAgent = userAgent;
-  }
-
   public IdleConnectionMonitorThread getIdleConnectionMonitorThread() {
     return this.idleConnectionMonitorThread;
   }
@@ -268,7 +223,7 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
         null,
         SSLConnectionSocketFactory.getDefaultHostnameVerifier());
     } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
-      this.log.error(e.getMessage(), e);
+      log.error("构建SSL连接工厂时发生异常!", e);
     }
 
     return null;

+ 6 - 1
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java

@@ -60,9 +60,14 @@ public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadReque
       return null;
     }
 
+    String baseName = FilenameUtils.getBaseName(fileName);
+    if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+      baseName = String.valueOf(System.currentTimeMillis());
+    }
+
     try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) {
       return FileUtils.createTmpFile(inputStream,
-        FilenameUtils.getBaseName(fileName),
+        baseName,
         FilenameUtils.getExtension(fileName),
         super.tmpDirFile);
     }

+ 1 - 7
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java

@@ -41,13 +41,7 @@ public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor<H
     HttpResponse response = request.send();
     response.charset(StringPool.UTF_8);
 
-    String responseContent = response.bodyText();
-
-    WxError error = WxError.fromJson(responseContent, wxType);
-    if (error.getErrorCode() != 0) {
-      throw new WxErrorException(error);
-    }
-    return responseContent;
+    return handleResponse(wxType, response.bodyText());
   }
 
 }

+ 1 - 16
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java

@@ -6,7 +6,6 @@ import jodd.http.HttpResponse;
 import jodd.http.ProxyInfo;
 import jodd.util.StringPool;
 import me.chanjar.weixin.common.WxType;
-import me.chanjar.weixin.common.error.WxError;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.util.http.RequestHttp;
 import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
@@ -40,21 +39,7 @@ public class JoddHttpSimplePostRequestExecutor extends SimplePostRequestExecutor
     HttpResponse response = request.send();
     response.charset(StringPool.UTF_8);
 
-    String responseContent = response.bodyText();
-    if (responseContent.isEmpty()) {
-      throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build());
-    }
-
-    if (responseContent.startsWith("<xml>")) {
-      //xml格式输出直接返回
-      return responseContent;
-    }
-
-    WxError error = WxError.fromJson(responseContent, wxType);
-    if (error.getErrorCode() != 0) {
-      throw new WxErrorException(error);
-    }
-    return responseContent;
+    return this.handleResponse(wxType, response.bodyText());
   }
 
 }

+ 6 - 3
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java

@@ -14,8 +14,6 @@ import okio.BufferedSink;
 import okio.Okio;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -58,8 +56,13 @@ public class OkHttpMediaDownloadRequestExecutor extends BaseMediaDownloadRequest
       return null;
     }
 
+    String baseName = FilenameUtils.getBaseName(fileName);
+    if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+      baseName = String.valueOf(System.currentTimeMillis());
+    }
+
     File file = File.createTempFile(
-      FilenameUtils.getBaseName(fileName), "." + FilenameUtils.getExtension(fileName), super.tmpDirFile
+      baseName, "." + FilenameUtils.getExtension(fileName), super.tmpDirFile
     );
 
     try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {

+ 1 - 6
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java

@@ -35,12 +35,7 @@ public class OkHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor<OkH
     OkHttpClient client = requestHttp.getRequestHttpClient();
     Request request = new Request.Builder().url(uri).build();
     Response response = client.newCall(request).execute();
-    String responseContent = response.body().string();
-    WxError error = WxError.fromJson(responseContent, wxType);
-    if (error.getErrorCode() != 0) {
-      throw new WxErrorException(error);
-    }
-    return responseContent;
+    return this.handleResponse(wxType, response.body().string());
   }
 
 }

+ 3 - 9
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java

@@ -2,13 +2,13 @@ package me.chanjar.weixin.common.util.http.okhttp;
 
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.WxType;
-import me.chanjar.weixin.common.error.WxError;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.util.http.RequestHttp;
 import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
 import okhttp3.*;
 
 import java.io.IOException;
+import java.util.Objects;
 
 /**
  * .
@@ -24,16 +24,10 @@ public class OkHttpSimplePostRequestExecutor extends SimplePostRequestExecutor<O
 
   @Override
   public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException {
-    RequestBody body = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), postEntity);
+    RequestBody body = RequestBody.Companion.create(postEntity, MediaType.parse("text/plain; charset=utf-8"));
     Request request = new Request.Builder().url(uri).post(body).build();
     Response response = requestHttp.getRequestHttpClient().newCall(request).execute();
-    String responseContent = response.body().string();
-    WxError error = WxError.fromJson(responseContent, wxType);
-    if (error.getErrorCode() != 0) {
-      throw new WxErrorException(error);
-    }
-
-    return responseContent;
+    return this.handleResponse(wxType, Objects.requireNonNull(response.body()).string());
   }
 
 }

+ 73 - 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java

@@ -0,0 +1,73 @@
+package me.chanjar.weixin.common.util.locks;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+import com.github.jedis.lock.JedisLock;
+import redis.clients.jedis.Jedis;
+import redis.clients.util.Pool;
+
+/**
+ * JedisPool 分布式锁
+ *
+ * @author <a href="https://github.com/007gzs">007</a>
+ */
+public class JedisDistributedLock implements Lock {
+  private final Pool<Jedis> jedisPool;
+  private final JedisLock lock;
+
+  public JedisDistributedLock(Pool<Jedis> jedisPool, String key){
+    this.jedisPool = jedisPool;
+    this.lock = new JedisLock(key);
+  }
+
+  @Override
+  public void lock() {
+    try (Jedis jedis = jedisPool.getResource()) {
+      if (!lock.acquire(jedis)) {
+        throw new RuntimeException("acquire timeouted");
+      }
+    } catch (InterruptedException e) {
+      throw new RuntimeException("lock failed", e);
+    }
+  }
+
+  @Override
+  public void lockInterruptibly() throws InterruptedException {
+    try (Jedis jedis = jedisPool.getResource()) {
+      if (!lock.acquire(jedis)) {
+        throw new RuntimeException("acquire timeouted");
+      }
+    }
+  }
+
+  @Override
+  public boolean tryLock() {
+    try (Jedis jedis = jedisPool.getResource()) {
+      return lock.acquire(jedis);
+    } catch (InterruptedException e) {
+      throw new RuntimeException("lock failed", e);
+    }
+  }
+
+  @Override
+  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
+    try (Jedis jedis = jedisPool.getResource()) {
+      return lock.acquire(jedis);
+    }
+  }
+
+  @Override
+  public void unlock() {
+    try (Jedis jedis = jedisPool.getResource()) {
+      lock.release(jedis);
+    }
+  }
+
+  @Override
+  public Condition newCondition() {
+    throw new RuntimeException("unsupported method");
+  }
+
+}

+ 31 - 6
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java

@@ -1,13 +1,18 @@
 package me.chanjar.weixin.common.util.xml;
 
-import java.io.Writer;
-
 import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.basic.*;
+import com.thoughtworks.xstream.converters.collections.CollectionConverter;
 import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
+import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
 import com.thoughtworks.xstream.core.util.QuickWriter;
 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
 import com.thoughtworks.xstream.io.xml.XppDriver;
+import com.thoughtworks.xstream.security.NoTypePermission;
+import com.thoughtworks.xstream.security.WildcardTypePermission;
+
+import java.io.Writer;
 
 public class XStreamInitializer {
   private static final XppDriver XPP_DRIVER = new XppDriver() {
@@ -41,14 +46,34 @@ public class XStreamInitializer {
   };
 
   public static XStream getInstance() {
-    XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER);
+    XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER) {
+      // only register the converters we need; other converters generate a private access warning in the console on Java9+...
+      @Override
+      protected void setupConverters() {
+        registerConverter(new NullConverter(), PRIORITY_VERY_HIGH);
+        registerConverter(new IntConverter(), PRIORITY_NORMAL);
+        registerConverter(new FloatConverter(), PRIORITY_NORMAL);
+        registerConverter(new DoubleConverter(), PRIORITY_NORMAL);
+        registerConverter(new LongConverter(), PRIORITY_NORMAL);
+        registerConverter(new ShortConverter(), PRIORITY_NORMAL);
+        registerConverter(new BooleanConverter(), PRIORITY_NORMAL);
+        registerConverter(new ByteConverter(), PRIORITY_NORMAL);
+        registerConverter(new StringConverter(), PRIORITY_NORMAL);
+        registerConverter(new DateConverter(), PRIORITY_NORMAL);
+        registerConverter(new CollectionConverter(getMapper()), PRIORITY_NORMAL);
+        registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);
+      }
+    };
     xstream.ignoreUnknownElements();
     xstream.setMode(XStream.NO_REFERENCES);
     XStream.setupDefaultSecurity(xstream);
-    xstream.allowTypesByWildcard(new String[]{
-      "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**"
-    });
+    xstream.autodetectAnnotations(true);
 
+    // setup proper security by limiting which classes can be loaded by XStream
+    xstream.addPermission(NoTypePermission.NONE);
+    xstream.addPermission(new WildcardTypePermission(new String[]{
+      "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**"
+    }));
     xstream.setClassLoader(Thread.currentThread().getContextClassLoader());
     return xstream;
   }

+ 51 - 0
weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java

@@ -0,0 +1,51 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.TimeUnit;
+
+public class CommonWxRedisOpsTest {
+
+  protected WxRedisOps wxRedisOps;
+  private String key = "access_token";
+  private String value = String.valueOf(System.currentTimeMillis());
+
+  @Test
+  public void testGetValue() {
+    wxRedisOps.setValue(key, value, 3, TimeUnit.SECONDS);
+    Assert.assertEquals(wxRedisOps.getValue(key), value);
+  }
+
+  @Test
+  public void testSetValue() {
+    String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+    wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+    wxRedisOps.setValue(key, value, 0, TimeUnit.SECONDS);
+    wxRedisOps.setValue(key, value, 1, TimeUnit.SECONDS);
+  }
+
+  @Test
+  public void testGetExpire() {
+    String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+    wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+    Assert.assertTrue(wxRedisOps.getExpire(key) < 0);
+    wxRedisOps.setValue(key, value, 4, TimeUnit.SECONDS);
+    Long expireSeconds = wxRedisOps.getExpire(key);
+    Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+  }
+
+  @Test
+  public void testExpire() {
+    String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+    wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+    wxRedisOps.expire(key, 4, TimeUnit.SECONDS);
+    Long expireSeconds = wxRedisOps.getExpire(key);
+    Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+  }
+
+  @Test
+  public void testGetLock() {
+    Assert.assertNotNull(wxRedisOps.getLock("access_token_lock"));
+  }
+}

+ 21 - 0
weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java

@@ -0,0 +1,21 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import redis.clients.jedis.JedisPool;
+
+public class JedisWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+  JedisPool jedisPool;
+
+  @BeforeTest
+  public void init() {
+    this.jedisPool = new JedisPool("127.0.0.1", 6379);
+    this.wxRedisOps = new JedisWxRedisOps(jedisPool);
+  }
+
+  @AfterTest
+  public void destroy() {
+    this.jedisPool.close();
+  }
+}

+ 26 - 0
weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java

@@ -0,0 +1,26 @@
+package me.chanjar.weixin.common.redis;
+
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedisTemplateWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+  StringRedisTemplate redisTemplate;
+
+  @BeforeTest
+  public void init() {
+    JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
+    connectionFactory.setHostName("127.0.0.1");
+    connectionFactory.setPort(6379);
+    connectionFactory.afterPropertiesSet();
+    StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
+    this.redisTemplate = redisTemplate;
+    this.wxRedisOps = new RedisTemplateWxRedisOps(this.redisTemplate);
+  }
+
+  @AfterTest
+  public void destroy() {
+  }
+}

+ 27 - 0
weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java

@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.redis;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedissonWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+  RedissonClient redissonClient;
+
+  @BeforeTest
+  public void init() {
+    Config config = new Config();
+    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
+    config.setTransportMode(TransportMode.NIO);
+    this.redissonClient = Redisson.create(config);
+    this.wxRedisOps = new RedissonWxRedisOps(this.redissonClient);
+  }
+
+  @AfterTest
+  public void destroy() {
+    this.redissonClient.shutdown();
+  }
+}

+ 13 - 2
weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java

@@ -1,10 +1,10 @@
 package me.chanjar.weixin.common.util;
 
+import org.testng.annotations.Test;
+
 import java.util.List;
 import java.util.Map;
 
-import org.testng.annotations.*;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -17,6 +17,17 @@ import static org.assertj.core.api.Assertions.assertThat;
  */
 public class XmlUtilsTest {
 
+  @Test(expectedExceptions = {RuntimeException.class})
+  public void testXml2Map_xxe() {
+    String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+      "<!DOCTYPE test [\n" +
+      "<!ENTITY xxe SYSTEM \"file:///etc/passwd\">\n" +
+      "<!ENTITY xxe2 SYSTEM \"http://localhost/test.php\">\n" +
+      "]>\n" +
+      "<xml></xml>";
+    XmlUtils.xml2Map(xml);
+  }
+
   @Test
   public void testXml2Map() {
     String xml = "<xml>\n" +

+ 6 - 2
weixin-java-cp/pom.xml

@@ -7,7 +7,7 @@
   <parent>
     <groupId>com.github.binarywang</groupId>
     <artifactId>wx-java</artifactId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
 
   <artifactId>weixin-java-cp</artifactId>
@@ -38,7 +38,11 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
-
+    <!--分布式锁支持-->
+    <dependency>
+      <groupId>org.redisson</groupId>
+      <artifactId>redisson</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.testng</groupId>
       <artifactId>testng</artifactId>

+ 79 - 2
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java

@@ -1,8 +1,10 @@
 package me.chanjar.weixin.cp.api;
 
 import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo;
+import me.chanjar.weixin.cp.bean.*;
 
+import java.util.Calendar;
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -83,6 +85,81 @@ public interface WxCpExternalContactService {
    * @return List of CpUser id
    * @throws WxErrorException .
    */
-  List<String> listFollowUser() throws WxErrorException;
+  List<String> listFollowers() throws WxErrorException;
 
+  /**
+   * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用离职成员的外部联系人再分配接口将这些客户重新分配给其他企业成员。
+   * @param page
+   * @param pageSize
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpUserExternalUnassignList listUnassignedList(Integer page, Integer pageSize) throws WxErrorException;
+
+  /**
+   * 企业可通过此接口,将已离职成员的外部联系人分配给另一个成员接替联系。
+   * @param externalUserid
+   * @param handOverUserid
+   * @param takeOverUserid
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpBaseResp transferExternalContact(String externalUserid,String handOverUserid,String takeOverUserid)throws WxErrorException;
+
+   /** <pre>
+    * 该接口用于获取配置过客户群管理的客户群列表。
+    * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+    * 暂不支持第三方调用。
+    * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+    * </pre>
+    */
+  WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex,Integer pageSize,int status,String[] userIds,String[] partyIds) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 通过客户群ID,获取详情。包括群名、群成员列表、群成员入群时间、入群方式。(客户群是由具有客户群使用权限的成员创建的外部群)
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92122
+   * </pre>
+   *
+   * @param chatId
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpUserExternalGroupChatInfo getGroupChat(String chatId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 企业可通过此接口获取成员联系客户的数据,包括发起申请数、新增客户数、聊天数、发送消息数和删除/拉黑成员的客户数等指标。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 第三方应用需拥有“企业客户”权限。
+   * 第三方/自建应用调用时传入的userid和partyid要在应用的可见范围内;
+   * </pre>
+   * @param startTime
+   * @param endTime
+   * @param userIds
+   * @param partyIds
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 获取指定日期全天的统计数据。注意,企业微信仅存储60天的数据。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * </pre>
+   * @param startTime
+   * @param orderBy
+   * @param orderAsc
+   * @param pageIndex
+   * @param pageSize
+   * @param userIds
+   * @param partyIds
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime,Integer orderBy,Integer orderAsc,Integer pageIndex,Integer pageSize, String[] userIds, String[] partyIds) throws WxErrorException;
 }

+ 40 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java

@@ -6,7 +6,9 @@ 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.cp.bean.WxCpMaJsCode2SessionResult;
+import me.chanjar.weixin.cp.bean.WxCpTpAuthInfo;
 import me.chanjar.weixin.cp.bean.WxCpTpCorp;
+import me.chanjar.weixin.cp.bean.WxCpTpPermanentCodeInfo;
 import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
 
 /**
@@ -88,9 +90,47 @@ public interface WxCpTpService {
    * @param authCode .
    * @return .
    */
+  @Deprecated
   WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException;
 
   /**
+   * 获取企业永久授权码信息
+   * <pre>
+   *   原来的方法实现不全
+   * </pre>
+   *
+   * @param authCode
+   * @return
+   *
+   * @author yuan
+   * @since 2020-03-18
+   *
+   * @throws WxErrorException
+   */
+  WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
+
+  /**
+   * <pre>
+   *   获取预授权链接
+   * </pre>
+   * @param redirectUri 授权完成后的回调网址
+   * @param state a-zA-Z0-9的参数值(不超过128个字节),用于第三方自行校验session,防止跨域攻击
+   * @return
+   * @throws WxErrorException
+   */
+  String getPreAuthUrl(String redirectUri,String state) throws WxErrorException;
+
+  /**
+   * 获取企业的授权信息
+   *
+   * @param authCorpId 授权企业的corpId
+   * @param permanentCode 授权企业的永久授权码
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpTpAuthInfo getAuthInfo(String authCorpId,String permanentCode) throws WxErrorException;
+
+  /**
    * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求.
    *
    * @param url        接口地址

+ 33 - 2
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java

@@ -3,6 +3,7 @@ package me.chanjar.weixin.cp.api.impl;
 import com.google.common.base.Joiner;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.WxType;
 import me.chanjar.weixin.common.bean.WxAccessToken;
@@ -15,12 +16,13 @@ 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.cp.api.WxCpTpService;
-import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult;
-import me.chanjar.weixin.cp.bean.WxCpTpCorp;
+import me.chanjar.weixin.cp.bean.*;
 import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import org.apache.commons.lang3.StringUtils;
 
 import java.io.File;
 import java.io.IOException;
+import java.net.URLEncoder;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -124,6 +126,35 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
   }
 
   @Override
+  public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException{
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("auth_code", authCode);
+    String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
+    return WxCpTpPermanentCodeInfo.fromJson(result);
+  }
+
+  @Override
+  @SneakyThrows
+  public String getPreAuthUrl(String redirectUri,String state) throws WxErrorException{
+    String result = get(configStorage.getApiUrl(GET_PREAUTH_CODE),null);
+    WxCpTpPreauthCode preauthCode = WxCpTpPreauthCode.fromJson(result);
+    String preAuthUrl = "https://open.work.weixin.qq.com/3rdapp/install?suite_id="+configStorage.getSuiteId()+
+      "&pre_auth_code="+preauthCode.getPreAuthCode()+"&redirect_uri="+ URLEncoder.encode(redirectUri,"utf-8");
+    if(StringUtils.isNotBlank(state))
+      preAuthUrl += "&state="+state;
+    return preAuthUrl;
+  }
+
+  @Override
+  public WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException{
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("auth_corpid", authCorpId);
+    jsonObject.addProperty("permanent_code", permanentCode);
+    String result = post(configStorage.getApiUrl(GET_AUTH_INFO), jsonObject.toString());
+    return WxCpTpAuthInfo.fromJson(result);
+  }
+
+  @Override
   public String get(String url, String queryParam) throws WxErrorException {
     return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
   }

+ 117 - 12
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java

@@ -1,22 +1,27 @@
 package me.chanjar.weixin.cp.api.impl;
 
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxCpErrorMsgEnum;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.cp.api.WxCpExternalContactService;
 import me.chanjar.weixin.cp.api.WxCpService;
-import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo;
-import me.chanjar.weixin.cp.bean.WxCpUserExternalContactList;
-import me.chanjar.weixin.cp.bean.WxCpUserWithExternalPermission;
+import me.chanjar.weixin.cp.bean.*;
+import org.apache.commons.lang3.ArrayUtils;
 
+import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 
 import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*;
 
+/**
+ * @author 曹祖鹏 & yuanqixun
+ */
+@RequiredArgsConstructor
 public class WxCpExternalContactServiceImpl implements WxCpExternalContactService {
-  private WxCpService mainService;
-
-  public WxCpExternalContactServiceImpl(WxCpService mainService) {
-    this.mainService = mainService;
-  }
+  private final WxCpService mainService;
 
   @Override
   public WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException {
@@ -35,14 +40,114 @@ public class WxCpExternalContactServiceImpl implements WxCpExternalContactServic
   @Override
   public List<String> listExternalContacts(String userId) throws WxErrorException {
     final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_EXTERNAL_CONTACT + userId);
-    String responseContent = this.mainService.get(url, null);
-    return WxCpUserExternalContactList.fromJson(responseContent).getExternalUserId();
+    try {
+      String responseContent = this.mainService.get(url, null);
+      return WxCpUserExternalContactList.fromJson(responseContent).getExternalUserId();
+    } catch (WxErrorException e) {
+      // not external contact,无客户则返回空列表
+      if (e.getError().getErrorCode() == WxCpErrorMsgEnum.CODE_84061.getCode()) {
+        return Collections.emptyList();
+      }
+      throw e;
+    }
   }
 
   @Override
-  public List<String> listFollowUser() throws WxErrorException {
+  public List<String> listFollowers() throws WxErrorException {
     final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_FOLLOW_USER_LIST);
     String responseContent = this.mainService.get(url, null);
-    return WxCpUserWithExternalPermission.fromJson(responseContent).getFollowUser();
+    return WxCpUserWithExternalPermission.fromJson(responseContent).getFollowers();
+  }
+
+  @Override
+  public WxCpUserExternalUnassignList listUnassignedList(Integer pageIndex, Integer pageSize) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("page_id", pageIndex == null ? 0 : pageIndex);
+    json.addProperty("page_size", pageSize == null ? 100 : pageSize);
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_UNASSIGNED_CONTACT);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpUserExternalUnassignList.fromJson(result);
+  }
+
+  @Override
+  public WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("external_userid", externalUserid);
+    json.addProperty("handover_userid", handOverUserid);
+    json.addProperty("takeover_userid", takeOverUserid);
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_UNASSIGNED_CONTACT);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpBaseResp.fromJson(result);
+  }
+
+  @Override
+  public WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, String[] partyIds) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("offset", pageIndex == null ? 0 : pageIndex);
+    json.addProperty("limit", pageSize == null ? 100 : pageSize);
+    json.addProperty("status_filter", status);
+    if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+      JsonObject ownerFilter = new JsonObject();
+      if (ArrayUtils.isNotEmpty(userIds)) {
+        json.add("userid", new Gson().toJsonTree(userIds).getAsJsonArray());
+      }
+      if (ArrayUtils.isNotEmpty(partyIds)) {
+        json.add("partyid", new Gson().toJsonTree(partyIds).getAsJsonArray());
+      }
+      json.add("owner_filter", ownerFilter);
+    }
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpUserExternalGroupChatList.fromJson(result);
+  }
+
+  @Override
+  public WxCpUserExternalGroupChatInfo getGroupChat(String chatId) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("chat_id", chatId);
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_INFO);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpUserExternalGroupChatInfo.fromJson(result);
+  }
+
+  @Override
+  public WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("start_time", startTime.getTime() / 1000);
+    json.addProperty("end_time", endTime.getTime() / 1000);
+    if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+      if (ArrayUtils.isNotEmpty(userIds)) {
+        json.add("userid", new Gson().toJsonTree(userIds).getAsJsonArray());
+      }
+      if (ArrayUtils.isNotEmpty(partyIds)) {
+        json.add("partyid", new Gson().toJsonTree(partyIds).getAsJsonArray());
+      }
+    }
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_USER_BEHAVIOR_DATA);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpUserExternalUserBehaviorStatistic.fromJson(result);
+  }
+
+  @Override
+  public WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, Integer pageIndex, Integer pageSize, String[] userIds, String[] partyIds) throws WxErrorException {
+    JsonObject json = new JsonObject();
+    json.addProperty("day_begin_time", startTime.getTime() / 1000);
+    json.addProperty("order_by", orderBy == null ? 1 : orderBy);
+    json.addProperty("order_asc", orderAsc == null ? 0 : orderAsc);
+    json.addProperty("offset", pageIndex == null ? 0 : pageIndex);
+    json.addProperty("limit", pageSize == null ? 500 : pageSize);
+    if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+      JsonObject ownerFilter = new JsonObject();
+      if (ArrayUtils.isNotEmpty(userIds)) {
+        json.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray());
+      }
+      if (ArrayUtils.isNotEmpty(partyIds)) {
+        json.add("userid_list", new Gson().toJsonTree(partyIds).getAsJsonArray());
+      }
+      json.add("owner_filter", ownerFilter);
+    }
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_GROUP_CHAT_DATA);
+    final String result = this.mainService.post(url, json.toString());
+    return WxCpUserExternalGroupChatStatistic.fromJson(result);
   }
 }

+ 113 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java

@@ -1,12 +1,125 @@
 package me.chanjar.weixin.cp.api.impl;
 
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.WxType;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+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.locks.Lock;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_AGENT_CONFIG_TICKET;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_JSAPI_TICKET;
+
 /**
  * <pre>
  *  默认接口实现类,使用apache httpclient实现
  * Created by Binary Wang on 2017-5-27.
  * </pre>
+ * <pre>
+ * 增加分布式锁(基于WxCpConfigStorage实现)的支持
+ * Updated by yuanqixun on 2020-05-13
+ * </pre>
+ *
  *
  * @author <a href="https://github.com/binarywang">Binary Wang</a>
  */
 public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl {
+  @Override
+  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) {
+      return getWxCpConfigStorage().getAccessToken();
+    }
+    Lock lock = getWxCpConfigStorage().getAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) {
+        return getWxCpConfigStorage().getAccessToken();
+      }
+      String url = String.format(getWxCpConfigStorage().getApiUrl(WxCpApiPathConsts.GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret());
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (getRequestHttpProxy() != null) {
+          RequestConfig config = RequestConfig.custom()
+            .setProxy(getRequestHttpProxy()).build();
+          httpGet.setConfig(config);
+        }
+        String resultContent;
+        try (CloseableHttpClient httpClient = getRequestHttpClient();
+             CloseableHttpResponse response = httpClient.execute(httpGet)) {
+          resultContent = new BasicResponseHandler().handleResponse(response);
+        } finally {
+          httpGet.releaseConnection();
+        }
+        WxError error = WxError.fromJson(resultContent, WxType.CP);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        getWxCpConfigStorage().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return getWxCpConfigStorage().getAccessToken();
+  }
+
+  @Override
+  public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    if (forceRefresh) {
+      getWxCpConfigStorage().expireAgentJsapiTicket();
+    }
+    if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) {
+      Lock lock = getWxCpConfigStorage().getAgentJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) {
+          String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_AGENT_CONFIG_TICKET), null);
+          JsonObject jsonObject = new JsonParser().parse(responseContent).getAsJsonObject();
+          getWxCpConfigStorage().updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(),
+            jsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return getWxCpConfigStorage().getAgentJsapiTicket();
+  }
+
+  @Override
+  public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    if (forceRefresh) {
+      getWxCpConfigStorage().expireJsapiTicket();
+    }
+
+    if (getWxCpConfigStorage().isJsapiTicketExpired()) {
+      Lock lock = getWxCpConfigStorage().getJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (getWxCpConfigStorage().isJsapiTicketExpired()) {
+          String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_JSAPI_TICKET), null);
+          JsonObject tmpJsonObject = new JsonParser().parse(responseContent).getAsJsonObject();
+          getWxCpConfigStorage().updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(),
+            tmpJsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return getWxCpConfigStorage().getJsapiTicket();
+  }
 }

+ 2 - 2
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java

@@ -3,8 +3,8 @@ package me.chanjar.weixin.cp.api.impl;
 import jodd.http.HttpConnectionProvider;
 import jodd.http.HttpRequest;
 import jodd.http.HttpResponse;
-import jodd.http.JoddHttp;
 import jodd.http.ProxyInfo;
+import jodd.http.net.SocketHttpConnectionProvider;
 import me.chanjar.weixin.common.WxType;
 import me.chanjar.weixin.common.bean.WxAccessToken;
 import me.chanjar.weixin.common.error.WxError;
@@ -68,7 +68,7 @@ public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl<HttpConnectionP
         configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
     }
 
-    httpClient = JoddHttp.httpConnectionProvider;
+    httpClient = new SocketHttpConnectionProvider();
   }
 
   @Override

+ 28 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java

@@ -0,0 +1,28 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+/**
+ * @author yqx
+ * @date 2020/3/16
+ */
+@Getter
+@Setter
+public class WxCpBaseResp {
+  @SerializedName("errcode")
+  protected Long errcode;
+
+  @SerializedName("errmsg")
+  protected String errmsg;
+
+  public boolean success() {
+    return getErrcode() == 0;
+  }
+
+  public static WxCpBaseResp fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpBaseResp.class);
+  }
+}

+ 3 - 2
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java

@@ -6,16 +6,17 @@ import lombok.Data;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 /**
- * 微信部门.
+ * 企业微信部门.
  *
  * @author Daniel Qian
  */
 @Data
 public class WxCpDepart implements Serializable {
-
   private static final long serialVersionUID = -5028321625140879571L;
+
   private Long id;
   private String name;
+  private String enName;
   private Long parentId;
   private Long order;
 

+ 202 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java

@@ -0,0 +1,202 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 服务商模式获取授权信息
+ *
+ * @author yuanqixun
+ */
+@Getter
+@Setter
+public class WxCpTpAuthInfo extends WxCpBaseResp {
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  /**
+   * 服务商信息
+   */
+  @SerializedName("dealer_corp_info")
+  private DealerCorpInfo dealerCorpInfo;
+
+  /**
+   * 授权企业信息
+   */
+  @SerializedName("auth_corp_info")
+  private AuthCorpInfo authCorpInfo;
+
+  /**
+   * 授权信息。如果是通讯录应用,且没开启实体应用,是没有该项的。通讯录应用拥有企业通讯录的全部信息读写权限
+   */
+  @SerializedName("auth_info")
+  private AuthInfo authInfo;
+
+  @Getter
+  @Setter
+  public static class DealerCorpInfo {
+    @SerializedName("corpid")
+    private String corpId;
+
+    @SerializedName("corp_name")
+    private String corpName;
+  }
+
+  @Getter
+  @Setter
+  public static class AuthCorpInfo {
+    @SerializedName("corpid")
+    private String corpId;
+
+    @SerializedName("corp_name")
+    private String corpName;
+
+    @SerializedName("corp_type")
+    private String corpType;
+
+    @SerializedName("corp_square_logo_url")
+    private String corpSquareLogoUrl;
+
+    @SerializedName("corp_round_logo_url")
+    private String corpRoundLogoUrl;
+
+    @SerializedName("corp_user_max")
+    private String corpUserMax;
+
+    @SerializedName("corp_agent_max")
+    private String corpAgentMax;
+
+    /**
+     * 所绑定的企业微信主体名称(仅认证过的企业有)
+     */
+    @SerializedName("corp_full_name")
+    private String corpFullName;
+
+    /**
+     * 认证到期时间
+     */
+    @SerializedName("verified_end_time")
+    private Long verifiedEndTime;
+
+    /**
+     * 企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队号
+     */
+    @SerializedName("subject_type")
+    private Integer subjectType;
+
+    /**
+     * 授权企业在微工作台(原企业号)的二维码,可用于关注微工作台
+     */
+    @SerializedName("corp_wxqrcode")
+    private String corpWxQrcode;
+
+    @SerializedName("corp_scale")
+    private String corpScale;
+
+    @SerializedName("corp_industry")
+    private String corpIndustry;
+
+    @SerializedName("corp_sub_industry")
+    private String corpSubIndustry;
+
+    @SerializedName("location")
+    private String location;
+
+  }
+
+  /**
+   * 授权信息
+   */
+  @Getter
+  @Setter
+  public static class AuthInfo {
+
+    /**
+     * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent
+     */
+    @SerializedName("agent")
+    private List<Agent> agents;
+
+  }
+
+  @Getter
+  @Setter
+  public static class Agent {
+    @SerializedName("agentid")
+    private Integer agentId;
+
+    @SerializedName("name")
+    private String name;
+
+    @SerializedName("round_logo_url")
+    private String roundLogoUrl;
+
+    @SerializedName("square_logo_url")
+    private String squareLogoUrl;
+
+    /**
+     * 旧的多应用套件中的对应应用id,新开发者请忽略
+     */
+    @SerializedName("appid")
+    @Deprecated
+    private String appid;
+
+    /**
+     * 应用权限
+     */
+    @SerializedName("privilege")
+    private Privilege privilege;
+
+  }
+
+  /**
+   * 应用对应的权限
+   */
+  @Getter
+  @Setter
+  public static class Privilege {
+
+    /**
+     * 权限等级。
+     * 1:通讯录基本信息只读
+     * 2:通讯录全部信息只读
+     * 3:通讯录全部信息读写
+     * 4:单个基本信息只读
+     * 5:通讯录全部信息只写
+     */
+    @SerializedName("level")
+    private Integer level;
+
+    @SerializedName("allow_party")
+    private List<Integer> allowParties;
+
+    @SerializedName("allow_user")
+    private List<String> allowUsers;
+
+    @SerializedName("allow_tag")
+    private List<Integer> allowTags;
+
+    @SerializedName("extra_party")
+    private List<Integer> extraParties;
+
+    @SerializedName("extra_user")
+    private List<String> extraUsers;
+
+    @SerializedName("extra_tag")
+    private List<Integer> extraTags;
+
+  }
+
+
+  public static WxCpTpAuthInfo fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpTpAuthInfo.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 44 - 42
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java

@@ -1,42 +1,44 @@
-package me.chanjar.weixin.cp.bean;
-
-import java.io.Serializable;
-
-import com.google.gson.annotations.SerializedName;
-
-import lombok.Data;
-import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
-
-/**
- * 微信部门.
- *
- * @author Daniel Qian
- */
-@Data
-public class WxCpTpCorp implements Serializable {
-
-  private static final long serialVersionUID = -5028321625140879571L;
-  @SerializedName("corpid")
-  private String corpId;
-  @SerializedName("corp_name")
-  private String corpName;
-  @SerializedName("corp_full_name")
-  private String corpFullName;
-  @SerializedName("corp_type")
-  private String corpType;
-  @SerializedName("corp_square_logo_url")
-  private String corpSquareLogoUrl;
-  @SerializedName("corp_user_max")
-  private String corpUserMax;
-  @SerializedName("permanent_code")
-  private String permanentCode;
-  
-  public static WxCpTpCorp fromJson(String json) {
-    return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorp.class);
-  }
-
-  public String toJson() {
-    return WxCpGsonBuilder.create().toJson(this);
-  }
-
-}
+package me.chanjar.weixin.cp.bean;
+
+import java.io.Serializable;
+
+import com.google.gson.annotations.SerializedName;
+
+import lombok.Data;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+/**
+ * 微信部门.
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxCpTpCorp implements Serializable {
+
+  private static final long serialVersionUID = -5028321625140879571L;
+  @SerializedName("corpid")
+  private String corpId;
+  @SerializedName("corp_name")
+  private String corpName;
+  @SerializedName("corp_full_name")
+  private String corpFullName;
+  @SerializedName("corp_type")
+  private String corpType;
+  @SerializedName("corp_square_logo_url")
+  private String corpSquareLogoUrl;
+  @SerializedName("corp_user_max")
+  private String corpUserMax;
+  @SerializedName("permanent_code")
+  private String permanentCode;
+  @SerializedName("auth_info")
+  private String authInfo;
+
+  public static WxCpTpCorp fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorp.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 219 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java

@@ -0,0 +1,219 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 服务商模式获取永久授权码信息
+ *
+ * @author yunaqixun
+ */
+@Getter
+@Setter
+public class WxCpTpPermanentCodeInfo extends WxCpBaseResp {
+
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("access_token")
+  private String accessToken;
+
+  @SerializedName("expires_in")
+  private Long expiresIn;
+
+  @SerializedName("permanent_code")
+  private String permanentCode;
+
+  /**
+   * 授权企业信息
+   */
+  @SerializedName("auth_corp_info")
+  private AuthCorpInfo authCorpInfo;
+
+  /**
+   * 授权信息。如果是通讯录应用,且没开启实体应用,是没有该项的。通讯录应用拥有企业通讯录的全部信息读写权限
+   */
+  @SerializedName("auth_info")
+  private AuthInfo authInfo;
+
+  /**
+   * 授权用户信息
+   */
+  @SerializedName("auth_user_info")
+  private AuthUserInfo authUserInfo;
+
+
+  @Getter
+  @Setter
+  public static class AuthCorpInfo {
+    @SerializedName("corpid")
+    private String corpId;
+
+    @SerializedName("corp_name")
+    private String corpName;
+
+    @SerializedName("corp_type")
+    private String corpType;
+
+    @SerializedName("corp_square_logo_url")
+    private String corpSquareLogoUrl;
+
+    @SerializedName("corp_round_logo_url")
+    private String corpRoundLogoUrl;
+
+    @SerializedName("corp_user_max")
+    private String corpUserMax;
+
+    @SerializedName("corp_agent_max")
+    private String corpAgentMax;
+
+    /**
+     * 所绑定的企业微信主体名称(仅认证过的企业有)
+     */
+    @SerializedName("corp_full_name")
+    private String corpFullName;
+
+    /**
+     * 认证到期时间
+     */
+    @SerializedName("verified_end_time")
+    private Long verifiedEndTime;
+
+    /**
+     * 企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队号
+     */
+    @SerializedName("subject_type")
+    private Integer subjectType;
+
+    /**
+     * 授权企业在微工作台(原企业号)的二维码,可用于关注微工作台
+     */
+    @SerializedName("corp_wxqrcode")
+    private String corpWxQrcode;
+
+    @SerializedName("corp_scale")
+    private String corpScale;
+
+    @SerializedName("corp_industry")
+    private String corpIndustry;
+
+    @SerializedName("corp_sub_industry")
+    private String corpSubIndustry;
+
+    @SerializedName("location")
+    private String location;
+
+  }
+
+  /**
+   * 授权信息
+   */
+  @Getter
+  @Setter
+  public static class AuthInfo {
+
+    /**
+     * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent
+     */
+    @SerializedName("agent")
+    private List<Agent> agents;
+
+  }
+
+  @Getter
+  @Setter
+  public static class Agent {
+    @SerializedName("agentid")
+    private Integer agentId;
+
+    @SerializedName("name")
+    private String name;
+
+    @SerializedName("round_logo_url")
+    private String roundLogoUrl;
+
+    @SerializedName("square_logo_url")
+    private String squareLogoUrl;
+
+    /**
+     * 旧的多应用套件中的对应应用id,新开发者请忽略
+     */
+    @SerializedName("appid")
+    @Deprecated
+    private String appid;
+
+    /**
+     * 应用权限
+     */
+    @SerializedName("privilege")
+    private Privilege privilege;
+
+  }
+
+  /**
+   * 授权人员信息
+   */
+  @Getter
+  @Setter
+  public static class AuthUserInfo {
+    @SerializedName("userid")
+    private String userId;
+
+    @SerializedName("name")
+    private String name;
+
+    @SerializedName("avatar")
+    private String avatar;
+  }
+
+  /**
+   * 应用对应的权限
+   */
+  @Getter
+  @Setter
+  public static class Privilege {
+
+    /**
+     * 权限等级。
+     * 1:通讯录基本信息只读
+     * 2:通讯录全部信息只读
+     * 3:通讯录全部信息读写
+     * 4:单个基本信息只读
+     * 5:通讯录全部信息只写
+     */
+    @SerializedName("level")
+    private Integer level;
+
+    @SerializedName("allow_party")
+    private List<Integer> allowParties;
+
+    @SerializedName("allow_user")
+    private List<String> allowUsers;
+
+    @SerializedName("allow_tag")
+    private List<Integer> allowTags;
+
+    @SerializedName("extra_party")
+    private List<Integer> extraParties;
+
+    @SerializedName("extra_user")
+    private List<String> extraUsers;
+
+    @SerializedName("extra_tag")
+    private List<Integer> extraTags;
+
+
+  }
+
+  public static WxCpTpPermanentCodeInfo fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpTpPermanentCodeInfo.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 26 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPreauthCode.java

@@ -0,0 +1,26 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+/**
+ * 预授权码返回
+ * @author yqx
+ * @date 2020/3/19
+ */
+@Getter
+@Setter
+public class WxCpTpPreauthCode extends WxCpBaseResp {
+
+  @SerializedName("pre_auth_code")
+  String preAuthCode;
+
+  @SerializedName("expires_in")
+  Long expiresIn;
+
+  public static WxCpTpPreauthCode fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpTpPreauthCode.class);
+  }
+}

+ 72 - 68
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java

@@ -1,68 +1,72 @@
-package me.chanjar.weixin.cp.bean;
-
-import java.io.Serializable;
-import java.util.Map;
-
-import com.thoughtworks.xstream.annotations.XStreamAlias;
-import com.thoughtworks.xstream.annotations.XStreamConverter;
-import lombok.Data;
-import lombok.extern.slf4j.Slf4j;
-import me.chanjar.weixin.common.util.XmlUtils;
-import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
-import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
-import me.chanjar.weixin.cp.bean.outxmlbuilder.ImageBuilder;
-import me.chanjar.weixin.cp.bean.outxmlbuilder.NewsBuilder;
-import me.chanjar.weixin.cp.bean.outxmlbuilder.TextBuilder;
-import me.chanjar.weixin.cp.bean.outxmlbuilder.VideoBuilder;
-import me.chanjar.weixin.cp.bean.outxmlbuilder.VoiceBuilder;
-import me.chanjar.weixin.cp.config.WxCpConfigStorage;
-import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
-import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
-import me.chanjar.weixin.cp.util.xml.XStreamTransformer;
-
-/**
- * 回调推送的message
- * https://work.weixin.qq.com/api/doc#90001/90143/90612
- *
- * @author zhenjun cai
- */
-@XStreamAlias("xml")
-@Slf4j
-@Data
-public class WxCpTpXmlMessage implements Serializable {
-
-  private static final long serialVersionUID = 6031833682211475786L;
-  /**
-   * 使用dom4j解析的存放所有xml属性和值的map.
-   */
-  private Map<String, Object> allFieldsMap;
-  
-  @XStreamAlias("SuiteId")
-  @XStreamConverter(value = XStreamCDataConverter.class)
-  protected String suiteId;
-
-  @XStreamAlias("InfoType")
-  @XStreamConverter(value = XStreamCDataConverter.class)
-  protected String infoType;
-
-  @XStreamAlias("TimeStamp")
-  @XStreamConverter(value = XStreamCDataConverter.class)
-  protected String timeStamp;
-  
-  @XStreamAlias("SuiteTicket")
-  @XStreamConverter(value = XStreamCDataConverter.class)
-  protected String suiteTicket;
-  
-  @XStreamAlias("AuthCode")
-  @XStreamConverter(value = XStreamCDataConverter.class)
-  protected String authCode;
-
-  public static WxCpTpXmlMessage fromXml(String xml) {
-    //修改微信变态的消息内容格式,方便解析
-    //xml = xml.replace("</PicList><PicList>", "");
-    final WxCpTpXmlMessage xmlPackage = XStreamTransformer.fromXml(WxCpTpXmlMessage.class, xml);
-    xmlPackage.setAllFieldsMap(XmlUtils.xml2Map(xml));
-    return xmlPackage;
-  }
-
-}
+package me.chanjar.weixin.cp.bean;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.util.XmlUtils;
+import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
+import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
+import me.chanjar.weixin.cp.bean.outxmlbuilder.ImageBuilder;
+import me.chanjar.weixin.cp.bean.outxmlbuilder.NewsBuilder;
+import me.chanjar.weixin.cp.bean.outxmlbuilder.TextBuilder;
+import me.chanjar.weixin.cp.bean.outxmlbuilder.VideoBuilder;
+import me.chanjar.weixin.cp.bean.outxmlbuilder.VoiceBuilder;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
+import me.chanjar.weixin.cp.util.xml.XStreamTransformer;
+
+/**
+ * 回调推送的message
+ * https://work.weixin.qq.com/api/doc#90001/90143/90612
+ *
+ * @author zhenjun cai
+ */
+@XStreamAlias("xml")
+@Slf4j
+@Data
+public class WxCpTpXmlMessage implements Serializable {
+
+  private static final long serialVersionUID = 6031833682211475786L;
+  /**
+   * 使用dom4j解析的存放所有xml属性和值的map.
+   */
+  private Map<String, Object> allFieldsMap;
+
+  @XStreamAlias("SuiteId")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String suiteId;
+
+  @XStreamAlias("InfoType")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String infoType;
+
+  @XStreamAlias("TimeStamp")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String timeStamp;
+
+  @XStreamAlias("SuiteTicket")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String suiteTicket;
+
+  @XStreamAlias("AuthCode")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String authCode;
+
+  @XStreamAlias("AuthCorpId")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  protected String authCorpId;
+
+  public static WxCpTpXmlMessage fromXml(String xml) {
+    //修改微信变态的消息内容格式,方便解析
+    //xml = xml.replace("</PicList><PicList>", "");
+    final WxCpTpXmlMessage xmlPackage = XStreamTransformer.fromXml(WxCpTpXmlMessage.class, xml);
+    xmlPackage.setAllFieldsMap(XmlUtils.xml2Map(xml));
+    return xmlPackage;
+  }
+
+}

+ 18 - 3
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java

@@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 import java.io.Serializable;
@@ -16,8 +17,10 @@ import java.util.List;
  * @author Daniel Qian
  */
 @Data
+@Accessors(chain = true)
 public class WxCpUser implements Serializable {
   private static final long serialVersionUID = -5696099236344075582L;
+
   private String userId;
   private String name;
   private Long[] departIds;
@@ -56,13 +59,19 @@ public class WxCpUser implements Serializable {
    * 成员对外信息.
    */
   private List<ExternalAttribute> externalAttrs = new ArrayList<>();
+  private String externalPosition;
+  private String externalCorpName;
 
   public void addExternalAttr(ExternalAttribute externalAttr) {
     this.externalAttrs.add(externalAttr);
   }
 
   public void addExtAttr(String name, String value) {
-    this.extAttrs.add(new Attr(name, value));
+    this.extAttrs.add(new Attr().setType(0).setName(name).setTextValue(value));
+  }
+
+  public void addExtAttr(Attr attr) {
+    this.extAttrs.add(attr);
   }
 
   public static WxCpUser fromJson(String json) {
@@ -74,10 +83,16 @@ public class WxCpUser implements Serializable {
   }
 
   @Data
-  @AllArgsConstructor
+  @Accessors(chain = true)
   public static class Attr {
+    /**
+     * 属性类型: 0-文本 1-网页
+     */
+    private int type;
     private String name;
-    private String value;
+    private String textValue;
+    private String webUrl;
+    private String webTitle;
   }
 
   @Data

+ 75 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatInfo.java

@@ -0,0 +1,75 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * @author yqx
+ * @date 2020/3/116
+ */
+@Getter
+@Setter
+public class WxCpUserExternalGroupChatInfo extends WxCpBaseResp{
+
+  @SerializedName("group_chat")
+  private GroupChat groupChat;
+
+  @Getter
+  @Setter
+  public static class GroupChat {
+    @SerializedName("chat_id")
+    private String chatId;
+
+    @SerializedName("name")
+    private String name;
+
+    @SerializedName("owner")
+    private String owner;
+
+    @SerializedName("create_time")
+    private Long createTime;
+
+    @SerializedName("notice")
+    private String notice;
+
+    @SerializedName("member_list")
+    private List<GroupMember> memberList;
+
+  }
+
+  @Getter
+  @Setter
+  public static class GroupMember {
+    @SerializedName("userid")
+    private String userId;
+
+    /**
+     * 成员类型。
+     * 1 - 企业成员
+     * 2 - 外部联系人
+     */
+    @SerializedName("type")
+    private int type;
+
+    @SerializedName("join_time")
+    private Long joinTime;
+
+    /**
+     * 入群方式。
+     * 1 - 由成员邀请入群(直接邀请入群)
+     * 2 - 由成员邀请入群(通过邀请链接入群)
+     * 3 - 通过扫描群二维码入群
+     */
+    @SerializedName("join_scene")
+    private int joinScene;
+
+  }
+
+  public static WxCpUserExternalGroupChatInfo fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpUserExternalGroupChatInfo.class);
+  }
+}

+ 46 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatList.java

@@ -0,0 +1,46 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * @author yqx
+ * @date 2020/3/116
+ */
+@Getter
+@Setter
+public class WxCpUserExternalGroupChatList extends WxCpBaseResp {
+
+  @SerializedName("group_chat_list")
+  private List<ChatStatus> groupChatList;
+
+  @Getter
+  @Setter
+  public static class ChatStatus {
+
+    /**
+     * 客户群ID
+     */
+    @SerializedName("chat_id")
+    private String chatId;
+
+    /**
+     * 客户群状态
+     * 0 - 正常
+     * 1 - 跟进人离职
+     * 2 - 离职继承中
+     * 3 - 离职继承完成
+     */
+    @SerializedName("status")
+    private int status;
+
+  }
+
+  public static WxCpUserExternalGroupChatList fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpUserExternalGroupChatList.class);
+  }
+}

+ 90 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalGroupChatStatistic.java

@@ -0,0 +1,90 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 联系客户群统计数据
+ *
+ * @author yqx
+ * @date 2020/3/16
+ */
+@Getter
+@Setter
+public class WxCpUserExternalGroupChatStatistic extends WxCpBaseResp{
+
+  @SerializedName("total")
+  int total;
+
+  @SerializedName("next_offset")
+  int nextOffset;
+
+  @SerializedName("items")
+  List<StatisticItem> itemList;
+
+  @Getter
+  @Setter
+  public static class StatisticItem {
+
+    @SerializedName("owner")
+    String owner;
+
+    @SerializedName("data")
+    ItemData itemData;
+  }
+
+  @Getter
+  @Setter
+  public static class ItemData {
+
+    /**
+     * 新增客户群数量
+     */
+    @SerializedName("new_chat_cnt")
+    int newChatCnt;
+
+    /**
+     * 截至当天客户群总数量
+     */
+    @SerializedName("chat_total")
+    int chatTotal;
+
+    /**
+     * 截至当天有发过消息的客户群数量
+     */
+    @SerializedName("chat_has_msg")
+    int chatHasMsg;
+
+    /**
+     * 客户群新增群人数。
+     */
+    @SerializedName("new_member_cnt")
+    int newMemberCnt;
+
+    /**
+     * 截至当天客户群总人数
+     */
+    @SerializedName("member_total")
+    int memberTotal;
+
+    /**
+     * 截至当天有发过消息的群成员数
+     */
+    @SerializedName("member_has_msg")
+    int memberHasMsg;
+
+    /**
+     * 截至当天客户群消息总数
+     */
+    @SerializedName("msg_total")
+    int msgTotal;
+  }
+
+  public static WxCpUserExternalGroupChatStatistic fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpUserExternalGroupChatStatistic.class);
+  }
+}

+ 52 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalUnassignList.java

@@ -0,0 +1,52 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 离职员工外部联系人列表
+ * @author yqx
+ * @date 2020/3/15
+ */
+@Getter
+@Setter
+public class WxCpUserExternalUnassignList extends WxCpBaseResp{
+
+  @SerializedName("info")
+  private List<UnassignInfo> unassignInfos;
+
+  @SerializedName("is_last")
+  private boolean isLast;
+
+  @Getter
+  @Setter
+  public static class UnassignInfo {
+
+    /**
+     * 离职成员userid
+     */
+    @SerializedName("handover_userid")
+    private String handoverUserid;
+
+    /**
+     * 外部联系人userid
+     */
+    @SerializedName("external_userid")
+    private String externalUserid;
+
+    /**
+     * 成员离职时间
+     */
+    @SerializedName("dimission_time")
+    private Long dimissionTime;
+  }
+
+  public static WxCpUserExternalUnassignList fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpUserExternalUnassignList.class);
+  }
+}

+ 78 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserExternalUserBehaviorStatistic.java

@@ -0,0 +1,78 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 联系客户统计数据
+ * @author yqx
+ * @date 2020/3/16
+ */
+@Getter
+@Setter
+public class WxCpUserExternalUserBehaviorStatistic extends WxCpBaseResp {
+
+  @SerializedName("behavior_data")
+  private List<Behavior> behaviorList;
+
+  @Getter
+  @Setter
+  public static class Behavior {
+
+    /**
+     * 数据日期,为当日0点的时间戳
+     */
+    @SerializedName("stat_time")
+    private Long statTime;
+
+    /**
+     * 聊天总数, 成员有主动发送过消息的聊天数,包括单聊和群聊。
+     */
+    @SerializedName("chat_cnt")
+    private int chatCnt;
+
+    /**
+     * 发送消息数,成员在单聊和群聊中发送的消息总数。
+     */
+    @SerializedName("message_cnt")
+    private int messageCnt;
+
+    /**
+     * 已回复聊天占比,客户主动发起聊天后,成员在一个自然日内有回复过消息的聊天数/客户主动发起的聊天数比例,不包括群聊,仅在确有回复时返回。
+     */
+    @SerializedName("reply_percentage")
+    private double replyPercentage;
+
+    /**
+     * 平均首次回复时长,单位为分钟,即客户主动发起聊天后,成员在一个自然日内首次回复的时长间隔为首次回复时长,所有聊天的首次回复总时长/已回复的聊天总数即为平均首次回复时长,不包括群聊,仅在确有回复时返回。
+     */
+    @SerializedName("avg_reply_time")
+    private int avgReplyTime;
+
+    /**
+     * 删除/拉黑成员的客户数,即将成员删除或加入黑名单的客户数。
+     */
+    @SerializedName("negative_fee_back_cnt")
+    private int negativeFeeBackCnt;
+
+    /**
+     * 发起申请数,成员通过「搜索手机号」、「扫一扫」、「从微信好友中添加」、「从群聊中添加」、「添加共享、分配给我的客户」、「添加单向、双向删除好友关系的好友」、「从新的联系人推荐中添加」等渠道主动向客户发起的好友申请数量。
+     */
+    @SerializedName("new_apply_cnt")
+    private int newApplyCnt;
+
+    /**
+     * 新增客户数,成员新添加的客户数量。
+     */
+    @SerializedName("new_contact_cnt")
+    private int newContactCnt;
+  }
+
+  public static WxCpUserExternalUserBehaviorStatistic fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpUserExternalUserBehaviorStatistic.class);
+  }
+}

+ 8 - 28
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUserWithExternalPermission.java

@@ -2,46 +2,26 @@ package me.chanjar.weixin.cp.bean;
 
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
+import lombok.Data;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 import java.util.List;
 
+/**
+ * @author 曹祖鹏
+ */
+@Data
 public class WxCpUserWithExternalPermission {
   @SerializedName("errcode")
   @Expose
-  private Long errcode;
+  private Long errCode;
   @SerializedName("errmsg")
   @Expose
-  private String errmsg;
+  private String errMsg;
 
   @SerializedName("follow_user")
   @Expose
-  private List<String> followUser = null;
-
-  public Long getErrcode() {
-    return errcode;
-  }
-
-  public void setErrcode(Long errcode) {
-    this.errcode = errcode;
-  }
-
-  public String getErrmsg() {
-    return errmsg;
-  }
-
-  public void setErrmsg(String errmsg) {
-    this.errmsg = errmsg;
-  }
-
-  public List<String> getFollowUser() {
-    return followUser;
-  }
-
-  public void setFollowUser(List<String> followUser) {
-    this.followUser = followUser;
-  }
-
+  private List<String> followers = null;
 
   public static WxCpUserWithExternalPermission fromJson(String json) {
     return WxCpGsonBuilder.create().fromJson(json, WxCpUserWithExternalPermission.class);

+ 66 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpXmlMessage.java

@@ -234,6 +234,13 @@ public class WxCpXmlMessage implements Serializable {
   private String position;
 
   /**
+   * 群ID.
+   */
+  @XStreamAlias("ChatId")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  private String chatId;
+
+  /**
    * 性别,1表示男性,2表示女性.
    */
   @XStreamAlias("Gender")
@@ -396,6 +403,12 @@ public class WxCpXmlMessage implements Serializable {
   @XStreamAlias("SendLocationInfo")
   private SendLocationInfo sendLocationInfo = new SendLocationInfo();
 
+
+  @XStreamAlias("ApprovalInfo")
+  private ApprovalInfo approvalInfo=new ApprovalInfo();
+
+
+
   protected static WxCpXmlMessage fromXml(String xml) {
     //修改微信变态的消息内容格式,方便解析
     xml = xml.replace("</PicList><PicList>", "");
@@ -515,4 +528,57 @@ public class WxCpXmlMessage implements Serializable {
 
   }
 
+  @XStreamAlias("ApprovalInfo")
+  @Data
+  public static class ApprovalInfo {
+
+    /**
+     * 审批编号
+     */
+    @XStreamAlias("SpNo")
+    private String spNo;
+    /**
+     * 审批申请类型名称(审批模板名称)
+     */
+    @XStreamAlias("SpName")
+    private String spName;
+    /**
+     * 申请单状态:1-审批中;2-已通过;3-已驳回;4-已撤销;6-通过后撤销;7-已删除;10-已支付
+     */
+    @XStreamAlias("SpStatus")
+    private Integer spStatus;
+
+    /**
+     * 审批模板id。
+     */
+    @XStreamAlias("templateId")
+    private String templateId;
+    /**
+     * 审批申请提交时间,Unix时间戳
+     */
+    @XStreamAlias("ApplyTime")
+    private Integer applyTime;
+
+    /**
+     * 申请人信息
+     */
+    @XStreamAlias("Applyer")
+    private Applyer applyer;
+    /**
+     * 审批申请单变化类型
+     */
+    @XStreamAlias("StatuChangeEvent")
+    private Integer statuChangeEvent;
+
+    @XStreamAlias("Applyer")
+    @Data
+    public static class Applyer {
+      @XStreamAlias("Applyer")
+      private String UserId;
+      @XStreamAlias("Party")
+      private String party;
+    }
+
+  }
+
 }

+ 4 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/article/MpnewsArticle.java

@@ -1,7 +1,9 @@
 package me.chanjar.weixin.cp.bean.article;
 
+import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 
@@ -13,6 +15,8 @@ import java.io.Serializable;
  * @author Binary Wang
  */
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 @Builder(builderMethodName = "newBuilder")
 public class MpnewsArticle implements Serializable {
   private static final long serialVersionUID = 6985871812170756481L;

+ 0 - 1
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/WxCpApprovalComment.java

@@ -13,7 +13,6 @@ import java.util.List;
  */
 @Data
 public class WxCpApprovalComment implements Serializable {
-
   private static final long serialVersionUID = -5430367411926856292L;
 
   /**

+ 4 - 4
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/WxCpApprovalDetail.java

@@ -13,7 +13,6 @@ import java.util.List;
  */
 @Data
 public class WxCpApprovalDetail implements Serializable {
-
   private static final long serialVersionUID = 1353393306564207170L;
 
   /**
@@ -49,19 +48,20 @@ public class WxCpApprovalDetail implements Serializable {
   /**
    * 申请人信息
    */
-  private WxCpApprovalApplyer applyer;
+  @SerializedName("applyer")
+  private WxCpApprovalApplyer applier;
 
   /**
    * 审批流程信息,可能有多个审批节点
    */
   @SerializedName("sp_record")
-  private WxCpApprovalRecord spRecord;
+  private WxCpApprovalRecord[] spRecords;
 
   /**
    * 抄送信息,可能有多个抄送节点
    */
   @SerializedName("notifyer")
-  private WxCpOperator notifyer;
+  private WxCpOperator[] notifiers;
 
   /**
    * 审批申请数据

+ 29 - 13
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/applydata/ContentValue.java

@@ -11,16 +11,15 @@ import java.util.List;
  */
 @Data
 public class ContentValue implements Serializable {
-
   private static final long serialVersionUID = -5607678965965065261L;
 
   private String text;
 
   @SerializedName("new_number")
-  private Integer newNumber;
+  private Double newNumber;
 
   @SerializedName("new_money")
-  private Integer newMoney;
+  private Double newMoney;
 
   private ContentValue.Date date;
 
@@ -34,26 +33,25 @@ public class ContentValue implements Serializable {
 
   private List<ContentValue.Child> children;
 
+  private Attendance attendance;
+
   @Data
   public static class Date implements Serializable {
-
     private static final long serialVersionUID = -6181554080062231138L;
     private String type;
 
     @SerializedName("s_timestamp")
-    private Long timestamp;
+    private Double timestamp;
   }
 
   @Data
   public static class Selector implements Serializable {
-
     private static final long serialVersionUID = 7305458759126951773L;
     private String type;
     private List<Option> options;
 
     @Data
     public static class Option implements Serializable {
-
       private static final long serialVersionUID = -3471071106328280252L;
       private String key;
 
@@ -74,30 +72,48 @@ public class ContentValue implements Serializable {
 
   @Data
   public static class Department implements Serializable {
-
     private static final long serialVersionUID = -2513762192924826234L;
 
     @SerializedName("openapi_id")
-
     private String openApiId;
     private String name;
   }
 
   @Data
   public static class File implements Serializable {
-
     private static final long serialVersionUID = 3890971381800855142L;
+
     @SerializedName("file_id")
     private String fileId;
-
-
   }
 
   @Data
   public static class Child implements Serializable {
-
     private static final long serialVersionUID = -3500102073821161558L;
     private List<Content> list;
   }
 
+
+  @Data
+  public static class Attendance implements Serializable {
+    private static final long serialVersionUID = -6627566040706594166L;
+    @SerializedName("date_range")
+    private DataRange dateRange;
+    private Integer type;
+
+    @Data
+    public static class DataRange implements Serializable {
+      private static final long serialVersionUID = -3411836592583718255L;
+      private String type;
+      @SerializedName("new_begin")
+      private Long begin;
+      @SerializedName("new_end")
+      private Long end;
+      @SerializedName("new_duration")
+      private Long duration;
+    }
+  }
+
+
+
 }

+ 3 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/TemplateOptions.java

@@ -1,11 +1,14 @@
 package me.chanjar.weixin.cp.bean.oa.templatedata;
 
+import lombok.Data;
+
 import java.io.Serializable;
 import java.util.List;
 
 /**
  * @author gyv123@163.com
  */
+@Data
 public class TemplateOptions implements Serializable {
 
   private static final long serialVersionUID = -7883792668568772078L;

+ 2 - 4
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/TemplateProperty.java

@@ -1,17 +1,15 @@
 package me.chanjar.weixin.cp.bean.oa.templatedata;
 
-import com.google.gson.JsonObject;
 import com.google.gson.annotations.SerializedName;
-import me.chanjar.weixin.cp.bean.oa.WxCpTemplateResult;
-import me.chanjar.weixin.cp.bean.oa.templatedata.control.TemplateContact;
+import lombok.Data;
 
 import java.io.Serializable;
 import java.util.List;
-import java.util.Map;
 
 /**
  * @author gyv12345@163.com
  */
+@Data
 public class TemplateProperty implements Serializable {
 
   private static final long serialVersionUID = -3429251158540167453L;

+ 3 - 1
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/oa/templatedata/control/TemplateSelector.java

@@ -1,13 +1,15 @@
 package me.chanjar.weixin.cp.bean.oa.templatedata.control;
 
+import lombok.Data;
 import me.chanjar.weixin.cp.bean.oa.templatedata.TemplateOptions;
 
 import java.io.Serializable;
 import java.util.List;
 
 /**
- * @author
+ * @author gyv12345@163.com
  */
+@Data
 public class TemplateSelector implements Serializable {
 
   private static final long serialVersionUID = 4995408101489736881L;

+ 4 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/taskcard/TaskCardButton.java

@@ -1,7 +1,9 @@
 package me.chanjar.weixin.cp.bean.taskcard;
 
+import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 /**
  * <pre>
@@ -14,6 +16,8 @@ import lombok.Data;
  */
 @Data
 @Builder
+@NoArgsConstructor
+@AllArgsConstructor
 public class TaskCardButton {
   private String key;
   private String name;

+ 7 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java

@@ -4,6 +4,7 @@ import me.chanjar.weixin.common.bean.WxAccessToken;
 import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
 
 import java.io.File;
+import java.util.concurrent.locks.Lock;
 
 /**
  * 微信客户端配置存储.
@@ -28,6 +29,8 @@ public interface WxCpConfigStorage {
 
   String getAccessToken();
 
+  Lock getAccessTokenLock();
+
   boolean isAccessTokenExpired();
 
   /**
@@ -41,6 +44,8 @@ public interface WxCpConfigStorage {
 
   String getJsapiTicket();
 
+  Lock getJsapiTicketLock();
+
   boolean isJsapiTicketExpired();
 
   /**
@@ -55,6 +60,8 @@ public interface WxCpConfigStorage {
 
   String getAgentJsapiTicket();
 
+  Lock getAgentJsapiTicketLock();
+
   boolean isAgentJsapiTicketExpired();
 
   /**

+ 20 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java

@@ -8,6 +8,8 @@ import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 import java.io.File;
 import java.io.Serializable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化.
@@ -22,6 +24,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
 
   private volatile String token;
   protected volatile String accessToken;
+  protected Lock accessTokenLock = new ReentrantLock();
   private volatile String aesKey;
   protected volatile Integer agentId;
   private volatile long expiresTime;
@@ -34,9 +37,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   private volatile String httpProxyPassword;
 
   private volatile String jsapiTicket;
+  protected Lock jsapiTicketLock = new ReentrantLock();
   private volatile long jsapiTicketExpiresTime;
 
   private volatile String agentJsapiTicket;
+  protected Lock agentJsapiTicketLock = new ReentrantLock();
   private volatile long agentJsapiTicketExpiresTime;
 
   private volatile File tmpDirFile;
@@ -63,6 +68,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
     return this.accessToken;
   }
 
+  @Override
+  public Lock getAccessTokenLock() {
+    return this.accessTokenLock;
+  }
+
   public void setAccessToken(String accessToken) {
     this.accessToken = accessToken;
   }
@@ -93,6 +103,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
     return this.jsapiTicket;
   }
 
+  @Override
+  public Lock getJsapiTicketLock() {
+    return this.jsapiTicketLock;
+  }
+
   public void setJsapiTicket(String jsapiTicket) {
     this.jsapiTicket = jsapiTicket;
   }
@@ -123,6 +138,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   }
 
   @Override
+  public Lock getAgentJsapiTicketLock() {
+    return this.agentJsapiTicketLock;
+  }
+
+  @Override
   public boolean isAgentJsapiTicketExpired() {
     return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime;
   }

+ 17 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java

@@ -9,6 +9,8 @@ import redis.clients.jedis.JedisPool;
 import redis.clients.jedis.JedisPoolConfig;
 
 import java.io.File;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * <pre>
@@ -90,6 +92,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getAccessTokenLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isAccessTokenExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY);
@@ -133,6 +140,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getJsapiTicketLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isJsapiTicketExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY);
@@ -171,6 +183,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getAgentJsapiTicketLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isAgentJsapiTicketExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(String.format(AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY, agentId));

+ 154 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java

@@ -0,0 +1,154 @@
+package me.chanjar.weixin.cp.config.impl;
+
+import lombok.NonNull;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RedissonClient;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 基于Redisson的实现
+ *
+ * @author yuanqixun
+ * @date 2020/5/13
+ */
+public class WxCpRedissonConfigImpl extends WxCpDefaultConfigImpl {
+  protected final static String LOCK_KEY = "wechat_cp_lock:";
+  protected final static String CP_ACCESS_TOKEN_KEY = "wechat_cp_access_token_key:";
+  protected final static String CP_JSAPI_TICKET_KEY = "wechat_cp_jsapi_ticket_key:";
+  protected final static String CP_AGENT_JSAPI_TICKET_KEY = "wechat_cp_agent_jsapi_ticket_key:";
+
+  /**
+   * redis 存储的 key 的前缀,可为空
+   */
+  protected String keyPrefix;
+  protected String accessTokenKey;
+  protected String jsapiTicketKey;
+  protected String agentJsapiTicketKey;
+  protected String lockKey;
+
+  private final WxRedisOps redisOps;
+
+  public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
+    this(new RedissonWxRedisOps(redissonClient), keyPrefix);
+  }
+
+  public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
+    this(redissonClient, null);
+  }
+
+  private WxCpRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) {
+    this.redisOps = redisOps;
+    this.keyPrefix = keyPrefix;
+  }
+
+  /**
+   * 设置企业微信自研应用ID(整数),同时初始化相关的redis key,注意要先调用setCorpId,再调用setAgentId
+   *
+   * @param agentId
+   */
+  @Override
+  public void setAgentId(Integer agentId) {
+    super.setAgentId(agentId);
+    String ukey = getCorpId().concat(":").concat(String.valueOf(agentId));
+    String prefix = StringUtils.isBlank(keyPrefix) ? "" :
+      (StringUtils.endsWith(keyPrefix, ":") ? keyPrefix : (keyPrefix + ":"));
+    lockKey = prefix + LOCK_KEY.concat(ukey);
+    accessTokenKey = prefix + CP_ACCESS_TOKEN_KEY.concat(ukey);
+    jsapiTicketKey = prefix + CP_JSAPI_TICKET_KEY.concat(ukey);
+    agentJsapiTicketKey = prefix + CP_AGENT_JSAPI_TICKET_KEY.concat(ukey);
+  }
+
+  protected Lock getLockByKey(String key) {
+    return redisOps.getLock(key);
+  }
+
+  @Override
+  public Lock getAccessTokenLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("accessToken"));
+  }
+
+  @Override
+  public Lock getAgentJsapiTicketLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("agentJsapiTicket"));
+
+  }
+
+  @Override
+  public Lock getJsapiTicketLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("jsapiTicket"));
+  }
+
+  @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 void updateAccessToken(WxAccessToken accessToken) {
+    redisOps.setValue(this.accessTokenKey, accessToken.getAccessToken(), accessToken.getExpiresIn(), TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateAccessToken(String accessToken, int expiresInSeconds) {
+    redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void expireAccessToken() {
+    redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public String getJsapiTicket() {
+    return redisOps.getValue(this.jsapiTicketKey);
+  }
+
+  @Override
+  public boolean isJsapiTicketExpired() {
+    Long expire = redisOps.getExpire(this.jsapiTicketKey);
+    return expire == null || expire < 2;
+  }
+
+  @Override
+  public void expireJsapiTicket() {
+    redisOps.expire(this.jsapiTicketKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
+    redisOps.setValue(this.jsapiTicketKey, jsapiTicket, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void expireAgentJsapiTicket() {
+    redisOps.expire(this.agentJsapiTicketKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateAgentJsapiTicket(String agentJsapiTicket, int expiresInSeconds) {
+    redisOps.setValue(this.agentJsapiTicketKey, agentJsapiTicket, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public String getAgentJsapiTicket() {
+    return redisOps.getValue(this.agentJsapiTicketKey);
+  }
+
+  @Override
+  public boolean isAgentJsapiTicketExpired() {
+    Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
+    return expire == null || expire < 2;
+  }
+
+}

+ 8 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java

@@ -93,6 +93,8 @@ public final class WxCpApiPathConsts {
     public static final String GET_PERMANENT_CODE = "/cgi-bin/service/get_permanent_code";
     public static final String GET_SUITE_TOKEN = "/cgi-bin/service/get_suite_token";
     public static final String GET_PROVIDER_TOKEN = "/cgi-bin/service/get_provider_token";
+    public static final String GET_PREAUTH_CODE = "/cgi-bin/service/get_pre_auth_code";
+    public static final String GET_AUTH_INFO = "/cgi-bin/service/get_auth_info";
   }
 
   public static class User {
@@ -118,5 +120,11 @@ public final class WxCpApiPathConsts {
     public static final String GET_FOLLOW_USER_LIST = "/cgi-bin/externalcontact/get_follow_user_list";
     public static final String GET_CONTACT_DETAIL = "/cgi-bin/externalcontact/get?external_userid=";
     public static final String LIST_EXTERNAL_CONTACT = "/cgi-bin/externalcontact/list?userid=";
+    public static final String LIST_UNASSIGNED_CONTACT = "/cgi-bin/externalcontact/get_unassigned_list";
+    public static final String TRANSFER_UNASSIGNED_CONTACT = "/cgi-bin/externalcontact/transfer";
+    public static final String GROUP_CHAT_LIST = "/cgi-bin/externalcontact/groupchat/list";
+    public static final String GROUP_CHAT_INFO = "/cgi-bin/externalcontact/groupchat/get";
+    public static final String LIST_USER_BEHAVIOR_DATA = "/cgi-bin/externalcontact/get_user_behavior_data";
+    public static final String LIST_GROUP_CHAT_DATA = "/cgi-bin/externalcontact/groupchat/statistic";
   }
 }

+ 7 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpDepartGsonAdapter.java

@@ -28,6 +28,7 @@ import me.chanjar.weixin.cp.bean.WxCpDepart;
 public class WxCpDepartGsonAdapter implements JsonSerializer<WxCpDepart>, JsonDeserializer<WxCpDepart> {
   private static final String ID = "id";
   private static final String NAME = "name";
+  private static final String EN_NAME = "name_en";
   private static final String PARENT_ID = "parentid";
   private static final String ORDER = "order";
 
@@ -40,6 +41,9 @@ public class WxCpDepartGsonAdapter implements JsonSerializer<WxCpDepart>, JsonDe
     if (group.getName() != null) {
       json.addProperty(NAME, group.getName());
     }
+    if (group.getEnName() != null) {
+      json.addProperty(EN_NAME, group.getEnName());
+    }
     if (group.getParentId() != null) {
       json.addProperty(PARENT_ID, group.getParentId());
     }
@@ -60,6 +64,9 @@ public class WxCpDepartGsonAdapter implements JsonSerializer<WxCpDepart>, JsonDe
     if (departJson.get(NAME) != null && !departJson.get(NAME).isJsonNull()) {
       depart.setName(GsonHelper.getAsString(departJson.get(NAME)));
     }
+    if (departJson.get(EN_NAME) != null && !departJson.get(EN_NAME).isJsonNull()) {
+      depart.setEnName(GsonHelper.getAsString(departJson.get(EN_NAME)));
+    }
     if (departJson.get(ORDER) != null && !departJson.get(ORDER).isJsonNull()) {
       depart.setOrder(GsonHelper.getAsLong(departJson.get(ORDER)));
     }

+ 60 - 25
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java

@@ -9,21 +9,13 @@
 
 package me.chanjar.weixin.cp.util.json;
 
-import java.lang.reflect.Type;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
+import com.google.gson.*;
 import me.chanjar.weixin.common.util.json.GsonHelper;
 import me.chanjar.weixin.cp.bean.Gender;
 import me.chanjar.weixin.cp.bean.WxCpUser;
 
+import java.lang.reflect.Type;
+
 /**
  * cp user gson adapter.
  *
@@ -32,15 +24,18 @@ import me.chanjar.weixin.cp.bean.WxCpUser;
 public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSerializer<WxCpUser> {
   private static final String EXTERNAL_PROFILE = "external_profile";
   private static final String EXTERNAL_ATTR = "external_attr";
-  private static final String EXTATTR = "extattr";
+  private static final String EXTRA_ATTR = "extattr";
+  private static final String EXTERNAL_POSITION = "external_position";
+  private static final String DEPARTMENT = "department";
+  private static final String EXTERNAL_CORP_NAME = "external_corp_name";
 
   @Override
   public WxCpUser deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
     JsonObject o = json.getAsJsonObject();
     WxCpUser user = new WxCpUser();
 
-    if (o.get("department") != null) {
-      JsonArray departJsonArray = o.get("department").getAsJsonArray();
+    if (o.get(DEPARTMENT) != null) {
+      JsonArray departJsonArray = o.get(DEPARTMENT).getAsJsonArray();
       Long[] departIds = new Long[departJsonArray.size()];
       int i = 0;
       for (JsonElement jsonElement : departJsonArray) {
@@ -80,25 +75,41 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri
     user.setQrCode(GsonHelper.getString(o, "qr_code"));
     user.setToInvite(GsonHelper.getBoolean(o, "to_invite"));
 
-    if (GsonHelper.isNotNull(o.get(EXTATTR))) {
+    if (GsonHelper.isNotNull(o.get(EXTRA_ATTR))) {
       this.buildExtraAttrs(o, user);
     }
 
     if (GsonHelper.isNotNull(o.get(EXTERNAL_PROFILE))) {
+      user.setExternalCorpName(GsonHelper.getString(o.getAsJsonObject().get(EXTERNAL_PROFILE).getAsJsonObject(), EXTERNAL_CORP_NAME));
       this.buildExternalAttrs(o, user);
     }
 
+    user.setExternalPosition(GsonHelper.getString(o, EXTERNAL_POSITION));
+
     return user;
   }
 
   private void buildExtraAttrs(JsonObject o, WxCpUser user) {
-    JsonArray attrJsonElements = o.get(EXTATTR).getAsJsonObject().get("attrs").getAsJsonArray();
+    JsonArray attrJsonElements = o.get(EXTRA_ATTR).getAsJsonObject().get("attrs").getAsJsonArray();
     for (JsonElement attrJsonElement : attrJsonElements) {
-      WxCpUser.Attr attr = new WxCpUser.Attr(
-        GsonHelper.getString(attrJsonElement.getAsJsonObject(), "name"),
-        GsonHelper.getString(attrJsonElement.getAsJsonObject(), "value")
-      );
+      final Integer type = GsonHelper.getInteger(attrJsonElement.getAsJsonObject(), "type");
+      final WxCpUser.Attr attr = new WxCpUser.Attr().setType(type)
+        .setName(GsonHelper.getString(attrJsonElement.getAsJsonObject(), "name"));
       user.getExtAttrs().add(attr);
+
+      switch (type) {
+        case 0: {
+          attr.setTextValue(GsonHelper.getString(attrJsonElement.getAsJsonObject().get("text").getAsJsonObject(), "value"));
+          break;
+        }
+        case 1: {
+          final JsonObject web = attrJsonElement.getAsJsonObject().get("web").getAsJsonObject();
+          attr.setWebTitle(GsonHelper.getString(web, "title"))
+            .setWebUrl(GsonHelper.getString(web, "url"));
+          break;
+        }
+        default://ignored
+      }
     }
   }
 
@@ -237,13 +248,39 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri
       JsonArray attrsJsonArray = new JsonArray();
       for (WxCpUser.Attr attr : user.getExtAttrs()) {
         JsonObject attrJson = new JsonObject();
-        attrJson.addProperty("name", attr.getName());
-        attrJson.addProperty("value", attr.getValue());
+
+        switch (attr.getType()) {
+          case 0: {
+            JsonObject text = new JsonObject();
+            text.addProperty("value", attr.getTextValue());
+            attrJson.add("text", text);
+            break;
+          }
+          case 1: {
+            JsonObject web = new JsonObject();
+            web.addProperty("url", attr.getWebUrl());
+            web.addProperty("title", attr.getWebTitle());
+            attrJson.add("web", web);
+            break;
+          }
+          default: //ignored
+        }
         attrsJsonArray.add(attrJson);
       }
       JsonObject attrsJson = new JsonObject();
       attrsJson.add("attrs", attrsJsonArray);
-      o.add(EXTATTR, attrsJson);
+      o.add(EXTRA_ATTR, attrsJson);
+    }
+
+    if (user.getExternalPosition() != null) {
+      o.addProperty(EXTERNAL_POSITION, user.getExternalPosition());
+    }
+
+    JsonObject attrsJson = new JsonObject();
+    o.add(EXTERNAL_PROFILE, attrsJson);
+
+    if (user.getExternalCorpName() != null) {
+      attrsJson.addProperty(EXTERNAL_CORP_NAME, user.getExternalCorpName());
     }
 
     if (user.getExternalAttrs().size() > 0) {
@@ -279,9 +316,7 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri
         attrsJsonArray.add(attrJson);
       }
 
-      JsonObject attrsJson = new JsonObject();
       attrsJson.add(EXTERNAL_ATTR, attrsJsonArray);
-      o.add(EXTERNAL_PROFILE, attrsJson);
     }
 
     return o;

+ 139 - 0
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImplTest.java

@@ -4,12 +4,15 @@ import com.google.gson.JsonObject;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.cp.api.WxCpService;
 import me.chanjar.weixin.cp.api.WxCpTpService;
+import me.chanjar.weixin.cp.bean.WxCpTpAuthInfo;
 import me.chanjar.weixin.cp.bean.WxCpTpCorp;
+import me.chanjar.weixin.cp.bean.WxCpTpPermanentCodeInfo;
 import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
 import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
 import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
 import org.testng.annotations.Test;
 
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_AUTH_INFO;
 import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_PERMANENT_CODE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
@@ -128,6 +131,142 @@ public class BaseWxCpTpServiceImplTest {
 
     final WxCpTpCorp tpCorp = tpService.getPermanentCode(authCode);
     assertThat(tpCorp.getPermanentCode()).isEqualTo("xxxx");
+
+    final WxCpTpPermanentCodeInfo tpPermanentCodeInfo = tpService.getPermanentCodeInfo(authCode);
+    assertThat(tpPermanentCodeInfo.getAuthInfo().getAgents().get(0).getAgentId()).isEqualTo(1);
+
+  }
+
+  @Test
+  public void testGetPermanentCodeInfo() throws WxErrorException{
+    String returnJson = "{\n" +
+      "  \"access_token\": \"u6SoEWyrEmworJ1uNzddbiXh42mCLNU_mdd6b01Afo2LKmyi-WdaaYqhEGFZjB1RGZ-rhjLcAJ86ger7b7Q0gowSw9iIDR8dm49aVH_MztzmQttP3XFG7np1Dxs_VQkVwhhRmfRpEonAmK1_JWIFqayJXXiPUS3LsFd3tWpE7rxmsRa7Ev2ml2htbRp_qGUjtFTErKoDsnNGSka6_RqFPA\", \n" +
+      "  \"expires_in\": 7200, \n" +
+      "  \"permanent_code\": \"lMLlxss77ntxzuEl1i1_AQ3-6-cvqMLYs209YNWVruk\", \n" +
+      "  \"auth_corp_info\": {\n" +
+      "    \"corpid\": \"xxxcorpid\", \n" +
+      "    \"corp_name\": \"xxxx有限公司\", \n" +
+      "    \"corp_type\": \"unverified\", \n" +
+      "    \"corp_round_logo_url\": \"http://p.qpic.cn/pic_wework/3777001839/4046834be7a5f2711feaaa3cc4e691e1bcb1e526cb4544b5/0\", \n" +
+      "    \"corp_square_logo_url\": \"https://p.qlogo.cn/bizmail/EsvsszIt9hJrjrx8QKXuIs0iczdnV4icaPibLIViaukn1iazCay8L1UXtibA/0\", \n" +
+      "    \"corp_user_max\": 200, \n" +
+      "    \"corp_agent_max\": 300, \n" +
+      "    \"corp_wxqrcode\": \"http://p.qpic.cn/pic_wework/211781738/a9af41a60af7519775dd7ac846a4942979dc4a14b8bb2c72/0\", \n" +
+      "    \"corp_full_name\": \"xxxx有限公司\", \n" +
+      "    \"subject_type\": 1, \n" +
+      "    \"corp_scale\": \"1-50人\", \n" +
+      "    \"corp_industry\": \"生活服务\", \n" +
+      "    \"corp_sub_industry\": \"租赁和商务服务\", \n" +
+      "    \"location\": \"北京市\"\n" +
+      "  }, \n" +
+      "  \"auth_info\": {\n" +
+      "    \"agent\": [\n" +
+      "      {\n" +
+      "        \"agentid\": 1000012, \n" +
+      "        \"name\": \"xxxxx\", \n" +
+      "        \"square_logo_url\": \"http://wx.qlogo.cn/mmhead/Q3auHgzwzM4ZCtdxicN8ghMOtTv7M7rLPKmeZ3amic00btdwbNmicaW3Q/0\", \n" +
+      "        \"privilege\": {\n" +
+      "          \"level\": 1, \n" +
+      "          \"allow_party\": [ ], \n" +
+      "          \"allow_user\": [\n" +
+      "            \"yuanqixun\"\n" +
+      "          ], \n" +
+      "          \"allow_tag\": [ ], \n" +
+      "          \"extra_party\": [ ], \n" +
+      "          \"extra_user\": [ ], \n" +
+      "          \"extra_tag\": [ ]\n" +
+      "        }\n" +
+      "      }\n" +
+      "    ]\n" +
+      "  }, \n" +
+      "  \"auth_user_info\": {\n" +
+      "    \"userid\": \"yuanqixun\", \n" +
+      "    \"name\": \"袁启勋\", \n" +
+      "    \"avatar\": \"http://wework.qpic.cn/bizmail/ZYqy8EswiaFyPnk7gy7eiafoicz3TL35f4bAvCf2eSe6RVYSK7aPDFxcw/0\"\n" +
+      "  }\n" +
+      "}";
+
+    final WxCpTpConfigStorage configStorage = new WxCpTpDefaultConfigImpl();
+    tpService.setWxCpTpConfigStorage(configStorage);
+    JsonObject jsonObject = new JsonObject();
+    String authCode = "";
+    jsonObject.addProperty("auth_code", authCode);
+    doReturn(returnJson).when(tpService).post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
+    final WxCpTpPermanentCodeInfo tpPermanentCodeInfo = tpService.getPermanentCodeInfo(authCode);
+    assertThat(tpPermanentCodeInfo.getAuthInfo().getAgents().get(0).getAgentId()).isEqualTo(1000012);
+    assertNotNull(tpPermanentCodeInfo.getAuthInfo().getAgents().get(0).getSquareLogoUrl());
+    assertNotNull(tpPermanentCodeInfo.getAuthCorpInfo().getCorpSquareLogoUrl());
+  }
+
+  @Test
+  public void testGetAuthInfo() throws WxErrorException{
+    String returnJson = "{\n" +
+      "    \"errcode\":0 ,\n" +
+      "    \"errmsg\":\"ok\" ,\n" +
+      "    \"dealer_corp_info\": \n" +
+      "    {\n" +
+      "        \"corpid\": \"xxxx\",\n" +
+      "        \"corp_name\": \"name\"\n" +
+      "    },\n" +
+      "    \"auth_corp_info\": \n" +
+      "    {\n" +
+      "        \"corpid\": \"xxxx\",\n" +
+      "        \"corp_name\": \"name\",\n" +
+      "        \"corp_type\": \"verified\",\n" +
+      "        \"corp_square_logo_url\": \"yyyyy\",\n" +
+      "        \"corp_user_max\": 50,\n" +
+      "        \"corp_agent_max\": 30,\n" +
+      "        \"corp_full_name\":\"full_name\",\n" +
+      "        \"verified_end_time\":1431775834,\n" +
+      "        \"subject_type\": 1,\n" +
+      "        \"corp_wxqrcode\": \"zzzzz\",\n" +
+      "        \"corp_scale\": \"1-50人\",\n" +
+      "        \"corp_industry\": \"IT服务\",\n" +
+      "        \"corp_sub_industry\": \"计算机软件/硬件/信息服务\",\n" +
+      "        \"location\":\"广东省广州市\"\n" +
+      "    },\n" +
+      "    \"auth_info\":\n" +
+      "    {\n" +
+      "        \"agent\" :\n" +
+      "        [\n" +
+      "            {\n" +
+      "                \"agentid\":1,\n" +
+      "                \"name\":\"NAME\",\n" +
+      "                \"round_logo_url\":\"xxxxxx\",\n" +
+      "                \"square_logo_url\":\"yyyyyy\",\n" +
+      "                \"appid\":1,\n" +
+      "                \"privilege\":\n" +
+      "                {\n" +
+      "                    \"level\":1,\n" +
+      "                    \"allow_party\":[1,2,3],\n" +
+      "                    \"allow_user\":[\"zhansan\",\"lisi\"],\n" +
+      "                    \"allow_tag\":[1,2,3],\n" +
+      "                    \"extra_party\":[4,5,6],\n" +
+      "                    \"extra_user\":[\"wangwu\"],\n" +
+      "                    \"extra_tag\":[4,5,6]\n" +
+      "                }\n" +
+      "            },\n" +
+      "            {\n" +
+      "                \"agentid\":2,\n" +
+      "                \"name\":\"NAME2\",\n" +
+      "                \"round_logo_url\":\"xxxxxx\",\n" +
+      "                \"square_logo_url\":\"yyyyyy\",\n" +
+      "                \"appid\":5\n" +
+      "            }\n" +
+      "        ]\n" +
+      "    }\n" +
+      "}\n";
+
+    final WxCpTpConfigStorage configStorage = new WxCpTpDefaultConfigImpl();
+    tpService.setWxCpTpConfigStorage(configStorage);
+    JsonObject jsonObject = new JsonObject();
+    String authCorpId = "xxxxx";
+    String permanentCode = "xxxxx";
+    jsonObject.addProperty("auth_corpid", authCorpId);
+    jsonObject.addProperty("permanent_code", permanentCode);
+    doReturn(returnJson).when(tpService).post(configStorage.getApiUrl(GET_AUTH_INFO), jsonObject.toString());
+    WxCpTpAuthInfo authInfo = tpService.getAuthInfo(authCorpId,permanentCode);
+    assertNotNull(authInfo.getAuthCorpInfo().getCorpId());
   }
 
   @Test

+ 1 - 1
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImplTest.java

@@ -38,7 +38,7 @@ public class WxCpExternalContactServiceImplTest {
 
   @Test
   public void testListExternalWithPermission() throws WxErrorException {
-    List<String> ret = this.wxCpService.getExternalContactService().listFollowUser();
+    List<String> ret = this.wxCpService.getExternalContactService().listFollowers();
     System.out.println(ret);
     assertNotNull(ret);
   }

+ 6 - 6
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/WxCpXmlMessageTest.java

@@ -70,9 +70,9 @@ public class WxCpXmlMessageTest {
     assertEquals(wxMessage.getMediaId(), "media_id");
     assertEquals(wxMessage.getFormat(), "Format");
     assertEquals(wxMessage.getThumbMediaId(), "thumb_media_id");
-    assertEquals(wxMessage.getLocationX(), 23.134521d);
-    assertEquals(wxMessage.getLocationY(), 113.358803d);
-    assertEquals(wxMessage.getScale(), 20d);
+    assertEquals(wxMessage.getLocationX().doubleValue(), 23.134521d);
+    assertEquals(wxMessage.getLocationY().doubleValue(), 113.358803d);
+    assertEquals(wxMessage.getScale().doubleValue(), 20d);
     assertEquals(wxMessage.getLabel(), "位置信息");
     assertEquals(wxMessage.getDescription(), "公众平台官网链接");
     assertEquals(wxMessage.getUrl(), "url");
@@ -80,9 +80,9 @@ public class WxCpXmlMessageTest {
     assertEquals(wxMessage.getEvent(), "subscribe");
     assertEquals(wxMessage.getEventKey(), "qrscene_123123");
     assertEquals(wxMessage.getTicket(), "TICKET");
-    assertEquals(wxMessage.getLatitude(), 23.137466);
-    assertEquals(wxMessage.getLongitude(), 113.352425);
-    assertEquals(wxMessage.getPrecision(), 119.385040);
+    assertEquals(wxMessage.getLatitude().doubleValue(), 23.137466);
+    assertEquals(wxMessage.getLongitude().doubleValue(), 113.352425);
+    assertEquals(wxMessage.getPrecision().doubleValue(), 119.385040);
     assertEquals(wxMessage.getScanCodeInfo().getScanType(), "qrcode");
     assertEquals(wxMessage.getScanCodeInfo().getScanResult(), "1");
     assertEquals(wxMessage.getSendPicsInfo().getCount(), new Long(1));

+ 42 - 12
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapterTest.java

@@ -35,18 +35,31 @@ public class WxCpUserGsonAdapterTest {
       "    \"enable\": 1,\n" +
       "    \"alias\": \"jackzhang\",\n" +
       "    \"extattr\": {\n" +
-      "        \"attrs\": [{\n" +
-      "            \"name\": \"爱好\",\n" +
-      "            \"value\": \"旅游\"\n" +
-      "        }, {\n" +
-      "            \"name\": \"卡号\",\n" +
-      "            \"value\": \"1234567234\"\n" +
-      "        }]\n" +
-      "    },\n" +
+      "        \"attrs\": [\n" +
+      "            {\n" +
+      "                \"type\": 0,\n" +
+      "                \"name\": \"文本名称\",\n" +
+      "                \"text\": {\n" +
+      "                    \"value\": \"文本\"\n" +
+      "                }\n" +
+      "            },\n" +
+      "            {\n" +
+      "                \"type\": 1,\n" +
+      "                \"name\": \"网页名称\",\n" +
+      "                \"web\": {\n" +
+      "                    \"url\": \"http://www.test.com\",\n" +
+      "                    \"title\": \"标题\"\n" +
+      "                }\n" +
+      "            }\n" +
+      "        ]\n" +
+      "    }," +
       "    \"status\": 1,\n" +
       "    \"qr_code\": \"https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx\",\n" +
+      "    \"external_position\": \"高级产品经理\",\n" +
       "    \"external_profile\": {\n" +
-      "        \"external_attr\": [{\n" +
+      "        \"external_corp_name\": \"企业简称\",\n" +
+      "        \"external_attr\": [\n" +
+      "            {\n" +
       "                \"type\": 0,\n" +
       "                \"name\": \"文本名称\",\n" +
       "                \"text\": {\n" +
@@ -65,13 +78,13 @@ public class WxCpUserGsonAdapterTest {
       "                \"type\": 2,\n" +
       "                \"name\": \"测试app\",\n" +
       "                \"miniprogram\": {\n" +
-      "                    \"appid\": \"wx8bd80126147df384\",\n" +
+      "                    \"appid\": \"wx8bd8012614784fake\",\n" +
       "                    \"pagepath\": \"/index\",\n" +
       "                    \"title\": \"my miniprogram\"\n" +
       "                }\n" +
       "            }\n" +
       "        ]\n" +
-      "    }\n" +
+      "    }" +
       "}";
 
     final WxCpUser user = WxCpUser.fromJson(userJson);
@@ -84,6 +97,23 @@ public class WxCpUserGsonAdapterTest {
 
     assertThat(user.getAddress()).isEqualTo("广州市海珠区新港中路");
     assertThat(user.getAlias()).isEqualTo("jackzhang");
+
+    assertThat(user.getExtAttrs()).isNotEmpty();
+
+    final WxCpUser.Attr extraAttr1 = user.getExtAttrs().get(0);
+    assertThat(extraAttr1.getType()).isEqualTo(0);
+    assertThat(extraAttr1.getName()).isEqualTo("文本名称");
+    assertThat(extraAttr1.getTextValue()).isEqualTo("文本");
+
+    final WxCpUser.Attr extraAttr2 = user.getExtAttrs().get(1);
+    assertThat(extraAttr2.getType()).isEqualTo(1);
+    assertThat(extraAttr2.getName()).isEqualTo("网页名称");
+    assertThat(extraAttr2.getWebTitle()).isEqualTo("标题");
+    assertThat(extraAttr2.getWebUrl()).isEqualTo("http://www.test.com");
+
+    assertThat(user.getExternalPosition()).isEqualTo("高级产品经理");
+    assertThat(user.getExternalCorpName()).isEqualTo("企业简称");
+
     assertThat(user.getExternalAttrs()).isNotEmpty();
 
     final WxCpUser.ExternalAttribute externalAttr1 = user.getExternalAttrs().get(0);
@@ -100,7 +130,7 @@ public class WxCpUserGsonAdapterTest {
     final WxCpUser.ExternalAttribute externalAttr3 = user.getExternalAttrs().get(2);
     assertThat(externalAttr3.getType()).isEqualTo(2);
     assertThat(externalAttr3.getName()).isEqualTo("测试app");
-    assertThat(externalAttr3.getAppid()).isEqualTo("wx8bd80126147df384");
+    assertThat(externalAttr3.getAppid()).isEqualTo("wx8bd8012614784fake");
     assertThat(externalAttr3.getPagePath()).isEqualTo("/index");
     assertThat(externalAttr3.getTitle()).isEqualTo("my miniprogram");
 

+ 6 - 2
weixin-java-miniapp/pom.xml

@@ -7,7 +7,7 @@
   <parent>
     <groupId>com.github.binarywang</groupId>
     <artifactId>wx-java</artifactId>
-    <version>3.7.0</version>
+    <version>3.8.0</version>
   </parent>
 
   <artifactId>weixin-java-miniapp</artifactId>
@@ -74,13 +74,17 @@
     <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bcpkix-jdk15on</artifactId>
-      <version>1.59</version>
+      <version>1.65</version>
     </dependency>
     <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.redisson</groupId>
+      <artifactId>redisson</artifactId>
+    </dependency>
+    <dependency>
       <groupId>com.github.jedis-lock</groupId>
       <artifactId>jedis-lock</artifactId>
       <version>1.0.0</version>

+ 84 - 9
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaCloudService.java

@@ -5,6 +5,7 @@ import com.google.gson.JsonArray;
 import me.chanjar.weixin.common.error.WxErrorException;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 云开发相关接口.
@@ -19,6 +20,7 @@ public interface WxMaCloudService {
   String DATABASE_COLLECTION_ADD_URL = "https://api.weixin.qq.com/tcb/databasecollectionadd";
   String GET_QCLOUD_TOKEN_URL = "https://api.weixin.qq.com/tcb/getqcloudtoken";
   String BATCH_DELETE_FILE_URL = "https://api.weixin.qq.com/tcb/batchdeletefile";
+  String BATCH_DOWNLOAD_FILE_URL = "https://api.weixin.qq.com/tcb/batchdownloadfile";
   String UPLOAD_FILE_URL = "https://api.weixin.qq.com/tcb/uploadfile";
   String DATABASE_MIGRATE_QUERY_INFO_URL = "https://api.weixin.qq.com/tcb/databasemigratequeryinfo";
   String DATABASE_MIGRATE_EXPORT_URL = "https://api.weixin.qq.com/tcb/databasemigrateexport";
@@ -31,6 +33,8 @@ public interface WxMaCloudService {
   String DATABASE_DELETE_URL = "https://api.weixin.qq.com/tcb/databasedelete";
   String DATABASE_ADD_URL = "https://api.weixin.qq.com/tcb/databaseadd";
 
+  String invokeCloudFunction(String name, String body) throws WxErrorException;
+
   /**
    * <pre>
    * 触发云函数。注意:HTTP API 途径触发云函数不包含用户信息。
@@ -49,6 +53,12 @@ public interface WxMaCloudService {
    */
   String invokeCloudFunction(String env, String name, String body) throws WxErrorException;
 
+  List<String> add(String collection, List list) throws WxErrorException;
+
+  String add(String collection, Object obj) throws WxErrorException;
+
+  JsonArray databaseAdd(String query) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库插入记录
@@ -64,6 +74,10 @@ public interface WxMaCloudService {
    */
   JsonArray databaseAdd(String env, String query) throws WxErrorException;
 
+  Integer delete(String collection, String whereJson) throws WxErrorException;
+
+  int databaseDelete(String query) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库删除记录
@@ -79,6 +93,10 @@ public interface WxMaCloudService {
    */
   int databaseDelete(String env, String query) throws WxErrorException;
 
+  WxCloudDatabaseUpdateResult update(String collection, String whereJson, String updateJson) throws WxErrorException;
+
+  WxCloudDatabaseUpdateResult databaseUpdate(String query) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库更新记录
@@ -95,6 +113,29 @@ public interface WxMaCloudService {
   WxCloudDatabaseUpdateResult databaseUpdate(String env, String query) throws WxErrorException;
 
   /**
+   * db.collection('geo')
+   *   .where({
+   *     price: _.gt(10)
+   *   })
+   *   .orderBy('_id', 'asc')
+   *   .orderBy('price', 'desc')
+   *   .skip(1)
+   *   .limit(10)
+   *   .get()
+   * @param collection
+   * @param whereJson
+   * @param orderBy
+   * @param skip
+   * @param limit
+   * @return
+   * @throws WxErrorException
+   */
+  WxCloudDatabaseQueryResult query(String collection, String whereJson, Map<String, String> orderBy,
+                                   Integer skip, Integer limit) throws WxErrorException;
+
+  WxCloudDatabaseQueryResult databaseQuery(String query) throws WxErrorException;
+
+  /**
    * <pre>
    * 数据库查询记录
    *
@@ -109,6 +150,8 @@ public interface WxMaCloudService {
    */
   WxCloudDatabaseQueryResult databaseQuery(String env, String query) throws WxErrorException;
 
+  JsonArray databaseAggregate(String query) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库聚合记录
@@ -124,6 +167,10 @@ public interface WxMaCloudService {
    */
   JsonArray databaseAggregate(String env, String query) throws WxErrorException;
 
+  Long count(String collection, String whereJson) throws WxErrorException;
+
+  Long databaseCount(String query) throws WxErrorException;
+
   /**
    * <pre>
    * 统计集合记录数或统计查询语句对应的结果记录数
@@ -139,6 +186,9 @@ public interface WxMaCloudService {
    */
   Long databaseCount(String env, String query) throws WxErrorException;
 
+  void updateIndex(String collectionName, List<WxCloudDatabaseCreateIndexRequest> createIndexes,
+                   List<String> dropIndexNames) throws WxErrorException;
+
   /**
    * <pre>
    * 变更数据库索引
@@ -156,11 +206,15 @@ public interface WxMaCloudService {
   void updateIndex(String env, String collectionName, List<WxCloudDatabaseCreateIndexRequest> createIndexes,
                    List<String> dropIndexNames) throws WxErrorException;
 
+  Long databaseMigrateImport(String collectionName, String filePath, int fileType,
+                             boolean stopOnError, int conflictMode) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库导入
    *
-   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport.html
+   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport
+   * .html
    * 请求地址: POST https://api.weixin.qq.com/tcb/databasemigrateimport?access_token=ACCESS_TOKEN
    * </pre>
    *
@@ -176,11 +230,14 @@ public interface WxMaCloudService {
   Long databaseMigrateImport(String env, String collectionName, String filePath, int fileType, boolean stopOnError,
                              int conflictMode) throws WxErrorException;
 
+  Long databaseMigrateExport(String filePath, int fileType, String query) throws WxErrorException;
+
   /**
    * <pre>
    * 数据库导出
    *
-   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport.html
+   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport
+   * .html
    * 请求地址: POST https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=ACCESS_TOKEN
    * </pre>
    *
@@ -193,11 +250,14 @@ public interface WxMaCloudService {
    */
   Long databaseMigrateExport(String env, String filePath, int fileType, String query) throws WxErrorException;
 
+  WxCloudCloudDatabaseMigrateQueryInfoResult databaseMigrateQueryInfo(Long jobId) throws WxErrorException;
+
   /**
    * <pre>
    *   数据库迁移状态查询
    *
-   *  文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateQueryInfo.html
+   *  文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database
+   *  /databaseMigrateQueryInfo.html
    *  请求地址:POST https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=ACCESS_TOKEN
    * </pre>
    *
@@ -208,6 +268,8 @@ public interface WxMaCloudService {
    */
   WxCloudCloudDatabaseMigrateQueryInfoResult databaseMigrateQueryInfo(String env, Long jobId) throws WxErrorException;
 
+  WxCloudUploadFileResult uploadFile(String path) throws WxErrorException;
+
   /**
    * <pre>
    * 获取文件上传链接
@@ -224,6 +286,8 @@ public interface WxMaCloudService {
    */
   WxCloudUploadFileResult uploadFile(String env, String path) throws WxErrorException;
 
+  WxCloudBatchDownloadFileResult batchDownloadFile(String[] fileIds, long[] maxAges) throws WxErrorException;
+
   /**
    * <pre>
    * 获取文件下载链接
@@ -241,6 +305,8 @@ public interface WxMaCloudService {
    */
   WxCloudBatchDownloadFileResult batchDownloadFile(String env, String[] fileIds, long[] maxAges) throws WxErrorException;
 
+  WxCloudBatchDeleteFileResult batchDeleteFile(String[] fileIds) throws WxErrorException;
+
   /**
    * <pre>
    * 删除文件
@@ -271,11 +337,14 @@ public interface WxMaCloudService {
    */
   WxCloudGetQcloudTokenResult getQcloudToken(long lifeSpan) throws WxErrorException;
 
+  void databaseCollectionAdd(String collectionName) throws WxErrorException;
+
   /**
    * <pre>
    * 新增集合
    *
-   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd.html
+   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd
+   * .html
    * 请求地址:POST https://api.weixin.qq.com/tcb/databasecollectionadd?access_token=ACCESS_TOKEN
    * </pre>
    *
@@ -285,11 +354,14 @@ public interface WxMaCloudService {
    */
   void databaseCollectionAdd(String env, String collectionName) throws WxErrorException;
 
+  void databaseCollectionDelete(String collectionName) throws WxErrorException;
+
   /**
    * <pre>
    * 删除集合
    *
-   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionDelete.html
+   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database
+   * /databaseCollectionDelete.html
    * 请求地址:POST https://api.weixin.qq.com/tcb/databasecollectionadd?access_token=ACCESS_TOKEN
    * </pre>
    *
@@ -299,17 +371,20 @@ public interface WxMaCloudService {
    */
   void databaseCollectionDelete(String env, String collectionName) throws WxErrorException;
 
+  WxCloudDatabaseCollectionGetResult databaseCollectionGet(Long limit, Long offset) throws WxErrorException;
+
   /**
    * <pre>
    * 获取特定云环境下集合信息
    *
-   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet.html
+   * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet
+   * .html
    * 请求地址:POST https://api.weixin.qq.com/tcb/databasecollectionget?access_token=ACCESS_TOKEN
    * </pre>
    *
-   * @param env            云环境ID
-   * @param limit          获取数量限制,默认值:10
-   * @param offset         偏移量,默认值:0
+   * @param env    云环境ID
+   * @param limit  获取数量限制,默认值:10
+   * @param offset 偏移量,默认值:0
    * @return .
    * @throws WxErrorException .
    */

+ 62 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaLiveService.java

@@ -0,0 +1,62 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.bean.WxMaGetLiveInfo;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *  直播相关操作接口.
+ *  Created by yjwang on 2020/4/5.
+ * </pre>
+ *
+ * @author <a href="https://github.com/yjwang3300300">yjwang</a>
+ */
+public interface WxMaLiveService {
+  String GET_LIVE_INFO = "http://api.weixin.qq.com/wxa/business/getliveinfo";
+
+  /**
+   * 获取直播房间列表.(分页)
+   *
+   * @param start 起始拉取房间,start = 0 表示从第 1 个房间开始拉取
+   * @param limit 每次拉取的个数上限,不要设置过大,建议 100 以内
+   * @return .
+   * @throws WxErrorException .
+   */
+  WxMaGetLiveInfo getLiveInfo(Integer start, Integer limit) throws WxErrorException;
+
+  /**
+   * 获取所有直播间信息(没有分页直接获取全部)
+   * @return
+   * @throws WxErrorException
+   */
+  List<WxMaGetLiveInfo.RoomInfo> getLiveinfos() throws WxErrorException;
+
+  /**
+   *
+   * 获取直播房间回放数据信息.
+   *
+   * @param action 获取回放
+   * @param room_id 直播间   id
+   * @param start 起始拉取视频,start =   0   表示从第    1   个视频片段开始拉取
+   * @param limit 每次拉取的个数上限,不要设置过大,建议  100 以内
+   * @return
+   * @throws WxErrorException
+   */
+  WxMaGetLiveInfo getLiveReplay(String action, Integer room_id, Integer start, Integer limit) throws WxErrorException;
+
+  /**
+   *
+   * 获取直播房间回放数据信息.
+   *
+   *  获取回放 (默认:get_replay)
+   * @param room_id 直播间   id
+   * @param start 起始拉取视频,start =   0   表示从第    1   个视频片段开始拉取
+   * @param limit 每次拉取的个数上限,不要设置过大,建议  100 以内
+   * @return
+   * @throws WxErrorException
+   */
+  WxMaGetLiveInfo getLiveReplay(Integer room_id, Integer start, Integer limit) throws WxErrorException;
+
+}

+ 50 - 17
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java

@@ -3,6 +3,7 @@ package cn.binarywang.wx.miniapp.api;
 import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.binarywang.wx.miniapp.config.WxMaConfig;
 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;
@@ -10,7 +11,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp;
 /**
  * @author <a href="https://github.com/binarywang">Binary Wang</a>
  */
-public interface WxMaService {
+public interface WxMaService extends WxService {
   /**
    * 获取access_token.
    */
@@ -23,6 +24,11 @@ public interface WxMaService {
   String GET_PAID_UNION_ID_URL = "https://api.weixin.qq.com/wxa/getpaidunionid";
 
   /**
+   * 导入抽样数据
+   */
+  String SET_DYNAMIC_DATA_URL = "https://api.weixin.qq.com/wxa/setdynamicdata";
+
+  /**
    * 获取登录后的session信息.
    *
    * @param jsCode 登录时获取的 code
@@ -30,6 +36,22 @@ public interface WxMaService {
   WxMaJscode2SessionResult jsCode2SessionInfo(String jsCode) throws WxErrorException;
 
   /**
+   * 导入抽样数据
+   * <pre>
+   * 第三方通过调用微信API,将数据写入到setdynamicdata这个API。每个Post数据包不超过5K,若数据过多可开多进(线)程并发导入数据(例如:数据量为十万量级可以开50个线程并行导数据)。
+   * 文档地址:https://wsad.weixin.qq.com/wsad/zh_CN/htmledition/widget-docs-v3/html/custom/quickstart/implement/import/index.html
+   * http请求方式:POST http(s)://api.weixin.qq.com/wxa/setdynamicdata?access_token=ACCESS_TOKEN
+   * </pre>
+   *
+   * @param data     推送到微信后台的数据列表,该数据被微信用于流量分配,注意该字段为string类型而不是object
+   * @param lifespan 数据有效时间,秒为单位,一般为86400,一天一次导入的频率
+   * @param scene    1代表用于搜索的数据
+   * @param type     用于标识数据所属的服务类目
+   * @throws WxErrorException .
+   */
+  void setDynamicData(int lifespan, String type, int scene, String data) throws WxErrorException;
+
+  /**
    * <pre>
    * 验证消息的确来自微信服务器.
    * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
@@ -73,30 +95,24 @@ public interface WxMaService {
    * @param transactionId 非必填 微信支付订单号
    * @param mchId         非必填 微信支付分配的商户号,和商户订单号配合使用
    * @param outTradeNo    非必填  微信支付商户订单号,和商户号配合使用
+   * @return UnionId.
+   * @throws WxErrorException .
    */
   String getPaidUnionId(String openid, String transactionId, String mchId, String outTradeNo) throws WxErrorException;
 
   /**
-   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求.
-   */
-  String get(String url, String queryParam) throws WxErrorException;
-
-  /**
-   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
-   */
-  String post(String url, String postData) throws WxErrorException;
-
-  /**
-   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
-   */
-  String post(String url, Object obj) throws WxErrorException;
-
-  /**
    * <pre>
    * Service没有实现某个API的时候,可以用这个,
    * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
    * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
    * </pre>
+   *
+   * @param <E>      .
+   * @param <T>      .
+   * @param data     参数或请求数据
+   * @param executor 执行器
+   * @param uri      接口请求地址
+   * @return .
    */
   <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException;
 
@@ -105,6 +121,8 @@ public interface WxMaService {
    * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
    * 默认:1000ms
    * </pre>
+   *
+   * @param retrySleepMillis 重试等待毫秒数
    */
   void setRetrySleepMillis(int retrySleepMillis);
 
@@ -113,6 +131,8 @@ public interface WxMaService {
    * 设置当微信系统响应系统繁忙时,最大重试次数.
    * 默认:5
    * </pre>
+   *
+   * @param maxRetryTimes 最大重试次数
    */
   void setMaxRetryTimes(int maxRetryTimes);
 
@@ -125,6 +145,8 @@ public interface WxMaService {
 
   /**
    * 注入 {@link WxMaConfig} 的实现.
+   *
+   * @param wxConfigProvider config
    */
   void setWxMaConfig(WxMaConfig wxConfigProvider);
 
@@ -233,18 +255,29 @@ public interface WxMaService {
 
   /**
    * 请求http请求相关信息.
+   *
+   * @return .
    */
   RequestHttp getRequestHttp();
 
   /**
    * 获取物流助手接口服务对象
    *
-   * @return
+   * @return .
    */
   WxMaExpressService getExpressService();
 
   /**
    * 获取云开发接口服务对象
+   *
+   * @return .
    */
   WxMaCloudService getCloudService();
+
+  /**
+   * 获取直播接口服务对象
+   *
+   * @return .
+   */
+  WxMaLiveService getLiveService();
 }

+ 0 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaCloudServiceImpl.java


Some files were not shown because too many files changed in this diff