Browse Source

Young - 增加 wx-java-cp

Young 2 years ago
parent
commit
adef70b116
100 changed files with 10828 additions and 11 deletions
  1. 1 0
      pom.xml
  2. 1 11
      support-http/src/main/java/cn/nosum/http/exception/HttpError.java
  3. 52 0
      wx-java-tools/pom.xml
  4. 42 0
      wx-java-tools/wx-java-common/pom.xml
  5. 441 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/api/WxConsts.java
  6. 15 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ToJson.java
  7. 25 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxAccessToken.java
  8. 40 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxCardApiSignature.java
  9. 32 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxJsapiSignature.java
  10. 42 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxNetCheckResult.java
  11. 66 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxOAuth2UserInfo.java
  12. 69 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcAiCropResult.java
  13. 103 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcQrCodeResult.java
  14. 28 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcSuperResolutionResult.java
  15. 52 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenu.java
  16. 87 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenuButton.java
  17. 35 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenuRule.java
  18. 48 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/oauth2/WxOAuth2AccessToken.java
  19. 29 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrBankCardResult.java
  20. 108 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrBizLicenseResult.java
  21. 45 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrCommResult.java
  22. 80 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrDrivingLicenseResult.java
  23. 133 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrDrivingResult.java
  24. 32 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrIdCardResult.java
  25. 25 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrImgSize.java
  26. 43 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrPos.java
  27. 31 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMediaUploadResult.java
  28. 40 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMinishopImageUploadResult.java
  29. 11 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMinishopPicFileResult.java
  30. 19 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/CategoryData.java
  31. 21 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/PubTemplateKeyword.java
  32. 36 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/PubTemplateTitleListResult.java
  33. 22 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/TemplateInfo.java
  34. 1134 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxCpErrorMsgEnum.java
  35. 50 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxError.java
  36. 30 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxErrorException.java
  37. 679 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxMaErrorMsgEnum.java
  38. 667 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxMpErrorMsgEnum.java
  39. 23 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxRuntimeException.java
  40. 63 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/service/WxService.java
  41. 24 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/DataUtils.java
  42. 17 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/RandomUtils.java
  43. 102 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/XmlUtils.java
  44. 26 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/ByteGroup.java
  45. 65 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/PKCS7Encoder.java
  46. 50 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/SHA1.java
  47. 281 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/WxCryptUtil.java
  48. 205 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/GsonHelper.java
  49. 26 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/GsonParser.java
  50. 27 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxAccessTokenAdapter.java
  51. 47 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxBooleanTypeAdapter.java
  52. 43 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxDateTypeAdapter.java
  53. 31 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxErrorAdapter.java
  54. 32 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxGsonBuilder.java
  55. 36 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxMediaUploadResultAdapter.java
  56. 122 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxMenuGsonAdapter.java
  57. 51 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxNetCheckResultGsonAdapter.java
  58. 37 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/IntegerArrayConverter.java
  59. 37 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/LongArrayConverter.java
  60. 30 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/StringArrayConverter.java
  61. 17 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamCDataConverter.java
  62. 95 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamInitializer.java
  63. 8 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamMediaIdConverter.java
  64. 8 0
      wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamReplaceNameConverter.java
  65. 34 0
      wx-java-tools/wx-java-cp/pom.xml
  66. 51 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpAgentService.java
  67. 18 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpAgentWorkBenchService.java
  68. 60 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpChatService.java
  69. 66 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpDepartmentService.java
  70. 551 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpExternalContactService.java
  71. 91 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpGroupRobotService.java
  72. 86 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMediaService.java
  73. 93 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMenuService.java
  74. 53 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMessageService.java
  75. 105 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOAuth2Service.java
  76. 84 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaCalendarService.java
  77. 89 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaScheduleService.java
  78. 190 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaService.java
  79. 422 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpService.java
  80. 99 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpTagService.java
  81. 31 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpTaskCardService.java
  82. 202 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpUserService.java
  83. 501 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/BaseWxCpServiceImpl.java
  84. 46 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpMessageServiceImpl.java
  85. 105 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
  86. 127 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceImpl.java
  87. 78 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceJoddHttpImpl.java
  88. 101 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceOkHttpImpl.java
  89. 246 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/config/WxCpConfigStorage.java
  90. 405 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/config/impl/WxCpDefaultConfigImpl.java
  91. 206 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/constant/WxCpApiPathConsts.java
  92. 357 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/constant/WxCpConsts.java
  93. 42 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/Gender.java
  94. 107 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpAgent.java
  95. 137 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpAgentWorkBench.java
  96. 32 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpBaseResp.java
  97. 22 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpChat.java
  98. 31 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpDepart.java
  99. 43 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpInviteResult.java
  100. 0 0
      wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpMaJsCode2SessionResult.java

+ 1 - 0
pom.xml

@@ -24,6 +24,7 @@
         <module>support-http</module>
         <module>support-core</module>
         <module>support-demo</module>
+        <module>wx-java-tools</module>
     </modules>
 
     <properties>

+ 1 - 11
support-http/src/main/java/cn/nosum/http/exception/HttpError.java

@@ -27,15 +27,5 @@ public class HttpError {
     /**
      * 错误原始信息(英文).
      */
-    private String getErrorMsgEn;
-
-    /**
-     * 通过 JSON 获取错误对象.
-     *
-     * @param json JSON 字符串
-     * @return 错误对象
-     */
-    public HttpError fromJson(String json) {
-        return new HttpError().setErrorCode(0);
-    }
+    private String errorMsgEn;
 }

+ 52 - 0
wx-java-tools/pom.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>nosum-support</artifactId>
+        <groupId>cn.nosum</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>wx-java-tools</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>wx-java-common</module>
+        <module>wx-java-cp</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.8</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>cn.nosum</groupId>
+                <artifactId>support-http</artifactId>
+                <version>1.0-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.code.gson</groupId>
+                <artifactId>gson</artifactId>
+                <version>2.8.0</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>30.0-jre</version>
+            </dependency>
+            <dependency>
+                <groupId>com.thoughtworks.xstream</groupId>
+                <artifactId>xstream</artifactId>
+                <version>1.4.17</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

+ 42 - 0
wx-java-tools/wx-java-common/pom.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>wx-java-tools</artifactId>
+        <groupId>cn.nosum</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>wx-java-common</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.nosum</groupId>
+            <artifactId>support-http</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.thoughtworks.xstream</groupId>
+            <artifactId>xstream</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.7</version>
+        </dependency>
+    </dependencies>
+</project>

+ 441 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/api/WxConsts.java

@@ -0,0 +1,441 @@
+package cn.nosum.wx.common.api;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.nosum.wx.common.error.WxMpErrorMsgEnum.*;
+
+/**
+ * 微信开发所使用到的常量类.
+ *
+ * @author Daniel Qian & binarywang
+ */
+public class WxConsts {
+    /**
+     * access_token 相关错误代码
+     * <pre>
+     * 发生以下情况时尝试刷新access_token
+     * 40001 获取access_token时AppSecret错误,或者access_token无效
+     * 42001 access_token超时
+     * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口
+     * </pre>
+     */
+    public static final List<Integer> ACCESS_TOKEN_ERROR_CODES = Arrays.asList(
+            CODE_40001.getCode(),
+            CODE_40014.getCode(),
+            CODE_42001.getCode()
+    );
+
+    /**
+     * 微信推送过来的消息的类型,和发送给微信xml格式消息的消息类型.
+     */
+    public static class XmlMsgType {
+        public static final String TEXT = "text";
+        public static final String IMAGE = "image";
+        public static final String VOICE = "voice";
+        public static final String SHORTVIDEO = "shortvideo";
+        public static final String VIDEO = "video";
+        public static final String NEWS = "news";
+        public static final String MUSIC = "music";
+        public static final String LOCATION = "location";
+        public static final String LINK = "link";
+        public static final String EVENT = "event";
+        public static final String DEVICE_TEXT = "device_text";
+        public static final String DEVICE_EVENT = "device_event";
+        public static final String DEVICE_STATUS = "device_status";
+        public static final String HARDWARE = "hardware";
+        public static final String TRANSFER_CUSTOMER_SERVICE = "transfer_customer_service";
+        public static final String UPDATE_TASKCARD = "update_taskcard";
+    }
+
+    /**
+     * 主动发送消息(即客服消息)的消息类型.
+     */
+    public static class KefuMsgType {
+        /**
+         * 文本消息.
+         */
+        public static final String TEXT = "text";
+        /**
+         * 图片消息.
+         */
+        public static final String IMAGE = "image";
+        /**
+         * 语音消息.
+         */
+        public static final String VOICE = "voice";
+        /**
+         * 视频消息.
+         */
+        public static final String VIDEO = "video";
+        /**
+         * 音乐消息.
+         */
+        public static final String MUSIC = "music";
+        /**
+         * 图文消息(点击跳转到外链).
+         */
+        public static final String NEWS = "news";
+        /**
+         * 图文消息(点击跳转到图文消息页面).
+         */
+        public static final String MPNEWS = "mpnews";
+        /**
+         * markdown消息.
+         * (目前仅支持markdown语法的子集,微工作台(原企业号)不支持展示markdown消息)
+         */
+        public static final String MARKDOWN = "markdown";
+        /**
+         * 发送文件(CP专用).
+         */
+        public static final String FILE = "file";
+        /**
+         * 文本卡片消息(CP专用).
+         */
+        public static final String TEXTCARD = "textcard";
+        /**
+         * 卡券消息.
+         */
+        public static final String WXCARD = "wxcard";
+        /**
+         * 转发到客服的消息.
+         */
+        public static final String TRANSFER_CUSTOMER_SERVICE = "transfer_customer_service";
+
+        /**
+         * 小程序卡片(要求小程序与公众号已关联).
+         */
+        public static final String MINIPROGRAMPAGE = "miniprogrampage";
+
+        /**
+         * 任务卡片消息.
+         */
+        public static final String TASKCARD = "taskcard";
+
+        /**
+         * 菜单消息.
+         */
+        public static final String MSGMENU = "msgmenu";
+
+        /**
+         * 小程序通知消息.
+         */
+        public static final String MINIPROGRAM_NOTICE = "miniprogram_notice";
+    }
+
+    /**
+     * 表示是否是保密消息,0表示否,1表示是,默认0.
+     */
+    public static class KefuMsgSafe {
+        public static final String NO = "0";
+        public static final String YES = "1";
+    }
+
+    /**
+     * 群发消息的消息类型.
+     */
+    public static class MassMsgType {
+        public static final String MPNEWS = "mpnews";
+        public static final String TEXT = "text";
+        public static final String VOICE = "voice";
+        public static final String IMAGE = "image";
+        public static final String MPVIDEO = "mpvideo";
+    }
+
+    /**
+     * 群发消息后微信端推送给服务器的反馈消息.
+     */
+    public static class MassMsgStatus {
+        public static final String SEND_SUCCESS = "send success";
+        public static final String SEND_FAIL = "send fail";
+        public static final String ERR_10001 = "err(10001)";
+        public static final String ERR_20001 = "err(20001)";
+        public static final String ERR_20004 = "err(20004)";
+        public static final String ERR_20002 = "err(20002)";
+        public static final String ERR_20006 = "err(20006)";
+        public static final String ERR_20008 = "err(20008)";
+        public static final String ERR_20013 = "err(20013)";
+        public static final String ERR_22000 = "err(22000)";
+        public static final String ERR_21000 = "err(21000)";
+        public static final String ERR_30001 = "err(30001)";
+        public static final String ERR_30002 = "err(30002)";
+        public static final String ERR_30003 = "err(30003)";
+        public static final String ERR_40001 = "err(40001)";
+        public static final String ERR_40002 = "err(40002)";
+
+
+        /**
+         * 群发反馈消息代码所对应的文字描述.
+         */
+        public static final Map<String, String> STATUS_DESC = new HashMap<>();
+
+        static {
+            STATUS_DESC.put(SEND_SUCCESS, "发送成功");
+            STATUS_DESC.put(SEND_FAIL, "发送失败");
+            STATUS_DESC.put(ERR_10001, "涉嫌广告");
+            STATUS_DESC.put(ERR_20001, "涉嫌政治");
+            STATUS_DESC.put(ERR_20004, "涉嫌社会");
+            STATUS_DESC.put(ERR_20002, "涉嫌色情");
+            STATUS_DESC.put(ERR_20006, "涉嫌违法犯罪");
+            STATUS_DESC.put(ERR_20008, "涉嫌欺诈");
+            STATUS_DESC.put(ERR_20013, "涉嫌版权");
+            STATUS_DESC.put(ERR_22000, "涉嫌互推_互相宣传");
+            STATUS_DESC.put(ERR_21000, "涉嫌其他");
+            STATUS_DESC.put(ERR_30001, "原创校验出现系统错误且用户选择了被判为转载就不群发");
+            STATUS_DESC.put(ERR_30002, "原创校验被判定为不能群发");
+            STATUS_DESC.put(ERR_30003, "原创校验被判定为转载文且用户选择了被判为转载就不群发");
+            STATUS_DESC.put(ERR_40001, "管理员拒绝");
+            STATUS_DESC.put(ERR_40002, "管理员30分钟内无响应,超时");
+        }
+    }
+
+    /**
+     * 微信端推送过来的事件类型.
+     */
+    public static class EventType {
+        public static final String SUBSCRIBE = "subscribe";
+        public static final String UNSUBSCRIBE = "unsubscribe";
+        public static final String SCAN = "SCAN";
+        public static final String LOCATION = "LOCATION";
+        public static final String CLICK = "CLICK";
+        public static final String VIEW = "VIEW";
+        public static final String MASS_SEND_JOB_FINISH = "MASSSENDJOBFINISH";
+        /**
+         * 扫码推事件的事件推送
+         */
+        public static final String SCANCODE_PUSH = "scancode_push";
+        /**
+         * 扫码推事件且弹出“消息接收中”提示框的事件推送.
+         */
+        public static final String SCANCODE_WAITMSG = "scancode_waitmsg";
+        /**
+         * 弹出系统拍照发图的事件推送.
+         */
+        public static final String PIC_SYSPHOTO = "pic_sysphoto";
+        /**
+         * 弹出拍照或者相册发图的事件推送.
+         */
+        public static final String PIC_PHOTO_OR_ALBUM = "pic_photo_or_album";
+        /**
+         * 弹出微信相册发图器的事件推送.
+         */
+        public static final String PIC_WEIXIN = "pic_weixin";
+        /**
+         * 弹出地理位置选择器的事件推送.
+         */
+        public static final String LOCATION_SELECT = "location_select";
+
+        public static final String TEMPLATE_SEND_JOB_FINISH = "TEMPLATESENDJOBFINISH";
+        /**
+         * 微信小店 订单付款通知.
+         */
+        public static final String MERCHANT_ORDER = "merchant_order";
+
+        /**
+         * 卡券事件:卡券通过审核
+         */
+        public static final String CARD_PASS_CHECK = "card_pass_check";
+
+        /**
+         * 卡券事件:卡券未通过审核
+         */
+        public static final String CARD_NOT_PASS_CHECK = "card_not_pass_check";
+
+        /**
+         * 卡券事件:用户领取卡券
+         */
+        public static final String CARD_USER_GET_CARD = "user_get_card";
+
+        /**
+         * 卡券事件:用户转赠卡券
+         */
+        public static final String CARD_USER_GIFTING_CARD = "user_gifting_card";
+
+
+        /**
+         * 卡券事件:用户核销卡券
+         */
+        public static final String CARD_USER_CONSUME_CARD = "user_consume_card";
+
+
+        /**
+         * 卡券事件:用户通过卡券的微信买单完成时推送
+         */
+        public static final String CARD_USER_PAY_FROM_PAY_CELL = "user_pay_from_pay_cell";
+
+
+        /**
+         * 卡券事件:用户提交会员卡开卡信息
+         */
+        public static final String CARD_SUBMIT_MEMBERCARD_USER_INFO = "submit_membercard_user_info";
+
+        /**
+         * 卡券事件:用户打开查看卡券
+         */
+        public static final String CARD_USER_VIEW_CARD = "user_view_card";
+
+        /**
+         * 卡券事件:用户删除卡券
+         */
+        public static final String CARD_USER_DEL_CARD = "user_del_card";
+
+        /**
+         * 卡券事件:用户在卡券里点击查看公众号进入会话时(需要用户已经关注公众号)
+         */
+        public static final String CARD_USER_ENTER_SESSION_FROM_CARD = "user_enter_session_from_card";
+
+        /**
+         * 卡券事件:当用户的会员卡积分余额发生变动时
+         */
+        public static final String CARD_UPDATE_MEMBER_CARD = "update_member_card";
+
+        /**
+         * 卡券事件:当某个card_id的初始库存数大于200且当前库存小于等于100时,用户尝试领券会触发发送事件给商户,事件每隔12h发送一次
+         */
+        public static final String CARD_SKU_REMIND = "card_sku_remind";
+
+        /**
+         * 卡券事件:当商户朋友的券券点发生变动时
+         */
+        public static final String CARD_PAY_ORDER = "card_pay_order";
+
+        /**
+         * 小程序审核事件:审核通过
+         */
+        public static final String WEAPP_AUDIT_SUCCESS = "weapp_audit_success";
+
+        /**
+         * 小程序审核事件:审核不通过
+         */
+        public static final String WEAPP_AUDIT_FAIL = "weapp_audit_fail";
+
+    }
+
+    /**
+     * 上传多媒体(临时素材)文件的类型.
+     */
+    public static class MediaFileType {
+        public static final String IMAGE = "image";
+        public static final String VOICE = "voice";
+        public static final String VIDEO = "video";
+        public static final String THUMB = "thumb";
+        public static final String FILE = "file";
+    }
+
+    /**
+     * 自定义菜单的按钮类型.
+     */
+    public static class MenuButtonType {
+        /**
+         * 点击推事件.
+         */
+        public static final String CLICK = "click";
+        /**
+         * 跳转URL.
+         */
+        public static final String VIEW = "view";
+        /**
+         * 跳转到小程序.
+         */
+        public static final String MINIPROGRAM = "miniprogram";
+        /**
+         * 扫码推事件.
+         */
+        public static final String SCANCODE_PUSH = "scancode_push";
+        /**
+         * 扫码推事件且弹出“消息接收中”提示框.
+         */
+        public static final String SCANCODE_WAITMSG = "scancode_waitmsg";
+        /**
+         * 弹出系统拍照发图.
+         */
+        public static final String PIC_SYSPHOTO = "pic_sysphoto";
+        /**
+         * 弹出拍照或者相册发图.
+         */
+        public static final String PIC_PHOTO_OR_ALBUM = "pic_photo_or_album";
+        /**
+         * 弹出微信相册发图器.
+         */
+        public static final String PIC_WEIXIN = "pic_weixin";
+        /**
+         * 弹出地理位置选择器.
+         */
+        public static final String LOCATION_SELECT = "location_select";
+        /**
+         * 下发消息(除文本消息).
+         */
+        public static final String MEDIA_ID = "media_id";
+        /**
+         * 跳转图文消息URL.
+         */
+        public static final String VIEW_LIMITED = "view_limited";
+    }
+
+    /**
+     * oauth2网页授权的scope.
+     */
+    public static class OAuth2Scope {
+        /**
+         * 不弹出授权页面,直接跳转,只能获取用户openid.
+         */
+        public static final String SNSAPI_BASE = "snsapi_base";
+
+        /**
+         * 弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息.
+         */
+        public static final String SNSAPI_USERINFO = "snsapi_userinfo";
+
+        /**
+         * 手动授权,可获取成员的详细信息,包含手机、邮箱。只适用于企业微信或企业号.
+         */
+        public static final String SNSAPI_PRIVATEINFO = "snsapi_privateinfo";
+    }
+
+    /**
+     * 网页应用登录授权作用域.
+     */
+    public static class QrConnectScope {
+        public static final String SNSAPI_LOGIN = "snsapi_login";
+    }
+
+    /**
+     * 永久素材类型.
+     */
+    public static class MaterialType {
+        public static final String NEWS = "news";
+        public static final String VOICE = "voice";
+        public static final String IMAGE = "image";
+        public static final String VIDEO = "video";
+    }
+
+
+    /**
+     * 网络检测入参.
+     */
+    public static class NetCheckArgs {
+        public static final String ACTIONDNS = "dns";
+        public static final String ACTIONPING = "ping";
+        public static final String ACTIONALL = "all";
+        public static final String OPERATORUNICOM = "UNICOM";
+        public static final String OPERATORCHINANET = "CHINANET";
+        public static final String OPERATORCAP = "CAP";
+        public static final String OPERATORDEFAULT = "DEFAULT";
+    }
+
+    /**
+     * appId 类型
+     */
+    public static class AppIdType {
+        /**
+         * 公众号appId类型
+         */
+        public static final String MP_TYPE = "mp";
+        /**
+         * 小程序appId类型
+         */
+        public static final String MINI_TYPE = "mini";
+    }
+}

+ 15 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ToJson.java

@@ -0,0 +1,15 @@
+package cn.nosum.wx.common.entity;
+
+/**
+ * 包含toJson()方法的接口.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-10-05
+ */
+public interface ToJson {
+  /**
+   * 转换为json字符串
+   * @return json字符串
+   */
+  String toJson();
+}

+ 25 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxAccessToken.java

@@ -0,0 +1,25 @@
+package cn.nosum.wx.common.entity;
+
+import lombok.Data;
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * access token.
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxAccessToken implements Serializable {
+  private static final long serialVersionUID = 8709719312922168909L;
+
+  private String accessToken;
+
+  private int expiresIn = -1;
+
+  public static WxAccessToken fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxAccessToken.class);
+  }
+
+}

+ 40 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxCardApiSignature.java

@@ -0,0 +1,40 @@
+package cn.nosum.wx.common.entity;
+
+import lombok.Data;
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 卡券Api签名.
+ *
+ * @author YuJian
+ * @version 15/11/8
+ */
+@Data
+public class WxCardApiSignature implements Serializable {
+  private static final long serialVersionUID = 158176707226975979L;
+
+  private String appId;
+
+  private String cardId;
+
+  private String cardType;
+
+  private String locationId;
+
+  private String code;
+
+  private String openId;
+
+  private Long timestamp;
+
+  private String nonceStr;
+
+  private String signature;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 32 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxJsapiSignature.java

@@ -0,0 +1,32 @@
+package cn.nosum.wx.common.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * jspai signature.
+ *
+ * @author Daniel Qian
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxJsapiSignature implements Serializable {
+  private static final long serialVersionUID = -1116808193154384804L;
+
+  private String appId;
+
+  private String nonceStr;
+
+  private long timestamp;
+
+  private String url;
+
+  private String signature;
+
+}

+ 42 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxNetCheckResult.java

@@ -0,0 +1,42 @@
+package cn.nosum.wx.common.entity;
+
+import lombok.Data;
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 网络检测.
+ * @author billytomato
+ */
+@Data
+public class WxNetCheckResult implements Serializable {
+  private static final long serialVersionUID = 6918924418847404172L;
+
+  private List<WxNetCheckDnsInfo> dnsInfos = new ArrayList<>();
+  private List<WxNetCheckPingInfo> pingInfos = new ArrayList<>();
+
+  public static WxNetCheckResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxNetCheckResult.class);
+  }
+
+  @Data
+  public static class WxNetCheckDnsInfo implements Serializable{
+    private static final long serialVersionUID = 82631178029516008L;
+    private String ip;
+    private String realOperator;
+  }
+
+  @Data
+  public static class WxNetCheckPingInfo implements Serializable{
+    private static final long serialVersionUID = -1871970825359178319L;
+    private String ip;
+    private String fromOperator;
+    private String packageLoss;
+    private String time;
+  }
+}
+
+

+ 66 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/WxOAuth2UserInfo.java

@@ -0,0 +1,66 @@
+package cn.nosum.wx.common.entity;
+
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * oauth2用户个人信息.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-10-11
+ */
+@Data
+public class WxOAuth2UserInfo implements Serializable {
+  private static final long serialVersionUID = 3181943506448954725L;
+
+  /**
+   * openid	普通用户的标识,对当前开发者帐号唯一
+   */
+  private String openid;
+  /**
+   * nickname	普通用户昵称
+   */
+  private String nickname;
+  /**
+   * sex	普通用户性别,1为男性,2为女性
+   */
+  private Integer sex;
+  /**
+   * city	普通用户个人资料填写的城市
+   */
+  private String city;
+
+  /**
+   * province	普通用户个人资料填写的省份
+   */
+  private String province;
+  /**
+   * country	国家,如中国为CN
+   */
+  private String country;
+  /**
+   * headimgurl	用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),
+   * 用户没有头像时该项为空
+   */
+  @SerializedName("headimgurl")
+  private String headImgUrl;
+  /**
+   * unionid	用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
+   */
+  @SerializedName("unionid")
+  private String unionId;
+
+  /**
+   * privilege	用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
+   */
+  @SerializedName("privilege")
+  private String[] privileges;
+
+  public static WxOAuth2UserInfo fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOAuth2UserInfo.class);
+  }
+}

+ 69 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcAiCropResult.java

@@ -0,0 +1,69 @@
+package cn.nosum.wx.common.entity.imgproc;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxImgProcAiCropResult implements Serializable {
+  private static final long serialVersionUID = -6470673963772979463L;
+
+  @SerializedName("img_size")
+  private ImgSize imgSize;
+
+  @SerializedName("results")
+  private List<Results> results;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  public static WxImgProcAiCropResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxImgProcAiCropResult.class);
+  }
+
+  @Data
+  public static class ImgSize implements Serializable {
+    private static final long serialVersionUID = -6470673963772979463L;
+
+    @SerializedName("w")
+    private int w;
+
+    @SerializedName("h")
+    private int h;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+
+  @Data
+  public static class Results implements Serializable {
+    private static final long serialVersionUID = -6470673963772979463L;
+
+    @SerializedName("crop_left")
+    private int cropLeft;
+
+    @SerializedName("crop_top")
+    private int cropTop;
+
+    @SerializedName("crop_right")
+    private int cropRight;
+
+    @SerializedName("crop_bottom")
+    private int cropBottom;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+}

+ 103 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcQrCodeResult.java

@@ -0,0 +1,103 @@
+package cn.nosum.wx.common.entity.imgproc;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 二维码/条码识别返回结果
+ *
+ * @author Theo Nie
+ */
+@Data
+public class WxImgProcQrCodeResult implements Serializable {
+  private static final long serialVersionUID = -1194154790100866123L;
+
+  @SerializedName("img_size")
+  private ImgSize imgSize;
+
+  @SerializedName("code_results")
+  private List<CodeResults> codeResults;
+
+  @Data
+  public static class ImgSize implements Serializable {
+    private static final long serialVersionUID = -8847603245514017839L;
+
+    @SerializedName("w")
+    private int w;
+    @SerializedName("h")
+    private int h;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+
+  @Data
+  public static class CodeResults implements Serializable {
+    private static final long serialVersionUID = -6138135951229076759L;
+
+    @SerializedName("type_name")
+    private String typeName;
+
+    @SerializedName("data")
+    private String data;
+
+    @SerializedName("pos")
+    private Pos pos;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+
+    @Data
+    public static class Pos implements Serializable {
+      private static final long serialVersionUID = 7754894061212819602L;
+      @SerializedName("left_top")
+      private Coordinate leftTop;
+
+      @SerializedName("right_top")
+      private Coordinate rightTop;
+
+      @SerializedName("right_bottom")
+      private Coordinate rightBottom;
+
+      @SerializedName("left_bottom")
+      private Coordinate leftBottom;
+
+      @Override
+      public String toString() {
+        return WxGsonBuilder.create().toJson(this);
+      }
+
+      @Data
+      public static class Coordinate implements Serializable {
+        private static final long serialVersionUID = 8930443668927359677L;
+        @SerializedName("x")
+        private int x;
+
+        @SerializedName("y")
+        private int y;
+
+        @Override
+        public String toString() {
+          return WxGsonBuilder.create().toJson(this);
+        }
+      }
+    }
+  }
+
+  public static WxImgProcQrCodeResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxImgProcQrCodeResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 28 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/imgproc/WxImgProcSuperResolutionResult.java

@@ -0,0 +1,28 @@
+package cn.nosum.wx.common.entity.imgproc;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 图片高清化返回结果
+ * @author Theo Nie
+ */
+@Data
+public class WxImgProcSuperResolutionResult implements Serializable {
+  private static final long serialVersionUID = 8007440280170407021L;
+
+  @SerializedName("media_id")
+  private String mediaId;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  public static WxImgProcSuperResolutionResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxImgProcSuperResolutionResult.class);
+  }
+}

+ 52 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenu.java

@@ -0,0 +1,52 @@
+package cn.nosum.wx.common.entity.menu;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import lombok.Data;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 菜单(公众号和企业号共用的).
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxMenu implements Serializable {
+  private static final long serialVersionUID = -7083914585539687746L;
+
+  private List<WxMenuButton> buttons = new ArrayList<>();
+
+  private WxMenuRule matchRule;
+
+  /**
+   * 要用 http://mp.weixin.qq.com/wiki/16/ff9b7b85220e1396ffa16794a9d95adc.html 格式来反序列化
+   * 相比 http://mp.weixin.qq.com/wiki/13/43de8269be54a0a6f64413e4dfa94f39.html 的格式,外层多套了一个menu
+   */
+  public static WxMenu fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMenu.class);
+  }
+
+  /**
+   * 要用 http://mp.weixin.qq.com/wiki/16/ff9b7b85220e1396ffa16794a9d95adc.html 格式来反序列化
+   * 相比 http://mp.weixin.qq.com/wiki/13/43de8269be54a0a6f64413e4dfa94f39.html 的格式,外层多套了一个menu
+   */
+  public static WxMenu fromJson(InputStream is) {
+    return WxGsonBuilder.create()
+      .fromJson(new InputStreamReader(is, StandardCharsets.UTF_8), WxMenu.class);
+  }
+
+  public String toJson() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  @Override
+  public String toString() {
+    return this.toJson();
+  }
+
+}

+ 87 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenuButton.java

@@ -0,0 +1,87 @@
+package cn.nosum.wx.common.entity.menu;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * menu button.
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxMenuButton implements Serializable {
+  private static final long serialVersionUID = -1070939403109776555L;
+
+  /**
+   * <pre>
+   * 菜单的响应动作类型.
+   * view表示网页类型,
+   * click表示点击类型,
+   * miniprogram表示小程序类型
+   * </pre>
+   */
+  private String type;
+
+  /**
+   * 菜单标题,不超过16个字节,子菜单不超过60个字节.
+   */
+  private String name;
+
+  /**
+   * <pre>
+   * 菜单KEY值,用于消息接口推送,不超过128字节.
+   * click等点击类型必须
+   * </pre>
+   */
+  private String key;
+
+  /**
+   * <pre>
+   * 网页链接.
+   * 用户点击菜单可打开链接,不超过1024字节。type为miniprogram时,不支持小程序的老版本客户端将打开本url。
+   * view、miniprogram类型必须
+   * </pre>
+   */
+  private String url;
+
+  /**
+   * <pre>
+   * 调用新增永久素材接口返回的合法media_id.
+   * media_id类型和view_limited类型必须
+   * </pre>
+   */
+  @SerializedName("media_id")
+  private String mediaId;
+
+  /**
+   * <pre>
+   * 小程序的appid.
+   * miniprogram类型必须
+   * </pre>
+   */
+  @SerializedName("appid")
+  private String appId;
+
+  /**
+   * <pre>
+   * 小程序的页面路径.
+   * miniprogram类型必须
+   * </pre>
+   */
+  @SerializedName("pagepath")
+  private String pagePath;
+
+  @SerializedName("sub_button")
+  private List<WxMenuButton> subButtons = new ArrayList<>();
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+}

+ 35 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/menu/WxMenuRule.java

@@ -0,0 +1,35 @@
+package cn.nosum.wx.common.entity.menu;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * menu rule.
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxMenuRule implements Serializable {
+  private static final long serialVersionUID = -4587181819499286670L;
+
+  /**
+   * 变态的微信接口,反序列化时这里反人类的使用和序列化时不一样的名字.
+   */
+  @SerializedName(value = "tag_id", alternate = "group_id")
+  private String tagId;
+  private String sex;
+  private String country;
+  private String province;
+  private String city;
+  @SerializedName("client_platform_type")
+  private String clientPlatformType;
+  private String language;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 48 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/oauth2/WxOAuth2AccessToken.java

@@ -0,0 +1,48 @@
+package cn.nosum.wx.common.entity.oauth2;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxOAuth2AccessToken implements Serializable {
+  private static final long serialVersionUID = -1345910558078620805L;
+
+  @SerializedName("access_token")
+  private String accessToken;
+
+  @SerializedName("expires_in")
+  private int expiresIn = -1;
+
+  @SerializedName("refresh_token")
+  private String refreshToken;
+
+  @SerializedName("openid")
+  private String openId;
+
+  @SerializedName("scope")
+  private String scope;
+
+  /**
+   * https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11513156443eZYea&version=&lang=zh_CN.
+   * 本接口在scope参数为snsapi_base时不再提供unionID字段。
+   */
+  @SerializedName("unionid")
+  private String unionId;
+
+  public static WxOAuth2AccessToken fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOAuth2AccessToken.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 29 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrBankCardResult.java

@@ -0,0 +1,29 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 银行卡OCR识别结果
+ *
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrBankCardResult implements Serializable {
+
+  private static final long serialVersionUID = 554136620394204143L;
+  @SerializedName("number")
+  private String number;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  public static WxOcrBankCardResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrBankCardResult.class);
+  }
+}

+ 108 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrBizLicenseResult.java

@@ -0,0 +1,108 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrBizLicenseResult implements Serializable {
+  private static final long serialVersionUID = -5007671093920178291L;
+
+  /**
+   * 注册号
+   */
+  @SerializedName("reg_num")
+  private String regNum;
+  /**
+   * 编号
+   */
+  @SerializedName("serial")
+  private String serial;
+  /**
+   * 法定代表人姓名
+   */
+  @SerializedName("legal_representative")
+  private String legalRepresentative;
+  /**
+   * 企业名称
+   */
+  @SerializedName("enterprise_name")
+  private String enterpriseName;
+  /**
+   * 组成形式
+   */
+  @SerializedName("type_of_organization")
+  private String typeOfOrganization;
+  /**
+   * 经营场所/企业住所
+   */
+  @SerializedName("address")
+  private String address;
+  /**
+   * 公司类型
+   */
+  @SerializedName("type_of_enterprise")
+  private String typeOfEnterprise;
+  /**
+   * 经营范围
+   */
+  @SerializedName("business_scope")
+  private String businessScope;
+  /**
+   * 注册资本
+   */
+  @SerializedName("registered_capital")
+  private String registeredCapital;
+  /**
+   * 实收资本
+   */
+  @SerializedName("paid_in_capital")
+  private String paidInCapital;
+  /**
+   * 营业期限
+   */
+  @SerializedName("valid_period")
+  private String validPeriod;
+  /**
+   * 注册日期/成立日期
+   */
+  @SerializedName("registered_date")
+  private String registeredDate;
+  /**
+   * 营业执照位置
+   */
+  @SerializedName("cert_position")
+  private CertPosition certPosition;
+  /**
+   * 图片大小
+   */
+  @SerializedName("img_size")
+  private WxOcrImgSize imgSize;
+
+  public static WxOcrBizLicenseResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrBizLicenseResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  @Data
+  public static class CertPosition implements Serializable {
+    private static final long serialVersionUID = 290286813344131863L;
+
+    @SerializedName("pos")
+    private WxOcrPos pos;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+}

+ 45 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrCommResult.java

@@ -0,0 +1,45 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrCommResult implements Serializable {
+  private static final long serialVersionUID = 455833771627756440L;
+
+  @SerializedName("img_size")
+  private WxOcrImgSize imgSize;
+  @SerializedName("items")
+  private List<Items> items;
+
+  public static WxOcrCommResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrCommResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  @Data
+  public static class Items implements Serializable {
+    private static final long serialVersionUID = 3066181677009102791L;
+
+    @SerializedName("text")
+    private String text;
+    @SerializedName("pos")
+    private WxOcrPos pos;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+}

+ 80 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrDrivingLicenseResult.java

@@ -0,0 +1,80 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrDrivingLicenseResult implements Serializable {
+  private static final long serialVersionUID = -6984670645802585738L;
+
+  /**
+   * 证号
+   */
+  @SerializedName("id_num")
+  private String idNum;
+  /**
+   * 姓名
+   */
+  @SerializedName("name")
+  private String name;
+  /**
+   * 性别
+   */
+  @SerializedName("sex")
+  private String sex;
+  /**
+   * 国籍
+   */
+  @SerializedName("nationality")
+  private String nationality;
+  /**
+   * 住址
+   */
+  @SerializedName("address")
+  private String address;
+  /**
+   * 出生日期
+   */
+  @SerializedName("birth_date")
+  private String birthDate;
+  /**
+   * 初次领证日期
+   */
+  @SerializedName("issue_date")
+  private String issueDate;
+  /**
+   * 准驾车型
+   */
+  @SerializedName("car_class")
+  private String carClass;
+  /**
+   * 有效期限起始日
+   */
+  @SerializedName("valid_from")
+  private String validFrom;
+  /**
+   * 有效期限终止日
+   */
+  @SerializedName("valid_to")
+  private String validTo;
+  /**
+   * 印章文字
+   */
+  @SerializedName("official_seal")
+  private String officialSeal;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  public static WxOcrDrivingLicenseResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrDrivingLicenseResult.class);
+  }
+}

+ 133 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrDrivingResult.java

@@ -0,0 +1,133 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrDrivingResult implements Serializable {
+  private static final long serialVersionUID = -7477484374200211303L;
+
+  /**
+   * 车牌号码
+   */
+  @SerializedName("plate_num")
+  private String plateNum;
+  /**
+   * 车辆类型
+   */
+  @SerializedName("vehicle_type")
+  private String vehicleType;
+  /**
+   * 所有人
+   */
+  @SerializedName("owner")
+  private String owner;
+  /**
+   * 住址
+   */
+  @SerializedName("addr")
+  private String addr;
+  /**
+   * 使用性质
+   */
+  @SerializedName("use_character")
+  private String useCharacter;
+  /**
+   * 品牌型号
+   */
+  @SerializedName("model")
+  private String model;
+  /**
+   * 车辆识别代码
+   */
+  @SerializedName("vin")
+  private String vin;
+  /**
+   * 发动机号码
+   */
+  @SerializedName("engine_num")
+  private String engineNum;
+  /**
+   * 注册日期
+   */
+  @SerializedName("register_date")
+  private String registerDate;
+  /**
+   * 发证日期
+   */
+  @SerializedName("issue_date")
+  private String issueDate;
+  /**
+   * 车牌号码
+   */
+  @SerializedName("plate_num_b")
+  private String plateNumB;
+  /**
+   * 号牌
+   */
+  @SerializedName("record")
+  private String record;
+  /**
+   * 核定载人数
+   */
+  @SerializedName("passengers_num")
+  private String passengersNum;
+  /**
+   * 总质量
+   */
+  @SerializedName("total_quality")
+  private String totalQuality;
+  /**
+   * 整备质量
+   */
+  @SerializedName("prepare_quality")
+  private String prepareQuality;
+  /**
+   * 外廓尺寸
+   */
+  @SerializedName("overall_size")
+  private String overallSize;
+  /**
+   * 卡片正面位置(检测到卡片正面才会返回)
+   */
+  @SerializedName("card_position_front")
+  private CardPosition cardPositionFront;
+  /**
+   * 卡片反面位置(检测到卡片反面才会返回)
+   */
+  @SerializedName("card_position_back")
+  private CardPosition cardPositionBack;
+  /**
+   * 图片大小
+   */
+  @SerializedName("img_size")
+  private WxOcrImgSize imgSize;
+
+  @Data
+  public static class CardPosition implements Serializable {
+    private static final long serialVersionUID = 2884515165228160517L;
+
+    @SerializedName("pos")
+    private WxOcrPos pos;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+
+  public static WxOcrDrivingResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrDrivingResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 32 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrIdCardResult.java

@@ -0,0 +1,32 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * OCR身份证识别结果.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2019-06-23
+ */
+@Data
+public class WxOcrIdCardResult implements Serializable {
+  private static final long serialVersionUID = 8184352486986729980L;
+
+  @SerializedName("type")
+  private String type;
+  @SerializedName("name")
+  private String name;
+  @SerializedName("id")
+  private String id;
+  @SerializedName("valid_date")
+  private String validDate;
+
+  public static WxOcrIdCardResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxOcrIdCardResult.class);
+  }
+
+}

+ 25 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrImgSize.java

@@ -0,0 +1,25 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrImgSize implements Serializable {
+  private static final long serialVersionUID = 5234409123551074168L;
+
+  @SerializedName("w")
+  private int w;
+  @SerializedName("h")
+  private int h;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 43 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/ocr/WxOcrPos.java

@@ -0,0 +1,43 @@
+package cn.nosum.wx.common.entity.ocr;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrPos implements Serializable {
+  private static final long serialVersionUID = 4204160206873907920L;
+
+  @SerializedName("left_top")
+  private Coordinate leftTop;
+  @SerializedName("right_top")
+  private Coordinate rightTop;
+  @SerializedName("right_bottom")
+  private Coordinate rightBottom;
+  @SerializedName("left_bottom")
+  private Coordinate leftBottom;
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+  @Data
+  public static class Coordinate implements Serializable {
+    private static final long serialVersionUID = 8675059935386304399L;
+    @SerializedName("x")
+    private int x;
+    @SerializedName("y")
+    private int y;
+
+    @Override
+    public String toString() {
+      return WxGsonBuilder.create().toJson(this);
+    }
+  }
+}

+ 31 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMediaUploadResult.java

@@ -0,0 +1,31 @@
+package cn.nosum.wx.common.entity.result;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ *
+ * @author Daniel Qian
+ */
+@Data
+public class WxMediaUploadResult implements Serializable {
+  private static final long serialVersionUID = 330834334738622341L;
+
+  private String url;
+  private String type;
+  private String mediaId;
+  private String thumbMediaId;
+  private long createdAt;
+
+  public static WxMediaUploadResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMediaUploadResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+
+}

+ 40 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMinishopImageUploadResult.java

@@ -0,0 +1,40 @@
+package cn.nosum.wx.common.entity.result;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class WxMinishopImageUploadResult  implements Serializable {
+  private static final long serialVersionUID = 330834334738622332L;
+
+  private String errcode;
+  private String errmsg;
+
+
+  private WxMinishopPicFileResult picFile;
+
+
+  public static WxMinishopImageUploadResult fromJson(String json) {
+    JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject();
+    WxMinishopImageUploadResult result = new WxMinishopImageUploadResult();
+    result.setErrcode(jsonObject.get("errcode").getAsNumber().toString());
+    if (result.getErrcode().equals("0")) {
+      WxMinishopPicFileResult picFileResult = new WxMinishopPicFileResult();
+      JsonObject picObject = jsonObject.get("pic_file").getAsJsonObject();
+      picFileResult.setMediaId(picObject.get("media_id").getAsString());
+      picFileResult.setPayMediaId(picObject.get("pay_media_id").getAsString());
+      result.setPicFile(picFileResult);
+
+    }
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+}

+ 11 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/result/WxMinishopPicFileResult.java

@@ -0,0 +1,11 @@
+package cn.nosum.wx.common.entity.result;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class WxMinishopPicFileResult implements Serializable {
+  private String mediaId;
+  private String payMediaId;
+}

+ 19 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/CategoryData.java

@@ -0,0 +1,19 @@
+package cn.nosum.wx.common.entity.subscribemsg;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * .
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2021-01-27
+ */
+@Data
+public class CategoryData implements Serializable {
+  private static final long serialVersionUID = -5935548352317679892L;
+
+  private int id;
+  private String name;
+}

+ 21 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/PubTemplateKeyword.java

@@ -0,0 +1,21 @@
+package cn.nosum.wx.common.entity.subscribemsg;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * .
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2021-01-27
+ */
+@Data
+public class PubTemplateKeyword implements Serializable {
+  private static final long serialVersionUID = -1100641668859815647L;
+
+  private int kid;
+  private String name;
+  private String example;
+  private String rule;
+}

+ 36 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/PubTemplateTitleListResult.java

@@ -0,0 +1,36 @@
+package cn.nosum.wx.common.entity.subscribemsg;
+
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author ArBing
+ */
+@Data
+public class PubTemplateTitleListResult implements Serializable {
+    private static final long serialVersionUID = -7718911668757837527L;
+
+    private int count;
+
+    private List<TemplateItem> data;
+
+    public static PubTemplateTitleListResult fromJson(String json) {
+        return WxGsonBuilder.create().fromJson(json, PubTemplateTitleListResult.class);
+    }
+
+    @Data
+    public static class TemplateItem implements Serializable {
+        private static final long serialVersionUID = 6888726696879905332L;
+
+        private Integer type;
+
+        private Integer tid;
+
+        private String categoryId;
+
+        private String title;
+    }
+}

+ 22 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/entity/subscribemsg/TemplateInfo.java

@@ -0,0 +1,22 @@
+package cn.nosum.wx.common.entity.subscribemsg;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * .
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2021-01-27
+ */
+@Data
+public class TemplateInfo implements Serializable {
+  private static final long serialVersionUID = 6971785763573992264L;
+
+  private String priTmplId;
+  private String title;
+  private String content;
+  private String example;
+  private int type;
+}

File diff suppressed because it is too large
+ 1134 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxCpErrorMsgEnum.java


+ 50 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxError.java

@@ -0,0 +1,50 @@
+package cn.nosum.wx.common.error;
+
+import cn.nosum.http.exception.HttpError;
+import cn.nosum.wx.common.utils.json.WxGsonBuilder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 微信错误码.
+ * 请阅读:
+ * 公众平台:<a href="http://mp.weixin.qq.com/wiki/10/6380dc743053a91c544ffd2b7c959166.html">全局返回码说明</a>
+ * 企业微信:<a href="https://work.weixin.qq.com/api/doc#10649">全局错误码</a>
+ *
+ * @author Daniel Qian & Binary Wang
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxError extends HttpError {
+    private static final long serialVersionUID = 7869786563361406291L;
+
+    private String json;
+
+    public static WxError fromJson(String json) {
+        final WxError wxError = WxGsonBuilder.create().fromJson(json, WxError.class);
+        if (wxError.getErrorCode() == 0) {
+            return wxError;
+        }
+
+        if (StringUtils.isNotEmpty(wxError.getErrorMsg())) {
+            wxError.setErrorMsg(wxError.getErrorMsg());
+        }
+
+        final String msg = WxCpErrorMsgEnum.findMsgByCode(wxError.getErrorCode());
+        if (msg != null) {
+            wxError.setErrorMsg(msg);
+        }
+
+        return wxError;
+    }
+
+    @Override
+    public String toString() {
+        if (this.json == null) {
+            return "错误代码:" + this.getErrorCode() + ", 错误信息:" + this.getErrorMsg();
+        }
+        return "错误代码:" + this.getErrorCode() + ", 错误信息:" + this.getErrorMsg() + ",微信原始报文:" + this.json;
+    }
+
+}

+ 30 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxErrorException.java

@@ -0,0 +1,30 @@
+package cn.nosum.wx.common.error;
+
+import cn.nosum.http.exception.HttpError;
+import cn.nosum.http.exception.HttpErrorException;
+
+/**
+ * @author Daniel Qian
+ */
+public class WxErrorException extends HttpErrorException {
+  private static final long serialVersionUID = -6357149550353160810L;
+
+  private static final int DEFAULT_ERROR_CODE = -99;
+
+
+  public WxErrorException(String message) {
+    super(message);
+  }
+
+  public WxErrorException(HttpError error) {
+    super(error);
+  }
+
+  public WxErrorException(HttpError error, Throwable cause) {
+    super(error, cause);
+  }
+
+  public WxErrorException(Throwable cause) {
+    super(cause);
+  }
+}

+ 679 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxMaErrorMsgEnum.java

@@ -0,0 +1,679 @@
+package cn.nosum.wx.common.error;
+
+import lombok.Getter;
+
+/**
+ * 微信小程序错误码
+ *
+ * @author <a href="https://github.com/biggates">biggates</a>
+ */
+@Getter
+public enum WxMaErrorMsgEnum {
+  /**
+   * <pre>
+   * 获取 access_token 时 AppSecret 错误,
+   * 或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的小程序调用接口
+   * 对应操作:<code>sendCustomerMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * </pre>
+   */
+  CODE_40001(40001, "access_token 无效或 AppSecret 错误"),
+  /**
+   * <pre>
+   * 不合法的凭证类型
+   * 对应操作:<code>sendCustomerMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * </pre>
+   */
+  CODE_40002(40002, "不合法的凭证类型"),
+  /**
+   * <pre>
+   * touser不是正确的openid.
+   * 对应操作:<code>sendCustomerMessage</code>, <code>sendUniformMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * </pre>
+   */
+  CODE_40003(40003, "openid 不正确"),
+  /**
+   * <pre>
+   * 无效媒体文件类型
+   * 对应操作:<code>uploadTempMedia</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/uploadTempMedia.html
+   * </pre>
+   */
+  CODE_40004(40004, "无效媒体文件类型"),
+  /**
+   * <pre>
+   * 无效媒体文件 ID.
+   * 对应操作:<code>getTempMedia</code>
+   * 对应地址:
+   * GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/getTempMedia.html
+   * </pre>
+   */
+  CODE_40007(40007, "无效媒体文件 ID"),
+  /**
+   * <pre>
+   * appid不正确,或者不符合绑定关系要求.
+   * 对应操作:<code>sendUniformMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * </pre>
+   */
+  CODE_40013(40013, "appid不正确,或者不符合绑定关系要求"),
+  /**
+   * <pre>
+   * template_id 不正确.
+   * 对应操作:<code>sendUniformMessage</code>, <code>sendTemplateMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+   * </pre>
+   */
+  CODE_40037(40037, "template_id 不正确"),
+  /**
+   * <pre>
+   * form_id不正确,或者过期.
+   * 对应操作:<code>sendUniformMessage</code>, <code>sendTemplateMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+   * </pre>
+   */
+  CODE_41028(41028, "form_id 不正确,或者过期"),
+  /**
+   * <pre>
+   * code 或 template_id 不正确.
+   * 对应操作:<code>code2Session</code>, <code>sendUniformMessage</code>, <code>sendTemplateMessage</code>
+   * 对应地址:
+   * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+   * </pre>
+   */
+  CODE_41029(41029, "请求的参数不正确"),
+  /**
+   * <pre>
+   * form_id 已被使用,或者所传page页面不存在,或者小程序没有发布
+   * 对应操作:<code>sendUniformMessage</coce>, <code>getWXACodeUnlimit</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   *  https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
+   * </pre>
+   */
+  CODE_41030(41030, "请求的参数不正确"),
+  /**
+   * <pre>
+   * 调用分钟频率受限.
+   * 对应操作:<code>getWXACodeUnlimit</code>, <code>sendUniformMessage</code>, <code>sendTemplateMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
+   * </pre>
+   */
+  CODE_45009(45009, "调用分钟频率受限"),
+  /**
+   * <pre>
+   * 频率限制,每个用户每分钟100次.
+   * 对应操作:<code>code2Session</code>
+   * 对应地址:
+   * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
+   * </pre>
+   */
+  CODE_45011(45011, "频率限制,每个用户每分钟100次"),
+  /**
+   * <pre>
+   * 回复时间超过限制.
+   * 对应操作:<code>sendCustomerMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * </pre>
+   */
+  CODE_45015(45015, "回复时间超过限制"),
+  /**
+   * <pre>
+   * 接口调用超过限额, 或生成码个数总和到达最大个数限制.
+   * 对应操作:<code>createWXAQRCode</code>, <code>sendTemplateMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACode.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+   * </pre>
+   */
+  CODE_45029(45029, "接口调用超过限额"),
+  /**
+   * <pre>
+   * 客服接口下行条数超过上限.
+   * 对应操作:<code>sendCustomerMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * </pre>
+   */
+  CODE_45047(45047, "客服接口下行条数超过上限"),
+  /**
+   * <pre>
+   * command字段取值不对
+   * 对应操作:<code>customerTyping</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+   * </pre>
+   */
+  CODE_45072(45072, "command字段取值不对"),
+  /**
+   * <pre>
+   * 下发输入状态,需要之前30秒内跟用户有过消息交互.
+   * 对应操作:<code>customerTyping</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+   */
+  CODE_45080(45080, "下发输入状态,需要之前30秒内跟用户有过消息交互"),
+  /**
+   * <pre>
+   * 已经在输入状态,不可重复下发.
+   * 对应操作:<code>customerTyping</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+   * </pre>
+   */
+  CODE_45081(45081, "已经在输入状态,不可重复下发"),
+  /**
+   * <pre>
+   * API 功能未授权,请确认小程序已获得该接口.
+   * 对应操作:<code>sendCustomerMessage</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+   * </pre>
+   */
+  CODE_48001(48001, "API 功能未授权"),
+  /**
+   * <pre>
+   * 内容含有违法违规内容.
+   * 对应操作:<code>imgSecCheck</code>, <code>msgSecCheck</code>
+   * 对应地址:
+   * POST https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN
+   * POST https://api.weixin.qq.com/wxa/msg_sec_check?access_token=ACCESS_TOKEN
+   * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/sec-check/imgSecCheck.html
+   * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/sec-check/msgSecCheck.html
+   * </pre>
+   */
+  CODE_87014(87014, "内容含有违法违规内容"),
+  /**
+   * 系统繁忙,此时请开发者稍候再试.
+   */
+  CODE_MINUS_1(-1, "系统繁忙,此时请开发者稍候再试"),
+  /**
+   * code 无效.
+   */
+  CODE_40029(40029, "code 无效"),
+  /**
+   * access_token 过期.
+   */
+  CODE_42001(42001, "access_token 过期"),
+  /**
+   * post 数据为空.
+   */
+  CODE_44002(44002, "post 数据为空"),
+  /**
+   * post 数据中参数缺失.
+   */
+  CODE_47001(47001, "post 数据中参数缺失"),
+  /**
+   * 参数 activity_id 错误.
+   */
+  CODE_47501(47501, "参数 activity_id 错误"),
+  /**
+   * 参数 target_state 错误.
+   */
+  CODE_47502(47502, "参数 target_state 错误"),
+  /**
+   * 参数 version_type 错误.
+   */
+  CODE_47503(47503, "参数 version_type 错误"),
+  /**
+   * activity_id 过期.
+   */
+  CODE_47504(47504, "activity_id 过期"),
+  /**
+   * 没有绑定开放平台帐号.
+   */
+  CODE_89002(89002, "没有绑定开放平台帐号"),
+  /**
+   * 订单无效.
+   */
+  CODE_89300(89300, "订单无效"),
+
+  /**
+   * 代小程序实现业务的错误码,部分和小程序业务一致
+   * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Intro.html
+   */
+  CODE_85060(85060, "无效的taskid"),
+
+  CODE_85027(85027, "身份证绑定管理员名额达到上限"),
+
+  CODE_85061(85061, "手机号绑定管理员名额达到上限"),
+
+  CODE_85026(85026, "微信号绑定管理员名额达到上限"),
+
+  CODE_85063(85063, "身份证黑名单"),
+
+  CODE_85062(85062, "手机号黑名单"),
+
+  CODE_85016(85016, "域名数量超过限制"),
+
+  CODE_85017(85017, "没有新增域名,请确认小程序已经添加了域名或该域名是否没有在第三方平台添加"),
+
+  CODE_85018(85018, "域名没有在第三方平台设置"),
+
+  CODE_89019(89019, "业务域名无更改,无需重复设置"),
+
+  CODE_89020(89020, "尚未设置小程序业务域名,请先在第三方平台中设置小程序业务域名后在调用本接口"),
+
+  CODE_89021(89021, "请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名"),
+
+  CODE_89029(89029, "业务域名数量超过限制"),
+
+  CODE_89231(89231, "个人小程序不支持调用 setwebviewdomain 接口"),
+
+  CODE_91001(91001, "不是公众号快速创建的小程序"),
+
+  CODE_91002(91002, "小程序发布后不可改名"),
+
+  CODE_91003(91003, "改名状态不合法"),
+
+  CODE_91004(91004, "昵称不合法"),
+
+  CODE_91005(91005, "昵称 15 天主体保护"),
+
+  CODE_91006(91006, "昵称命中微信号"),
+
+  CODE_91007(91007, "昵称已被占用"),
+
+  CODE_91008(91008, "昵称命中 7 天侵权保护期"),
+
+  CODE_91009(91009, "需要提交材料"),
+
+  CODE_91010(91010, "其他错误"),
+
+  CODE_91011(91011, "查不到昵称修改审核单信息"),
+
+  CODE_91012(91012, "其他错误"),
+
+  CODE_91013(91013, "占用名字过多"),
+
+  CODE_91014(91014, "+号规则 同一类型关联名主体不一致"),
+
+  CODE_91015(91015, "原始名不同类型主体不一致"),
+
+  CODE_91016(91016, "名称占用者 ≥2"),
+
+  CODE_91017(91017, "+号规则 不同类型关联名主体不一致"),
+
+  CODE_40097(40097, "参数错误"),
+
+  CODE_41006(41006, "media_id 不能为空"),
+
+  CODE_46001(46001, "media_id 不存在"),
+
+  CODE_40009(40009, "图片尺寸太大"),
+
+  CODE_53202(53202, "本月头像修改次数已用完"),
+
+  CODE_53200(53200, "本月功能介绍修改次数已用完"),
+
+  CODE_53201(53201, "功能介绍内容命中黑名单关键字"),
+
+  CODE_85083(85083, "搜索标记位被封禁,无法修改"),
+
+  CODE_85084(85084, "非法的 status 值,只能填 0 或者 1"),
+
+  CODE_85013(85013, "无效的自定义配置"),
+
+  CODE_85014(85014, "无效的模版编号"),
+
+  CODE_85043(85043, "模版错误"),
+
+  CODE_85044(85044, "代码包超过大小限制"),
+
+  CODE_85045(85045, "ext_json 有不存在的路径"),
+
+  CODE_85046(85046, "tabBar 中缺少 path"),
+
+  CODE_85047(85047, "pages 字段为空"),
+
+  CODE_85048(85048, "ext_json 解析失败"),
+
+  CODE_80082(80082, "没有权限使用该插件"),
+
+  CODE_80067(80067, "找不到使用的插件"),
+
+  CODE_80066(80066, "非法的插件版本"),
+
+  CODE_86000(86000, "不是由第三方代小程序进行调用"),
+
+  CODE_86001(86001, "不存在第三方的已经提交的代码"),
+
+  CODE_85006(85006, "标签格式错误"),
+
+  CODE_85007(85007, "页面路径错误"),
+
+  CODE_85008(85008, "类目填写错误"),
+
+  CODE_85009(85009, "已经有正在审核的版本"),
+
+  CODE_85010(85010, "item_list 有项目为空"),
+
+  CODE_85011(85011, "标题填写错误"),
+
+  CODE_85023(85023, "审核列表填写的项目数不在 1-5 以内"),
+
+  CODE_85077(85077, "小程序类目信息失效(类目中含有官方下架的类目,请重新选择类目)"),
+
+  CODE_86002(86002, "小程序还未设置昵称、头像、简介。请先设置完后再重新提交"),
+
+  CODE_85085(85085, "近 7 天提交审核的小程序数量过多,请耐心等待审核完毕后再次提交"),
+
+  CODE_85086(85086, "提交代码审核之前需提前上传代码"),
+
+  CODE_85087(85087, "小程序已使用 api navigateToMiniProgram,请声明跳转 appid 列表后再次提交"),
+
+  CODE_85012(85012, "无效的审核 id"),
+
+  CODE_87013(87013, "撤回次数达到上限(每天一次,每个月 10 次)"),
+
+  CODE_85019(85019, "没有审核版本"),
+
+  CODE_85020(85020, "审核状态未满足发布"),
+
+  CODE_87011(87011, "现网已经在灰度发布,不能进行版本回退"),
+
+  CODE_87012(87012, "该版本不能回退,可能的原因:1:无上一个线上版用于回退 2:此版本为已回退版本,不能回退 3:此版本为回退功能上线之前的版本,不能回退"),
+
+  CODE_85079(85079, "小程序没有线上版本,不能进行灰度"),
+
+  CODE_85080(85080, "小程序提交的审核未审核通过"),
+
+  CODE_85081(85081, "无效的发布比例"),
+
+  CODE_85082(85082, "当前的发布比例需要比之前设置的高"),
+
+  CODE_85021(85021, "状态不可变"),
+
+  CODE_85022(85022, "action 非法"),
+
+  CODE_89401(89401, "系统不稳定,请稍后再试,如多次失败请通过社区反馈"),
+
+  CODE_89402(89402, "该审核单不在待审核队列,请检查是否已提交审核或已审完"),
+
+  CODE_89403(89403, "本单属于平台不支持加急种类,请等待正常审核流程"),
+
+  CODE_89404(89404, "本单已加速成功,请勿重复提交"),
+
+  CODE_89405(89405, "本月加急额度不足,请提升提审质量以获取更多额度"),
+
+  CODE_85064(85064, "找不到模版/草稿"),
+
+  CODE_85065(85065, "模版库已满"),
+
+  /**
+   * 小程序订阅消息错误码
+   * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
+   */
+  CODE_43101(43101, "用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系"),
+
+  CODE_47003(47003, "模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错"),
+
+  /**
+   * 小程序绑定体验者
+   */
+  CODE_85001(85001, "微信号不存在或微信号设置为不可搜索"),
+
+  CODE_85002(85002, "小程序绑定的体验者数量达到上限"),
+
+  CODE_85003(85003, "微信号绑定的小程序体验者达到上限"),
+
+  CODE_85004(85004, "微信号已经绑定"),
+
+  /**
+   * 53010
+   * 名称格式不合法
+   */
+  CODE_53010(53010, "名称格式不合法"),
+
+  /**
+   * 53011
+   * 名称检测命中频率限制
+   */
+  CODE_53011(53011, "名称检测命中频率限制"),
+
+  /**
+   * 53012
+   * 禁止使用该名称
+   */
+  CODE_53012(53012, "禁止使用该名称"),
+
+  /**
+   * 53013
+   * 公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复
+   */
+  CODE_53013(53013, "公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复"),
+
+  /**
+   * 53014
+   * 公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}
+   */
+  CODE_53014(53014, "公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"),
+
+  /**
+   * 53015
+   * 公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请
+   */
+  CODE_53015(53015, "公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请"),
+
+  /**
+   * 53016
+   * 公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请
+   */
+  CODE_53016(53016, "公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请"),
+
+  /**
+   * 53017
+   * 公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}
+   */
+  CODE_53017(53017, "公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"),
+
+  /**
+   * 53018
+   * 名称命中微信号
+   */
+  CODE_53018(53018, "名称命中微信号"),
+
+  /**
+   * 53019
+   * 名称在保护期内
+   */
+  CODE_53019(53019, "名称在保护期内"),
+
+  /**
+   * 61070
+   * 法人姓名与微信号不一致 name, wechat name not in accordance
+   */
+  CODE_61070(61070, "法人姓名与微信号不一致"),
+
+  /**
+   * 85015
+   * 该账号不是小程序账号
+   */
+  CODE_85015(85015, "该账号不是小程序账号"),
+
+  /**
+   * 85066
+   * 链接错误
+   */
+  CODE_85066(85066, "链接错误"),
+
+  /**
+   * 85068
+   * 测试链接不是子链接
+   */
+  CODE_85068(85068, "测试链接不是子链接"),
+
+  /**
+   * 85069
+   * 校验文件失败
+   */
+  CODE_85069(85069, "校验文件失败"),
+
+  /**
+   * 85070
+   * 个人类型小程序无法设置二维码规则
+   */
+  CODE_85070(85070, "个人类型小程序无法设置二维码规则"),
+
+  /**
+   * 85071
+   * 已添加该链接,请勿重复添加
+   */
+  CODE_85071(85071, "已添加该链接,请勿重复添加"),
+
+  /**
+   * 85072
+   * 该链接已被占用
+   */
+  CODE_85072(85072, "该链接已被占用"),
+
+  /**
+   * 85073
+   * 二维码规则已满
+   */
+  CODE_85073(85073, "二维码规则已满"),
+
+  /**
+   * 85074
+   * 小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则
+   */
+  CODE_85074(85074, "小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则"),
+
+  /**
+   * 85075
+   * 个人类型小程序无法设置二维码规则
+   */
+  CODE_85075(85075, "个人类型小程序无法设置二维码规则"),
+
+  /**
+   * 86004
+   * 无效微信号 invalid wechat
+   */
+  CODE_86004(86004, "无效微信号"),
+
+  /**
+   * 89247
+   * 内部错误 inner error
+   */
+  CODE_89247(89247, "内部错误"),
+
+  /**
+   * 89248
+   * 企业代码类型无效,请选择正确类型填写 invalid code_type type
+   */
+  CODE_89248(89248, "企业代码类型无效,请选择正确类型填写"),
+
+  /**
+   * 89249
+   * 该主体已有任务执行中,距上次任务 24h 后再试 task running
+   */
+  CODE_89249(89249, "该主体已有任务执行中,距上次任务 24h 后再试"),
+
+  /**
+   * 89250
+   * 未找到该任务 task not found
+   */
+  CODE_89250(89250, "未找到该任务"),
+
+
+  /**
+   * 89251
+   * 待法人人脸核身校验 legal person checking
+   */
+  CODE_89251(89251, "待法人人脸核身校验"),
+
+  /**
+   * 89252
+   * 法人&企业信息一致性校验中 front checking
+   */
+  CODE_89252(89252, "法人&企业信息一致性校验中"),
+
+  /**
+   * 89253
+   * 缺少参数 lack of some params
+   */
+  CODE_89253(89253, "缺少参数s"),
+
+
+  /**
+   * 89254
+   * 第三方权限集不全,补全权限集全网发布后生效 lack of some component rights
+   */
+  CODE_89254(89254, "第三方权限集不全,补全权限集全网发布后生效"),
+
+  /**
+   * 89255
+   * code参数无效,请检查code长度以及内容是否正确 code参数无效,请检查code长度以及内容是否正确_;
+   * 注意code_type的值不同需要传的code长度不一样 ;注意code_type的值不同需要传的code长度不一样 enterprise code_invalid invalid
+   */
+  CODE_89255(89255, "code参数无效,请检查code长度以及内容是否正确_;注意code_type的值不同需要传的code长度不一样 ;注意code_type的值不同需要传的code长度不一样"),
+
+//  CODE_504002(-504002, "云函数未找到 Function not found"),
+  ;
+
+  private final int code;
+  private final String msg;
+
+  WxMaErrorMsgEnum(int code, String msg) {
+    this.code = code;
+    this.msg = msg;
+  }
+
+  /**
+   * 通过错误代码查找其中文含义.
+   */
+  public static String findMsgByCode(int code) {
+    for (WxMaErrorMsgEnum value : WxMaErrorMsgEnum.values()) {
+      if (value.code == code) {
+        return value.msg;
+      }
+    }
+
+    return null;
+  }
+}

+ 667 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxMpErrorMsgEnum.java

@@ -0,0 +1,667 @@
+package cn.nosum.wx.common.error;
+
+import lombok.Getter;
+
+/**
+ * <pre>
+ * 微信公众平台全局返回码.
+ * 参考文档:<a href="http://mp.weixin.qq.com/wiki/10/6380dc743053a91c544ffd2b7c959166.html">公众平台全局返回码</a>
+ * Created by Binary Wang on 2018/5/13.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+@Getter
+public enum WxMpErrorMsgEnum {
+  /**
+   * 系统繁忙,此时请开发者稍候再试.
+   */
+  CODE_1(-1, "系统繁忙,此时请开发者稍候再试"),
+  /**
+   * 请求成功.
+   */
+  CODE_0(0, "请求成功"),
+  /**
+   * 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口.
+   */
+  CODE_40001(40001, "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口"),
+  /**
+   * 不合法的凭证类型.
+   */
+  CODE_40002(40002, "不合法的凭证类型"),
+  /**
+   * 不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID.
+   */
+  CODE_40003(40003, "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID"),
+  /**
+   * 不合法的媒体文件类型.
+   */
+  CODE_40004(40004, "不合法的媒体文件类型"),
+  /**
+   * 不合法的文件类型.
+   */
+  CODE_40005(40005, "不合法的文件类型"),
+  /**
+   * 不合法的文件大小.
+   */
+  CODE_40006(40006, "不合法的文件大小"),
+  /**
+   * 不合法的媒体文件 id.
+   */
+  CODE_40007(40007, "不合法的媒体文件 id"),
+  /**
+   * 不合法的消息类型.
+   */
+  CODE_40008(40008, "不合法的消息类型"),
+  /**
+   * 不合法的图片文件大小.
+   */
+  CODE_40009(40009, "不合法的图片文件大小"),
+  /**
+   * 不合法的语音文件大小.
+   */
+  CODE_40010(40010, "不合法的语音文件大小"),
+  /**
+   * 不合法的视频文件大小.
+   */
+  CODE_40011(40011, "不合法的视频文件大小"),
+  /**
+   * 不合法的缩略图文件大小.
+   */
+  CODE_40012(40012, "不合法的缩略图文件大小"),
+  /**
+   * 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写.
+   */
+  CODE_40013(40013, "不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写"),
+  /**
+   * 不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口.
+   */
+  CODE_40014(40014, "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口"),
+  /**
+   * 不合法的菜单类型.
+   */
+  CODE_40015(40015, "不合法的菜单类型"),
+  /**
+   * 不合法的按钮个数.
+   */
+  CODE_40016(40016, "不合法的按钮个数"),
+  /**
+   * 不合法的按钮类型.
+   */
+  CODE_40017(40017, "不合法的按钮类型"),
+  /**
+   * 不合法的按钮名字长度.
+   */
+  CODE_40018(40018, "不合法的按钮名字长度"),
+  /**
+   * 不合法的按钮 KEY 长度.
+   */
+  CODE_40019(40019, "不合法的按钮 KEY 长度"),
+  /**
+   * 不合法的按钮 URL 长度.
+   */
+  CODE_40020(40020, "不合法的按钮 URL 长度"),
+  /**
+   * 不合法的菜单版本号.
+   */
+  CODE_40021(40021, "不合法的菜单版本号"),
+  /**
+   * 不合法的子菜单级数.
+   */
+  CODE_40022(40022, "不合法的子菜单级数"),
+  /**
+   * 不合法的子菜单按钮个数.
+   */
+  CODE_40023(40023, "不合法的子菜单按钮个数"),
+  /**
+   * 不合法的子菜单按钮类型.
+   */
+  CODE_40024(40024, "不合法的子菜单按钮类型"),
+  /**
+   * 不合法的子菜单按钮名字长度.
+   */
+  CODE_40025(40025, "不合法的子菜单按钮名字长度"),
+  /**
+   * 不合法的子菜单按钮 KEY 长度.
+   */
+  CODE_40026(40026, "不合法的子菜单按钮 KEY 长度"),
+  /**
+   * 不合法的子菜单按钮 URL 长度.
+   */
+  CODE_40027(40027, "不合法的子菜单按钮 URL 长度"),
+  /**
+   * 不合法的自定义菜单使用用户.
+   */
+  CODE_40028(40028, "不合法的自定义菜单使用用户"),
+  /**
+   * 不合法的 oauth_code.
+   */
+  CODE_40029(40029, "不合法的 oauth_code"),
+  /**
+   * 不合法的 refresh_token.
+   */
+  CODE_40030(40030, "不合法的 refresh_token"),
+  /**
+   * 不合法的 openid 列表.
+   */
+  CODE_40031(40031, "不合法的 openid 列表"),
+  /**
+   * 不合法的 openid 列表长度.
+   */
+  CODE_40032(40032, "不合法的 openid 列表长度"),
+  /**
+   * 不合法的请求字符,不能包含\\uxxxx 格式的字符.
+   */
+  CODE_40033(40033, "不合法的请求字符,不能包含\\uxxxx 格式的字符"),
+  /**
+   * 不合法的参数.
+   */
+  CODE_40035(40035, "不合法的参数"),
+  /**
+   * 不合法的请求格式.
+   */
+  CODE_40038(40038, "不合法的请求格式"),
+  /**
+   * 不合法的 URL 长度.
+   */
+  CODE_40039(40039, "不合法的 URL 长度"),
+  /**
+   * 不合法的分组 id.
+   */
+  CODE_40050(40050, "不合法的分组 id"),
+  /**
+   * 分组名字不合法.
+   */
+  CODE_40051(40051, "分组名字不合法"),
+  /**
+   * 删除单篇图文时,指定的 article_idx 不合法.
+   */
+  CODE_40060(40060, "删除单篇图文时,指定的 article_idx 不合法"),
+  /**
+   * 分组名字不合法.
+   */
+  CODE_40117(40117, "分组名字不合法"),
+  /**
+   * media_id 大小不合法.
+   */
+  CODE_40118(40118, "media_id 大小不合法"),
+  /**
+   * button 类型错误.
+   */
+  CODE_40119(40119, "button 类型错误"),
+  /**
+   * button 类型错误.
+   */
+  CODE_40120(40120, "button 类型错误"),
+  /**
+   * 不合法的 media_id 类型.
+   */
+  CODE_40121(40121, "不合法的 media_id 类型"),
+  /**
+   * 微信号不合法.
+   */
+  CODE_40132(40132, "微信号不合法"),
+  /**
+   * 不支持的图片格式.
+   */
+  CODE_40137(40137, "不支持的图片格式"),
+  /**
+   * 请勿添加其他公众号的主页链接.
+   */
+  CODE_40155(40155, "请勿添加其他公众号的主页链接"),
+  /**
+   * 缺少 access_token 参数.
+   */
+  CODE_41001(41001, "缺少 access_token 参数"),
+  /**
+   * 缺少 appid 参数.
+   */
+  CODE_41002(41002, "缺少 appid 参数"),
+  /**
+   * 缺少 refresh_token 参数.
+   */
+  CODE_41003(41003, "缺少 refresh_token 参数"),
+  /**
+   * 缺少 secret 参数.
+   */
+  CODE_41004(41004, "缺少 secret 参数"),
+  /**
+   * 缺少多媒体文件数据.
+   */
+  CODE_41005(41005, "缺少多媒体文件数据"),
+  /**
+   * 缺少 media_id 参数.
+   */
+  CODE_41006(41006, "缺少 media_id 参数"),
+  /**
+   * 缺少子菜单数据.
+   */
+  CODE_41007(41007, "缺少子菜单数据"),
+  /**
+   * 缺少 oauth code.
+   */
+  CODE_41008(41008, "缺少 oauth code"),
+  /**
+   * 缺少 openid.
+   */
+  CODE_41009(41009, "缺少 openid"),
+  /**
+   * access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明.
+   */
+  CODE_42001(42001, "access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明"),
+  /**
+   * refresh_token 超时.
+   */
+  CODE_42002(42002, "refresh_token 超时"),
+  /**
+   * oauth_code 超时.
+   */
+  CODE_42003(42003, "oauth_code 超时"),
+  /**
+   * 用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权.
+   */
+  CODE_42007(42007, "用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权"),
+  /**
+   * 需要 GET 请求.
+   */
+  CODE_43001(43001, "需要 GET 请求"),
+  /**
+   * 需要 POST 请求.
+   */
+  CODE_43002(43002, "需要 POST 请求"),
+  /**
+   * 需要 HTTPS 请求.
+   */
+  CODE_43003(43003, "需要 HTTPS 请求"),
+  /**
+   * 需要接收者关注.
+   */
+  CODE_43004(43004, "需要接收者关注"),
+  /**
+   * 需要好友关系.
+   */
+  CODE_43005(43005, "需要好友关系"),
+  /**
+   * 需要将接收者从黑名单中移除.
+   */
+  CODE_43019(43019, "需要将接收者从黑名单中移除"),
+  /**
+   * 多媒体文件为空.
+   */
+  CODE_44001(44001, "多媒体文件为空"),
+  /**
+   * POST 的数据包为空.
+   */
+  CODE_44002(44002, "POST 的数据包为空"),
+  /**
+   * 图文消息内容为空.
+   */
+  CODE_44003(44003, "图文消息内容为空"),
+  /**
+   * 文本消息内容为空.
+   */
+  CODE_44004(44004, "文本消息内容为空"),
+  /**
+   * 多媒体文件大小超过限制.
+   */
+  CODE_45001(45001, "多媒体文件大小超过限制"),
+  /**
+   * 消息内容超过限制.
+   */
+  CODE_45002(45002, "消息内容超过限制"),
+  /**
+   * 标题字段超过限制.
+   */
+  CODE_45003(45003, "标题字段超过限制"),
+  /**
+   * 描述字段超过限制.
+   */
+  CODE_45004(45004, "描述字段超过限制"),
+  /**
+   * 链接字段超过限制.
+   */
+  CODE_45005(45005, "链接字段超过限制"),
+  /**
+   * 图片链接字段超过限制.
+   */
+  CODE_45006(45006, "图片链接字段超过限制"),
+  /**
+   * 语音播放时间超过限制.
+   */
+  CODE_45007(45007, "语音播放时间超过限制"),
+  /**
+   * 图文消息超过限制.
+   */
+  CODE_45008(45008, "图文消息超过限制"),
+  /**
+   * 接口调用超过限制.
+   */
+  CODE_45009(45009, "接口调用超过限制"),
+  /**
+   * 创建菜单个数超过限制.
+   */
+  CODE_45010(45010, "创建菜单个数超过限制"),
+  /**
+   * API 调用太频繁,请稍候再试.
+   */
+  CODE_45011(45011, "API 调用太频繁,请稍候再试"),
+  /**
+   * 回复时间超过限制.
+   */
+  CODE_45015(45015, "回复时间超过限制"),
+  /**
+   * 系统分组,不允许修改.
+   */
+  CODE_45016(45016, "系统分组,不允许修改"),
+  /**
+   * 分组名字过长.
+   */
+  CODE_45017(45017, "分组名字过长"),
+  /**
+   * 分组数量超过上限.
+   */
+  CODE_45018(45018, "分组数量超过上限"),
+  /**
+   * 客服接口下行条数超过上限.
+   */
+  CODE_45047(45047, "客服接口下行条数超过上限"),
+  /**
+   * 非法的tag_id.
+   */
+  CODE_45159(45159, "非法的tag_id"),
+  /**
+   * 不存在媒体数据.
+   */
+  CODE_46001(46001, "不存在媒体数据"),
+  /**
+   * 不存在的菜单版本.
+   */
+  CODE_46002(46002, "不存在的菜单版本"),
+  /**
+   * 不存在的菜单数据.
+   */
+  CODE_46003(46003, "不存在的菜单数据"),
+  /**
+   * 不存在的用户.
+   */
+  CODE_46004(46004, "不存在的用户"),
+  /**
+   * 解析 JSON/XML 内容错误.
+   */
+  CODE_47001(47001, "解析 JSON/XML 内容错误"),
+  /**
+   * api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限.
+   */
+  CODE_48001(48001, "api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限"),
+  /**
+   * 粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” ).
+   */
+  CODE_48002(48002, "粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” )"),
+  /**
+   * api 接口被封禁,请登录 mp.weixin.qq.com 查看详情.
+   */
+  CODE_48004(48004, "api 接口被封禁,请登录 mp.weixin.qq.com 查看详情"),
+  /**
+   * api 禁止删除被自动回复和自定义菜单引用的素材.
+   */
+  CODE_48005(48005, "api 禁止删除被自动回复和自定义菜单引用的素材"),
+  /**
+   * api 禁止清零调用次数,因为清零次数达到上限.
+   */
+  CODE_48006(48006, "api 禁止清零调用次数,因为清零次数达到上限"),
+  /**
+   * 没有该类型消息的发送权限.
+   */
+  CODE_48008(48008, "没有该类型消息的发送权限"),
+  /**
+   * 用户未授权该 api.
+   */
+  CODE_50001(50001, "用户未授权该 api"),
+  /**
+   * 用户受限,可能是违规后接口被封禁.
+   */
+  CODE_50002(50002, "用户受限,可能是违规后接口被封禁"),
+  /**
+   * 用户未关注公众号.
+   */
+  CODE_50005(50005, "用户未关注公众号"),
+  /**
+   * 参数错误 (invalid parameter).
+   */
+  CODE_61451(61451, "参数错误 (invalid parameter)"),
+  /**
+   * 无效客服账号 (invalid kf_account).
+   */
+  CODE_61452(61452, "无效客服账号 (invalid kf_account)"),
+  /**
+   * 客服帐号已存在 (kf_account exsited).
+   */
+  CODE_61453(61453, "客服帐号已存在 (kf_account exsited)"),
+  /**
+   * 客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length).
+   */
+  CODE_61454(61454, "客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length)"),
+  /**
+   * 客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account).
+   */
+  CODE_61455(61455, "客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account)"),
+  /**
+   * 客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded).
+   */
+  CODE_61456(61456, "客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded)"),
+  /**
+   * 无效头像文件类型 (invalid file type).
+   */
+  CODE_61457(61457, "无效头像文件类型 (invalid file type)"),
+  /**
+   * 系统错误 (system error).
+   */
+  CODE_61450(61450, "系统错误 (system error)"),
+  /**
+   * 日期格式错误.
+   */
+  CODE_61500(61500, "日期格式错误"),
+  /**
+   * 不存在此 menuid 对应的个性化菜单.
+   */
+  CODE_65301(65301, "不存在此 menuid 对应的个性化菜单"),
+  /**
+   * 没有相应的用户.
+   */
+  CODE_65302(65302, "没有相应的用户"),
+  /**
+   * 没有默认菜单,不能创建个性化菜单.
+   */
+  CODE_65303(65303, "没有默认菜单,不能创建个性化菜单"),
+  /**
+   * MatchRule 信息为空.
+   */
+  CODE_65304(65304, "MatchRule 信息为空"),
+  /**
+   * 个性化菜单数量受限.
+   */
+  CODE_65305(65305, "个性化菜单数量受限"),
+  /**
+   * 不支持个性化菜单的帐号.
+   */
+  CODE_65306(65306, "不支持个性化菜单的帐号"),
+  /**
+   * 个性化菜单信息为空.
+   */
+  CODE_65307(65307, "个性化菜单信息为空"),
+  /**
+   * 包含没有响应类型的 button.
+   */
+  CODE_65308(65308, "包含没有响应类型的 button"),
+  /**
+   * 个性化菜单开关处于关闭状态.
+   */
+  CODE_65309(65309, "个性化菜单开关处于关闭状态"),
+  /**
+   * 填写了省份或城市信息,国家信息不能为空.
+   */
+  CODE_65310(65310, "填写了省份或城市信息,国家信息不能为空"),
+  /**
+   * 填写了城市信息,省份信息不能为空.
+   */
+  CODE_65311(65311, "填写了城市信息,省份信息不能为空"),
+  /**
+   * 不合法的国家信息.
+   */
+  CODE_65312(65312, "不合法的国家信息"),
+  /**
+   * 不合法的省份信息.
+   */
+  CODE_65313(65313, "不合法的省份信息"),
+  /**
+   * 不合法的城市信息.
+   */
+  CODE_65314(65314, "不合法的城市信息"),
+  /**
+   * 该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接).
+   */
+  CODE_65316(65316, "该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接)"),
+  /**
+   * 不合法的 URL.
+   */
+  CODE_65317(65317, "不合法的 URL"),
+  /**
+   * POST 数据参数不合法.
+   */
+  CODE_9001001(9001001, "POST 数据参数不合法"),
+  /**
+   * 远端服务不可用.
+   */
+  CODE_9001002(9001002, "远端服务不可用"),
+  /**
+   * Ticket 不合法.
+   */
+  CODE_9001003(9001003, "Ticket 不合法"),
+  /**
+   * 获取摇周边用户信息失败.
+   */
+  CODE_9001004(9001004, "获取摇周边用户信息失败"),
+  /**
+   * 获取商户信息失败.
+   */
+  CODE_9001005(9001005, "获取商户信息失败"),
+  /**
+   * 获取 OpenID 失败.
+   */
+  CODE_9001006(9001006, "获取 OpenID 失败"),
+  /**
+   * 上传文件缺失.
+   */
+  CODE_9001007(9001007, "上传文件缺失"),
+  /**
+   * 上传素材的文件类型不合法.
+   */
+  CODE_9001008(9001008, "上传素材的文件类型不合法"),
+  /**
+   * 上传素材的文件尺寸不合法.
+   */
+  CODE_9001009(9001009, "上传素材的文件尺寸不合法"),
+  /**
+   * 上传失败.
+   */
+  CODE_9001010(9001010, "上传失败"),
+  /**
+   * 帐号不合法.
+   */
+  CODE_9001020(9001020, "帐号不合法"),
+  /**
+   * 已有设备激活率低于 50% ,不能新增设备.
+   */
+  CODE_9001021(9001021, "已有设备激活率低于 50% ,不能新增设备"),
+  /**
+   * 设备申请数不合法,必须为大于 0 的数字.
+   */
+  CODE_9001022(9001022, "设备申请数不合法,必须为大于 0 的数字"),
+  /**
+   * 已存在审核中的设备 ID 申请.
+   */
+  CODE_9001023(9001023, "已存在审核中的设备 ID 申请"),
+  /**
+   * 一次查询设备 ID 数量不能超过 50.
+   */
+  CODE_9001024(9001024, "一次查询设备 ID 数量不能超过 50"),
+  /**
+   * 设备 ID 不合法.
+   */
+  CODE_9001025(9001025, "设备 ID 不合法"),
+  /**
+   * 页面 ID 不合法.
+   */
+  CODE_9001026(9001026, "页面 ID 不合法"),
+  /**
+   * 页面参数不合法.
+   */
+  CODE_9001027(9001027, "页面参数不合法"),
+  /**
+   * 一次删除页面 ID 数量不能超过 10.
+   */
+  CODE_9001028(9001028, "一次删除页面 ID 数量不能超过 10"),
+  /**
+   * 页面已应用在设备中,请先解除应用关系再删除.
+   */
+  CODE_9001029(9001029, "页面已应用在设备中,请先解除应用关系再删除"),
+  /**
+   * 一次查询页面 ID 数量不能超过 50.
+   */
+  CODE_9001030(9001030, "一次查询页面 ID 数量不能超过 50"),
+  /**
+   * 时间区间不合法.
+   */
+  CODE_9001031(9001031, "时间区间不合法"),
+  /**
+   * 保存设备与页面的绑定关系参数错误.
+   */
+  CODE_9001032(9001032, "保存设备与页面的绑定关系参数错误"),
+  /**
+   * 门店 ID 不合法.
+   */
+  CODE_9001033(9001033, "门店 ID 不合法"),
+  /**
+   * 设备备注信息过长.
+   */
+  CODE_9001034(9001034, "设备备注信息过长"),
+  /**
+   * 设备申请参数不合法.
+   */
+  CODE_9001035(9001035, "设备申请参数不合法"),
+  /**
+   * 查询起始值 begin 不合法.
+   */
+  CODE_9001036(9001036, "查询起始值 begin 不合法"),
+
+  /**
+   * 设置的 speed 参数不在0到4的范围内
+   */
+  CODE_45083(45083, "设置的 speed 参数不在0到4的范围内"),
+
+  /**
+   * 没有设置 speed 参数
+   */
+  CODE_45084(45084, "没有设置 speed 参数");
+
+  private int code;
+  private String msg;
+
+  WxMpErrorMsgEnum(int code, String msg) {
+    this.code = code;
+    this.msg = msg;
+  }
+
+  /**
+   * 通过错误代码查找其中文含义..
+   */
+  public static String findMsgByCode(int code) {
+    for (WxMpErrorMsgEnum value : WxMpErrorMsgEnum.values()) {
+      if (value.code == code) {
+        return value.msg;
+      }
+    }
+
+    return null;
+  }
+}

+ 23 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/error/WxRuntimeException.java

@@ -0,0 +1,23 @@
+package cn.nosum.wx.common.error;
+
+/**
+ * WxJava专用的runtime exception.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-09-26
+ */
+public class WxRuntimeException extends RuntimeException {
+  private static final long serialVersionUID = 4881698471192264412L;
+
+  public WxRuntimeException(Throwable e) {
+    super(e);
+  }
+
+  public WxRuntimeException(String msg) {
+    super(msg);
+  }
+
+  public WxRuntimeException(String msg, Throwable e) {
+    super(msg, e);
+  }
+}

+ 63 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/service/WxService.java

@@ -0,0 +1,63 @@
+package cn.nosum.wx.common.service;
+
+import cn.nosum.wx.common.entity.ToJson;
+import cn.nosum.wx.common.error.WxErrorException;
+import com.google.gson.JsonObject;
+
+/**
+ * 微信服务接口.
+ *
+ * @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;
+
+  /**
+   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+   *
+   * @param url        请求接口地址
+   * @param jsonObject 请求对象
+   * @return 接口响应字符串
+   * @throws WxErrorException 异常
+   */
+  String post(String url, JsonObject jsonObject) throws WxErrorException;
+
+  /**
+   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+   *
+   * @param url 请求接口地址
+   * @param obj 请求对象,实现了ToJson接口
+   * @return 接口响应字符串
+   * @throws WxErrorException 异常
+   */
+  String post(String url, ToJson obj) throws WxErrorException;
+}

+ 24 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/DataUtils.java

@@ -0,0 +1,24 @@
+package cn.nosum.wx.common.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * <pre>
+ *  数据处理工具类
+ *  Created by BinaryWang on 2018/5/8.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public class DataUtils {
+  /**
+   * 将数据中包含的secret字符使用星号替换,防止日志打印时被输出
+   */
+  public static <E> E handleDataWithSecret(E data) {
+    E dataForLog = data;
+    if(data instanceof String && StringUtils.contains((String)data, "&secret=")){
+      dataForLog = (E) StringUtils.replaceAll((String)data,"&secret=\\w+&","&secret=******&");
+    }
+    return dataForLog;
+  }
+}

+ 17 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/RandomUtils.java

@@ -0,0 +1,17 @@
+package cn.nosum.wx.common.utils;
+
+public class RandomUtils {
+
+  private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+  private static final java.util.Random RANDOM = new java.util.Random();
+
+  public static String getRandomStr() {
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < 16; i++) {
+      sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length())));
+    }
+    return sb.toString();
+  }
+
+}

+ 102 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/XmlUtils.java

@@ -0,0 +1,102 @@
+package cn.nosum.wx.common.utils;
+
+import cn.nosum.wx.common.error.WxRuntimeException;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.dom4j.*;
+import org.dom4j.io.SAXReader;
+import org.dom4j.tree.DefaultText;
+import org.xml.sax.SAXException;
+
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <pre>
+ * XML转换工具类.
+ * Created by Binary Wang on 2018/11/4.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public class XmlUtils {
+
+  public static Map<String, Object> xml2Map(String xmlString) {
+    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));
+      }
+    } catch (DocumentException | SAXException e) {
+      throw new WxRuntimeException(e);
+    }
+
+    return map;
+  }
+
+  private static Object element2MapOrString(Element element) {
+
+    final List<Node> content = element.content();
+    final Set<String> names = names(content);
+
+    // 判断节点下有无非文本节点(非Text和CDATA),如无,直接取Text文本内容
+    if (names.size() < 1) {
+      return element.getText();
+    }
+
+    Map<String, Object> result = Maps.newHashMap();
+    if (names.size() == 1) {
+      // 说明是个列表,各个子对象是相同的name
+      List<Object> list = Lists.newArrayList();
+      for (Node node : content) {
+        if (node instanceof DefaultText) {
+          continue;
+        }
+
+        if (node instanceof Element) {
+          list.add(element2MapOrString((Element) node));
+        }
+      }
+
+      result.put(names.iterator().next(), list);
+    } else {
+      for (Node node : content) {
+        if (node instanceof DefaultText) {
+          continue;
+        }
+
+        if (node instanceof Element) {
+          result.put(node.getName(), element2MapOrString((Element) node));
+        }
+      }
+    }
+
+    return result;
+  }
+
+  private static Set<String> names(List<Node> nodes) {
+    Set<String> names = Sets.newHashSet();
+    for (Node node : nodes) {
+      // 如果节点类型是Text或CDATA跳过
+      if (node instanceof DefaultText || node instanceof CDATA) {
+        continue;
+      }
+      names.add(node.getName());
+    }
+
+    return names;
+  }
+}

+ 26 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/ByteGroup.java

@@ -0,0 +1,26 @@
+package cn.nosum.wx.common.utils.crypto;
+
+import java.util.ArrayList;
+
+public class ByteGroup {
+  ArrayList<Byte> byteContainer = new ArrayList<>();
+
+  public byte[] toBytes() {
+    byte[] bytes = new byte[this.byteContainer.size()];
+    for (int i = 0; i < this.byteContainer.size(); i++) {
+      bytes[i] = this.byteContainer.get(i);
+    }
+    return bytes;
+  }
+
+  public ByteGroup addBytes(byte[] bytes) {
+    for (byte b : bytes) {
+      this.byteContainer.add(b);
+    }
+    return this;
+  }
+
+  public int size() {
+    return this.byteContainer.size();
+  }
+}

+ 65 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/PKCS7Encoder.java

@@ -0,0 +1,65 @@
+/*
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+package cn.nosum.wx.common.utils.crypto;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解.
+ *
+ * @author tencent
+ */
+public class PKCS7Encoder {
+  private static final Charset CHARSET = StandardCharsets.UTF_8;
+  private static final int BLOCK_SIZE = 32;
+
+  /**
+   * 获得对明文进行补位填充的字节.
+   *
+   * @param count 需要进行填充补位操作的明文字节个数
+   * @return 补齐用的字节数组
+   */
+  public static byte[] encode(int count) {
+    // 计算需要填充的位数
+    int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+    // 获得补位所用的字符
+    char padChr = chr(amountToPad);
+    StringBuilder tmp = new StringBuilder();
+    for (int index = 0; index < amountToPad; index++) {
+      tmp.append(padChr);
+    }
+    return tmp.toString().getBytes(CHARSET);
+  }
+
+  /**
+   * 删除解密后明文的补位字符.
+   *
+   * @param decrypted 解密后的明文
+   * @return 删除补位字符后的明文
+   */
+  public static byte[] decode(byte[] decrypted) {
+    int pad = decrypted[decrypted.length - 1];
+    if (pad < 1 || pad > 32) {
+      pad = 0;
+    }
+    return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+  }
+
+  /**
+   * 将数字转化成ASCII码对应的字符,用于对明文进行补码.
+   *
+   * @param a 需要转化的数字
+   * @return 转化得到的字符
+   */
+  private static char chr(int a) {
+    byte target = (byte) (a & 0xFF);
+    return (char) target;
+  }
+
+}

+ 50 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/SHA1.java

@@ -0,0 +1,50 @@
+package cn.nosum.wx.common.utils.crypto;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ *
+ * @author Daniel Qian
+ * @date 14/10/19
+ */
+public class SHA1 {
+
+  /**
+   * 串接arr参数,生成sha1 digest.
+   */
+  public static String gen(String... arr) {
+    if (StringUtils.isAnyEmpty(arr)) {
+      throw new IllegalArgumentException("非法请求参数,有部分参数为空 : " + Arrays.toString(arr));
+    }
+
+    Arrays.sort(arr);
+    StringBuilder sb = new StringBuilder();
+    for (String a : arr) {
+      sb.append(a);
+    }
+    return DigestUtils.sha1Hex(sb.toString());
+  }
+
+  /**
+   * 用&串接arr参数,生成sha1 digest.
+   */
+  public static String genWithAmple(String... arr) {
+    if (StringUtils.isAnyEmpty(arr)) {
+      throw new IllegalArgumentException("非法请求参数,有部分参数为空 : " + Arrays.toString(arr));
+    }
+
+    Arrays.sort(arr);
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < arr.length; i++) {
+      String a = arr[i];
+      sb.append(a);
+      if (i != arr.length - 1) {
+        sb.append('&');
+      }
+    }
+    return DigestUtils.sha1Hex(sb.toString());
+  }
+}

+ 281 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/crypto/WxCryptUtil.java

@@ -0,0 +1,281 @@
+package cn.nosum.wx.common.utils.crypto;
+
+import cn.nosum.wx.common.error.WxRuntimeException;
+import com.google.common.base.CharMatcher;
+import org.apache.commons.codec.binary.Base64;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * <pre>
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ * Copyright (c) 1998-2014 Tencent Inc.
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ * </pre>
+ *
+ * @author Tencent
+ */
+public class WxCryptUtil {
+
+  private static final Base64 BASE64 = new Base64();
+  private static final Charset CHARSET = StandardCharsets.UTF_8;
+
+  private static final ThreadLocal<DocumentBuilder> BUILDER_LOCAL = ThreadLocal.withInitial(() -> {
+    try {
+      final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      factory.setExpandEntityReferences(false);
+      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+      return factory.newDocumentBuilder();
+    } catch (ParserConfigurationException exc) {
+      throw new IllegalArgumentException(exc);
+    }
+  });
+
+  protected byte[] aesKey;
+  protected String token;
+  protected String appidOrCorpid;
+
+  public WxCryptUtil() {
+  }
+
+  /**
+   * 构造函数.
+   *
+   * @param token          公众平台上,开发者设置的token
+   * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+   * @param appidOrCorpid  公众平台appid/corpid
+   */
+  public WxCryptUtil(String token, String encodingAesKey, String appidOrCorpid) {
+    this.token = token;
+    this.appidOrCorpid = appidOrCorpid;
+    this.aesKey = Base64.decodeBase64(CharMatcher.whitespace().removeFrom(encodingAesKey));
+  }
+
+  private static String extractEncryptPart(String xml) {
+    try {
+      DocumentBuilder db = BUILDER_LOCAL.get();
+      Document document = db.parse(new InputSource(new StringReader(xml)));
+
+      Element root = document.getDocumentElement();
+      return root.getElementsByTagName("Encrypt").item(0).getTextContent();
+    } catch (Exception e) {
+      throw new WxRuntimeException(e);
+    }
+  }
+
+  /**
+   * 将一个数字转换成生成4个字节的网络字节序bytes数组.
+   */
+  private static byte[] number2BytesInNetworkOrder(int number) {
+    byte[] orderBytes = new byte[4];
+    orderBytes[3] = (byte) (number & 0xFF);
+    orderBytes[2] = (byte) (number >> 8 & 0xFF);
+    orderBytes[1] = (byte) (number >> 16 & 0xFF);
+    orderBytes[0] = (byte) (number >> 24 & 0xFF);
+    return orderBytes;
+  }
+
+  /**
+   * 4个字节的网络字节序bytes数组还原成一个数字.
+   */
+  private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
+    int sourceNumber = 0;
+    for (int i = 0; i < 4; i++) {
+      sourceNumber <<= 8;
+      sourceNumber |= bytesInNetworkOrder[i] & 0xff;
+    }
+    return sourceNumber;
+  }
+
+  /**
+   * 随机生成16位字符串.
+   */
+  private static String genRandomStr() {
+    String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    Random random = new Random();
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < 16; i++) {
+      int number = random.nextInt(base.length());
+      sb.append(base.charAt(number));
+    }
+    return sb.toString();
+  }
+
+  /**
+   * 生成xml消息.
+   *
+   * @param encrypt   加密后的消息密文
+   * @param signature 安全签名
+   * @param timestamp 时间戳
+   * @param nonce     随机字符串
+   * @return 生成的xml字符串
+   */
+  private static String generateXml(String encrypt, String signature, String timestamp, String nonce) {
+    String format = "<xml>\n" + "<Encrypt><![CDATA[%1$s]]></Encrypt>\n"
+      + "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n"
+      + "<TimeStamp>%3$s</TimeStamp>\n" + "<Nonce><![CDATA[%4$s]]></Nonce>\n"
+      + "</xml>";
+    return String.format(format, encrypt, signature, timestamp, nonce);
+  }
+
+  /**
+   * 将公众平台回复用户的消息加密打包.
+   * <ol>
+   * <li>对要发送的消息进行AES-CBC加密</li>
+   * <li>生成安全签名</li>
+   * <li>将消息密文和安全签名打包成xml格式</li>
+   * </ol>
+   *
+   * @param plainText 公众平台待回复用户的消息,xml格式的字符串
+   * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
+   */
+  public String encrypt(String plainText) {
+    // 加密
+    String encryptedXml = encrypt(genRandomStr(), plainText);
+
+    // 生成安全签名
+    String timeStamp = Long.toString(System.currentTimeMillis() / 1000L);
+    String nonce = genRandomStr();
+
+    String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedXml);
+    return generateXml(encryptedXml, signature, timeStamp, nonce);
+  }
+
+  /**
+   * 对明文进行加密.
+   *
+   * @param plainText 需要加密的明文
+   * @return 加密后base64编码的字符串
+   */
+  public String encrypt(String randomStr, String plainText) {
+    ByteGroup byteCollector = new ByteGroup();
+    byte[] randomStringBytes = randomStr.getBytes(CHARSET);
+    byte[] plainTextBytes = plainText.getBytes(CHARSET);
+    byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
+    byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET);
+
+    // randomStr + networkBytesOrder + text + appid
+    byteCollector.addBytes(randomStringBytes);
+    byteCollector.addBytes(bytesOfSizeInNetworkOrder);
+    byteCollector.addBytes(plainTextBytes);
+    byteCollector.addBytes(appIdBytes);
+
+    // ... + pad: 使用自定义的填充方式对明文进行补位填充
+    byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
+    byteCollector.addBytes(padBytes);
+
+    // 获得最终的字节流, 未加密
+    byte[] unencrypted = byteCollector.toBytes();
+
+    try {
+      // 设置加密模式为AES的CBC模式
+      Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+      SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES");
+      IvParameterSpec iv = new IvParameterSpec(this.aesKey, 0, 16);
+      cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+
+      // 加密
+      byte[] encrypted = cipher.doFinal(unencrypted);
+
+      // 使用BASE64对加密后的字符串进行编码
+      return BASE64.encodeToString(encrypted);
+    } catch (Exception e) {
+      throw new WxRuntimeException(e);
+    }
+  }
+
+  /**
+   * 检验消息的真实性,并且获取解密后的明文.
+   * <ol>
+   * <li>利用收到的密文生成安全签名,进行签名验证</li>
+   * <li>若验证通过,则提取xml中的加密消息</li>
+   * <li>对消息进行解密</li>
+   * </ol>
+   *
+   * @param msgSignature 签名串,对应URL参数的msg_signature
+   * @param timeStamp    时间戳,对应URL参数的timestamp
+   * @param nonce        随机串,对应URL参数的nonce
+   * @param encryptedXml 密文,对应POST请求的数据
+   * @return 解密后的原文
+   */
+  public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) {
+    // 密钥,公众账号的app corpSecret
+    // 提取密文
+    String cipherText = extractEncryptPart(encryptedXml);
+
+    // 验证安全签名
+    String signature = SHA1.gen(this.token, timeStamp, nonce, cipherText);
+    if (!signature.equals(msgSignature)) {
+      throw new WxRuntimeException("加密消息签名校验失败");
+    }
+
+    // 解密
+    return decrypt(cipherText);
+  }
+
+  /**
+   * 对密文进行解密.
+   *
+   * @param cipherText 需要解密的密文
+   * @return 解密得到的明文
+   */
+  public String decrypt(String cipherText) {
+    byte[] original;
+    try {
+      // 设置解密模式为AES的CBC模式
+      Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+      SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES");
+      IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(this.aesKey, 0, 16));
+      cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
+
+      // 使用BASE64对密文进行解码
+      byte[] encrypted = Base64.decodeBase64(cipherText);
+
+      // 解密
+      original = cipher.doFinal(encrypted);
+    } catch (Exception e) {
+      throw new WxRuntimeException(e);
+    }
+
+    String xmlContent;
+    String fromAppid;
+    try {
+      // 去除补位字符
+      byte[] bytes = PKCS7Encoder.decode(original);
+
+      // 分离16位随机字符串,网络字节序和AppId
+      byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+      int xmlLength = bytesNetworkOrder2Number(networkOrder);
+
+      xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
+      fromAppid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
+    } catch (Exception e) {
+      throw new WxRuntimeException(e);
+    }
+
+    // appid不相同的情况 暂时忽略这段判断
+//    if (!fromAppid.equals(this.appidOrCorpid)) {
+//      throw new WxRuntimeException("AppID不正确,请核实!");
+//    }
+
+    return xmlContent;
+
+  }
+
+}

+ 205 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/GsonHelper.java

@@ -0,0 +1,205 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.error.WxRuntimeException;
+import com.google.common.collect.Lists;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.util.List;
+
+public class GsonHelper {
+
+  public static boolean isNull(JsonElement element) {
+    return element == null || element.isJsonNull();
+  }
+
+  public static boolean isNotNull(JsonElement element) {
+    return !isNull(element);
+  }
+
+  public static Long getLong(JsonObject json, String property) {
+    return getAsLong(json.get(property));
+  }
+
+  public static long getPrimitiveLong(JsonObject json, String property) {
+    return getAsPrimitiveLong(json.get(property));
+  }
+
+  public static Integer getInteger(JsonObject json, String property) {
+    return getAsInteger(json.get(property));
+  }
+
+  public static int getPrimitiveInteger(JsonObject json, String property) {
+    return getAsPrimitiveInt(json.get(property));
+  }
+
+  public static Double getDouble(JsonObject json, String property) {
+    return getAsDouble(json.get(property));
+  }
+
+  public static double getPrimitiveDouble(JsonObject json, String property) {
+    return getAsPrimitiveDouble(json.get(property));
+  }
+
+  public static Float getFloat(JsonObject json, String property) {
+    return getAsFloat(json.get(property));
+  }
+
+  public static float getPrimitiveFloat(JsonObject json, String property) {
+    return getAsPrimitiveFloat(json.get(property));
+  }
+
+  public static Boolean getBoolean(JsonObject json, String property) {
+    return getAsBoolean(json.get(property));
+  }
+
+  public static String getString(JsonObject json, String property) {
+    return getAsString(json.get(property));
+  }
+
+  public static String getAsString(JsonElement element) {
+    return isNull(element) ? null : element.getAsString();
+  }
+
+  public static Long getAsLong(JsonElement element) {
+    return isNull(element) ? null : element.getAsLong();
+  }
+
+  public static long getAsPrimitiveLong(JsonElement element) {
+    Long r = getAsLong(element);
+    return r == null ? 0L : r;
+  }
+
+  public static Integer getAsInteger(JsonElement element) {
+    return isNull(element) ? null : element.getAsInt();
+  }
+
+  public static int getAsPrimitiveInt(JsonElement element) {
+    Integer r = getAsInteger(element);
+    return r == null ? 0 : r;
+  }
+
+  public static Boolean getAsBoolean(JsonElement element) {
+    return isNull(element) ? null : element.getAsBoolean();
+  }
+
+  public static boolean getAsPrimitiveBool(JsonElement element) {
+    Boolean r = getAsBoolean(element);
+    return r != null && r;
+  }
+
+  public static Double getAsDouble(JsonElement element) {
+    return isNull(element) ? null : element.getAsDouble();
+  }
+
+  public static double getAsPrimitiveDouble(JsonElement element) {
+    Double r = getAsDouble(element);
+    return r == null ? 0d : r;
+  }
+
+  public static Float getAsFloat(JsonElement element) {
+    return isNull(element) ? null : element.getAsFloat();
+  }
+
+  public static float getAsPrimitiveFloat(JsonElement element) {
+    Float r = getAsFloat(element);
+    return r == null ? 0f : r;
+  }
+
+  public static Integer[] getIntArray(JsonObject o, String string) {
+    JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+    if (jsonArray == null) {
+      return null;
+    }
+
+    List<Integer> result = Lists.newArrayList();
+    for (int i = 0; i < jsonArray.size(); i++) {
+      result.add(jsonArray.get(i).getAsInt());
+    }
+
+    return result.toArray(new Integer[0]);
+  }
+
+  public static String[] getStringArray(JsonObject o, String string) {
+    JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+    if (jsonArray == null) {
+      return null;
+    }
+
+    List<String> result = Lists.newArrayList();
+    for (int i = 0; i < jsonArray.size(); i++) {
+      result.add(jsonArray.get(i).getAsString());
+    }
+
+    return result.toArray(new String[0]);
+  }
+
+  public static Long[] getLongArray(JsonObject o, String string) {
+    JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+    if (jsonArray == null) {
+      return null;
+    }
+
+    List<Long> result = Lists.newArrayList();
+    for (int i = 0; i < jsonArray.size(); i++) {
+      result.add(jsonArray.get(i).getAsLong());
+    }
+
+    return result.toArray(new Long[0]);
+  }
+
+  public static JsonArray getAsJsonArray(JsonElement element) {
+    return element == null ? null : element.getAsJsonArray();
+  }
+
+  /**
+   * 快速构建JsonObject对象,批量添加一堆属性
+   *
+   * @param keyOrValue 包含key或value的数组
+   * @return JsonObject对象.
+   */
+  public static JsonObject buildJsonObject(Object... keyOrValue) {
+    JsonObject result = new JsonObject();
+    put(result, keyOrValue);
+    return result;
+  }
+
+  /**
+   * 批量向JsonObject对象中添加属性
+   *
+   * @param jsonObject 原始JsonObject对象
+   * @param keyOrValue 包含key或value的数组
+   */
+  public static void put(JsonObject jsonObject, Object... keyOrValue) {
+    if (keyOrValue.length % 2 == 1) {
+      throw new WxRuntimeException("参数个数必须为偶数");
+    }
+
+    for (int i = 0; i < keyOrValue.length / 2; i++) {
+      final Object key = keyOrValue[2 * i];
+      final Object value = keyOrValue[2 * i + 1];
+      if (value == null) {
+        jsonObject.add(key.toString(), null);
+        continue;
+      }
+
+      if (value instanceof Boolean) {
+        jsonObject.addProperty(key.toString(), (Boolean) value);
+      } else if (value instanceof Character) {
+        jsonObject.addProperty(key.toString(), (Character) value);
+      } else if (value instanceof Number) {
+        jsonObject.addProperty(key.toString(), (Number) value);
+      } else if (value instanceof JsonElement) {
+        jsonObject.add(key.toString(), (JsonElement) value);
+      } else if (value instanceof List) {
+        JsonArray array = new JsonArray();
+        ((List<?>) value).forEach(a -> array.add(a.toString()));
+        jsonObject.add(key.toString(), array);
+      } else {
+        jsonObject.add(key.toString(), WxGsonBuilder.create().toJsonTree(value));
+      }
+    }
+
+  }
+}

+ 26 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/GsonParser.java

@@ -0,0 +1,26 @@
+package cn.nosum.wx.common.utils.json;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.stream.JsonReader;
+
+import java.io.Reader;
+
+/**
+ * @author niefy
+ */
+public class GsonParser {
+  private static final JsonParser JSON_PARSER = new JsonParser();
+
+  public static JsonObject parse(String json) {
+    return JSON_PARSER.parse(json).getAsJsonObject();
+  }
+
+  public static JsonObject parse(Reader json) {
+    return JSON_PARSER.parse(json).getAsJsonObject();
+  }
+
+  public static JsonObject parse(JsonReader json) {
+    return JSON_PARSER.parse(json).getAsJsonObject();
+  }
+}

+ 27 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxAccessTokenAdapter.java

@@ -0,0 +1,27 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.entity.WxAccessToken;
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Daniel Qian
+ */
+public class WxAccessTokenAdapter implements JsonDeserializer<WxAccessToken> {
+
+  @Override
+  public WxAccessToken deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxAccessToken accessToken = new WxAccessToken();
+    JsonObject accessTokenJsonObject = json.getAsJsonObject();
+
+    if (accessTokenJsonObject.get("access_token") != null && !accessTokenJsonObject.get("access_token").isJsonNull()) {
+      accessToken.setAccessToken(GsonHelper.getAsString(accessTokenJsonObject.get("access_token")));
+    }
+    if (accessTokenJsonObject.get("expires_in") != null && !accessTokenJsonObject.get("expires_in").isJsonNull()) {
+      accessToken.setExpiresIn(GsonHelper.getAsPrimitiveInt(accessTokenJsonObject.get("expires_in")));
+    }
+    return accessToken;
+  }
+
+}

+ 47 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxBooleanTypeAdapter.java

@@ -0,0 +1,47 @@
+package cn.nosum.wx.common.utils.json;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import org.apache.commons.lang3.BooleanUtils;
+
+import java.io.IOException;
+
+/**
+ * <pre>
+ * Gson 布尔类型类型转换器
+ * Created by Binary Wang on 2017-7-8.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public class WxBooleanTypeAdapter extends TypeAdapter<Boolean> {
+  @Override
+  public void write(JsonWriter out, Boolean value) throws IOException {
+    if (value == null) {
+      out.nullValue();
+    } else {
+      out.value(value);
+    }
+  }
+
+  @Override
+  public Boolean read(JsonReader in) throws IOException {
+    JsonToken peek = in.peek();
+    switch (peek) {
+      case BOOLEAN:
+        return in.nextBoolean();
+      case NULL:
+        in.nextNull();
+        return null;
+      case NUMBER:
+        return BooleanUtils.toBoolean(in.nextInt());
+      case STRING:
+        return BooleanUtils.toBoolean(in.nextString());
+      default:
+        throw new JsonParseException("Expected BOOLEAN or NUMBER but was " + peek);
+    }
+  }
+}

+ 43 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxDateTypeAdapter.java

@@ -0,0 +1,43 @@
+package cn.nosum.wx.common.utils.json;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ * <pre>
+ * Gson 日期类型转换器
+ * Created by Binary Wang on 2017-7-8.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public class WxDateTypeAdapter extends TypeAdapter<Date> {
+  @Override
+  public void write(JsonWriter out, Date value) throws IOException {
+    if (value == null) {
+      out.nullValue();
+    } else {
+      out.value(value.getTime() / 1000);
+    }
+  }
+
+  @Override
+  public Date read(JsonReader in) throws IOException {
+    JsonToken peek = in.peek();
+    switch (peek) {
+      case NULL:
+        in.nextNull();
+        return null;
+      case NUMBER:
+        return new Date(in.nextInt() * 1000);
+      default:
+        throw new JsonParseException("Expected NUMBER but was " + peek);
+    }
+  }
+}

+ 31 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxErrorAdapter.java

@@ -0,0 +1,31 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.error.WxError;
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Daniel Qian.
+ */
+public class WxErrorAdapter implements JsonDeserializer<WxError> {
+
+    @Override
+    public WxError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+            throws JsonParseException {
+        WxError error = new WxError();
+        JsonObject wxErrorJsonObject = json.getAsJsonObject();
+
+        if (wxErrorJsonObject.get("errcode") != null && !wxErrorJsonObject.get("errcode").isJsonNull()) {
+            error.setErrorCode(GsonHelper.getAsPrimitiveInt(wxErrorJsonObject.get("errcode")));
+        }
+        if (wxErrorJsonObject.get("errmsg") != null && !wxErrorJsonObject.get("errmsg").isJsonNull()) {
+            error.setErrorMsg(GsonHelper.getAsString(wxErrorJsonObject.get("errmsg")));
+        }
+
+        error.setJson(json.toString());
+
+        return error;
+    }
+
+}

+ 32 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxGsonBuilder.java

@@ -0,0 +1,32 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.common.entity.WxNetCheckResult;
+import cn.nosum.wx.common.entity.menu.WxMenu;
+import cn.nosum.wx.common.entity.result.WxMediaUploadResult;
+import cn.nosum.wx.common.error.WxError;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * .
+ * @author chanjarster
+ */
+public class WxGsonBuilder {
+  private static final GsonBuilder INSTANCE = new GsonBuilder();
+
+  static {
+    INSTANCE.disableHtmlEscaping();
+    INSTANCE.registerTypeAdapter(WxAccessToken.class, new WxAccessTokenAdapter());
+    INSTANCE.registerTypeAdapter(WxError.class, new WxErrorAdapter());
+    INSTANCE.registerTypeAdapter(WxMenu.class, new WxMenuGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMediaUploadResult.class, new WxMediaUploadResultAdapter());
+    INSTANCE.registerTypeAdapter(WxNetCheckResult.class, new WxNetCheckResultGsonAdapter());
+
+  }
+
+  public static Gson create() {
+    return INSTANCE.create();
+  }
+
+}

+ 36 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxMediaUploadResultAdapter.java

@@ -0,0 +1,36 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.entity.result.WxMediaUploadResult;
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Daniel Qian
+ */
+public class WxMediaUploadResultAdapter implements JsonDeserializer<WxMediaUploadResult> {
+
+  @Override
+  public WxMediaUploadResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxMediaUploadResult result = new WxMediaUploadResult();
+    JsonObject jsonObject = json.getAsJsonObject();
+    if (jsonObject.get("url") != null && !jsonObject.get("url").isJsonNull()) {
+      result.setUrl(GsonHelper.getAsString(jsonObject.get("url")));
+    }
+
+    if (jsonObject.get("type") != null && !jsonObject.get("type").isJsonNull()) {
+      result.setType(GsonHelper.getAsString(jsonObject.get("type")));
+    }
+    if (jsonObject.get("media_id") != null && !jsonObject.get("media_id").isJsonNull()) {
+      result.setMediaId(GsonHelper.getAsString(jsonObject.get("media_id")));
+    }
+    if (jsonObject.get("thumb_media_id") != null && !jsonObject.get("thumb_media_id").isJsonNull()) {
+      result.setThumbMediaId(GsonHelper.getAsString(jsonObject.get("thumb_media_id")));
+    }
+    if (jsonObject.get("created_at") != null && !jsonObject.get("created_at").isJsonNull()) {
+      result.setCreatedAt(GsonHelper.getAsPrimitiveLong(jsonObject.get("created_at")));
+    }
+    return result;
+  }
+
+}

+ 122 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxMenuGsonAdapter.java

@@ -0,0 +1,122 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.entity.menu.WxMenu;
+import cn.nosum.wx.common.entity.menu.WxMenuButton;
+import cn.nosum.wx.common.entity.menu.WxMenuRule;
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+
+
+/**
+ * @author Daniel Qian
+ */
+public class WxMenuGsonAdapter implements JsonSerializer<WxMenu>, JsonDeserializer<WxMenu> {
+
+  @Override
+  public JsonElement serialize(WxMenu menu, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject json = new JsonObject();
+
+    JsonArray buttonArray = new JsonArray();
+    for (WxMenuButton button : menu.getButtons()) {
+      JsonObject buttonJson = convertToJson(button);
+      buttonArray.add(buttonJson);
+    }
+    json.add("button", buttonArray);
+
+    if (menu.getMatchRule() != null) {
+      json.add("matchrule", convertToJson(menu.getMatchRule()));
+    }
+
+    return json;
+  }
+
+  protected JsonObject convertToJson(WxMenuButton button) {
+    JsonObject buttonJson = new JsonObject();
+    buttonJson.addProperty("type", button.getType());
+    buttonJson.addProperty("name", button.getName());
+    buttonJson.addProperty("key", button.getKey());
+    buttonJson.addProperty("url", button.getUrl());
+    buttonJson.addProperty("media_id", button.getMediaId());
+    buttonJson.addProperty("appid", button.getAppId());
+    buttonJson.addProperty("pagepath", button.getPagePath());
+    if (button.getSubButtons() != null && button.getSubButtons().size() > 0) {
+      JsonArray buttonArray = new JsonArray();
+      for (WxMenuButton sub_button : button.getSubButtons()) {
+        buttonArray.add(convertToJson(sub_button));
+      }
+      buttonJson.add("sub_button", buttonArray);
+    }
+    return buttonJson;
+  }
+
+  protected JsonObject convertToJson(WxMenuRule menuRule) {
+    JsonObject matchRule = new JsonObject();
+    matchRule.addProperty("tag_id", menuRule.getTagId());
+    matchRule.addProperty("sex", menuRule.getSex());
+    matchRule.addProperty("country", menuRule.getCountry());
+    matchRule.addProperty("province", menuRule.getProvince());
+    matchRule.addProperty("city", menuRule.getCity());
+    matchRule.addProperty("client_platform_type", menuRule.getClientPlatformType());
+    matchRule.addProperty("language", menuRule.getLanguage());
+    return matchRule;
+  }
+
+  @Deprecated
+  private WxMenuRule convertToRule(JsonObject json) {
+    WxMenuRule menuRule = new WxMenuRule();
+    //变态的微信接口,这里居然反人类的使用和序列化时不一样的名字
+    //menuRule.setTagId(GsonHelper.getString(json,"tag_id"));
+    menuRule.setTagId(GsonHelper.getString(json, "group_id"));
+    menuRule.setSex(GsonHelper.getString(json, "sex"));
+    menuRule.setCountry(GsonHelper.getString(json, "country"));
+    menuRule.setProvince(GsonHelper.getString(json, "province"));
+    menuRule.setCity(GsonHelper.getString(json, "city"));
+    menuRule.setClientPlatformType(GsonHelper.getString(json, "client_platform_type"));
+    menuRule.setLanguage(GsonHelper.getString(json, "language"));
+    return menuRule;
+  }
+
+  @Override
+  public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    /*
+     * 操蛋的微信
+     * 创建菜单时是 { button : ... }
+     * 查询菜单时是 { menu : { button : ... } }
+     * 现在企业号升级为企业微信后,没有此问题,因此需要单独处理
+     */
+    JsonArray buttonsJson = json.getAsJsonObject().get("menu").getAsJsonObject().get("button").getAsJsonArray();
+    return this.buildMenuFromJson(buttonsJson);
+  }
+
+  protected WxMenu buildMenuFromJson(JsonArray buttonsJson) {
+    WxMenu menu = new WxMenu();
+    for (int i = 0; i < buttonsJson.size(); i++) {
+      JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject();
+      WxMenuButton button = convertFromJson(buttonJson);
+      menu.getButtons().add(button);
+      if (buttonJson.get("sub_button") == null || buttonJson.get("sub_button").isJsonNull()) {
+        continue;
+      }
+      JsonArray sub_buttonsJson = buttonJson.get("sub_button").getAsJsonArray();
+      for (int j = 0; j < sub_buttonsJson.size(); j++) {
+        JsonObject sub_buttonJson = sub_buttonsJson.get(j).getAsJsonObject();
+        button.getSubButtons().add(convertFromJson(sub_buttonJson));
+      }
+    }
+    return menu;
+  }
+
+  protected WxMenuButton convertFromJson(JsonObject json) {
+    WxMenuButton button = new WxMenuButton();
+    button.setName(GsonHelper.getString(json, "name"));
+    button.setKey(GsonHelper.getString(json, "key"));
+    button.setUrl(GsonHelper.getString(json, "url"));
+    button.setType(GsonHelper.getString(json, "type"));
+    button.setMediaId(GsonHelper.getString(json, "media_id"));
+    button.setAppId(GsonHelper.getString(json, "appid"));
+    button.setPagePath(GsonHelper.getString(json, "pagepath"));
+    return button;
+  }
+
+}

+ 51 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/json/WxNetCheckResultGsonAdapter.java

@@ -0,0 +1,51 @@
+package cn.nosum.wx.common.utils.json;
+
+import cn.nosum.wx.common.entity.WxNetCheckResult;
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author billytomato
+ */
+public class WxNetCheckResultGsonAdapter implements JsonDeserializer<WxNetCheckResult> {
+
+
+  @Override
+  public WxNetCheckResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxNetCheckResult result = new WxNetCheckResult();
+
+    JsonArray dnssJson = json.getAsJsonObject().get("dns").getAsJsonArray();
+    List<WxNetCheckResult.WxNetCheckDnsInfo> dnsInfoList = new ArrayList<>();
+    if (dnssJson != null && dnssJson.size() > 0) {
+      for (int i = 0; i < dnssJson.size(); i++) {
+        JsonObject buttonJson = dnssJson.get(i).getAsJsonObject();
+        WxNetCheckResult.WxNetCheckDnsInfo dnsInfo = new WxNetCheckResult.WxNetCheckDnsInfo();
+        dnsInfo.setIp(GsonHelper.getString(buttonJson, "ip"));
+        dnsInfo.setRealOperator(GsonHelper.getString(buttonJson, "real_operator"));
+        dnsInfoList.add(dnsInfo);
+      }
+    }
+
+    JsonArray pingsJson = json.getAsJsonObject().get("ping").getAsJsonArray();
+    List<WxNetCheckResult.WxNetCheckPingInfo> pingInfoList = new ArrayList<>();
+    if (pingsJson != null && pingsJson.size() > 0) {
+      for (int i = 0; i < pingsJson.size(); i++) {
+        JsonObject pingJson = pingsJson.get(i).getAsJsonObject();
+        WxNetCheckResult.WxNetCheckPingInfo pingInfo = new WxNetCheckResult.WxNetCheckPingInfo();
+        pingInfo.setIp(GsonHelper.getString(pingJson, "ip"));
+        pingInfo.setFromOperator(GsonHelper.getString(pingJson, "from_operator"));
+        pingInfo.setPackageLoss(GsonHelper.getString(pingJson, "package_loss"));
+        pingInfo.setTime(GsonHelper.getString(pingJson, "time"));
+        pingInfoList.add(pingInfo);
+      }
+    }
+    result.setDnsInfos(dnsInfoList);
+    result.setPingInfos(pingInfoList);
+    return result;
+  }
+
+}

+ 37 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/IntegerArrayConverter.java

@@ -0,0 +1,37 @@
+package cn.nosum.wx.common.utils.xml;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.thoughtworks.xstream.converters.basic.StringConverter;
+
+/**
+ * Integer型数组转换器.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2019-08-22
+ */
+public class IntegerArrayConverter extends StringConverter {
+  @Override
+  public boolean canConvert(Class type) {
+    return type == Integer[].class;
+  }
+
+  @Override
+  public String toString(Object obj) {
+    return "<![CDATA[" + Joiner.on(",").join((Integer[]) obj) + "]]>";
+  }
+
+  @Override
+  public Object fromString(String str) {
+    final Iterable<String> iterable = Splitter.on(",").split(str);
+    final String[] strings = Iterables.toArray(iterable, String.class);
+    Integer[] result = new Integer[strings.length];
+    int index = 0;
+    for (String string : strings) {
+      result[index++] = Integer.parseInt(string);
+    }
+
+    return result;
+  }
+}

+ 37 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/LongArrayConverter.java

@@ -0,0 +1,37 @@
+package cn.nosum.wx.common.utils.xml;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.thoughtworks.xstream.converters.basic.StringConverter;
+
+/**
+ * Long型数组转换器.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2019-08-22
+ */
+public class LongArrayConverter extends StringConverter {
+  @Override
+  public boolean canConvert(Class type) {
+    return type == Long[].class;
+  }
+
+  @Override
+  public String toString(Object obj) {
+    return "<![CDATA[" + Joiner.on(",").join((Long[]) obj) + "]]>";
+  }
+
+  @Override
+  public Object fromString(String str) {
+    final Iterable<String> iterable = Splitter.on(",").split(str);
+    final String[] strings = Iterables.toArray(iterable, String.class);
+    Long[] result = new Long[strings.length];
+    int index = 0;
+    for (String string : strings) {
+      result[index++] = Long.parseLong(string);
+    }
+
+    return result;
+  }
+}

+ 30 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/StringArrayConverter.java

@@ -0,0 +1,30 @@
+package cn.nosum.wx.common.utils.xml;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.thoughtworks.xstream.converters.basic.StringConverter;
+
+
+/**
+ * String 数组转换
+ * @author chily.lin
+ */
+public class StringArrayConverter  extends StringConverter {
+  @Override
+  public boolean canConvert(Class type) {
+    return type == String[].class;
+  }
+
+  @Override
+  public String toString(Object obj) {
+    return "<![CDATA[" + Joiner.on(",").join((String[]) obj) + "]]>";
+  }
+
+  @Override
+  public Object fromString(String str) {
+    final Iterable<String> iterable = Splitter.on(",").split(str);
+    String[] results = Iterables.toArray(iterable, String.class);
+    return results;
+  }
+}

+ 17 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamCDataConverter.java

@@ -0,0 +1,17 @@
+package cn.nosum.wx.common.utils.xml;
+
+import com.thoughtworks.xstream.converters.basic.StringConverter;
+
+/**
+ * CDATA 内容转换器,加上CDATA标签.
+ *
+ * @author Daniel Qian
+ */
+public class XStreamCDataConverter extends StringConverter {
+
+  @Override
+  public String toString(Object obj) {
+    return "<![CDATA[" + super.toString(obj) + "]]>";
+  }
+
+}

+ 95 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamInitializer.java

@@ -0,0 +1,95 @@
+package cn.nosum.wx.common.utils.xml;
+
+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;
+
+/**
+ * The type X stream initializer.
+ *
+ * @author Daniel Qian
+ */
+public class XStreamInitializer {
+  private static final XppDriver XPP_DRIVER = new XppDriver() {
+    @Override
+    public HierarchicalStreamWriter createWriter(Writer out) {
+      return new PrettyPrintWriter(out, getNameCoder()) {
+        private static final String PREFIX_CDATA = "<![CDATA[";
+        private static final String SUFFIX_CDATA = "]]>";
+        private static final String PREFIX_MEDIA_ID = "<MediaId>";
+        private static final String SUFFIX_MEDIA_ID = "</MediaId>";
+        private static final String PREFIX_REPLACE_NAME = "<ReplaceName>";
+        private static final String SUFFIX_REPLACE_NAME = "</ReplaceName>";
+
+        @Override
+        protected void writeText(QuickWriter writer, String text) {
+          if (text.startsWith(PREFIX_CDATA) && text.endsWith(SUFFIX_CDATA)) {
+            writer.write(text);
+          } else if (text.startsWith(PREFIX_MEDIA_ID) && text.endsWith(SUFFIX_MEDIA_ID)) {
+            writer.write(text);
+          } else if (text.startsWith(PREFIX_REPLACE_NAME) && text.endsWith(SUFFIX_REPLACE_NAME)){
+            writer.write(text);
+          } else {
+            super.writeText(writer, text);
+          }
+
+        }
+
+        @Override
+        public String encodeNode(String name) {
+          //防止将_转换成__
+          return name;
+        }
+      };
+    }
+  };
+
+  /**
+   * Gets instance.
+   *
+   * @return the instance
+   */
+  public static XStream getInstance() {
+    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.autodetectAnnotations(true);
+
+    // setup proper security by limiting which classes can be loaded by XStream
+    xstream.addPermission(NoTypePermission.NONE);
+    xstream.addPermission(new WildcardTypePermission(new String[]{
+      "cn.nosum.wx.**", "cn.binarywang.wx.**", "com.github.binarywang.**"
+    }));
+    xstream.setClassLoader(Thread.currentThread().getContextClassLoader());
+    return xstream;
+  }
+
+}

+ 8 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamMediaIdConverter.java

@@ -0,0 +1,8 @@
+package cn.nosum.wx.common.utils.xml;
+
+public class XStreamMediaIdConverter extends XStreamCDataConverter {
+  @Override
+  public String toString(Object obj) {
+    return "<MediaId>" + super.toString(obj) + "</MediaId>";
+  }
+}

+ 8 - 0
wx-java-tools/wx-java-common/src/main/java/cn/nosum/wx/common/utils/xml/XStreamReplaceNameConverter.java

@@ -0,0 +1,8 @@
+package cn.nosum.wx.common.utils.xml;
+
+public class XStreamReplaceNameConverter extends XStreamCDataConverter {
+  @Override
+  public String toString(Object obj) {
+    return "<ReplaceName>" + super.toString(obj) + "</ReplaceName>";
+  }
+}

+ 34 - 0
wx-java-tools/wx-java-cp/pom.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>wx-java-tools</artifactId>
+        <groupId>cn.nosum</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>wx-java-cp</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.nosum</groupId>
+            <artifactId>wx-java-common</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jodd</groupId>
+            <artifactId>jodd-http</artifactId>
+            <version>5.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.5.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 51 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpAgentService.java

@@ -0,0 +1,51 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpAgent;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *  管理企业号应用
+ *  文档地址:https://work.weixin.qq.com/api/doc#10087
+ *  Created by huansinho on 2018/4/13.
+ * </pre>
+ *
+ * @author <a href="https://github.com/huansinho">huansinho</a>
+ */
+public interface WxCpAgentService {
+  /**
+   * <pre>
+   * 获取企业号应用信息
+   * 该API用于获取企业号某个应用的基本信息,包括头像、昵称、帐号类型、认证类型、可见范围等信息
+   * 详情请见: https://work.weixin.qq.com/api/doc#10087
+   * </pre>
+   *
+   * @param agentId 企业应用的id
+   * @return 部门id
+   */
+  WxCpAgent get(Integer agentId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 设置应用.
+   * 仅企业可调用,可设置当前凭证对应的应用;第三方不可调用。
+   * 详情请见: https://work.weixin.qq.com/api/doc#10088
+   * </pre>
+   *
+   * @param agentInfo 应用信息
+   */
+  void set(WxCpAgent agentInfo) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 获取应用列表.
+   * 企业仅可获取当前凭证对应的应用;第三方仅可获取被授权的应用。
+   * 详情请见: https://work.weixin.qq.com/api/doc#11214
+   * </pre>
+   *
+   */
+  List<WxCpAgent> list() throws WxErrorException;
+
+}

+ 18 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpAgentWorkBenchService.java

@@ -0,0 +1,18 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpAgentWorkBench;
+
+/**
+ * @author songshiyu
+ * @date : create in 16:16 2020/9/27
+ * @description: 工作台自定义展示:https://work.weixin.qq.com/api/doc/90000/90135/92535
+ */
+public interface WxCpAgentWorkBenchService {
+
+  void setWorkBenchTemplate(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException;
+
+  String getWorkBenchTemplate(Long agentid) throws WxErrorException;
+
+  void setWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException;
+}

+ 60 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpChatService.java

@@ -0,0 +1,60 @@
+package cn.nosum.wx.cp.api;
+
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpChat;
+import cn.nosum.wx.cp.entity.message.WxCpAppChatMessage;
+
+import java.util.List;
+
+/**
+ * 群聊服务.
+ *
+ * @author gaigeshen
+ */
+public interface WxCpChatService {
+  /**
+   * 创建群聊会话,注意:刚创建的群,如果没有下发消息,在企业微信不会出现该群.
+   *
+   * @param name   群聊名,最多50个utf8字符,超过将截断
+   * @param owner  指定群主的id。如果不指定,系统会随机从userlist中选一人作为群主
+   * @param users  群成员id列表。至少2人,至多500人
+   * @param chatId 群聊的唯一标志,不能与已有的群重复;字符串类型,最长32个字符。只允许字符0-9及字母a-zA-Z。如果不填,系统会随机生成群id
+   * @return 创建的群聊会话chatId
+   * @throws WxErrorException 异常
+   */
+  String create(String name, String owner, List<String> users, String chatId) throws WxErrorException;
+
+  /**
+   * 修改群聊会话.
+   *
+   * @param chatId        群聊id
+   * @param name          新的群聊名。若不需更新,请忽略此参数(null or empty)。最多50个utf8字符,超过将截断
+   * @param owner         新群主的id。若不需更新,请忽略此参数(null or empty)
+   * @param usersToAdd    添加成员的id列表,若不需要更新,则传递空对象或者空集合
+   * @param usersToDelete 踢出成员的id列表,若不需要更新,则传递空对象或者空集合
+   * @throws WxErrorException 异常
+   */
+  void update(String chatId, String name, String owner, List<String> usersToAdd, List<String> usersToDelete) throws WxErrorException;
+
+  /**
+   * 获取群聊会话.
+   *
+   * @param chatId 群聊编号
+   * @return 群聊会话
+   * @throws WxErrorException 异常
+   */
+  WxCpChat get(String chatId) throws WxErrorException;
+
+  /**
+   * 应用支持推送文本、图片、视频、文件、图文等类型.
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=ACCESS_TOKEN
+   * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90248
+   *
+   * @param message 要发送的消息内容对象
+   * @throws WxErrorException 异常
+   */
+  void sendMsg(WxCpAppChatMessage message) throws WxErrorException;
+
+}

+ 66 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpDepartmentService.java

@@ -0,0 +1,66 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpDepart;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *  部门管理接口
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpDepartmentService {
+
+  /**
+   * <pre>
+   * 部门管理接口 - 创建部门.
+   * 最多支持创建500个部门
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90205
+   * </pre>
+   *
+   * @param depart 部门
+   * @return 部门id
+   * @throws WxErrorException 异常
+   */
+  Long create(WxCpDepart depart) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 部门管理接口 - 获取部门列表.
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90208
+   * </pre>
+   *
+   * @param id 部门id。获取指定部门及其下的子部门。非必需,可为null
+   * @return 获取的部门列表
+   * @throws WxErrorException 异常
+   */
+  List<WxCpDepart> list(Long id) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 部门管理接口 - 更新部门.
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90206
+   * 如果id为0(未部门),1(黑名单),2(星标组),或者不存在的id,微信会返回系统繁忙的错误
+   * </pre>
+   *
+   * @param group 要更新的group,group的id,name必须设置
+   * @throws WxErrorException 异常
+   */
+  void update(WxCpDepart group) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 部门管理接口 - 删除部门.
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90207
+   * 应用须拥有指定部门的管理权限
+   * </pre>
+   *
+   * @param departId 部门id
+   * @throws WxErrorException 异常
+   */
+  void delete(Long departId) throws WxErrorException;
+}

+ 551 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpExternalContactService.java

@@ -0,0 +1,551 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpBaseResp;
+import cn.nosum.wx.cp.entity.external.*;
+import cn.nosum.wx.cp.entity.external.contact.WxCpExternalContactBatchInfo;
+import cn.nosum.wx.cp.entity.external.contact.WxCpExternalContactInfo;
+import com.sun.istack.internal.NotNull;
+import lombok.NonNull;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * <pre>
+ * 外部联系人管理接口,企业微信的外部联系人的接口和通讯录接口已经拆离
+ *  Created by Joe Cao on 2019/6/14
+ * </pre>
+ *
+ * @author <a href="https://github.com/JoeCao">JoeCao</a>
+ */
+public interface WxCpExternalContactService {
+
+    /**
+     * 配置客户联系「联系我」方式
+     * <pre>
+     * 企业可以在管理后台-客户联系中配置成员的「联系我」的二维码或者小程序按钮,客户通过扫描二维码或点击小程序上的按钮,即可获取成员联系方式,主动联系到成员。
+     * 企业可通过此接口为具有客户联系功能的成员生成专属的「联系我」二维码或者「联系我」按钮。
+     * 如果配置的是「联系我」按钮,需要开发者的小程序接入小程序插件。
+     *
+     * 注意:
+     * 通过API添加的「联系我」不会在管理端进行展示,每个企业可通过API最多配置50万个「联系我」。
+     * 用户需要妥善存储返回的config_id,config_id丢失可能导致用户无法编辑或删除「联系我」。
+     * 临时会话模式不占用「联系我」数量,但每日最多添加10万个,并且仅支持单人。
+     * 临时会话模式的二维码,添加好友完成后该二维码即刻失效。
+     * </pre>
+     *
+     * @param info 客户联系「联系我」方式
+     * @return wx cp contact way result
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpContactWayResult addContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException;
+
+    /**
+     * 获取企业已配置的「联系我」方式
+     *
+     * <pre>
+     * <b>批量</b>获取企业配置的「联系我」二维码和「联系我」小程序按钮。
+     * </pre>
+     *
+     * @param configId 联系方式的配置id,必填
+     * @return contact way
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpContactWayInfo getContactWay(@NonNull String configId) throws WxErrorException;
+
+    /**
+     * 更新企业已配置的「联系我」方式
+     *
+     * <pre>
+     * 更新企业配置的「联系我」二维码和「联系我」小程序按钮中的信息,如使用人员和备注等。
+     * </pre>
+     *
+     * @param info 客户联系「联系我」方式
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp updateContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException;
+
+    /**
+     * 删除企业已配置的「联系我」方式
+     *
+     * <pre>
+     * 删除一个已配置的「联系我」二维码或者「联系我」小程序按钮。
+     * </pre>
+     *
+     * @param configId 企业联系方式的配置id,必填
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp deleteContactWay(@NonNull String configId) throws WxErrorException;
+
+    /**
+     * 结束临时会话
+     *
+     * <pre>
+     * 将指定的企业成员和客户之前的临时会话断开,断开前会自动下发已配置的结束语。
+     *
+     * 注意:请保证传入的企业成员和客户之间有仍然有效的临时会话, 通过<b>其他方式的添加外部联系人无法通过此接口关闭会话</b>。
+     * </pre>
+     *
+     * @param userId         the user id
+     * @param externalUserId the external user id
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String externalUserId) throws WxErrorException;
+
+
+    /**
+     * 获取外部联系人详情.
+     * <pre>
+     *   企业可通过此接口,根据外部联系人的userid,拉取外部联系人详情。权限说明:
+     * 企业需要使用外部联系人管理secret所获取的accesstoken来调用
+     * 第三方应用需拥有“企业客户”权限。
+     * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+     * </pre>
+     *
+     * @param userId 外部联系人的userid
+     * @return . external contact
+     * @throws WxErrorException the wx error exception
+     * @deprecated 建议使用 {@link #getContactDetail(String)}
+     */
+    @Deprecated
+    WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException;
+
+    /**
+     * 获取客户详情.
+     * <pre>
+     *
+     * 企业可通过此接口,根据外部联系人的userid(如何获取?),拉取客户详情。
+     *
+     * 请求方式:GET(HTTPS)
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID
+     *
+     * 权限说明:
+     *
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+     * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+     * </pre>
+     *
+     * @param userId 外部联系人的userid,注意不是企业成员的帐号
+     * @return . contact detail
+     * @throws WxErrorException .
+     */
+    WxCpExternalContactInfo getContactDetail(String userId) throws WxErrorException;
+
+    /**
+     * 企业和服务商可通过此接口,将微信外部联系人的userid转为微信openid,用于调用支付相关接口。暂不支持企业微信外部联系人(ExternalUserid为wo开头)的userid转openid。
+     *
+     * @param externalUserid 微信外部联系人的userid
+     * @return 该企业的外部联系人openid
+     * @throws WxErrorException .
+     */
+    String convertToOpenid(String externalUserid) throws WxErrorException;
+
+    /**
+     * 批量获取客户详情.
+     * <pre>
+     *
+     * 企业/第三方可通过此接口获取指定成员添加的客户信息列表。
+     *
+     * 请求方式:POST(HTTPS)
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user?access_token=ACCESS_TOKEN
+     *
+     * 权限说明:
+     *
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+     * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+     * </pre>
+     *
+     * @param userId 企业成员的userid,注意不是外部联系人的帐号
+     * @param cursor the cursor
+     * @param limit  the  limit
+     * @return wx cp user external contact batch info
+     * @throws WxErrorException .
+     */
+    WxCpExternalContactBatchInfo getContactDetailBatch(String userId, String cursor,
+                                                       Integer limit)
+            throws WxErrorException;
+
+    /**
+     * 修改客户备注信息.
+     * <pre>
+     * 企业可通过此接口修改指定用户添加的客户的备注信息。
+     * 请求方式: POST(HTTP)
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remark?access_token=ACCESS_TOKEN
+     * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92115
+     * </pre>
+     *
+     * @param request 备注信息请求
+     * @throws WxErrorException .
+     */
+    void updateRemark(WxCpUpdateRemarkRequest request) throws WxErrorException;
+
+    /**
+     * 获取客户列表.
+     * <pre>
+     *   企业可通过此接口获取指定成员添加的客户列表。客户是指配置了客户联系功能的成员所添加的外部联系人。没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。
+     *
+     * 请求方式:GET(HTTPS)
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID
+     *
+     * 权限说明:
+     *
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+     * 第三方应用需拥有“企业客户”权限。
+     * 第三方/自建应用只能获取到可见范围内的配置了客户联系功能的成员。
+     * </pre>
+     *
+     * @param userId 企业成员的userid
+     * @return List of External wx id
+     * @throws WxErrorException .
+     */
+    List<String> listExternalContacts(String userId) throws WxErrorException;
+
+    /**
+     * 企业和第三方服务商可通过此接口获取配置了客户联系功能的成员(Customer Contact)列表。
+     * <pre>
+     *   企业需要使用外部联系人管理secret所获取的accesstoken来调用(accesstoken如何获取?);
+     *   第三方应用需拥有“企业客户”权限。
+     *   第三方应用只能获取到可见范围内的配置了客户联系功能的成员
+     * </pre>
+     *
+     * @return List of CpUser id
+     * @throws WxErrorException .
+     */
+    List<String> listFollowers() throws WxErrorException;
+
+    /**
+     * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用离职成员的外部联系人再分配接口将这些客户重新分配给其他企业成员。
+     *
+     * @param page     the page
+     * @param pageSize the page size
+     * @return wx cp user external unassign list
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalUnassignList listUnassignedList(Integer page, Integer pageSize) throws WxErrorException;
+
+    /**
+     * 企业可通过此接口,将已离职成员的外部联系人分配给另一个成员接替联系。
+     *
+     * @param externalUserid the external userid
+     * @param handOverUserid the hand over userid
+     * @param takeOverUserid the take over userid
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     * @deprecated 此后续将不再更新维护, 建议使用 {@link #transferCustomer(WxCpUserTransferCustomerReq)}
+     */
+    @Deprecated
+    WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException;
+
+    /**
+     * 企业可通过此接口,转接在职成员的客户给其他成员。
+     * <per>
+     * external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
+     * 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
+     * <p>
+     * 权限说明:
+     * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+     * 接替成员必须在此第三方应用或自建应用的可见范围内。
+     * 接替成员需要配置了客户联系功能。
+     * 接替成员需要在企业微信激活且已经过实名认证。
+     * </per>
+     *
+     * @param req 转接在职成员的客户给其他成员请求实体
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserTransferCustomerResp transferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException;
+
+    /**
+     * 企业和第三方可通过此接口查询在职成员的客户转接情况。
+     * <per>
+     * 权限说明:
+     * <p>
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+     * 接替成员必须在此第三方应用或自建应用的可见范围内。
+     * </per>
+     *
+     * @param handOverUserid 原添加成员的userid
+     * @param takeOverUserid 接替成员的userid
+     * @param cursor         分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页;
+     * @return 客户转接接口实体
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserTransferResultResp transferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException;
+
+    /**
+     * 企业可通过此接口,分配离职成员的客户给其他成员。
+     * <per>
+     * handover_userid必须是已离职用户。
+     * external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
+     * 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
+     * <p>
+     * 权限说明:
+     * <p>
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限
+     * 接替成员必须在此第三方应用或自建应用的可见范围内。
+     * 接替成员需要配置了客户联系功能。
+     * 接替成员需要在企业微信激活且已经过实名认证。
+     * </per>
+     *
+     * @param req 转接在职成员的客户给其他成员请求实体
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserTransferCustomerResp resignedTransferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException;
+
+    /**
+     * 企业和第三方可通过此接口查询离职成员的客户分配情况。
+     * <per>
+     * 权限说明:
+     * <p>
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+     * 接替成员必须在此第三方应用或自建应用的可见范围内。
+     * </per>
+     *
+     * @param handOverUserid 原添加成员的userid
+     * @param takeOverUserid 接替成员的userid
+     * @param cursor         分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页;
+     * @return 客户转接接口实体
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserTransferResultResp resignedTransferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 该接口用于获取配置过客户群管理的客户群列表。
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 暂不支持第三方调用。
+     * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+     * </pre>
+     *
+     * @param pageIndex the page index
+     * @param pageSize  the page size
+     * @param status    the status
+     * @param userIds   the user ids
+     * @param partyIds  the party ids
+     * @return the wx cp user external group chat list
+     * @throws WxErrorException the wx error exception
+     * @deprecated 请使用 {@link WxCpExternalContactService#listGroupChat(Integer, String, int, String[])}
+     */
+    @Deprecated
+    WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, String[] partyIds) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 该接口用于获取配置过客户群管理的客户群列表。
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 暂不支持第三方调用。
+     * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+     * </pre>
+     *
+     * @param limit   分页,预期请求的数据量,取值范围 1 ~ 1000
+     * @param cursor  用于分页查询的游标,字符串类型,由上一次调用返回,首次调用不填
+     * @param status  客户群跟进状态过滤。0 - 所有列表(即不过滤)  1 - 离职待继承  2 - 离职继承中  3 - 离职继承完成 默认为0
+     * @param userIds 群主过滤。如果不填,表示获取应用可见范围内全部群主的数据(但是不建议这么用,如果可见范围人数超过1000人,为了防止数据包过大,会报错 81017);用户ID列表。最多100个
+     * @return the wx cp user external group chat list
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalGroupChatList listGroupChat(Integer limit, String cursor, int status, String[] userIds) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 通过客户群ID,获取详情。包括群名、群成员列表、群成员入群时间、入群方式。(客户群是由具有客户群使用权限的成员创建的外部群)
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 暂不支持第三方调用。
+     * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92122
+     * </pre>
+     *
+     * @param chatId the chat id
+     * @return group chat
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalGroupChatInfo getGroupChat(String chatId, Integer needName) throws WxErrorException;
+
+    /**
+     * 企业可通过此接口,将已离职成员为群主的群,分配给另一个客服成员。
+     *
+     * <per>
+     * 注意::
+     * <p>
+     * 群主离职了的客户群,才可继承
+     * 继承给的新群主,必须是配置了客户联系功能的成员
+     * 继承给的新群主,必须有设置实名
+     * 继承给的新群主,必须有激活企业微信
+     * 同一个人的群,限制每天最多分配300个给新群主
+     * <p>
+     * 权限说明:
+     * <p>
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限
+     * 对于第三方/自建应用,群主必须在应用的可见范围。
+     * </per>
+     *
+     * @param chatIds  需要转群主的客户群ID列表。取值范围: 1 ~ 100
+     * @param newOwner 新群主ID
+     * @return 分配结果,主要是分配失败的群列表
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalGroupChatTransferResp transferGroupChat(String[] chatIds, String newOwner) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口获取成员联系客户的数据,包括发起申请数、新增客户数、聊天数、发送消息数和删除/拉黑成员的客户数等指标。
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 第三方应用需拥有“企业客户”权限。
+     * 第三方/自建应用调用时传入的userid和partyid要在应用的可见范围内;
+     * </pre>
+     *
+     * @param startTime the start time
+     * @param endTime   the end time
+     * @param userIds   the user ids
+     * @param partyIds  the party ids
+     * @return user behavior statistic
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获取指定日期全天的统计数据。注意,企业微信仅存储60天的数据。
+     * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+     * 暂不支持第三方调用。
+     * </pre>
+     *
+     * @param startTime the start time
+     * @param orderBy   the order by
+     * @param orderAsc  the order asc
+     * @param pageIndex the page index
+     * @param pageSize  the page size
+     * @param userIds   the user ids
+     * @param partyIds  the party ids
+     * @return group chat statistic
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, Integer pageIndex, Integer pageSize, String[] userIds, String[] partyIds) throws WxErrorException;
+
+    /**
+     * 添加企业群发消息任务
+     * 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上)
+     * 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本)
+     * 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。
+     * <p>
+     * 请求方式: POST(HTTP)
+     * <p>
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=ACCESS_TOKEN
+     * <p>
+     * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92135
+     *
+     * @param wxCpMsgTemplate the wx cp msg template
+     * @return the wx cp msg template add result
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpMsgTemplateAddResult addMsgTemplate(WxCpMsgTemplate wxCpMsgTemplate) throws WxErrorException;
+
+    /**
+     * 发送新客户欢迎语
+     * <pre>
+     * 企业微信在向企业推送添加外部联系人事件时,会额外返回一个welcome_code,企业以此为凭据调用接口,即可通过成员向新添加的客户发送个性化的欢迎语。
+     * 为了保证用户体验以及避免滥用,企业仅可在收到相关事件后20秒内调用,且只可调用一次。
+     * 如果企业已经在管理端为相关成员配置了可用的欢迎语,则推送添加外部联系人事件时不会返回welcome_code。
+     * 每次添加新客户时可能有多个企业自建应用/第三方应用收到带有welcome_code的回调事件,但仅有最先调用的可以发送成功。后续调用将返回41051(externaluser has started chatting)错误,请用户根据实际使用需求,合理设置应用可见范围,避免冲突。
+     * 请求方式: POST(HTTP)
+     *
+     * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=ACCESS_TOKEN
+     *
+     * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92137
+     * </pre>
+     *
+     * @param msg .
+     * @throws WxErrorException .
+     */
+    void sendWelcomeMsg(WxCpWelcomeMsg msg) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口获取企业客户标签详情。
+     * </pre>
+     *
+     * @param tagId the tag id
+     * @return corp tag list
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口获取企业客户标签详情。
+     * 若tag_id和group_id均为空,则返回所有标签。
+     * 同时传递tag_id和group_id时,忽略tag_id,仅以group_id作为过滤条件。
+     * </pre>
+     *
+     * @param tagId   the tag id
+     * @param groupId the tagGroup id
+     * @return corp tag list
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalTagGroupList getCorpTagList(String[] tagId, String[] groupId) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口向客户标签库中添加新的标签组和标签,每个企业最多可配置3000个企业标签。
+     * 暂不支持第三方调用。
+     * </pre>
+     *
+     * @param tagGroup the tag group
+     * @return wx cp user external tag group info
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpUserExternalTagGroupInfo addCorpTag(WxCpUserExternalTagGroupInfo tagGroup) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口编辑客户标签/标签组的名称或次序值。
+     * 暂不支持第三方调用。
+     * </pre>
+     *
+     * @param id    the id
+     * @param name  the name
+     * @param order the order
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp editCorpTag(String id, String name, Integer order) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口删除客户标签库中的标签,或删除整个标签组。
+     * 暂不支持第三方调用。
+     * </pre>
+     *
+     * @param tagId   the tag id
+     * @param groupId the group id
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 企业可通过此接口为指定成员的客户添加上由企业统一配置的标签。
+     * https://work.weixin.qq.com/api/doc/90000/90135/92117
+     * </pre>
+     *
+     * @param userid         the userid
+     * @param externalUserid the external userid
+     * @param addTag         the add tag
+     * @param removeTag      the remove tag
+     * @return wx cp base resp
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpBaseResp markTag(String userid, String externalUserid, String[] addTag, String[] removeTag) throws WxErrorException;
+
+
+}

+ 91 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpGroupRobotService.java

@@ -0,0 +1,91 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.article.NewArticle;
+
+import java.util.List;
+
+/**
+ * 微信群机器人消息发送api
+ * 文档地址:https://work.weixin.qq.com/help?doc_id=13376
+ * 调用地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=
+ *
+ * @author yr
+ * @date 2020-8-20
+ */
+public interface WxCpGroupRobotService {
+
+  /**
+   * 发送text类型的消息
+   *
+   * @param content       文本内容,最长不超过2048个字节,必须是utf8编码
+   * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list
+   * @param mobileList    手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
+   * @throws WxErrorException 异常
+   */
+  void sendText(String content, List<String> mentionedList, List<String> mobileList) throws WxErrorException;
+
+  /**
+   * 发送markdown类型的消息
+   *
+   * @param content markdown内容,最长不超过4096个字节,必须是utf8编码
+   * @throws WxErrorException 异常
+   */
+  void sendMarkdown(String content) throws WxErrorException;
+
+  /**
+   * 发送image类型的消息
+   *
+   * @param base64 图片内容的base64编码
+   * @param md5    图片内容(base64编码前)的md5值
+   * @throws WxErrorException 异常
+   */
+  void sendImage(String base64, String md5) throws WxErrorException;
+
+  /**
+   * 发送news类型的消息
+   *
+   * @param articleList 图文消息,支持1到8条图文
+   * @throws WxErrorException 异常
+   */
+  void sendNews(List<NewArticle> articleList) throws WxErrorException;
+
+  /**
+   * 发送text类型的消息
+   *
+   * @param webhookUrl    webhook地址
+   * @param content       文本内容,最长不超过2048个字节,必须是utf8编码
+   * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list
+   * @param mobileList    手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
+   * @throws WxErrorException 异常
+   */
+  void sendText(String webhookUrl, String content, List<String> mentionedList, List<String> mobileList) throws WxErrorException;
+
+  /**
+   * 发送markdown类型的消息
+   *
+   * @param webhookUrl webhook地址
+   * @param content    markdown内容,最长不超过4096个字节,必须是utf8编码
+   * @throws WxErrorException 异常
+   */
+  void sendMarkdown(String webhookUrl, String content) throws WxErrorException;
+
+  /**
+   * 发送image类型的消息
+   *
+   * @param webhookUrl webhook地址
+   * @param base64     图片内容的base64编码
+   * @param md5        图片内容(base64编码前)的md5值
+   * @throws WxErrorException 异常
+   */
+  void sendImage(String webhookUrl, String base64, String md5) throws WxErrorException;
+
+  /**
+   * 发送news类型的消息
+   *
+   * @param webhookUrl  webhook地址
+   * @param articleList 图文消息,支持1到8条图文
+   * @throws WxErrorException 异常
+   */
+  void sendNews(String webhookUrl, List<NewArticle> articleList) throws WxErrorException;
+}

+ 86 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMediaService.java

@@ -0,0 +1,86 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.entity.result.WxMediaUploadResult;
+import cn.nosum.wx.common.error.WxErrorException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * <pre>
+ *  媒体管理接口.
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpMediaService {
+
+  /**
+   * <pre>
+   * 上传多媒体文件.
+   * 上传的多媒体文件有格式和大小限制,如下:
+   *   图片(image): 1M,支持JPG格式
+   *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
+   *   视频(video):10MB,支持MP4格式
+   *   缩略图(thumb):64KB,支持JPG格式
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * </pre>
+   *
+   * @param mediaType   媒体类型, 请看{@link cn.nosum.wx.common.api.WxConsts}
+   * @param fileType    文件类型,请看{@link cn.nosum.wx.common.api.WxConsts}
+   * @param inputStream 输入流,需要调用方控制关闭该输入流
+   */
+  WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException;
+
+  /**
+   * 上传多媒体文件.
+   *
+   * @param mediaType 媒体类型
+   * @param file      文件对象
+   * @see #upload(String, String, InputStream)
+   */
+  WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 下载多媒体文件.
+   * 根据微信文档,视频文件下载不了,会返回null
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * </pre>
+   *
+   * @param mediaId 媒体id
+   * @return 保存到本地的临时文件
+   */
+  File download(String mediaId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 获取高清语音素材.
+   * 可以使用本接口获取从JSSDK的uploadVoice接口上传的临时语音素材,格式为speex,16K采样率。该音频比上文的临时素材获取接口(格式为amr,8K采样率)更加清晰,适合用作语音识别等对音质要求较高的业务。
+   * 请求方式:GET(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+   * 仅企业微信2.4及以上版本支持。
+   * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90255
+   * </pre>
+   *
+   * @param mediaId 媒体id
+   * @return 保存到本地的临时文件
+   */
+  File getJssdkFile(String mediaId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 上传图片.
+   * 上传图片得到图片URL,该URL永久有效
+   * 返回的图片URL,仅能用于图文消息(mpnews)正文中的图片展示;若用于非企业微信域名下的页面,图片将被屏蔽。
+   * 每个企业每天最多可上传100张图片
+   * 接口url格式:https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN
+   * </pre>
+   *
+   * @param file 上传的文件对象
+   * @return 返回图片url
+   */
+  String uploadImg(File file) throws WxErrorException;
+}

+ 93 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMenuService.java

@@ -0,0 +1,93 @@
+package cn.nosum.wx.cp.api;
+
+
+import cn.nosum.wx.common.entity.menu.WxMenu;
+import cn.nosum.wx.common.error.WxErrorException;
+
+/**
+ * <pre>
+ *  菜单管理相关接口
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpMenuService {
+
+  /**
+   * <pre>
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * </pre>
+   *
+   * @param menu 菜单对象
+   * @see #create(Integer, WxMenu)
+   */
+  void create(WxMenu menu) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * </pre>
+   *
+   * @param agentId 企业号应用的id
+   * @param menu    菜单对象
+   * @see #create(cn.nosum.wx.common.bean.menu.WxMenu)
+   */
+  void create(Integer agentId, WxMenu menu) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * </pre>
+   *
+   * @see #delete(Integer)
+   */
+  void delete() throws WxErrorException;
+
+  /**
+   * <pre>
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * </pre>
+   *
+   * @param agentId 企业号应用的id
+   * @see #delete()
+   */
+  void delete(Integer agentId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * </pre>
+   *
+   * @see #get(Integer)
+   */
+  WxMenu get() throws WxErrorException;
+
+  /**
+   * <pre>
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * </pre>
+   *
+   * @param agentId 企业号应用的id
+   * @see #get()
+   */
+  WxMenu get(Integer agentId) throws WxErrorException;
+}

+ 53 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpMessageService.java

@@ -0,0 +1,53 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.message.*;
+
+/**
+ * 消息推送接口.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020 -08-30
+ */
+public interface WxCpMessageService {
+  /**
+   * <pre>
+   * 发送消息
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
+   * </pre>
+   *
+   * @param message 要发送的消息对象
+   * @return the wx cp message send result
+   * @throws WxErrorException the wx error exception
+   */
+  WxCpMessageSendResult send(WxCpMessage message) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 查询应用消息发送统计
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
+   *
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
+   * </pre>
+   *
+   * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。
+   * @return 统计结果
+   * @throws WxErrorException the wx error exception
+   */
+  WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 互联企业的应用支持推送文本、图片、视频、文件、图文等类型。
+   *
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
+   * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
+   * </pre>
+   *
+   * @param message 要发送的消息对象
+   * @return the wx cp message send result
+   * @throws WxErrorException the wx error exception
+   */
+  WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message) throws WxErrorException;
+}

+ 105 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOAuth2Service.java

@@ -0,0 +1,105 @@
+package cn.nosum.wx.cp.api;
+
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpOauth2UserInfo;
+import cn.nosum.wx.cp.entity.WxCpUserDetail;
+
+/**
+ * <pre>
+ * OAuth2相关管理接口.
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpOAuth2Service {
+
+  /**
+   * <pre>
+   * 构造oauth2授权的url连接.
+   * </pre>
+   *
+   * @param state 状态码
+   * @return url
+   */
+  String buildAuthorizationUrl(String state);
+
+  /**
+   * <pre>
+   * 构造oauth2授权的url连接.
+   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
+   * </pre>
+   *
+   * @param redirectUri 跳转链接地址
+   * @param state       状态码
+   * @return url
+   */
+  String buildAuthorizationUrl(String redirectUri, String state);
+
+  /**
+   * <pre>
+   * 构造oauth2授权的url连接
+   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
+   * </pre>
+   *
+   * @param redirectUri 跳转链接地址
+   * @param state       状态码
+   * @param scope       取值参考cn.nosum.wx.common.api.WxConsts.OAuth2Scope类
+   * @return url
+   */
+  String buildAuthorizationUrl(String redirectUri, String state, String scope);
+
+  /**
+   * <pre>
+   * 用oauth2获取用户信息
+   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * </pre>
+   *
+   * @param code 微信oauth授权返回的代码
+   * @return WxCpOauth2UserInfo
+   * @throws WxErrorException 异常
+   * @see #getUserInfo(Integer, String)
+   */
+  WxCpOauth2UserInfo getUserInfo(String code) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 根据code获取成员信息
+   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+   * https://work.weixin.qq.com/api/doc#10028/根据code获取成员信息
+   * https://work.weixin.qq.com/api/doc#90000/90135/91023  获取访问用户身份
+   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * </pre>
+   *
+   * @param agentId 企业号应用的id
+   * @param code    通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
+   * @return WxCpOauth2UserInfo
+   * @throws WxErrorException 异常
+   * @see #getUserInfo(String)
+   */
+  WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 使用user_ticket获取成员详情.
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc#10028/%E4%BD%BF%E7%94%A8user_ticket%E8%8E%B7%E5%8F%96%E6%88%90%E5%91%98%E8%AF%A6%E6%83%85
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN
+   *
+   * 权限说明:
+   * 需要有对应应用的使用权限,且成员必须在授权应用的可见范围内。
+   * </pre>
+   *
+   * @param userTicket 成员票据
+   * @return WxCpUserDetail
+   * @throws WxErrorException 异常
+   */
+  WxCpUserDetail getUserDetail(String userTicket) throws WxErrorException;
+}

+ 84 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaCalendarService.java

@@ -0,0 +1,84 @@
+package cn.nosum.wx.cp.api;
+
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.oa.calendar.WxCpOaCalendar;
+
+import java.util.List;
+
+/**
+ * 企业微信日历接口.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-09-20
+ */
+public interface WxCpOaCalendarService {
+  /**
+   * 创建日历.
+   * <pre>
+   * 该接口用于通过应用在企业内创建一个日历。
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/add?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92618
+   * </pre>
+   *
+   * @param calendar 日历对象
+   * @return 日历ID
+   * @throws WxErrorException .
+   */
+  String add(WxCpOaCalendar calendar) throws WxErrorException;
+
+  /**
+   * 更新日历.
+   * <pre>
+   * 该接口用于修改指定日历的信息。
+   * 注意,更新操作是覆盖式,而不是增量式
+   * 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/update?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92619
+   * </pre>
+   *
+   * @param calendar 日历对象
+   * @throws WxErrorException .
+   */
+  void update(WxCpOaCalendar calendar) throws WxErrorException;
+
+  /**
+   * 获取日历.
+   * <pre>
+   * 该接口用于获取应用在企业内创建的日历信息。
+   *
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   *
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/get?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92621
+   * </pre>
+   *
+   * @param calIds 日历id列表
+   * @return 日历对象列表
+   * @throws WxErrorException .
+   */
+  List<WxCpOaCalendar> get(List<String> calIds) throws WxErrorException;
+
+  /**
+   * 删除日历.
+   * <pre>
+   * 该接口用于删除指定日历。
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/del?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92620
+   * </pre>
+   *
+   * @param calId 日历id
+   * @throws WxErrorException .
+   */
+  void delete(String calId) throws WxErrorException;
+}

+ 89 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaScheduleService.java

@@ -0,0 +1,89 @@
+package cn.nosum.wx.cp.api;
+
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.oa.WxCpOaSchedule;
+
+import java.util.List;
+
+/**
+ * 企业微信日程接口.
+ * 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/93648
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020 -12-25
+ */
+public interface WxCpOaScheduleService {
+  /**
+   * 创建日程
+   * <p>
+   * 该接口用于在日历中创建一个日程。
+   * <p>
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/add?access_token=ACCESS_TOKEN
+   *
+   * @param schedule the schedule
+   * @param agentId  授权方安装的应用agentid。仅旧的第三方多应用套件需要填此参数
+   * @return 日程ID string
+   * @throws WxErrorException the wx error exception
+   */
+  String add(WxCpOaSchedule schedule, Integer agentId) throws WxErrorException;
+
+  /**
+   * 更新日程
+   * <p>
+   * 该接口用于在日历中更新指定的日程。
+   * <p>
+   * 注意,更新操作是覆盖式,而不是增量式
+   * 不可更新组织者和日程所属日历ID
+   * <p>
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/update?access_token=ACCESS_TOKEN
+   *
+   * @param schedule the schedule
+   * @throws WxErrorException the wx error exception
+   */
+  void update(WxCpOaSchedule schedule) throws WxErrorException;
+
+  /**
+   * 获取日程详情
+   * <p>
+   * 该接口用于获取指定的日程详情。
+   * <p>
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/get?access_token=ACCESS_TOKEN
+   *
+   * @param scheduleIds the schedule ids
+   * @return the details
+   * @throws WxErrorException the wx error exception
+   */
+  List<WxCpOaSchedule> getDetails(List<String> scheduleIds) throws WxErrorException;
+
+  /**
+   * 取消日程
+   * 该接口用于取消指定的日程。
+   * <p>
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/del?access_token=ACCESS_TOKEN
+   *
+   * @param scheduleId 日程id
+   * @throws WxErrorException the wx error exception
+   */
+  void delete(String scheduleId) throws WxErrorException;
+
+  /**
+   * 获取日历下的日程列表
+   * 该接口用于获取指定的日历下的日程列表。
+   * 仅可获取应用自己创建的日历下的日程。
+   * <p>
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/get_by_calendar?access_token=ACCESS_TOKEN
+   *
+   * @param calId  日历ID
+   * @param offset 分页,偏移量, 默认为0
+   * @param limit  分页,预期请求的数据量,默认为500,取值范围 1 ~ 1000
+   * @return the string
+   * @throws WxErrorException the wx error exception
+   */
+  List<WxCpOaSchedule> listByCalendar(String calId, Integer offset, Integer limit) throws WxErrorException;
+}

+ 190 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpOaService.java

@@ -0,0 +1,190 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.oa.*;
+import lombok.NonNull;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企业微信OA相关接口.
+ *
+ * @author Element
+ * @date 2019-04-06 10:52
+ */
+public interface WxCpOaService {
+
+    /**
+     * <pre>提交审批申请
+     * 调试工具
+     * 企业可通过审批应用或自建应用Secret调用本接口,代应用可见范围内员工在企业微信“审批应用”内提交指定类型的审批申请。
+     *
+     * 请求方式:POST(HTTPS)
+     * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN
+     * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91853
+     * </pre>
+     *
+     * @param request 请求
+     * @return 表单提交成功后 ,返回的表单编号
+     * @throws WxErrorException .
+     */
+    String apply(WxCpOaApplyEventRequest request) throws WxErrorException;
+
+    /**
+     * <pre>
+     *  获取打卡数据
+     *  API doc : https://work.weixin.qq.com/api/doc#90000/90135/90262
+     * </pre>
+     *
+     * @param openCheckinDataType 打卡类型。1:上下班打卡;2:外出打卡;3:全部打卡
+     * @param startTime           获取打卡记录的开始时间
+     * @param endTime             获取打卡记录的结束时间
+     * @param userIdList          需要获取打卡记录的用户列表
+     * @return 打卡数据列表 checkin data
+     * @throws WxErrorException 异常
+     */
+    List<WxCpCheckinData> getCheckinData(Integer openCheckinDataType, Date startTime, Date endTime,
+                                         List<String> userIdList) throws WxErrorException;
+
+    /**
+     * <pre>
+     *   获取打卡规则
+     *   API doc : https://work.weixin.qq.com/api/doc#90000/90135/90263
+     * </pre>
+     *
+     * @param datetime   需要获取规则的当天日期
+     * @param userIdList 需要获取打卡规则的用户列表
+     * @return 打卡规则列表 checkin option
+     * @throws WxErrorException .
+     */
+    List<WxCpCheckinOption> getCheckinOption(Date datetime, List<String> userIdList) throws WxErrorException;
+
+
+    /**
+     * <pre>
+     *   获取企业所有打卡规则
+     *   API doc : https://work.weixin.qq.com/api/doc/90000/90135/93384
+     * </pre>
+     *
+     * @return 打卡规则列表
+     * @throws WxErrorException the wx error exception
+     */
+    List<WxCpCropCheckinOption> getCropCheckinOption() throws WxErrorException;
+
+    /**
+     * <pre>
+     *
+     * 批量获取审批单号
+     *
+     * 审批应用及有权限的自建应用,可通过Secret调用本接口,以获取企业一段时间内企业微信“审批应用”单据的审批编号,支持按模板类型、申请人、部门、申请单审批状态等条件筛选。
+     * 自建应用调用此接口,需在“管理后台-应用管理-审批-API-审批数据权限”中,授权应用允许提交审批单据。
+     *
+     * 一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。
+     *
+     * API doc : https://work.weixin.qq.com/api/doc/90000/90135/91816
+     * </pre>
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @param cursor    分页查询游标,默认为0,后续使用返回的next_cursor进行分页拉取
+     * @param size      一次请求拉取审批单数量,默认值为100,上限值为100
+     * @param filters   筛选条件,可对批量拉取的审批申请设置约束条件,支持设置多个条件,nullable
+     * @return WxCpApprovalInfo approval info
+     * @throws WxErrorException .
+     */
+    WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, Integer cursor, Integer size,
+                                     List<WxCpApprovalInfoQueryFilter> filters) throws WxErrorException;
+
+    /**
+     * short method
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return WxCpApprovalInfo approval info
+     * @throws WxErrorException .
+     * @see WxCpOaService#getApprovalInfo cn.nosum.wx.cp.api.WxCpOaService#getApprovalInfo
+     */
+    WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime) throws WxErrorException;
+
+    /**
+     * <pre>
+     *   获取审批申请详情
+     *
+     *   企业可通过审批应用或自建应用Secret调用本接口,根据审批单号查询企业微信“审批应用”的审批申请详情。
+     *
+     *   API Doc : https://work.weixin.qq.com/api/doc/90000/90135/91983
+     * </pre>
+     *
+     * @param spNo 审批单编号。
+     * @return WxCpApprovaldetail approval detail
+     * @throws WxErrorException .
+     */
+    WxCpApprovalDetailResult getApprovalDetail(@NonNull String spNo) throws WxErrorException;
+
+    /**
+     * 获取公费电话拨打记录
+     *
+     * @param startTime 查询的起始时间戳
+     * @param endTime   查询的结束时间戳
+     * @param offset    分页查询的偏移量
+     * @param limit     分页查询的每页大小,默认为100条,如该参数大于100则按100处理
+     * @return . dial record
+     * @throws WxErrorException .
+     */
+    List<WxCpDialRecord> getDialRecord(Date startTime, Date endTime, Integer offset,
+                                       Integer limit) throws WxErrorException;
+
+    /**
+     * 获取审批模板详情
+     *
+     * @param templateId 模板ID
+     * @return . template detail
+     * @throws WxErrorException .
+     */
+    WxCpTemplateResult getTemplateDetail(@NonNull String templateId) throws WxErrorException;
+
+
+    /**
+     * 获取打卡日报数据
+     *
+     * @param startTime  获取日报的开始时间
+     * @param endTime    获取日报的结束时间
+     * @param userIdList 获取日报的userid列表
+     * @return 日报数据列表 checkin day data
+     * @throws WxErrorException the wx error exception
+     */
+    List<WxCpCheckinDayData> getCheckinDayData(Date startTime, Date endTime, List<String> userIdList) throws WxErrorException;
+
+
+    /**
+     * 获取打卡月报数据
+     *
+     * @param startTime  获取月报的开始时间
+     * @param endTime    获取月报的结束时间
+     * @param userIdList 获取月报的userid列表
+     * @return 月报数据列表
+     * @throws WxErrorException the wx error exception
+     */
+    List<WxCpCheckinMonthData> getCheckinMonthData(Date startTime, Date endTime, List<String> userIdList) throws WxErrorException;
+
+    /**
+     * 获取打卡人员排班信息
+     *
+     * @param startTime  获取排班信息的开始时间。Unix时间戳
+     * @param endTime    获取排班信息的结束时间。Unix时间戳(与starttime跨度不超过一个月)
+     * @param userIdList 需要获取排班信息的用户列表(不超过100个)
+     * @return 排班表信息
+     * @throws WxErrorException the wx error exception
+     */
+    List<WxCpCheckinSchedule> getCheckinScheduleList(Date startTime, Date endTime, List<String> userIdList) throws WxErrorException;
+
+
+    /**
+     * 为打卡人员排班
+     *
+     * @param wxCpSetCheckinSchedule the wx cp set checkin schedule
+     * @throws WxErrorException the wx error exception
+     */
+    void setCheckinScheduleList(WxCpSetCheckinSchedule wxCpSetCheckinSchedule) throws WxErrorException;
+}

+ 422 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpService.java

@@ -0,0 +1,422 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.http.RequestExecutor;
+import cn.nosum.http.RequestHttp;
+import cn.nosum.wx.common.entity.WxJsapiSignature;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.common.service.WxService;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.entity.WxCpMaJsCode2SessionResult;
+import cn.nosum.wx.cp.entity.WxCpProviderToken;
+
+/**
+ * 微信API的Service.
+ *
+ * @author chanjaster
+ */
+public interface WxCpService extends WxService {
+    /**
+     * <pre>
+     * 验证推送过来的消息的正确性
+     * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=验证消息真实性
+     * </pre>
+     *
+     * @param msgSignature 消息签名
+     * @param timestamp    时间戳
+     * @param nonce        随机数
+     * @param data         微信传输过来的数据,有可能是echoStr,有可能是xml消息
+     * @return the boolean
+     */
+    boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
+
+    /**
+     * 获取access_token, 不强制刷新access_token
+     *
+     * @return the access token
+     * @throws WxErrorException the wx error exception
+     * @see #getAccessToken(boolean) #getAccessToken(boolean)#getAccessToken(boolean)
+     */
+    String getAccessToken() throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获取access_token,本方法线程安全
+     * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+     * 另:本service的所有方法都会在access_token过期是调用此方法
+     * 程序员在非必要情况下尽量不要主动调用此方法
+     * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token
+     * </pre>
+     *
+     * @param forceRefresh 强制刷新
+     * @return the access token
+     * @throws WxErrorException the wx error exception
+     */
+    String getAccessToken(boolean forceRefresh) throws WxErrorException;
+
+    /**
+     * 获得jsapi_ticket,不强制刷新jsapi_ticket
+     *
+     * @return the jsapi ticket
+     * @throws WxErrorException the wx error exception
+     * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)
+     */
+    String getJsapiTicket() throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获得jsapi_ticket
+     * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+     *
+     * 详情请见:http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
+     * </pre>
+     *
+     * @param forceRefresh 强制刷新
+     * @return the jsapi ticket
+     * @throws WxErrorException the wx error exception
+     */
+    String getJsapiTicket(boolean forceRefresh) throws WxErrorException;
+
+    /**
+     * 获得jsapi_ticket,不强制刷新jsapi_ticket
+     * 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别:
+     * <p>
+     * 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。
+     * 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。
+     *
+     * @return the agent jsapi ticket
+     * @throws WxErrorException the wx error exception
+     * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)
+     */
+    String getAgentJsapiTicket() throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获取应用的jsapi_ticket
+     * 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别:
+     *
+     * 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。
+     * 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。
+     *
+     * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+     *
+     * 详情请见:https://work.weixin.qq.com/api/doc#10029/%E8%8E%B7%E5%8F%96%E5%BA%94%E7%94%A8%E7%9A%84jsapi_ticket
+     * </pre>
+     *
+     * @param forceRefresh 强制刷新
+     * @return the agent jsapi ticket
+     * @throws WxErrorException the wx error exception
+     */
+    String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 创建调用jsapi时所需要的签名
+     *
+     * 详情请见:http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
+     * </pre>
+     *
+     * @param url url
+     * @return the wx jsapi signature
+     * @throws WxErrorException the wx error exception
+     */
+    WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
+
+
+    /**
+     * 小程序登录凭证校验
+     *
+     * @param jsCode 登录时获取的 code
+     * @return the wx cp ma js code 2 session result
+     * @throws WxErrorException the wx error exception
+     */
+    WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获取微信服务器的ip段
+     * http://qydev.weixin.qq.com/wiki/index.php?title=回调模式#.E8.8E.B7.E5.8F.96.E5.BE.AE.E4.BF.A1.E6.9C.8D.E5.8A.A1.E5.99.A8.E7.9A.84ip.E6.AE.B5
+     * </pre>
+     *
+     * @return { "ip_list": ["101.226.103.*", "101.226.62.*"] }
+     * @throws WxErrorException the wx error exception
+     */
+    String[] getCallbackIp() throws WxErrorException;
+
+    /**
+     * <pre>
+     * 获取服务商凭证
+     * 文档地址:https://work.weixin.qq.com/api/doc#90001/90143/91200
+     * 请求方式:POST(HTTPS)
+     * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token
+     * </pre>
+     *
+     * @param corpId         服务商的corpid
+     * @param providerSecret 服务商的secret,在服务商管理后台可见
+     * @return { "errcode":0 , "errmsg":"ok" , "provider_access_token":"enLSZ5xxxxxxJRL", "expires_in":7200 }
+     * @throws WxErrorException .
+     */
+    WxCpProviderToken getProviderToken(String corpId, String providerSecret) throws WxErrorException;
+
+    /**
+     * 当不需要自动带accessToken的时候,可以用这个发起post请求
+     *
+     * @param url      接口地址
+     * @param postData 请求body字符串
+     * @return the string
+     * @throws WxErrorException the wx error exception
+     */
+    String postWithoutToken(String url, String postData) throws WxErrorException;
+
+    /**
+     * <pre>
+     * Service没有实现某个API的时候,可以用这个,
+     * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型
+     * </pre>
+     *
+     * @param <T>      请求值类型
+     * @param <E>      返回值类型
+     * @param executor 执行器
+     * @param uri      请求地址
+     * @param data     参数
+     * @return the t
+     * @throws WxErrorException the wx error exception
+     */
+    <T, R> R execute(RequestExecutor<T, R> executor, String uri, T data) throws WxErrorException;
+
+    /**
+     * <pre>
+     * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试
+     * 默认:1000ms
+     * </pre>
+     *
+     * @param retrySleepMillis 重试休息时间
+     */
+    void setRetrySleepMillis(int retrySleepMillis);
+
+    /**
+     * <pre>
+     * 设置当微信系统响应系统繁忙时,最大重试次数
+     * 默认:5次
+     * </pre>
+     *
+     * @param maxRetryTimes 最大重试次数
+     */
+    void setMaxRetryTimes(int maxRetryTimes);
+
+    /**
+     * 上传部门列表覆盖企业号上的部门信息
+     *
+     * @param mediaId 媒体id
+     * @return the string
+     * @throws WxErrorException the wx error exception
+     */
+    String replaceParty(String mediaId) throws WxErrorException;
+
+    /**
+     * 上传用户列表覆盖企业号上的用户信息
+     *
+     * @param mediaId 媒体id
+     * @return the string
+     * @throws WxErrorException the wx error exception
+     */
+    String replaceUser(String mediaId) throws WxErrorException;
+
+    /**
+     * 获取异步任务结果
+     *
+     * @param joinId the join id
+     * @return the task result
+     * @throws WxErrorException the wx error exception
+     */
+    String getTaskResult(String joinId) throws WxErrorException;
+
+    /**
+     * 初始化http请求对象
+     */
+    void initHttp();
+
+    /**
+     * 获取WxCpConfigStorage 对象
+     *
+     * @return WxCpConfigStorage wx cp config storage
+     */
+    WxCpConfigStorage getWxCpConfigStorage();
+
+    /**
+     * 注入 {@link WxCpConfigStorage} 的实现
+     *
+     * @param wxConfigProvider 配置对象
+     */
+    void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider);
+
+    /**
+     * 构造扫码登录链接 - 构造独立窗口登录二维码
+     *
+     * @param redirectUri 重定向地址,需要进行UrlEncode
+     * @param state       用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验
+     * @return .
+     */
+    String buildQrConnectUrl(String redirectUri, String state);
+
+    /**
+     * 获取部门相关接口的服务类对象
+     *
+     * @return the department service
+     */
+    WxCpDepartmentService getDepartmentService();
+
+    /**
+     * 获取媒体相关接口的服务类对象
+     *
+     * @return the media service
+     */
+    WxCpMediaService getMediaService();
+
+    /**
+     * 获取菜单相关接口的服务类对象
+     *
+     * @return the menu service
+     */
+    WxCpMenuService getMenuService();
+
+    /**
+     * 获取Oauth2相关接口的服务类对象
+     *
+     * @return the oauth 2 service
+     */
+    WxCpOAuth2Service getOauth2Service();
+
+    /**
+     * 获取标签相关接口的服务类对象
+     *
+     * @return the tag service
+     */
+    WxCpTagService getTagService();
+
+    /**
+     * 获取用户相关接口的服务类对象
+     *
+     * @return the user service
+     */
+    WxCpUserService getUserService();
+
+    /**
+     * Gets external contact service.
+     *
+     * @return the external contact service
+     */
+    WxCpExternalContactService getExternalContactService();
+
+    /**
+     * 获取群聊服务
+     *
+     * @return 群聊服务 chat service
+     */
+    WxCpChatService getChatService();
+
+    /**
+     * 获取任务卡片服务
+     *
+     * @return 任务卡片服务 task card service
+     */
+    WxCpTaskCardService getTaskCardService();
+
+    /**
+     * Gets agent service.
+     *
+     * @return the agent service
+     */
+    WxCpAgentService getAgentService();
+
+    /**
+     * Gets message service.
+     *
+     * @return the message service
+     */
+    WxCpMessageService getMessageService();
+
+    /**
+     * Gets oa service.
+     *
+     * @return the oa service
+     */
+    WxCpOaService getOaService();
+
+    /**
+     * 获取日历相关接口的服务类对象
+     *
+     * @return the oa calendar service
+     */
+    WxCpOaCalendarService getOaCalendarService();
+
+    /**
+     * 获取日程相关接口的服务类对象
+     *
+     * @return the oa schedule service
+     */
+    WxCpOaScheduleService getOaScheduleService();
+
+    /**
+     * 获取群机器人消息推送服务
+     *
+     * @return 群机器人消息推送服务 group robot service
+     */
+    WxCpGroupRobotService getGroupRobotService();
+
+    /**
+     * 获取工作台服务
+     *
+     * @return the workbench service
+     */
+    WxCpAgentWorkBenchService getWorkBenchService();
+
+    /**
+     * http请求对象
+     *
+     * @return the request http
+     */
+    RequestHttp<?, ?> getRequestHttp();
+
+    /**
+     * Sets user service.
+     *
+     * @param userService the user service
+     */
+    void setUserService(WxCpUserService userService);
+
+    /**
+     * Sets department service.
+     *
+     * @param departmentService the department service
+     */
+    void setDepartmentService(WxCpDepartmentService departmentService);
+
+    /**
+     * Sets media service.
+     *
+     * @param mediaService the media service
+     */
+    void setMediaService(WxCpMediaService mediaService);
+
+    /**
+     * Sets menu service.
+     *
+     * @param menuService the menu service
+     */
+    void setMenuService(WxCpMenuService menuService);
+
+    /**
+     * Sets oauth 2 service.
+     *
+     * @param oauth2Service the oauth 2 service
+     */
+    void setOauth2Service(WxCpOAuth2Service oauth2Service);
+
+    /**
+     * Sets tag service.
+     *
+     * @param tagService the tag service
+     */
+    void setTagService(WxCpTagService tagService);
+
+}

+ 99 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpTagService.java

@@ -0,0 +1,99 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpTag;
+import cn.nosum.wx.cp.entity.WxCpTagAddOrRemoveUsersResult;
+import cn.nosum.wx.cp.entity.WxCpTagGetResult;
+import cn.nosum.wx.cp.entity.WxCpUser;
+import java.util.List;
+
+/**
+ * <pre>
+ *  标签管理接口.
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpTagService {
+  /**
+   * 创建标签.
+   * <pre>
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=ACCESS_TOKEN
+   * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90210
+   * </pre>
+   *
+   * @param name 标签名称,长度限制为32个字以内(汉字或英文字母),标签名不可与其他标签重名。
+   * @param id   标签id,非负整型,指定此参数时新增的标签会生成对应的标签id,不指定时则以目前最大的id自增。
+   * @return 标签id
+   * @throws WxErrorException .
+   */
+  String create(String name, Integer id) throws WxErrorException;
+
+  /**
+   * 更新标签.
+   *
+   * @param tagId   标签id
+   * @param tagName 标签名
+   * @throws WxErrorException .
+   */
+  void update(String tagId, String tagName) throws WxErrorException;
+
+  /**
+   * 删除标签.
+   *
+   * @param tagId 标签id
+   * @throws WxErrorException .
+   */
+  void delete(String tagId) throws WxErrorException;
+
+  /**
+   * 获得标签列表.
+   *
+   * @return 标签列表
+   * @throws WxErrorException .
+   */
+  List<WxCpTag> listAll() throws WxErrorException;
+
+  /**
+   * 获取标签成员.
+   *
+   * @param tagId 标签ID
+   * @return 成员列表
+   * @throws WxErrorException .
+   */
+  List<WxCpUser> listUsersByTagId(String tagId) throws WxErrorException;
+
+  /**
+   * 获取标签成员.
+   * 对应: http://qydev.weixin.qq.com/wiki/index.php?title=管理标签 中的get接口
+   *
+   * @param tagId 标签id
+   * @return .
+   * @throws WxErrorException .
+   */
+  WxCpTagGetResult get(String tagId) throws WxErrorException;
+
+  /**
+   * 增加标签成员.
+   *
+   * @param tagId    标签id
+   * @param userIds  用户ID 列表
+   * @param partyIds 企业部门ID列表
+   * @return .
+   * @throws WxErrorException .
+   */
+  WxCpTagAddOrRemoveUsersResult addUsers2Tag(String tagId, List<String> userIds, List<String> partyIds) throws WxErrorException;
+
+  /**
+   * 移除标签成员.
+   *
+   * @param tagId    标签id
+   * @param userIds  用户id列表
+   * @param partyIds 企业部门ID列表
+   * @return .
+   * @throws WxErrorException .
+   */
+  WxCpTagAddOrRemoveUsersResult removeUsersFromTag(String tagId, List<String> userIds, List<String> partyIds) throws WxErrorException;
+
+}

+ 31 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpTaskCardService.java

@@ -0,0 +1,31 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *  任务卡片管理接口.
+ *  Created by Jeff on 2019-05-16.
+ * </pre>
+ *
+ * @author <a href="https://github.com/domainname">Jeff</a>
+ * @date 2019-05-16
+ */
+public interface WxCpTaskCardService {
+
+  /**
+   * <pre>
+   * 更新任务卡片消息状态
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/91579
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * </pre>
+   *
+   * @param userIds    企业的成员ID列表
+   * @param taskId     任务卡片ID
+   * @param replaceName 替换文案
+   */
+  void update(List<String> userIds, String taskId, String replaceName) throws WxErrorException;
+}

+ 202 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/WxCpUserService.java

@@ -0,0 +1,202 @@
+package cn.nosum.wx.cp.api;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.entity.WxCpInviteResult;
+import cn.nosum.wx.cp.entity.WxCpUser;
+import cn.nosum.wx.cp.entity.external.contact.WxCpExternalContactInfo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <pre>
+ * 用户管理接口
+ *  Created by BinaryWang on 2017/6/24.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpUserService {
+
+  /**
+   * <pre>
+   *   用在二次验证的时候.
+   *   企业在员工验证成功后,调用本方法告诉企业号平台该员工关注成功。
+   * </pre>
+   *
+   * @param userId 用户id
+   * @throws WxErrorException the wx error exception
+   */
+  void authenticate(String userId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 获取部门成员详情
+   * 请求方式:GET(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90201
+   * </pre>
+   *
+   * @param departId   必填。部门id
+   * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+   * @param status     非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+   * @return the list
+   * @throws WxErrorException the wx error exception
+   */
+  List<WxCpUser> listByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 获取部门成员.
+   *
+   * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
+   * </pre>
+   *
+   * @param departId   必填。部门id
+   * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+   * @param status     非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+   * @return the list
+   * @throws WxErrorException the wx error exception
+   */
+  List<WxCpUser> listSimpleByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException;
+
+  /**
+   * 新建用户.
+   *
+   * @param user 用户对象
+   * @throws WxErrorException the wx error exception
+   */
+  void create(WxCpUser user) throws WxErrorException;
+
+  /**
+   * 更新用户.
+   *
+   * @param user 用户对象
+   * @throws WxErrorException the wx error exception
+   */
+  void update(WxCpUser user) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 删除用户/批量删除成员.
+   * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E6.89.B9.E9.87.8F.E5.88.A0.E9.99.A4.E6.88.90.E5.91.98
+   * </pre>
+   *
+   * @param userIds 员工UserID列表。对应管理端的帐号
+   * @throws WxErrorException the wx error exception
+   */
+  void delete(String... userIds) throws WxErrorException;
+
+  /**
+   * 获取用户.
+   *
+   * @param userid 用户id
+   * @return the by id
+   * @throws WxErrorException the wx error exception
+   */
+  WxCpUser getById(String userid) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 邀请成员.
+   * 企业可通过接口批量邀请成员使用企业微信,邀请后将通过短信或邮件下发通知。
+   * 请求方式:POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=ACCESS_TOKEN
+   * 文档地址:https://work.weixin.qq.com/api/doc#12543
+   * </pre>
+   *
+   * @param userIds  成员ID列表, 最多支持1000个。
+   * @param partyIds 部门ID列表,最多支持100个。
+   * @param tagIds   标签ID列表,最多支持100个。
+   * @return the wx cp invite result
+   * @throws WxErrorException the wx error exception
+   */
+  WxCpInviteResult invite(List<String> userIds, List<String> partyIds, List<String> tagIds) throws WxErrorException;
+
+  /**
+   * <pre>
+   *  userid转openid.
+   *  该接口使用场景为微信支付、微信红包和企业转账。
+   *
+   * 在使用微信支付的功能时,需要自行将企业微信的userid转成openid。
+   * 在使用微信红包功能时,需要将应用id和userid转成appid和openid才能使用。
+   * 注:需要成员使用微信登录企业微信或者关注微信插件才能转成openid
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc#11279
+   * </pre>
+   *
+   * @param userId  企业内的成员id
+   * @param agentId 非必填,整型,仅用于发红包。其它场景该参数不要填,如微信支付、企业转账、电子发票
+   * @return map对象 ,可能包含以下值: - openid 企业微信成员userid对应的openid,若有传参agentid,则是针对该agentid的openid。否则是针对企业微信corpid的openid - appid 应用的appid,若请求包中不包含agentid则不返回appid。该appid在使用微信红包时会用到
+   * @throws WxErrorException the wx error exception
+   */
+  Map<String, String> userId2Openid(String userId, Integer agentId) throws WxErrorException;
+
+  /**
+   * <pre>
+   * openid转userid.
+   *
+   * 该接口主要应用于使用微信支付、微信红包和企业转账之后的结果查询。
+   * 开发者需要知道某个结果事件的openid对应企业微信内成员的信息时,可以通过调用该接口进行转换查询。
+   * 权限说明:
+   * 管理组需对openid对应的企业微信成员有查看权限。
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc#11279
+   * </pre>
+   *
+   * @param openid 在使用微信支付、微信红包和企业转账之后,返回结果的openid
+   * @return userid 该openid在企业微信对应的成员userid
+   * @throws WxErrorException the wx error exception
+   */
+  String openid2UserId(String openid) throws WxErrorException;
+
+  /**
+   * <pre>
+   *
+   * 通过手机号获取其所对应的userid。
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc#90001/90143/91693
+   * </pre>
+   *
+   * @param mobile 手机号码。长度为5~32个字节
+   * @return userid mobile对应的成员userid
+   * @throws WxErrorException .
+   */
+  String getUserId(String mobile) throws WxErrorException;
+
+  /**
+   * 获取外部联系人详情.
+   * <pre>
+   *   企业可通过此接口,根据外部联系人的userid,拉取外部联系人详情。权限说明:
+   * 企业需要使用外部联系人管理secret所获取的accesstoken来调用
+   * 第三方应用需拥有“企业客户”权限。
+   * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+   * </pre>
+   *
+   * @param userId 外部联系人的userid
+   * @return 联系人详情 external contact
+   * @throws WxErrorException .
+   */
+  WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException;
+
+  /**
+   * <pre>
+   *
+   * 获取加入企业二维码。
+   *
+   * 请求方式:GET(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode?access_token=ACCESS_TOKEN&size_type=SIZE_TYPE
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91714
+   * </pre>
+   *
+   * @param sizeType qrcode尺寸类型,1: 171 x 171; 2: 399 x 399; 3: 741 x 741; 4: 2052 x 2052
+   * @return join_qrcode 二维码链接,有效期7天
+   * @throws WxErrorException .
+   */
+  String getJoinQrCode(int sizeType) throws WxErrorException;
+}

+ 501 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/BaseWxCpServiceImpl.java

@@ -0,0 +1,501 @@
+package cn.nosum.wx.cp.api.impl;
+
+import cn.nosum.http.RequestExecutor;
+import cn.nosum.http.RequestHttp;
+import cn.nosum.http.SimpleGetRequestExecutor;
+import cn.nosum.http.SimplePostRequestExecutor;
+import cn.nosum.http.exception.HttpError;
+import cn.nosum.http.exception.HttpErrorException;
+import cn.nosum.http.utils.UriUtil;
+import cn.nosum.wx.common.api.WxConsts;
+import cn.nosum.wx.common.entity.ToJson;
+import cn.nosum.wx.common.entity.WxJsapiSignature;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.common.error.WxRuntimeException;
+import cn.nosum.wx.common.utils.DataUtils;
+import cn.nosum.wx.common.utils.RandomUtils;
+import cn.nosum.wx.common.utils.crypto.SHA1;
+import cn.nosum.wx.common.utils.json.GsonParser;
+import cn.nosum.wx.cp.api.*;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.entity.WxCpMaJsCode2SessionResult;
+import cn.nosum.wx.cp.entity.WxCpProviderToken;
+import com.google.common.base.Joiner;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cn.nosum.wx.cp.constant.WxCpApiPathConsts.*;
+
+/**
+ * .
+ *
+ * @author chanjarster
+ */
+@Slf4j
+public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestHttp<H, P> {
+
+    private WxCpUserService userService = null;
+    private WxCpChatService chatService = null;
+    private WxCpDepartmentService departmentService = null;
+    private WxCpMediaService mediaService = null;
+    private WxCpMenuService menuService = null;
+    private WxCpOAuth2Service oauth2Service = null;
+    private WxCpTagService tagService = null;
+    private WxCpAgentService agentService = null;
+    private WxCpOaService oaService = null;
+    private WxCpTaskCardService taskCardService = null;
+    private WxCpExternalContactService externalContactService = null;
+    private WxCpGroupRobotService groupRobotService = null;
+    private WxCpMessageService messageService = null;
+    private WxCpOaCalendarService oaCalendarService = null;
+    private WxCpOaScheduleService oaScheduleService = null;
+    private WxCpAgentWorkBenchService workBenchService = null;
+
+    /**
+     * 全局的是否正在刷新access token的锁.
+     */
+    protected final Object globalAccessTokenRefreshLock = new Object();
+
+    /**
+     * 全局的是否正在刷新jsapi_ticket的锁.
+     */
+    protected final Object globalJsapiTicketRefreshLock = new Object();
+
+    /**
+     * 全局的是否正在刷新agent的jsapi_ticket的锁.
+     */
+    protected final Object globalAgentJsapiTicketRefreshLock = new Object();
+
+    protected WxCpConfigStorage configStorage;
+
+    /**
+     * 临时文件目录.
+     */
+    private File tmpDirFile;
+    private int retrySleepMillis = 1000;
+    private int maxRetryTimes = 5;
+
+    @Override
+    public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
+        try {
+            return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data).equals(msgSignature);
+        } catch (Exception e) {
+            log.error("Checking signature failed, and the reason is :" + e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public String getAccessToken() throws WxErrorException {
+        return getAccessToken(false);
+    }
+
+    @Override
+    public String getAgentJsapiTicket() throws WxErrorException {
+        return this.getAgentJsapiTicket(false);
+    }
+
+    @Override
+    public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
+        if (forceRefresh) {
+            this.configStorage.expireAgentJsapiTicket();
+        }
+
+        if (this.configStorage.isAgentJsapiTicketExpired()) {
+            synchronized (this.globalAgentJsapiTicketRefreshLock) {
+                if (this.configStorage.isAgentJsapiTicketExpired()) {
+                    String responseContent = this.get(this.configStorage.getApiUrl(GET_AGENT_CONFIG_TICKET), null);
+                    JsonObject jsonObject = GsonParser.parse(responseContent);
+                    this.configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(),
+                            jsonObject.get("expires_in").getAsInt());
+                }
+            }
+        }
+
+        return this.configStorage.getAgentJsapiTicket();
+    }
+
+    @Override
+    public String getJsapiTicket() throws WxErrorException {
+        return getJsapiTicket(false);
+    }
+
+    @Override
+    public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+        if (forceRefresh) {
+            this.configStorage.expireJsapiTicket();
+        }
+
+        if (this.configStorage.isJsapiTicketExpired()) {
+            synchronized (this.globalJsapiTicketRefreshLock) {
+                if (this.configStorage.isJsapiTicketExpired()) {
+                    String responseContent = this.get(this.configStorage.getApiUrl(GET_JSAPI_TICKET), null);
+                    JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+                    this.configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(),
+                            tmpJsonObject.get("expires_in").getAsInt());
+                }
+            }
+        }
+
+        return this.configStorage.getJsapiTicket();
+    }
+
+    @Override
+    public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
+        long timestamp = System.currentTimeMillis() / 1000;
+        String noncestr = RandomUtils.getRandomStr();
+        String jsapiTicket = getJsapiTicket(false);
+        String signature = SHA1.genWithAmple(
+                "jsapi_ticket=" + jsapiTicket,
+                "noncestr=" + noncestr,
+                "timestamp=" + timestamp,
+                "url=" + url
+        );
+        WxJsapiSignature jsapiSignature = new WxJsapiSignature();
+        jsapiSignature.setTimestamp(timestamp);
+        jsapiSignature.setNonceStr(noncestr);
+        jsapiSignature.setUrl(url);
+        jsapiSignature.setSignature(signature);
+
+        // Fixed bug
+        jsapiSignature.setAppId(this.configStorage.getCorpId());
+
+        return jsapiSignature;
+    }
+
+    @Override
+    public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException {
+        Map<String, String> params = new HashMap<>(2);
+        params.put("js_code", jsCode);
+        params.put("grant_type", "authorization_code");
+
+        final String url = this.configStorage.getApiUrl(JSCODE_TO_SESSION);
+        return WxCpMaJsCode2SessionResult.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params)));
+    }
+
+    @Override
+    public String[] getCallbackIp() throws WxErrorException {
+        String responseContent = get(this.configStorage.getApiUrl(GET_CALLBACK_IP), null);
+        JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+        JsonArray jsonArray = tmpJsonObject.get("ip_list").getAsJsonArray();
+        String[] ips = new String[jsonArray.size()];
+        for (int i = 0; i < jsonArray.size(); i++) {
+            ips[i] = jsonArray.get(i).getAsString();
+        }
+        return ips;
+    }
+
+    @Override
+    public WxCpProviderToken getProviderToken(String corpId, String providerSecret) throws WxErrorException {
+        JsonObject jsonObject = new JsonObject();
+        jsonObject.addProperty("corpid", corpId);
+        jsonObject.addProperty("provider_secret", providerSecret);
+        return WxCpProviderToken.fromJson(this.post(this.configStorage.getApiUrl(Tp.GET_PROVIDER_TOKEN), jsonObject.toString()));
+    }
+
+    @Override
+    public String get(String url, String queryParam) throws WxErrorException {
+        return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
+    }
+
+    @Override
+    public String post(String url, String postData) throws WxErrorException {
+        return execute(SimplePostRequestExecutor.create(this), url, postData);
+    }
+
+    @Override
+    public String post(String url, JsonObject jsonObject) throws WxErrorException {
+        return this.post(url, jsonObject.toString());
+    }
+
+    @Override
+    public String post(String url, ToJson obj) throws WxErrorException {
+        return this.post(url, obj.toJson());
+    }
+
+    @Override
+    public String post(String url, Object obj) throws WxErrorException {
+        return this.post(url, obj.toString());
+    }
+
+    @Override
+    public String postWithoutToken(String url, String postData) throws WxErrorException {
+        return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData);
+    }
+
+    /**
+     * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
+     */
+    @Override
+    public <T, R> R execute(RequestExecutor<T, R> executor, String uri, T data) throws WxErrorException {
+        int retryTimes = 0;
+        do {
+            try {
+                return this.executeInternal(executor, uri, data, false);
+            } catch (WxErrorException e) {
+                if (retryTimes + 1 > this.maxRetryTimes) {
+                    log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+                    //最后一次重试失败后,直接抛出异常,不再等待
+                    throw new WxRuntimeException("微信服务端异常,超出重试次数");
+                }
+
+                HttpError error = e.getError();
+                /*
+                 * -1 系统繁忙, 1000ms后重试
+                 */
+                if (error.getErrorCode() == -1) {
+                    int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
+                    try {
+                        log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
+                        Thread.sleep(sleepMillis);
+                    } catch (InterruptedException e1) {
+                        Thread.currentThread().interrupt();
+                    }
+                } else {
+                    throw e;
+                }
+            }
+        } while (retryTimes++ < this.maxRetryTimes);
+
+        log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+        throw new WxRuntimeException("微信服务端异常,超出重试次数");
+    }
+
+    protected <T, R> R executeInternal(RequestExecutor<T, R> executor, String uri, T data, boolean doNotAutoRefresh) throws WxErrorException {
+        T dataForLog = DataUtils.handleDataWithSecret(data);
+
+        if (uri.contains("access_token=")) {
+            throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
+        }
+        String accessToken = getAccessToken(false);
+
+        String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
+
+        try {
+            R result = executor.execute(uriWithAccessToken, data);
+            log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
+            return result;
+        } catch (HttpErrorException e) {
+            HttpError error = e.getError();
+
+            if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
+                // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
+                this.configStorage.expireAccessToken();
+                if (this.getWxCpConfigStorage().autoRefreshToken() && !doNotAutoRefresh) {
+                    log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
+                    //下一次不再自动重试
+                    //当小程序误调用第三方平台专属接口时,第三方无法使用小程序的access token,如果可以继续自动获取token会导致无限循环重试,直到栈溢出
+                    return this.executeInternal(executor, uri, data, true);
+                }
+            }
+
+            if (error.getErrorCode() != 0) {
+                log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error);
+                throw new WxErrorException(error, e);
+            }
+            return null;
+        } catch (IOException e) {
+            log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage());
+            throw new WxRuntimeException(e);
+        }
+    }
+
+    /**
+     * 普通请求,不自动带accessToken
+     */
+    private <T, R> R executeNormal(RequestExecutor<T, R> executor, String uri, T data) throws WxErrorException {
+        try {
+            R result = executor.execute(uri, data);
+            log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
+            return result;
+        } catch (HttpErrorException e) {
+            HttpError error = e.getError();
+            if (error.getErrorCode() != 0) {
+                log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
+                throw new WxErrorException(error, e);
+            }
+            return null;
+        } catch (IOException e) {
+            log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
+            throw new WxErrorException(e);
+        }
+    }
+
+    @Override
+    public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
+        this.configStorage = wxConfigProvider;
+        this.initHttp();
+    }
+
+    @Override
+    public void setRetrySleepMillis(int retrySleepMillis) {
+        this.retrySleepMillis = retrySleepMillis;
+    }
+
+
+    @Override
+    public void setMaxRetryTimes(int maxRetryTimes) {
+        this.maxRetryTimes = maxRetryTimes;
+    }
+
+    @Override
+    public String replaceParty(String mediaId) throws WxErrorException {
+        JsonObject jsonObject = new JsonObject();
+        jsonObject.addProperty("media_id", mediaId);
+        return post(this.configStorage.getApiUrl(BATCH_REPLACE_PARTY), jsonObject.toString());
+    }
+
+    @Override
+    public String replaceUser(String mediaId) throws WxErrorException {
+        JsonObject jsonObject = new JsonObject();
+        jsonObject.addProperty("media_id", mediaId);
+        return post(this.configStorage.getApiUrl(BATCH_REPLACE_USER), jsonObject.toString());
+    }
+
+    @Override
+    public String getTaskResult(String joinId) throws WxErrorException {
+        String url = this.configStorage.getApiUrl(BATCH_GET_RESULT + joinId);
+        return get(url, null);
+    }
+
+    @Override
+    public String buildQrConnectUrl(String redirectUri, String state) {
+        return String.format("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s",
+                this.configStorage.getCorpId(), this.configStorage.getAgentId(),
+                UriUtil.encodeUriComponent(redirectUri), StringUtils.trimToEmpty(state));
+    }
+
+    public File getTmpDirFile() {
+        return this.tmpDirFile;
+    }
+
+    public void setTmpDirFile(File tmpDirFile) {
+        this.tmpDirFile = tmpDirFile;
+    }
+
+    @Override
+    public WxCpDepartmentService getDepartmentService() {
+        return departmentService;
+    }
+
+    @Override
+    public WxCpMediaService getMediaService() {
+        return mediaService;
+    }
+
+    @Override
+    public WxCpMenuService getMenuService() {
+        return menuService;
+    }
+
+    @Override
+    public WxCpOAuth2Service getOauth2Service() {
+        return oauth2Service;
+    }
+
+    @Override
+    public WxCpTagService getTagService() {
+        return tagService;
+    }
+
+    @Override
+    public WxCpUserService getUserService() {
+        return userService;
+    }
+
+    @Override
+    public WxCpExternalContactService getExternalContactService() {
+        return externalContactService;
+    }
+
+    @Override
+    public WxCpChatService getChatService() {
+        return chatService;
+    }
+
+    @Override
+    public WxCpOaService getOaService() {
+        return oaService;
+    }
+
+    @Override
+    public WxCpOaCalendarService getOaCalendarService() {
+        return this.oaCalendarService;
+    }
+
+    @Override
+    public WxCpGroupRobotService getGroupRobotService() {
+        return groupRobotService;
+    }
+
+    @Override
+    public WxCpAgentWorkBenchService getWorkBenchService() {
+        return workBenchService;
+    }
+
+    @Override
+    public WxCpTaskCardService getTaskCardService() {
+        return taskCardService;
+    }
+
+    @Override
+    public RequestHttp<?, ?> getRequestHttp() {
+        return this;
+    }
+
+    @Override
+    public void setUserService(WxCpUserService userService) {
+        this.userService = userService;
+    }
+
+    @Override
+    public void setDepartmentService(WxCpDepartmentService departmentService) {
+        this.departmentService = departmentService;
+    }
+
+    @Override
+    public void setMediaService(WxCpMediaService mediaService) {
+        this.mediaService = mediaService;
+    }
+
+    @Override
+    public void setMenuService(WxCpMenuService menuService) {
+        this.menuService = menuService;
+    }
+
+    @Override
+    public void setOauth2Service(WxCpOAuth2Service oauth2Service) {
+        this.oauth2Service = oauth2Service;
+    }
+
+    @Override
+    public void setTagService(WxCpTagService tagService) {
+        this.tagService = tagService;
+    }
+
+    @Override
+    public WxCpAgentService getAgentService() {
+        return agentService;
+    }
+
+    @Override
+    public WxCpMessageService getMessageService() {
+        return this.messageService;
+    }
+
+    public void setAgentService(WxCpAgentService agentService) {
+        this.agentService = agentService;
+    }
+
+    @Override
+    public WxCpOaScheduleService getOaScheduleService() {
+        return this.oaScheduleService;
+    }
+}

+ 46 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpMessageServiceImpl.java

@@ -0,0 +1,46 @@
+package cn.nosum.wx.cp.api.impl;
+
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.api.WxCpMessageService;
+import cn.nosum.wx.cp.api.WxCpService;
+import cn.nosum.wx.cp.entity.message.*;
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+import com.google.common.collect.ImmutableMap;
+import lombok.RequiredArgsConstructor;
+
+import static cn.nosum.wx.cp.constant.WxCpApiPathConsts.*;
+
+/**
+ * 消息推送接口实现类.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ * @date 2020-08-30
+ */
+@RequiredArgsConstructor
+public class WxCpMessageServiceImpl implements WxCpMessageService {
+  private final WxCpService cpService;
+
+  @Override
+  public WxCpMessageSendResult send(WxCpMessage message) throws WxErrorException {
+    Integer agentId = message.getAgentId();
+    if (null == agentId) {
+      message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
+    }
+    return WxCpMessageSendResult.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(Message.MESSAGE_SEND), message.toJson()));
+  }
+
+  @Override
+  public WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorException {
+    return WxCpMessageSendStatistics.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(Message.GET_STATISTICS),
+      WxCpGsonBuilder.create().toJson(ImmutableMap.of("time_type", timeType))));
+  }
+
+  @Override
+  public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message) throws WxErrorException {
+    Integer agentId = message.getAgentId();
+    if (null == agentId) {
+      message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
+    }
+    return WxCpLinkedCorpMessageSendResult.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(Message.LINKEDCORP_MESSAGE_SEND), message.toJson()));
+  }
+}

+ 105 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceApacheHttpClientImpl.java

@@ -0,0 +1,105 @@
+package cn.nosum.wx.cp.api.impl;
+
+
+import cn.nosum.http.apache.ApacheHttpClientBuilder;
+import cn.nosum.http.apache.DefaultApacheHttpClientBuilder;
+import cn.nosum.http.enums.HttpType;
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.common.error.WxError;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.common.error.WxRuntimeException;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.constant.WxCpApiPathConsts;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+
+/**
+ * @author someone
+ */
+public class WxCpServiceApacheHttpClientImpl extends BaseWxCpServiceImpl<CloseableHttpClient, HttpHost> {
+  private CloseableHttpClient httpClient;
+  private HttpHost httpProxy;
+
+  @Override
+  public CloseableHttpClient getRequestHttpClient() {
+    return httpClient;
+  }
+
+  @Override
+  public HttpHost getRequestHttpProxy() {
+    return httpProxy;
+  }
+
+  @Override
+  public HttpType getRequestType() {
+    return HttpType.APACHE_HTTP;
+  }
+
+  @Override
+  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getAccessToken();
+    }
+
+    synchronized (this.globalAccessTokenRefreshLock) {
+      String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret());
+
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (this.httpProxy != null) {
+          RequestConfig config = RequestConfig.custom()
+            .setProxy(this.httpProxy).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);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new WxRuntimeException(e);
+      }
+    }
+    return this.configStorage.getAccessToken();
+  }
+
+  @Override
+  public void initHttp() {
+    ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage.getApacheHttpClientBuilder();
+    if (null == apacheHttpClientBuilder) {
+      apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
+    }
+
+    apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost())
+      .httpProxyPort(this.configStorage.getHttpProxyPort())
+      .httpProxyUsername(this.configStorage.getHttpProxyUsername())
+      .httpProxyPassword(this.configStorage.getHttpProxyPassword());
+
+    if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
+      this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort());
+    }
+
+    this.httpClient = apacheHttpClientBuilder.build();
+  }
+
+  @Override
+  public WxCpConfigStorage getWxCpConfigStorage() {
+    return this.configStorage;
+  }
+
+}

+ 127 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceImpl.java

@@ -0,0 +1,127 @@
+package cn.nosum.wx.cp.api.impl;
+
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.common.error.WxError;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.common.error.WxRuntimeException;
+import cn.nosum.wx.common.utils.json.GsonParser;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.constant.WxCpApiPathConsts;
+import com.google.gson.JsonObject;
+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 cn.nosum.wx.cp.constant.WxCpApiPathConsts.GET_TOKEN;
+
+/**
+ * <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 {
+    final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+    if (!configStorage.isAccessTokenExpired() && !forceRefresh) {
+      return configStorage.getAccessToken();
+    }
+    Lock lock = configStorage.getAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!configStorage.isAccessTokenExpired() && !forceRefresh) {
+        return configStorage.getAccessToken();
+      }
+      String url = String.format(configStorage.getApiUrl(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);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new WxRuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return configStorage.getAccessToken();
+  }
+
+  @Override
+  public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+    if (forceRefresh) {
+      configStorage.expireAgentJsapiTicket();
+    }
+    if (configStorage.isAgentJsapiTicketExpired()) {
+      Lock lock = configStorage.getAgentJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (configStorage.isAgentJsapiTicketExpired()) {
+          String responseContent = this.get(configStorage.getApiUrl(WxCpApiPathConsts.GET_AGENT_CONFIG_TICKET), null);
+          JsonObject jsonObject = GsonParser.parse(responseContent);
+          configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(),
+            jsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return configStorage.getAgentJsapiTicket();
+  }
+
+  @Override
+  public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+    if (forceRefresh) {
+      configStorage.expireJsapiTicket();
+    }
+
+    if (configStorage.isJsapiTicketExpired()) {
+      Lock lock = configStorage.getJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (configStorage.isJsapiTicketExpired()) {
+          String responseContent = this.get(configStorage.getApiUrl(WxCpApiPathConsts.GET_JSAPI_TICKET), null);
+          JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+          configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(),
+            tmpJsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return configStorage.getJsapiTicket();
+  }
+}

+ 78 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceJoddHttpImpl.java

@@ -0,0 +1,78 @@
+package cn.nosum.wx.cp.api.impl;
+
+
+import cn.nosum.http.enums.HttpType;
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.common.error.WxError;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.constant.WxCpApiPathConsts;
+import jodd.http.HttpConnectionProvider;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.http.ProxyInfo;
+import jodd.http.net.SocketHttpConnectionProvider;
+
+/**
+ * @author someone
+ */
+public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl<HttpConnectionProvider, ProxyInfo> {
+  private HttpConnectionProvider httpClient;
+  private ProxyInfo httpProxy;
+
+  @Override
+  public HttpConnectionProvider getRequestHttpClient() {
+    return httpClient;
+  }
+
+  @Override
+  public ProxyInfo getRequestHttpProxy() {
+    return httpProxy;
+  }
+
+  @Override
+  public HttpType getRequestType() {
+    return HttpType.JODD_HTTP;
+  }
+
+  @Override
+  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getAccessToken();
+    }
+
+    synchronized (this.globalAccessTokenRefreshLock) {
+      HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+        this.configStorage.getCorpId(), this.configStorage.getCorpSecret()));
+      if (this.httpProxy != null) {
+        httpClient.useProxy(this.httpProxy);
+      }
+      request.withConnectionProvider(httpClient);
+      HttpResponse response = request.send();
+
+      String resultContent = response.bodyText();
+      WxError error = WxError.fromJson(resultContent);
+      if (error.getErrorCode() != 0) {
+        throw new WxErrorException(error);
+      }
+      WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+      this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+    }
+    return this.configStorage.getAccessToken();
+  }
+
+  @Override
+  public void initHttp() {
+    if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
+      httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(),
+        configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
+    }
+
+    httpClient = new SocketHttpConnectionProvider();
+  }
+
+  @Override
+  public WxCpConfigStorage getWxCpConfigStorage() {
+    return this.configStorage;
+  }
+}

+ 101 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/api/impl/WxCpServiceOkHttpImpl.java

@@ -0,0 +1,101 @@
+package cn.nosum.wx.cp.api.impl;
+
+import cn.nosum.http.enums.HttpType;
+import cn.nosum.http.okhttp.OkHttpProxyInfo;
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.common.error.WxError;
+import cn.nosum.wx.common.error.WxErrorException;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+
+import java.io.IOException;
+
+import static cn.nosum.wx.cp.constant.WxCpApiPathConsts.GET_TOKEN;
+/**
+ * @author someone
+ */
+@Slf4j
+public class WxCpServiceOkHttpImpl extends BaseWxCpServiceImpl<OkHttpClient, OkHttpProxyInfo> {
+
+  private OkHttpClient httpClient;
+  private OkHttpProxyInfo httpProxy;
+
+  @Override
+  public OkHttpClient getRequestHttpClient() {
+    return httpClient;
+  }
+
+  @Override
+  public OkHttpProxyInfo getRequestHttpProxy() {
+    return httpProxy;
+  }
+
+  @Override
+  public HttpType getRequestType() {
+    return HttpType.OK_HTTP;
+  }
+
+  @Override
+  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getAccessToken();
+    }
+
+    synchronized (this.globalAccessTokenRefreshLock) {
+      //得到httpClient
+      OkHttpClient client = getRequestHttpClient();
+      //请求的request
+      Request request = new Request.Builder()
+        .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret()))
+        .get()
+        .build();
+      String resultContent = null;
+      try {
+        Response response = client.newCall(request).execute();
+        resultContent = response.body().string();
+      } catch (IOException e) {
+        log.error(e.getMessage(), e);
+      }
+
+      WxError error = WxError.fromJson(resultContent);
+      if (error.getErrorCode() != 0) {
+        throw new WxErrorException(error);
+      }
+      WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+      this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+    }
+    return this.configStorage.getAccessToken();
+  }
+
+  @Override
+  public void initHttp() {
+    log.debug("WxCpServiceOkHttpImpl initHttp");
+    //设置代理
+    if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
+      httpProxy = OkHttpProxyInfo.httpProxy(configStorage.getHttpProxyHost(),
+        configStorage.getHttpProxyPort(),
+        configStorage.getHttpProxyUsername(),
+        configStorage.getHttpProxyPassword());
+    }
+
+    OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
+    if (httpProxy != null) {
+      clientBuilder.proxy(getRequestHttpProxy().getProxy());
+
+      //设置授权
+      clientBuilder.authenticator((route, response) -> {
+        String credential = Credentials.basic(httpProxy.getProxyUsername(), httpProxy.getProxyPassword());
+        return response.request().newBuilder()
+          .header("Authorization", credential)
+          .build();
+      });
+    }
+    httpClient = clientBuilder.build();
+  }
+
+  @Override
+  public WxCpConfigStorage getWxCpConfigStorage() {
+    return this.configStorage;
+  }
+}

+ 246 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/config/WxCpConfigStorage.java

@@ -0,0 +1,246 @@
+package cn.nosum.wx.cp.config;
+
+import cn.nosum.http.apache.ApacheHttpClientBuilder;
+import cn.nosum.wx.common.entity.WxAccessToken;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 微信客户端配置存储.
+ *
+ * @author Daniel Qian
+ */
+public interface WxCpConfigStorage {
+
+  /**
+   * 设置企业微信服务器 baseUrl.
+   * 默认值是 https://qyapi.weixin.qq.com , 如果使用默认值,则不需要调用 setBaseApiUrl
+   *
+   * @param baseUrl 企业微信服务器 Url
+   */
+  void setBaseApiUrl(String baseUrl);
+
+  /**
+   * 读取企业微信 API Url.
+   * 支持私有化企业微信服务器.
+   *
+   * @param path the path
+   * @return the api url
+   */
+  String getApiUrl(String path);
+
+  /**
+   * Gets access token.
+   *
+   * @return the access token
+   */
+  String getAccessToken();
+
+  /**
+   * Gets access token lock.
+   *
+   * @return the access token lock
+   */
+  Lock getAccessTokenLock();
+
+  /**
+   * Is access token expired boolean.
+   *
+   * @return the boolean
+   */
+  boolean isAccessTokenExpired();
+
+  /**
+   * 强制将access token过期掉.
+   */
+  void expireAccessToken();
+
+  /**
+   * Update access token.
+   *
+   * @param accessToken the access token
+   */
+  void updateAccessToken(WxAccessToken accessToken);
+
+  /**
+   * Update access token.
+   *
+   * @param accessToken the access token
+   * @param expiresIn   the expires in
+   */
+  void updateAccessToken(String accessToken, int expiresIn);
+
+  /**
+   * Gets jsapi ticket.
+   *
+   * @return the jsapi ticket
+   */
+  String getJsapiTicket();
+
+  /**
+   * Gets jsapi ticket lock.
+   *
+   * @return the jsapi ticket lock
+   */
+  Lock getJsapiTicketLock();
+
+  /**
+   * Is jsapi ticket expired boolean.
+   *
+   * @return the boolean
+   */
+  boolean isJsapiTicketExpired();
+
+  /**
+   * 强制将jsapi ticket过期掉.
+   */
+  void expireJsapiTicket();
+
+  /**
+   * 应该是线程安全的.
+   *
+   * @param jsapiTicket      the jsapi ticket
+   * @param expiresInSeconds the expires in seconds
+   */
+  void updateJsapiTicket(String jsapiTicket, int expiresInSeconds);
+
+  /**
+   * Gets agent jsapi ticket.
+   *
+   * @return the agent jsapi ticket
+   */
+  String getAgentJsapiTicket();
+
+  /**
+   * Gets agent jsapi ticket lock.
+   *
+   * @return the agent jsapi ticket lock
+   */
+  Lock getAgentJsapiTicketLock();
+
+  /**
+   * Is agent jsapi ticket expired boolean.
+   *
+   * @return the boolean
+   */
+  boolean isAgentJsapiTicketExpired();
+
+  /**
+   * 强制将jsapi ticket过期掉.
+   */
+  void expireAgentJsapiTicket();
+
+  /**
+   * 应该是线程安全的.
+   *
+   * @param jsapiTicket      the jsapi ticket
+   * @param expiresInSeconds the expires in seconds
+   */
+  void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds);
+
+  /**
+   * Gets corp id.
+   *
+   * @return the corp id
+   */
+  String getCorpId();
+
+  /**
+   * Gets corp secret.
+   *
+   * @return the corp secret
+   */
+  String getCorpSecret();
+
+  /**
+   * Gets agent id.
+   *
+   * @return the agent id
+   */
+  Integer getAgentId();
+
+  /**
+   * Gets token.
+   *
+   * @return the token
+   */
+  String getToken();
+
+  /**
+   * Gets aes key.
+   *
+   * @return the aes key
+   */
+  String getAesKey();
+
+  /**
+   * Gets expires time.
+   *
+   * @return the expires time
+   */
+  long getExpiresTime();
+
+  /**
+   * Gets oauth 2 redirect uri.
+   *
+   * @return the oauth 2 redirect uri
+   */
+  String getOauth2redirectUri();
+
+  /**
+   * Gets http proxy host.
+   *
+   * @return the http proxy host
+   */
+  String getHttpProxyHost();
+
+  /**
+   * Gets http proxy port.
+   *
+   * @return the http proxy port
+   */
+  int getHttpProxyPort();
+
+  /**
+   * Gets http proxy username.
+   *
+   * @return the http proxy username
+   */
+  String getHttpProxyUsername();
+
+  /**
+   * Gets http proxy password.
+   *
+   * @return the http proxy password
+   */
+  String getHttpProxyPassword();
+
+  /**
+   * Gets tmp dir file.
+   *
+   * @return the tmp dir file
+   */
+  File getTmpDirFile();
+
+  /**
+   * http client builder.
+   *
+   * @return ApacheHttpClientBuilder apache http client builder
+   */
+  ApacheHttpClientBuilder getApacheHttpClientBuilder();
+
+  /**
+   * 是否自动刷新token
+   *
+   * @return . boolean
+   */
+  boolean autoRefreshToken();
+
+  /**
+   * 获取群机器人webhook的key
+   *
+   * @return key webhook key
+   */
+  String getWebhookKey();
+}

+ 405 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/config/impl/WxCpDefaultConfigImpl.java

@@ -0,0 +1,405 @@
+package cn.nosum.wx.cp.config.impl;
+
+import cn.nosum.http.apache.ApacheHttpClientBuilder;
+import cn.nosum.wx.common.entity.WxAccessToken;
+import cn.nosum.wx.cp.config.WxCpConfigStorage;
+import cn.nosum.wx.cp.constant.WxCpApiPathConsts;
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化.
+ *
+ * @author Daniel Qian
+ */
+public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
+  private static final long serialVersionUID = 1154541446729462780L;
+  /**
+   * The Access token.
+   */
+  protected volatile String accessToken;
+  /**
+   * The Access token lock.
+   */
+  protected transient Lock accessTokenLock = new ReentrantLock();
+  /**
+   * The Agent id.
+   */
+  protected volatile Integer agentId;
+  /**
+   * The Jsapi ticket lock.
+   */
+  protected transient Lock jsapiTicketLock = new ReentrantLock();
+  /**
+   * The Agent jsapi ticket lock.
+   */
+  protected transient Lock agentJsapiTicketLock = new ReentrantLock();
+  private volatile String corpId;
+  private volatile String corpSecret;
+  private volatile String token;
+  private volatile String aesKey;
+  private volatile long expiresTime;
+  private volatile String oauth2redirectUri;
+  private volatile String httpProxyHost;
+  private volatile int httpProxyPort;
+  private volatile String httpProxyUsername;
+  private volatile String httpProxyPassword;
+  private volatile String jsapiTicket;
+  private volatile long jsapiTicketExpiresTime;
+  private volatile String agentJsapiTicket;
+  private volatile long agentJsapiTicketExpiresTime;
+
+  private volatile File tmpDirFile;
+
+  private transient volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
+
+  private volatile String baseApiUrl;
+
+  private volatile String webhookKey;
+
+  @Override
+  public void setBaseApiUrl(String baseUrl) {
+    this.baseApiUrl = baseUrl;
+  }
+
+  @Override
+  public String getApiUrl(String path) {
+    if (baseApiUrl == null) {
+      baseApiUrl = WxCpApiPathConsts.DEFAULT_CP_BASE_URL;
+    }
+    return baseApiUrl + path;
+  }
+
+  @Override
+  public String getAccessToken() {
+    return this.accessToken;
+  }
+
+  /**
+   * Sets access token.
+   *
+   * @param accessToken the access token
+   */
+  public void setAccessToken(String accessToken) {
+    this.accessToken = accessToken;
+  }
+
+  @Override
+  public Lock getAccessTokenLock() {
+    return this.accessTokenLock;
+  }
+
+  @Override
+  public boolean isAccessTokenExpired() {
+    return System.currentTimeMillis() > this.expiresTime;
+  }
+
+  @Override
+  public void expireAccessToken() {
+    this.expiresTime = 0;
+  }
+
+  @Override
+  public synchronized void updateAccessToken(WxAccessToken accessToken) {
+    updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+  }
+
+  @Override
+  public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
+    this.accessToken = accessToken;
+    this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
+  }
+
+  @Override
+  public String getJsapiTicket() {
+    return this.jsapiTicket;
+  }
+
+  /**
+   * Sets jsapi ticket.
+   *
+   * @param jsapiTicket the jsapi ticket
+   */
+  public void setJsapiTicket(String jsapiTicket) {
+    this.jsapiTicket = jsapiTicket;
+  }
+
+  @Override
+  public Lock getJsapiTicketLock() {
+    return this.jsapiTicketLock;
+  }
+
+  /**
+   * Gets jsapi ticket expires time.
+   *
+   * @return the jsapi ticket expires time
+   */
+  public long getJsapiTicketExpiresTime() {
+    return this.jsapiTicketExpiresTime;
+  }
+
+  /**
+   * Sets jsapi ticket expires time.
+   *
+   * @param jsapiTicketExpiresTime the jsapi ticket expires time
+   */
+  public void setJsapiTicketExpiresTime(long jsapiTicketExpiresTime) {
+    this.jsapiTicketExpiresTime = jsapiTicketExpiresTime;
+  }
+
+  @Override
+  public boolean isJsapiTicketExpired() {
+    return System.currentTimeMillis() > this.jsapiTicketExpiresTime;
+  }
+
+  @Override
+  public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
+    this.jsapiTicket = jsapiTicket;
+    // 预留200秒的时间
+    this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
+  }
+
+  @Override
+  public String getAgentJsapiTicket() {
+    return this.agentJsapiTicket;
+  }
+
+  @Override
+  public Lock getAgentJsapiTicketLock() {
+    return this.agentJsapiTicketLock;
+  }
+
+  @Override
+  public boolean isAgentJsapiTicketExpired() {
+    return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime;
+  }
+
+  @Override
+  public void expireAgentJsapiTicket() {
+    this.agentJsapiTicketExpiresTime = 0;
+  }
+
+  @Override
+  public void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds) {
+    this.agentJsapiTicket = jsapiTicket;
+    // 预留200秒的时间
+    this.agentJsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
+  }
+
+  @Override
+  public void expireJsapiTicket() {
+    this.jsapiTicketExpiresTime = 0;
+  }
+
+  @Override
+  public String getCorpId() {
+    return this.corpId;
+  }
+
+  /**
+   * Sets corp id.
+   *
+   * @param corpId the corp id
+   */
+  public void setCorpId(String corpId) {
+    this.corpId = corpId;
+  }
+
+  @Override
+  public String getCorpSecret() {
+    return this.corpSecret;
+  }
+
+  /**
+   * Sets corp secret.
+   *
+   * @param corpSecret the corp secret
+   */
+  public void setCorpSecret(String corpSecret) {
+    this.corpSecret = corpSecret;
+  }
+
+  @Override
+  public String getToken() {
+    return this.token;
+  }
+
+  /**
+   * Sets token.
+   *
+   * @param token the token
+   */
+  public void setToken(String token) {
+    this.token = token;
+  }
+
+  @Override
+  public long getExpiresTime() {
+    return this.expiresTime;
+  }
+
+  /**
+   * Sets expires time.
+   *
+   * @param expiresTime the expires time
+   */
+  public void setExpiresTime(long expiresTime) {
+    this.expiresTime = expiresTime;
+  }
+
+  @Override
+  public String getAesKey() {
+    return this.aesKey;
+  }
+
+  /**
+   * Sets aes key.
+   *
+   * @param aesKey the aes key
+   */
+  public void setAesKey(String aesKey) {
+    this.aesKey = aesKey;
+  }
+
+  @Override
+  public Integer getAgentId() {
+    return this.agentId;
+  }
+
+  /**
+   * Sets agent id.
+   *
+   * @param agentId the agent id
+   */
+  public void setAgentId(Integer agentId) {
+    this.agentId = agentId;
+  }
+
+  @Override
+  public String getOauth2redirectUri() {
+    return this.oauth2redirectUri;
+  }
+
+  /**
+   * Sets oauth 2 redirect uri.
+   *
+   * @param oauth2redirectUri the oauth 2 redirect uri
+   */
+  public void setOauth2redirectUri(String oauth2redirectUri) {
+    this.oauth2redirectUri = oauth2redirectUri;
+  }
+
+  @Override
+  public String getHttpProxyHost() {
+    return this.httpProxyHost;
+  }
+
+  /**
+   * Sets http proxy host.
+   *
+   * @param httpProxyHost the http proxy host
+   */
+  public void setHttpProxyHost(String httpProxyHost) {
+    this.httpProxyHost = httpProxyHost;
+  }
+
+  @Override
+  public int getHttpProxyPort() {
+    return this.httpProxyPort;
+  }
+
+  /**
+   * Sets http proxy port.
+   *
+   * @param httpProxyPort the http proxy port
+   */
+  public void setHttpProxyPort(int httpProxyPort) {
+    this.httpProxyPort = httpProxyPort;
+  }
+
+  @Override
+  public String getHttpProxyUsername() {
+    return this.httpProxyUsername;
+  }
+
+  /**
+   * Sets http proxy username.
+   *
+   * @param httpProxyUsername the http proxy username
+   */
+  public void setHttpProxyUsername(String httpProxyUsername) {
+    this.httpProxyUsername = httpProxyUsername;
+  }
+
+  @Override
+  public String getHttpProxyPassword() {
+    return this.httpProxyPassword;
+  }
+
+  /**
+   * Sets http proxy password.
+   *
+   * @param httpProxyPassword the http proxy password
+   */
+  public void setHttpProxyPassword(String httpProxyPassword) {
+    this.httpProxyPassword = httpProxyPassword;
+  }
+
+  @Override
+  public String toString() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+  @Override
+  public File getTmpDirFile() {
+    return this.tmpDirFile;
+  }
+
+  /**
+   * Sets tmp dir file.
+   *
+   * @param tmpDirFile the tmp dir file
+   */
+  public void setTmpDirFile(File tmpDirFile) {
+    this.tmpDirFile = tmpDirFile;
+  }
+
+  @Override
+  public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
+    return this.apacheHttpClientBuilder;
+  }
+
+  /**
+   * Sets apache http client builder.
+   *
+   * @param apacheHttpClientBuilder the apache http client builder
+   */
+  public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
+    this.apacheHttpClientBuilder = apacheHttpClientBuilder;
+  }
+
+  @Override
+  public boolean autoRefreshToken() {
+    return true;
+  }
+
+  @Override
+  public String getWebhookKey() {
+    return this.webhookKey;
+  }
+
+  /**
+   * Sets webhook key.
+   *
+   * @param webhookKey the webhook key
+   * @return the webhook key
+   */
+  public WxCpDefaultConfigImpl setWebhookKey(String webhookKey) {
+    this.webhookKey = webhookKey;
+    return this;
+  }
+}

+ 206 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/constant/WxCpApiPathConsts.java

@@ -0,0 +1,206 @@
+package cn.nosum.wx.cp.constant;
+
+
+/**
+ * <pre>
+ *  企业微信api地址常量类
+ *  Created by BinaryWang on 2019-06-02.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+public interface WxCpApiPathConsts {
+  String DEFAULT_CP_BASE_URL = "https://qyapi.weixin.qq.com";
+
+  String GET_JSAPI_TICKET = "/cgi-bin/get_jsapi_ticket";
+  String GET_AGENT_CONFIG_TICKET = "/cgi-bin/ticket/get?&type=agent_config";
+  String GET_CALLBACK_IP = "/cgi-bin/getcallbackip";
+  String BATCH_REPLACE_PARTY = "/cgi-bin/batch/replaceparty";
+  String BATCH_REPLACE_USER = "/cgi-bin/batch/replaceuser";
+  String BATCH_GET_RESULT = "/cgi-bin/batch/getresult?jobid=";
+  String JSCODE_TO_SESSION = "/cgi-bin/miniprogram/jscode2session";
+  String GET_TOKEN = "/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
+  String WEBHOOK_SEND = "/cgi-bin/webhook/send?key=";
+
+  /**
+   * 消息推送相关接口
+   * https://work.weixin.qq.com/api/doc/90000/90135/90235
+   */
+  interface Message {
+    /**
+     * 发送应用消息
+     */
+    String MESSAGE_SEND = "/cgi-bin/message/send";
+
+    /**
+     * 查询应用消息发送统计
+     */
+    String GET_STATISTICS = "/cgi-bin/message/get_statistics";
+
+    /**
+     * 互联企业发送应用消息
+     */
+    String LINKEDCORP_MESSAGE_SEND = "/cgi-bin/linkedcorp/message/send";
+  }
+
+  interface Agent {
+    String AGENT_GET = "/cgi-bin/agent/get?agentid=%d";
+    String AGENT_SET = "/cgi-bin/agent/set";
+    String AGENT_LIST = "/cgi-bin/agent/list";
+  }
+
+  interface WorkBench {
+    String WORKBENCH_TEMPLATE_SET = "/cgi-bin/agent/set_workbench_template";
+    String WORKBENCH_TEMPLATE_GET = "/cgi-bin/agent/get_workbench_template";
+    String WORKBENCH_DATA_SET = "/cgi-bin/agent/set_workbench_data";
+  }
+
+  interface OAuth2 {
+    String GET_USER_INFO = "/cgi-bin/user/getuserinfo?code=%s&agentid=%d";
+    String GET_USER_DETAIL = "/cgi-bin/user/getuserdetail";
+    String URL_OAUTH2_AUTHORIZE = "https://open.weixin.qq.com/connect/oauth2/authorize";
+  }
+
+  interface Chat {
+    String APPCHAT_CREATE = "/cgi-bin/appchat/create";
+    String APPCHAT_UPDATE = "/cgi-bin/appchat/update";
+    String APPCHAT_GET_CHATID = "/cgi-bin/appchat/get?chatid=";
+    String APPCHAT_SEND = "/cgi-bin/appchat/send";
+  }
+
+  interface Department {
+    String DEPARTMENT_CREATE = "/cgi-bin/department/create";
+    String DEPARTMENT_UPDATE = "/cgi-bin/department/update";
+    String DEPARTMENT_DELETE = "/cgi-bin/department/delete?id=%d";
+    String DEPARTMENT_LIST = "/cgi-bin/department/list";
+  }
+
+  interface Media {
+    String MEDIA_GET = "/cgi-bin/media/get";
+    String MEDIA_UPLOAD = "/cgi-bin/media/upload?type=";
+    String IMG_UPLOAD = "/cgi-bin/media/uploadimg";
+    String JSSDK_MEDIA_GET = "/cgi-bin/media/get/jssdk";
+  }
+
+  interface Menu {
+    String MENU_CREATE = "/cgi-bin/menu/create?agentid=%d";
+    String MENU_DELETE = "/cgi-bin/menu/delete?agentid=%d";
+    String MENU_GET = "/cgi-bin/menu/get?agentid=%d";
+  }
+
+  interface Oa {
+    String GET_CORP_CHECKIN_OPTION = "/cgi-bin/checkin/getcorpcheckinoption";
+    String GET_CHECKIN_DATA = "/cgi-bin/checkin/getcheckindata";
+    String GET_CHECKIN_OPTION = "/cgi-bin/checkin/getcheckinoption";
+    String GET_CHECKIN_DAY_DATA = "/cgi-bin/checkin/getcheckin_daydata";
+    String GET_CHECKIN_MONTH_DATA = "/cgi-bin/checkin/getcheckin_monthdata";
+    String GET_CHECKIN_SCHEDULE_DATA = "/cgi-bin/checkin/getcheckinschedulist";
+    String SET_CHECKIN_SCHEDULE_DATA = "/cgi-bin/checkin/setcheckinschedulist";
+    String GET_APPROVAL_INFO = "/cgi-bin/oa/getapprovalinfo";
+    String GET_APPROVAL_DETAIL = "/cgi-bin/oa/getapprovaldetail";
+    String GET_DIAL_RECORD = "/cgi-bin/dial/get_dial_record";
+    String GET_TEMPLATE_DETAIL = "/cgi-bin/oa/gettemplatedetail";
+    String APPLY_EVENT = "/cgi-bin/oa/applyevent";
+
+    String CALENDAR_ADD = "/cgi-bin/oa/calendar/add";
+    String CALENDAR_UPDATE = "/cgi-bin/oa/calendar/update";
+    String CALENDAR_GET = "/cgi-bin/oa/calendar/get";
+    String CALENDAR_DEL = "/cgi-bin/oa/calendar/del";
+
+    String SCHEDULE_ADD = "/cgi-bin/oa/schedule/add";
+    String SCHEDULE_UPDATE = "/cgi-bin/oa/schedule/update";
+    String SCHEDULE_GET = "/cgi-bin/oa/schedule/get";
+    String SCHEDULE_DEL = "/cgi-bin/oa/schedule/del";
+    String SCHEDULE_LIST = "/cgi-bin/oa/schedule/get_by_calendar";
+
+    String COPY_TEMPLATE = "/cgi-bin/oa/approval/copytemplate";
+  }
+
+  interface Tag {
+    String TAG_CREATE = "/cgi-bin/tag/create";
+    String TAG_UPDATE = "/cgi-bin/tag/update";
+    String TAG_DELETE = "/cgi-bin/tag/delete?tagid=%s";
+    String TAG_LIST = "/cgi-bin/tag/list";
+    String TAG_GET = "/cgi-bin/tag/get?tagid=%s";
+    String TAG_ADD_TAG_USERS = "/cgi-bin/tag/addtagusers";
+    String TAG_DEL_TAG_USERS = "/cgi-bin/tag/deltagusers";
+  }
+
+  interface TaskCard {
+    String UPDATE_TASK_CARD = "/cgi-bin/message/update_taskcard";
+  }
+
+  interface Tp {
+    String JSCODE_TO_SESSION = "/cgi-bin/service/miniprogram/jscode2session";
+    String GET_CORP_TOKEN = "/cgi-bin/service/get_corp_token";
+    String GET_PERMANENT_CODE = "/cgi-bin/service/get_permanent_code";
+    String GET_SUITE_TOKEN = "/cgi-bin/service/get_suite_token";
+    String GET_PROVIDER_TOKEN = "/cgi-bin/service/get_provider_token";
+    String GET_PREAUTH_CODE = "/cgi-bin/service/get_pre_auth_code";
+    String GET_AUTH_INFO = "/cgi-bin/service/get_auth_info";
+    String GET_AUTH_CORP_JSAPI_TICKET = "/cgi-bin/get_jsapi_ticket";
+    String GET_SUITE_JSAPI_TICKET = "/cgi-bin/ticket/get";
+    String GET_USERINFO3RD = "/cgi-bin/service/getuserinfo3rd";
+    String GET_USERDETAIL3RD = "/cgi-bin/service/getuserdetail3rd";
+    String GET_LOGIN_INFO = "/cgi-bin/service/get_login_info";
+
+
+    String CONTACT_SEARCH = "/cgi-bin/service/contact/search";
+    String GET_ADMIN_LIST = "/cgi-bin/service/get_admin_list";
+
+  }
+
+  interface User {
+    String USER_AUTHENTICATE = "/cgi-bin/user/authsucc?userid=";
+    String USER_CREATE = "/cgi-bin/user/create";
+    String USER_UPDATE = "/cgi-bin/user/update";
+    String USER_DELETE = "/cgi-bin/user/delete?userid=";
+    String USER_BATCH_DELETE = "/cgi-bin/user/batchdelete";
+    String USER_GET = "/cgi-bin/user/get?userid=";
+    String USER_LIST = "/cgi-bin/user/list?department_id=";
+    String USER_SIMPLE_LIST = "/cgi-bin/user/simplelist?department_id=";
+    String BATCH_INVITE = "/cgi-bin/batch/invite";
+    String USER_CONVERT_TO_OPENID = "/cgi-bin/user/convert_to_openid";
+    String USER_CONVERT_TO_USERID = "/cgi-bin/user/convert_to_userid";
+    String GET_USER_ID = "/cgi-bin/user/getuserid";
+    String GET_EXTERNAL_CONTACT = "/cgi-bin/crm/get_external_contact?external_userid=";
+    String GET_JOIN_QR_CODE = "/cgi-bin/corp/get_join_qrcode?size_type=";
+  }
+
+  interface ExternalContact {
+    @Deprecated
+    String GET_EXTERNAL_CONTACT = "/cgi-bin/crm/get_external_contact?external_userid=";
+
+    String ADD_CONTACT_WAY = "/cgi-bin/externalcontact/add_contact_way";
+    String GET_CONTACT_WAY = "/cgi-bin/externalcontact/get_contact_way";
+    String UPDATE_CONTACT_WAY = "/cgi-bin/externalcontact/update_contact_way";
+    String DEL_CONTACT_WAY = "/cgi-bin/externalcontact/del_contact_way";
+    String CLOSE_TEMP_CHAT = "/cgi-bin/externalcontact/close_temp_chat";
+    String GET_FOLLOW_USER_LIST = "/cgi-bin/externalcontact/get_follow_user_list";
+    String GET_CONTACT_DETAIL = "/cgi-bin/externalcontact/get?external_userid=";
+    String CONVERT_TO_OPENID = "/cgi-bin/externalcontact/convert_to_openid";
+    String GET_CONTACT_DETAIL_BATCH = "/cgi-bin/externalcontact/batch/get_by_user?";
+    String UPDATE_REMARK = "/cgi-bin/externalcontact/remark";
+    String LIST_EXTERNAL_CONTACT = "/cgi-bin/externalcontact/list?userid=";
+    String LIST_UNASSIGNED_CONTACT = "/cgi-bin/externalcontact/get_unassigned_list";
+    @Deprecated
+    String TRANSFER_UNASSIGNED_CONTACT = "/cgi-bin/externalcontact/transfer";
+    String TRANSFER_CUSTOMER = "/cgi-bin/externalcontact/transfer_customer";
+    String TRANSFER_RESULT = "/cgi-bin/externalcontact/transfer_result";
+    String RESIGNED_TRANSFER_CUSTOMER = "/cgi-bin/externalcontact/resigned/transfer_customer";
+    String RESIGNED_TRANSFER_RESULT = "/cgi-bin/externalcontact/resigned/transfer_result";
+    String GROUP_CHAT_LIST = "/cgi-bin/externalcontact/groupchat/list";
+    String GROUP_CHAT_INFO = "/cgi-bin/externalcontact/groupchat/get";
+    String GROUP_CHAT_TRANSFER = "/cgi-bin/externalcontact/groupchat/transfer";
+    String LIST_USER_BEHAVIOR_DATA = "/cgi-bin/externalcontact/get_user_behavior_data";
+    String LIST_GROUP_CHAT_DATA = "/cgi-bin/externalcontact/groupchat/statistic";
+    String ADD_MSG_TEMPLATE = "/cgi-bin/externalcontact/add_msg_template";
+    String SEND_WELCOME_MSG = "/cgi-bin/externalcontact/send_welcome_msg";
+
+    String GET_CORP_TAG_LIST = "/cgi-bin/externalcontact/get_corp_tag_list";
+    String ADD_CORP_TAG = "/cgi-bin/externalcontact/add_corp_tag";
+    String EDIT_CORP_TAG = "/cgi-bin/externalcontact/edit_corp_tag";
+    String DEL_CORP_TAG = "/cgi-bin/externalcontact/del_corp_tag";
+    String MARK_TAG = "/cgi-bin/externalcontact/mark_tag";
+  }
+}

+ 357 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/constant/WxCpConsts.java

@@ -0,0 +1,357 @@
+package cn.nosum.wx.cp.constant;
+
+import lombok.experimental.UtilityClass;
+
+/**
+ * <pre>
+ * 企业微信常量
+ * Created by Binary Wang on 2018/8/25.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+@UtilityClass
+public class WxCpConsts {
+  /**
+   * 企业微信端推送过来的事件类型.
+   * 参考文档:https://work.weixin.qq.com/api/doc#12974
+   */
+  @UtilityClass
+  public static class EventType {
+    /**
+     * 成员关注事件.
+     */
+    public static final String SUBSCRIBE = "subscribe";
+
+    /**
+     * 成员取消关注事件.
+     */
+    public static final String UNSUBSCRIBE = "unsubscribe";
+
+    /**
+     * 进入应用事件.
+     */
+    public static final String ENTER_AGENT = "enter_agent";
+
+    /**
+     * 上报地理位置.
+     */
+    public static final String LOCATION = "LOCATION";
+
+    /**
+     * 异步任务完成事件推送.
+     */
+    public static final String BATCH_JOB_RESULT = "batch_job_result";
+
+    /**
+     * 企业微信通讯录变更事件.
+     */
+    public static final String CHANGE_CONTACT = "change_contact";
+
+    /**
+     * 点击菜单拉取消息的事件推送.
+     */
+    public static final String CLICK = "click";
+
+    /**
+     * 点击菜单跳转链接的事件推送.
+     */
+    public static final String VIEW = "view";
+
+    /**
+     * 扫码推事件的事件推送.
+     */
+    public static final String SCANCODE_PUSH = "scancode_push";
+
+    /**
+     * 扫码推事件且弹出“消息接收中”提示框的事件推送.
+     */
+    public static final String SCANCODE_WAITMSG = "scancode_waitmsg";
+
+    /**
+     * 弹出系统拍照发图的事件推送.
+     */
+    public static final String PIC_SYSPHOTO = "pic_sysphoto";
+
+    /**
+     * 弹出拍照或者相册发图的事件推送.
+     */
+    public static final String PIC_PHOTO_OR_ALBUM = "pic_photo_or_album";
+
+    /**
+     * 弹出微信相册发图器的事件推送.
+     */
+    public static final String PIC_WEIXIN = "pic_weixin";
+
+    /**
+     * 弹出地理位置选择器的事件推送.
+     */
+    public static final String LOCATION_SELECT = "location_select";
+
+    /**
+     * 任务卡片事件推送.
+     */
+    public static final String TASKCARD_CLICK = "taskcard_click";
+
+    /**
+     * 企业成员添加外部联系人事件推送
+     */
+    public static final String CHANGE_EXTERNAL_CONTACT = "change_external_contact";
+
+    /**
+     * 企业微信审批事件推送(自建应用审批)
+     */
+    public static final String OPEN_APPROVAL_CHANGE = "open_approval_change";
+
+    /**
+     * 企业微信审批事件推送(系统审批)
+     */
+    public static final String SYS_APPROVAL_CHANGE = "sys_approval_change";
+
+    /**
+     * 修改日历事件
+     */
+    public static final String MODIFY_CALENDAR = "modify_calendar";
+
+    /**
+     * 删除日历事件
+     */
+    public static final String DELETE_CALENDAR = "delete_calendar";
+
+    /**
+     * 添加日程事件
+     */
+    public static final String ADD_SCHEDULE = "add_schedule";
+
+    /**
+     * 修改日程事件
+     */
+    public static final String MODIFY_SCHEDULE = "modify_schedule";
+
+    /**
+     * 删除日程事件
+     */
+    public static final String DELETE_SCHEDULE = "delete_schedule";
+
+  }
+
+  /**
+   * 企业外部联系人变更事件的CHANGE_TYPE
+   */
+  @UtilityClass
+  public static class ExternalContactChangeType {
+    /**
+     * 新增外部联系人
+     */
+    public static final String ADD_EXTERNAL_CONTACT = "add_external_contact";
+    /**
+     * 删除外部联系人
+     */
+    public static final String DEL_EXTERNAL_CONTACT = "del_external_contact";
+
+    /**
+     * 外部联系人免验证添加成员事件
+     */
+    public static final String ADD_HALF_EXTERNAL_CONTACT = "add_half_external_contact";
+    /**
+     * 删除跟进成员事件
+     */
+    public static final String DEL_FOLLOW_USER = "del_follow_user";
+  }
+
+  /**
+   * 企业微信通讯录变更事件.
+   */
+  @UtilityClass
+  public static class ContactChangeType {
+    /**
+     * 新增成员事件.
+     */
+    public static final String CREATE_USER = "create_user";
+
+    /**
+     * 更新成员事件.
+     */
+    public static final String UPDATE_USER = "update_user";
+
+    /**
+     * 删除成员事件.
+     */
+    public static final String DELETE_USER = "delete_user";
+
+    /**
+     * 新增部门事件.
+     */
+    public static final String CREATE_PARTY = "create_party";
+
+    /**
+     * 更新部门事件.
+     */
+    public static final String UPDATE_PARTY = "update_party";
+
+    /**
+     * 删除部门事件.
+     */
+    public static final String DELETE_PARTY = "delete_party";
+
+    /**
+     * 标签成员变更事件.
+     */
+    public static final String UPDATE_TAG = "update_tag";
+
+  }
+
+  /**
+   * 互联企业发送应用消息的消息类型.
+   */
+  @UtilityClass
+  public static class LinkedCorpMsgType {
+    /**
+     * 文本消息.
+     */
+    public static final String TEXT = "text";
+    /**
+     * 图片消息.
+     */
+    public static final String IMAGE = "image";
+    /**
+     * 视频消息.
+     */
+    public static final String VIDEO = "video";
+    /**
+     * 图文消息(点击跳转到外链).
+     */
+    public static final String NEWS = "news";
+    /**
+     * 图文消息(点击跳转到图文消息页面).
+     */
+    public static final String MPNEWS = "mpnews";
+    /**
+     * markdown消息.
+     * (目前仅支持markdown语法的子集,微工作台(原企业号)不支持展示markdown消息)
+     */
+    public static final String MARKDOWN = "markdown";
+    /**
+     * 发送文件.
+     */
+    public static final String FILE = "file";
+    /**
+     * 文本卡片消息.
+     */
+    public static final String TEXTCARD = "textcard";
+
+    /**
+     * 小程序通知消息.
+     */
+    public static final String MINIPROGRAM_NOTICE = "miniprogram_notice";
+  }
+
+  /**
+   * 群机器人的消息类型.
+   */
+  @UtilityClass
+  public static class GroupRobotMsgType {
+    /**
+     * 文本消息.
+     */
+    public static final String TEXT = "text";
+
+    /**
+     * 图片消息.
+     */
+    public static final String IMAGE = "image";
+
+    /**
+     * markdown消息.
+     */
+    public static final String MARKDOWN = "markdown";
+
+    /**
+     * 图文消息(点击跳转到外链).
+     */
+    public static final String NEWS = "news";
+  }
+
+  /**
+   * 应用推送消息的消息类型.
+   */
+  @UtilityClass
+  public static class AppChatMsgType {
+    /**
+     * 文本消息.
+     */
+    public static final String TEXT = "text";
+    /**
+     * 图片消息.
+     */
+    public static final String IMAGE = "image";
+    /**
+     * 语音消息.
+     */
+    public static final String VOICE = "voice";
+    /**
+     * 视频消息.
+     */
+    public static final String VIDEO = "video";
+    /**
+     * 发送文件(CP专用).
+     */
+    public static final String FILE = "file";
+    /**
+     * 文本卡片消息(CP专用).
+     */
+    public static final String TEXTCARD = "textcard";
+    /**
+     * 图文消息(点击跳转到外链).
+     */
+    public static final String NEWS = "news";
+    /**
+     * 图文消息(点击跳转到图文消息页面).
+     */
+    public static final String MPNEWS = "mpnews";
+    /**
+     * markdown消息.
+     */
+    public static final String MARKDOWN = "markdown";
+  }
+
+  @UtilityClass
+  public static class WorkBenchType {
+    /*
+    * 关键数据型
+    * */
+    public static final String KEYDATA = "keydata";
+    /*
+    * 图片型
+    * */
+    public static final String IMAGE = "image";
+    /*
+    * 列表型
+    * */
+    public static final String LIST = "list";
+    /*
+    * webview型
+    * */
+    public static final String WEBVIEW = "webview";
+  }
+
+  @UtilityClass
+  public static class WelcomeMsgType {
+    /**
+     * 图片消息.
+     */
+    public static final String IMAGE = "image";
+    /**
+     * 图文消息.
+     */
+    public static final String LINK = "link";
+    /**
+     * 视频消息.
+     */
+    public static final String VIDEO = "video";
+    /**
+     * 小程序消息.
+     */
+    public static final String MINIPROGRAM = "miniprogram";
+  }
+}

+ 42 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/Gender.java

@@ -0,0 +1,42 @@
+package cn.nosum.wx.cp.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * <pre>
+ *  性别枚举
+ *  Created by BinaryWang on 2018/4/22.
+ * </pre>
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+@Getter
+@AllArgsConstructor
+public enum Gender {
+  /**
+   * 未定义
+   */
+  UNDEFINED("未定义", "0"),
+  /**
+   * 男
+   */
+  MALE("男", "1"),
+  /**
+   * 女
+   */
+  FEMALE("女", "2");
+
+  private final String genderName;
+  private final String code;
+
+  public static Gender fromCode(String code) {
+    for(Gender a: Gender.values()){
+      if(a.code.equals(code)){
+        return a;
+      }
+    }
+
+    return null;
+  }
+}

+ 107 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpAgent.java

@@ -0,0 +1,107 @@
+package cn.nosum.wx.cp.entity;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * <pre>
+ * 企业号应用信息.
+ * Created by huansinho on 2018/4/13.
+ * </pre>
+ *
+ * @author <a href="https://github.com/huansinho">huansinho</a>
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxCpAgent implements Serializable {
+  private static final long serialVersionUID = 5002894979081127234L;
+
+  @SerializedName("errcode")
+  private Integer errCode;
+
+  @SerializedName("errmsg")
+  private String errMsg;
+
+  @SerializedName("agentid")
+  private Integer agentId;
+
+  @SerializedName("name")
+  private String name;
+
+  @SerializedName("square_logo_url")
+  private String squareLogoUrl;
+
+  @SerializedName("logo_mediaid")
+  private String logoMediaId;
+
+  @SerializedName("description")
+  private String description;
+
+  @SerializedName("allow_userinfos")
+  private Users allowUserInfos;
+
+  @SerializedName("allow_partys")
+  private Parties allowParties;
+
+  @SerializedName("allow_tags")
+  private Tags allowTags;
+
+  @SerializedName("close")
+  private Integer close;
+
+  @SerializedName("redirect_domain")
+  private String redirectDomain;
+
+  @SerializedName("report_location_flag")
+  private Integer reportLocationFlag;
+
+  @SerializedName("isreportenter")
+  private Integer isReportEnter;
+
+  @SerializedName("home_url")
+  private String homeUrl;
+
+  public static WxCpAgent fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpAgent.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+  @Data
+  public static class Users implements Serializable {
+    private static final long serialVersionUID = 8801100463558788565L;
+    @SerializedName("user")
+    private List<User> users;
+  }
+
+  @Data
+  public class User implements Serializable {
+    private static final long serialVersionUID = 7287632514385508024L;
+    @SerializedName("userid")
+    private String userId;
+  }
+
+  @Data
+  public class Parties {
+    @SerializedName("partyid")
+    private List<Long> partyIds = null;
+  }
+
+  @Data
+  public class Tags {
+    @SerializedName("tagid")
+    private List<Integer> tagIds = null;
+  }
+
+}

+ 137 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpAgentWorkBench.java

@@ -0,0 +1,137 @@
+package cn.nosum.wx.cp.entity;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import cn.nosum.wx.cp.entity.workbench.WorkBenchKeyData;
+import cn.nosum.wx.cp.entity.workbench.WorkBenchList;
+import cn.nosum.wx.cp.constant.WxCpConsts;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author songshiyu
+ * @date : create in 16:09 2020/9/27
+ * @description: 工作台自定义展示
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxCpAgentWorkBench implements Serializable {
+  private static final long serialVersionUid = 1L;
+
+  /*
+  * 展示类型,目前支持 “keydata”、 “image”、 “list” 、”webview”
+  * */
+  private String type;
+  /*
+  * 用户的userid
+  * */
+  private String userId;
+  /*
+  * 应用id
+  * */
+  private Long agentId;
+  /*
+  * 点击跳转url,若不填且应用设置了主页url,则跳转到主页url,否则跳到应用会话窗口
+  * */
+  private String jumpUrl;
+  /*
+  * 若应用为小程序类型,该字段填小程序pagepath,若未设置,跳到小程序主页
+  * */
+  private String pagePath;
+  /*
+  * 图片url:图片的最佳比例为3.35:1;webview:渲染展示的url
+  * */
+  private String url;
+  /*
+  * 是否覆盖用户工作台的数据。设置为true的时候,会覆盖企业所有用户当前设置的数据。若设置为false,则不会覆盖用户当前设置的所有数据
+  * */
+  private Boolean replaceUserData;
+
+  private List<WorkBenchKeyData> keyDataList;
+
+  private List<WorkBenchList> lists;
+
+  // 生成模板Json字符串
+  public String toTemplateString() {
+    JsonObject templateObject = new JsonObject();
+    templateObject.addProperty("agentid", this.agentId);
+    templateObject.addProperty("type", this.type);
+    if (this.replaceUserData != null) {
+      templateObject.addProperty("replace_user_data", this.replaceUserData);
+    }
+    this.handle(templateObject);
+    return templateObject.toString();
+  }
+
+  // 生成用户数据Json字符串
+  public String toUserDataString() {
+    JsonObject userDataObject = new JsonObject();
+    userDataObject.addProperty("agentid", this.agentId);
+    userDataObject.addProperty("userid", this.userId);
+    userDataObject.addProperty("type", this.type);
+    this.handle(userDataObject);
+    return userDataObject.toString();
+  }
+
+  // 处理不用类型的工作台数据
+  private void handle(JsonObject templateObject) {
+    switch (this.getType()) {
+      case WxCpConsts.WorkBenchType.KEYDATA: {
+        JsonArray keyDataArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchKeyData keyDataItem : this.keyDataList) {
+          JsonObject keyDataObject = new JsonObject();
+          keyDataObject.addProperty("key", keyDataItem.getKey());
+          keyDataObject.addProperty("data", keyDataItem.getData());
+          keyDataObject.addProperty("jump_url", keyDataItem.getJumpUrl());
+          keyDataObject.addProperty("pagepath", keyDataItem.getPagePath());
+          keyDataArray.add(keyDataObject);
+        }
+        itemsObject.add("items", keyDataArray);
+        templateObject.add("keydata", itemsObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.IMAGE: {
+        JsonObject image = new JsonObject();
+        image.addProperty("url", this.url);
+        image.addProperty("jump_url", this.jumpUrl);
+        image.addProperty("pagepath", this.pagePath);
+        templateObject.add("image", image);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.LIST: {
+        JsonArray listArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchList listItem : this.lists) {
+          JsonObject listObject = new JsonObject();
+          listObject.addProperty("title", listItem.getTitle());
+          listObject.addProperty("jump_url", listItem.getJumpUrl());
+          listObject.addProperty("pagepath", listItem.getPagePath());
+          listArray.add(listObject);
+        }
+        itemsObject.add("items",listArray);
+        templateObject.add("list", itemsObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.WEBVIEW: {
+        JsonObject webview = new JsonObject();
+        webview.addProperty("url", this.url);
+        webview.addProperty("jump_url", this.jumpUrl);
+        webview.addProperty("pagepath", this.pagePath);
+        templateObject.add("webview", webview);
+        break;
+      }
+      default: {
+        //do nothing
+      }
+    }
+  }
+
+}

+ 32 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpBaseResp.java

@@ -0,0 +1,32 @@
+package cn.nosum.wx.cp.entity;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author yqx
+ * @date 2020/3/16
+ */
+@Getter
+@Setter
+public class WxCpBaseResp implements Serializable {
+  private static final long serialVersionUID = -4301684507150486556L;
+
+  @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);
+  }
+}

+ 22 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpChat.java

@@ -0,0 +1,22 @@
+package cn.nosum.wx.cp.entity;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 群聊
+ *
+ * @author gaigeshen
+ */
+@Data
+public class WxCpChat implements Serializable {
+  private static final long serialVersionUID = -4301684507150486556L;
+  
+  private String id;
+  private String name;
+  private String owner;
+  private List<String> users;
+
+}

+ 31 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpDepart.java

@@ -0,0 +1,31 @@
+package cn.nosum.wx.cp.entity;
+
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信的部门.
+ *
+ * @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;
+
+  public static WxCpDepart fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpDepart.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 43 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpInviteResult.java

@@ -0,0 +1,43 @@
+package cn.nosum.wx.cp.entity;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import cn.nosum.wx.cp.utils.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 邀请成员的结果对象类.
+ * Created by Binary Wang on 2018-5-13.
+ *
+ * @author <a href="https://github.com/binarywang">Binary Wang</a>
+ */
+@Data
+public class WxCpInviteResult implements Serializable {
+  private static final long serialVersionUID = 1420065684270213578L;
+
+  @Override
+  public String toString() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+  public static WxCpInviteResult fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpInviteResult.class);
+  }
+
+  @SerializedName("errcode")
+  private Integer errCode;
+
+  @SerializedName("errmsg")
+  private String errMsg;
+
+  @SerializedName("invaliduser")
+  private String[] invalidUsers;
+
+  @SerializedName("invalidparty")
+  private String[] invalidParties;
+
+  @SerializedName("invalidtag")
+  private String[] invalidTags;
+
+}

+ 0 - 0
wx-java-tools/wx-java-cp/src/main/java/cn/nosum/wx/cp/entity/WxCpMaJsCode2SessionResult.java


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