Browse Source

微信企业号

Daniel Qian 10 years ago
parent
commit
23e4a321ea
100 changed files with 7111 additions and 46 deletions
  1. 1 1
      .gitignore
  2. 0 35
      src/test/resources/testng.xml
  3. 0 0
      weixin-java-enterprise/LICENSE
  4. 56 0
      weixin-java-enterprise/README.md
  5. 226 0
      weixin-java-enterprise/pom.xml
  6. 128 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxConsts.java
  7. 30 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpConfigStorage.java
  8. 23 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageHandler.java
  9. 22 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageInterceptor.java
  10. 293 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageRouter.java
  11. 183 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpService.java
  12. 278 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpServiceImpl.java
  13. 85 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxInMemoryCpConfigStorage.java
  14. 31 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxAccessToken.java
  15. 639 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxCpXmlMessage.java
  16. 193 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxCustomMessage.java
  17. 46 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxDepartment.java
  18. 70 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassGroupMessage.java
  19. 117 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassNews.java
  20. 79 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassOpenIdsMessage.java
  21. 43 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassVideo.java
  22. 90 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMenu.java
  23. 32 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutImageMessage.java
  24. 137 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMessage.java
  25. 100 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMewsMessage.java
  26. 129 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMusicMessage.java
  27. 33 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutTextMessage.java
  28. 90 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutVideoMessage.java
  29. 32 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutVoiceMessage.java
  30. 20 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/BaseBuilder.java
  31. 31 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/ImageBuilder.java
  32. 64 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/MusicBuilder.java
  33. 36 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/NewsBuilder.java
  34. 31 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/TextBuilder.java
  35. 58 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/VideoBuilder.java
  36. 31 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/VoiceBuilder.java
  37. 29 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/BaseBuilder.java
  38. 25 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/ImageBuilder.java
  39. 54 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/MusicBuilder.java
  40. 4 4
      src/main/java/me/chanjar/weixin/bean/outxmlbuilder/NewsBuilder.java
  41. 24 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/TextBuilder.java
  42. 38 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/VideoBuilder.java
  43. 25 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/VoiceBuilder.java
  44. 136 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxError.java
  45. 55 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMassSendResult.java
  46. 52 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMassUploadResult.java
  47. 54 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMediaUploadResult.java
  48. 46 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxQrCodeTicket.java
  49. 95 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxUser.java
  50. 47 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxUserList.java
  51. 21 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/exception/WxErrorException.java
  52. 26 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/ByteGroup.java
  53. 68 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/PKCS7Encoder.java
  54. 42 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/SHA1.java
  55. 322 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/WxCryptUtil.java
  56. 47 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/fs/FileUtils.java
  57. 27 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/InputStreamResponseHandler.java
  58. 68 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/MediaDownloadRequestExecutor.java
  59. 44 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/MediaUploadRequestExecutor.java
  60. 56 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/QrCodeRequestExecutor.java
  61. 24 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/RequestExecutor.java
  62. 37 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/SimpleGetRequestExecutor.java
  63. 36 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/SimplePostRequestExecutor.java
  64. 32 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/Utf8ResponseHandler.java
  65. 115 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/GsonHelper.java
  66. 36 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxAccessTokenAdapter.java
  67. 87 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxCustomMessageGsonAdapter.java
  68. 2 3
      src/main/java/me/chanjar/weixin/util/json/WxErrorAdapter.java
  69. 58 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxGroupGsonAdapter.java
  70. 36 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxGsonBuilder.java
  71. 64 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassMessageGsonAdapter.java
  72. 55 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassNewsGsonAdapter.java
  73. 68 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassOpenIdsMessageGsonAdapter.java
  74. 39 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassSendResultAdapter.java
  75. 39 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassUploadResultAdapter.java
  76. 31 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassVideoAdapter.java
  77. 42 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMediaUploadResultAdapter.java
  78. 94 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMenuGsonAdapter.java
  79. 2 3
      src/main/java/me/chanjar/weixin/util/json/WxQrCodeTicketAdapter.java
  80. 52 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxUserGsonAdapter.java
  81. 42 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxUserListGsonAdapter.java
  82. 24 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/AdapterCDATA.java
  83. 21 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/MediaIdMarshaller.java
  84. 94 0
      weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/XmlTransformer.java
  85. 61 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/ApiTestModule.java
  86. 35 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxBaseAPITest.java
  87. 159 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxCpMessageRouterTest.java
  88. 33 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxCustomMessageAPITest.java
  89. 50 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxGroupAPITest.java
  90. 72 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxMediaAPITest.java
  91. 91 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxMenuAPITest.java
  92. 19 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxAccessTokenTest.java
  93. 91 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxCpXmlMessageTest.java
  94. 133 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxCustomMessageTest.java
  95. 38 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxErrorTest.java
  96. 112 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxMenuTest.java
  97. 52 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutImageMessageTest.java
  98. 76 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMusicMessageTest.java
  99. 97 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutNewsMessageTest.java
  100. 0 0
      weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutTextMessageTest.java

+ 1 - 1
.gitignore

@@ -16,6 +16,6 @@ target
 .project
 .classpath
 
-src/test/resources/test-config.xml
 sw-pom.xml
 *.iml
+test-config.xml

+ 0 - 35
src/test/resources/testng.xml

@@ -1,35 +0,0 @@
-<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
-
-<suite name="Weixin-java-tool-suite" verbose="1">
-	<test name="API_Test">
-		<classes>
-			<class name="me.chanjar.weixin.api.WxBaseAPITest" />
-			<class name="me.chanjar.weixin.api.WxCustomMessageAPITest" />
-			<class name="me.chanjar.weixin.api.WxMenuAPITest" />
-			<class name="me.chanjar.weixin.api.WxGroupAPITest" />
-			<class name="me.chanjar.weixin.api.WxMassMessageAPITest" />
-			<class name="me.chanjar.weixin.api.WxMediaAPITest" />
-			<class name="me.chanjar.weixin.api.WxUserAPITest" />
-			<class name="me.chanjar.weixin.api.WxQrCodeAPITest" />
-			<class name="me.chanjar.weixin.api.WxShortUrlAPITest" />
-			<class name="me.chanjar.weixin.api.WxMessageRouterTest" />
-		</classes>
-	</test>
-
-	<test name="Bean_Test">
-		<classes>
-			<class name="me.chanjar.weixin.bean.WxAccessTokenTest" />
-			<class name="me.chanjar.weixin.bean.WxCustomMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxErrorTest" />
-			<class name="me.chanjar.weixin.bean.WxMenuTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutImageMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutMusicMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutNewsMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutVideoMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutVoiceMessageTest" />
-			<class name="me.chanjar.weixin.bean.WxXmlOutTextMessageTest" />
-            <class name="me.chanjar.weixin.util.crypto.WxCryptUtilTest" />
-		</classes>
-	</test>
-</suite>

LICENSE → weixin-java-enterprise/LICENSE


+ 56 - 0
weixin-java-enterprise/README.md

@@ -0,0 +1,56 @@
+weixin-java-tools
+===========
+
+微信java开发工具集,本项目主要分为两大块:微信消息路由器、微信Java API
+
+特性列表:
+
+1. 不基于Servlet、和其他MVC框架,仅作为工具使用,提供更多的灵活性
+2. 详尽的单元测试代码,可以拿来当example用
+3. 详尽的javadoc
+4. access token过期自动刷新的功能
+5. 微信服务端繁忙自动重试的功能
+6. 提供微信错误信息的异常处理机制
+
+
+详细文档请看 [wiki](https://github.com/chanjarster/weixin-java-tools/wiki)
+
+# Quickstart
+
+在你的maven项目中添加:
+```xml
+<dependency>
+  <groupId>me.chanjar</groupId>
+  <artifactId>weixin-java-tools</artifactId>
+  <version>1.0.2</version>
+</dependency>
+```
+
+如果要使用``*-SNAPSHOT``版,则需要在你的``pom.xml``中添加这段:
+
+```xml
+<repositories>
+  <repository>
+      <snapshots />
+      <id>sonatype snapshots</id>
+      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+  </repository>
+</repositories>
+```
+
+## Hello World
+```java
+WxConfigStorage config = new WxInMemoryConfigStorage();
+config.setAppId(...); // 设置微信公众号的appid
+config.setSecret(...); // 设置微信公众号的app corpSecret
+config.setToken(...); // 设置微信公众号的token
+
+WxServiceImpl wxService = new WxServiceImpl();
+wxService.setWxConfigStorage(config);
+
+// 用户的openid在下面地址获得 
+// https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=用户管理&form=获取关注者列表接口%20/user/get 
+String openId = ...; 
+WxCustomMessage message = WxCustomMessage.TEXT().toUser(openId).content("Hello World").build();
+wxService.customMessageSend(message);
+```

+ 226 - 0
weixin-java-enterprise/pom.xml

@@ -0,0 +1,226 @@
+<?xml version="1.0"?>
+<project
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>me.chanjar</groupId>
+    <artifactId>weixin-java-enterprise-tools</artifactId>
+    <version>1.0.3-SNAPSHOT</version>
+    <name>WeiXin Java Enterprise Tools</name>
+    <description>微信企业号Java SDK</description>
+    <url>https://github.com/chanjarster/weixin-java-tools</url>
+
+    <licenses>
+        <license>
+            <name>The Apache License, Version 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+        </license>
+    </licenses>
+
+    <developers>
+        <developer>
+            <name>Daniel Qian</name>
+            <email>chanjarster@gmail.com</email>
+        </developer>
+    </developers>
+
+    <scm>
+        <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection>
+        <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection>
+        <url>https://github.com/chanjarster/weixin-java-tools</url>
+    </scm>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <downloadJavadocs>true</downloadJavadocs>
+        <downloadSources>true</downloadSources>
+        <httpclient.version>4.3.5</httpclient.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>fluent-hc</artifactId>
+            <version>${httpclient.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+            <version>${httpclient.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.2.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.xml.bind</groupId>
+            <artifactId>jaxb-impl</artifactId>
+            <version>2.2.7</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.oltu.oauth2</groupId>
+            <artifactId>org.apache.oltu.oauth2.client</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.2.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.8.7</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.inject</groupId>
+            <artifactId>guice</artifactId>
+            <version>3.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.9</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>9.3.0.M0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>9.3.0.M0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+        <repository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+        </repository>
+    </distributionManagement>
+
+    <profiles>
+        <profile>
+            <id>release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-source-plugin</artifactId>
+                        <version>2.2.1</version>
+                        <executions>
+                            <execution>
+                                <id>attach-sources</id>
+                                <goals>
+                                    <goal>jar-no-fork</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <version>2.9.1</version>
+                        <executions>
+                            <execution>
+                                <id>attach-javadocs</id>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                        <configuration>
+                            <charset>UTF-8</charset>
+                            <locale>zh_CN</locale>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-gpg-plugin</artifactId>
+                        <version>1.5</version>
+                        <executions>
+                            <execution>
+                                <id>sign-artifacts</id>
+                                <phase>verify</phase>
+                                <goals>
+                                    <goal>sign</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.17</version>
+                <configuration>
+                    <suiteXmlFiles>
+                        <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+                    </suiteXmlFiles>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.sonatype.plugins</groupId>
+                <artifactId>nexus-staging-maven-plugin</artifactId>
+                <version>1.6.3</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <serverId>ossrh</serverId>
+                    <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+                    <autoReleaseAfterClose>false</autoReleaseAfterClose>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-release-plugin</artifactId>
+                <version>2.5</version>
+                <configuration>
+                    <autoVersionSubmodules>true</autoVersionSubmodules>
+                    <useReleaseProfile>false</useReleaseProfile>
+                    <releaseProfiles>release</releaseProfiles>
+                    <goals>deploy</goals>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+
+</project>

+ 128 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxConsts.java

@@ -0,0 +1,128 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class WxConsts {
+
+  ///////////////////////
+  // 微信推送过来的消息的类型,和发送给微信xml格式消息的消息类型
+  ///////////////////////
+  public static final String XML_MSG_TEXT = "text";
+  public static final String XML_MSG_IMAGE = "image";
+  public static final String XML_MSG_VOICE = "voice";
+  public static final String XML_MSG_VIDEO = "video";
+  public static final String XML_MSG_NEWS = "news";
+  public static final String XML_MSG_MUSIC = "music";
+  public static final String XML_MSG_LOCATION = "location";
+  public static final String XML_MSG_LINK = "link";
+  public static final String XML_MSG_EVENT = "event";
+  public static final String XML_TRANSFER_CUSTOMER_SERVICE = "transfer_customer_service";
+  
+  ///////////////////////
+  // 客服消息的消息类型
+  ///////////////////////
+  public static final String CUSTOM_MSG_TEXT = "text";
+  public static final String CUSTOM_MSG_IMAGE = "image";
+  public static final String CUSTOM_MSG_VOICE = "voice";
+  public static final String CUSTOM_MSG_VIDEO = "video";
+  public static final String CUSTOM_MSG_MUSIC = "music";
+  public static final String CUSTOM_MSG_NEWS = "news";
+  
+  ///////////////////////
+  // 群发消息的消息类型
+  ///////////////////////
+  public static final String MASS_MSG_NEWS = "mpnews";
+  public static final String MASS_MSG_TEXT = "text";
+  public static final String MASS_MSG_VOICE = "voice";
+  public static final String MASS_MSG_IMAGE = "image";
+  public static final String MASS_MSG_VIDEO = "mpvideo";
+  
+  ///////////////////////
+  // 群发消息后微信端推送给服务器的反馈消息
+  ///////////////////////
+  public static final String MASS_ST_SUCCESS = "send success";
+  public static final String MASS_ST_FAIL = "send fail";
+  public static final String MASS_ST_涉嫌广告 = "err(10001)"; 
+  public static final String MASS_ST_涉嫌政治 = "err(20001)";
+  public static final String MASS_ST_涉嫌社会 = "err(20004)";
+  public static final String MASS_ST_涉嫌色情 = "err(20002)";
+  public static final String MASS_ST_涉嫌违法犯罪 = "err(20006)";
+  public static final String MASS_ST_涉嫌欺诈 = "err(20008)";
+  public static final String MASS_ST_涉嫌版权 = "err(20013)";
+  public static final String MASS_ST_涉嫌互推_互相宣传 = "err(22000)";
+  public static final String MASS_ST_涉嫌其他 = "err(21000)";
+  
+  /**
+   * 群发反馈消息代码所对应的文字描述
+   */
+  public static final Map<String, String> MASS_ST_2_DESC = new HashMap<String, String>();
+  static {
+    MASS_ST_2_DESC.put(MASS_ST_SUCCESS, "发送成功");
+    MASS_ST_2_DESC.put(MASS_ST_FAIL, "发送失败");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌广告, "涉嫌广告");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌政治, "涉嫌政治");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌社会, "涉嫌社会");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌色情, "涉嫌色情");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌违法犯罪, "涉嫌违法犯罪");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌欺诈, "涉嫌欺诈");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌版权, "涉嫌版权");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌互推_互相宣传, "涉嫌互推_互相宣传");
+    MASS_ST_2_DESC.put(MASS_ST_涉嫌其他, "涉嫌其他");
+  }
+  
+  ///////////////////////
+  // 微信端推送过来的事件类型
+  ///////////////////////
+  public static final String EVT_SUBSCRIBE = "subscribe";
+  public static final String EVT_UNSUBSCRIBE = "unsubscribe";
+  public static final String EVT_SCAN = "SCAN";
+  public static final String EVT_LOCATION = "LOCATION";
+  public static final String EVT_CLICK = "CLICK";
+  public static final String EVT_VIEW = "VIEW";
+  public static final String EVT_MASS_SEND_JOB_FINISH = "MASSSENDJOBFINISH";
+  public static final String EVT_SCANCODE_PUSH = "scancode_push";
+  public static final String EVT_SCANCODE_WAITMSG = "scancode_waitmsg";
+  public static final String EVT_PIC_SYSPHOTO = "pic_sysphoto";
+  public static final String EVT_PIC_PHOTO_OR_ALBUM = "pic_photo_or_album";
+  public static final String EVT_PIC_WEIXIN = "pic_weixin";
+  public static final String EVT_LOCATION_SELECT = "location_select";
+
+  ///////////////////////
+  // 上传多媒体文件的类型
+  ///////////////////////
+  public static final String MEDIA_IMAGE = "image";
+  public static final String MEDIA_VOICE = "voice";
+  public static final String MEDIA_VIDEO = "video";
+  public static final String MEDIA_THUMB = "thumb";
+  
+  ///////////////////////
+  // 文件类型
+  ///////////////////////
+  public static final String FILE_JPG = "jpeg";
+  public static final String FILE_MP3 = "mp3";
+  public static final String FILE_ARM = "arm";
+  public static final String FILE_MP4 = "mp4";
+
+
+  ///////////////////////
+  // 自定义菜单的按钮类型
+  ///////////////////////
+  /** 点击推事件 */
+  public static final String BUTTON_CLICK = "click";
+  /** 跳转URL */
+  public static final String BUTTON_VIEW = "view";
+  /** 扫码推事件 */
+  public static final String BUTTON_SCANCODE_PUSH = "scancode_push";
+  /** 扫码推事件且弹出“消息接收中”提示框 */
+  public static final String BUTTON_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";
+
+}

+ 30 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpConfigStorage.java

@@ -0,0 +1,30 @@
+package me.chanjar.weixin.enterprise.api;
+
+import me.chanjar.weixin.enterprise.bean.WxAccessToken;
+
+/**
+ * 微信客户端配置存储
+ * @author Daniel Qian
+ *
+ */
+public interface WxCpConfigStorage {
+
+  public void updateAccessToken(WxAccessToken accessToken);
+  
+  public void updateAccessToken(String accessToken, int expiresIn);
+  
+  public String getAccessToken();
+  
+  public String getCorpId();
+  
+  public String getCorpSecret();
+
+  public String getAgentId();
+
+  public String getToken();
+
+  public String getAesKey();
+
+  public int getExpiresIn();
+
+}

+ 23 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageHandler.java

@@ -0,0 +1,23 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.Map;
+
+import me.chanjar.weixin.enterprise.bean.WxCpXmlMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+
+/**
+ * 处理微信推送消息的处理器接口
+ * @author Daniel Qian
+ *
+ */
+public interface WxCpMessageHandler {
+
+  /**
+   * 
+   * @param wxMessage
+   * @param context  上下文,如果handler或interceptor之间有信息要传递,可以用这个
+   * @return xml格式的消息,如果在异步规则里处理的话,可以返回null
+   */
+  public WxXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context);
+  
+}

+ 22 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageInterceptor.java

@@ -0,0 +1,22 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.Map;
+
+import me.chanjar.weixin.enterprise.bean.WxCpXmlMessage;
+
+/**
+ * 微信消息拦截器,可以用来做验证
+ * @author Daniel Qian
+ *
+ */
+public interface WxCpMessageInterceptor {
+
+  /**
+   * 拦截微信消息
+   * @param wxMessage
+   * @param context  上下文,如果handler或interceptor之间有信息要传递,可以用这个
+   * @return  true代表OK,false代表不OK
+   */
+  public boolean intercept(WxCpXmlMessage wxMessage, Map<String, Object> context);
+  
+}

+ 293 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpMessageRouter.java

@@ -0,0 +1,293 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Pattern;
+
+import me.chanjar.weixin.enterprise.bean.WxCpXmlMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+
+/**
+ * <pre>
+ * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
+ * 
+ * 说明:
+ * 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理
+ * 2. 默认情况下消息只会被处理一次,除非使用 {@link Rule#next()}
+ * 3. 规则的结束必须用{@link Rule#end()}或者{@link Rule#next()},否则不会生效
+ * 
+ * 使用方法:
+ * WxMessageRouter router = new WxMessageRouter();
+ * router
+ *   .rule()
+ *       .msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT")
+ *       .interceptor(interceptor, ...).handler(handler, ...)
+ *   .end()
+ *   .rule()
+ *       // 另外一个匹配规则
+ *   .end()
+ * ;
+ * 
+ * // 将WxXmlMessage交给消息路由器
+ * router.route(message);
+ * 
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public class WxCpMessageRouter {
+  
+  private final List<Rule> rules = new ArrayList<Rule>();
+
+  private final ExecutorService es = Executors.newCachedThreadPool();
+  
+  /**
+   * 开始一个新的Route规则
+   * @return
+   */
+  public Rule rule() {
+    return new Rule(this);
+  }
+
+  /**
+   * 处理微信消息
+   * @param wxMessage
+   */
+  public WxXmlOutMessage route(final WxCpXmlMessage wxMessage) {
+    final List<Rule> matchRules = new ArrayList<Rule>();
+    // 收集匹配的规则
+    for (final Rule rule : rules) {
+      if (rule.test(wxMessage)) {
+        matchRules.add(rule);
+      }
+    }
+    
+    if (matchRules.size() == 0) {
+      return null;
+    }
+    
+    if (matchRules.get(0).async) {
+      // 只要第一个是异步的,那就异步执行
+      // 在另一个线程里执行
+      es.submit(new Runnable() {
+        public void run() {
+          for (final Rule rule : matchRules) {
+            rule.service(wxMessage);
+            if (!rule.reEnter) {
+              break;
+            }
+          }
+        }
+      });
+      return null;
+    }
+    
+    WxXmlOutMessage res = null;
+    for (final Rule rule : matchRules) {
+      // 返回最后一个匹配规则的结果
+      res = rule.service(wxMessage);
+      if (!rule.reEnter) {
+        break;
+      }
+    }
+    return res;
+  }
+  
+  public static class Rule {
+    
+    private final WxCpMessageRouter routerBuilder;
+
+    private boolean async = true;
+    
+    private String msgType;
+
+    private String event;
+    
+    private String eventKey;
+    
+    private String content;
+    
+    private String rContent;
+    
+    private boolean reEnter = false;
+    
+    private List<WxCpMessageHandler> handlers = new ArrayList<WxCpMessageHandler>();
+    
+    private List<WxCpMessageInterceptor> interceptors = new ArrayList<WxCpMessageInterceptor>();
+    
+    protected Rule(WxCpMessageRouter routerBuilder) {
+      this.routerBuilder = routerBuilder;
+    }
+    
+    /**
+     * 设置是否异步执行,默认是true
+     * @param async
+     * @return
+     */
+    public Rule async(boolean async) {
+      this.async = async;
+      return this;
+    }
+    
+    /**
+     * 如果msgType等于某值
+     * @param msgType
+     * @return
+     */
+    public Rule msgType(String msgType) {
+      this.msgType = msgType;
+      return this;
+    }
+    
+    /**
+     * 如果event等于某值
+     * @param event
+     * @return
+     */
+    public Rule event(String event) {
+      this.event = event;
+      return this;
+    }
+    
+    /**
+     * 如果eventKey等于某值
+     * @param eventKey
+     * @return
+     */
+    public Rule eventKey(String eventKey) {
+      this.eventKey = eventKey;
+      return this;
+    }
+    
+    /**
+     * 如果content等于某值
+     * @param content
+     * @return
+     */
+    public Rule content(String content) {
+      this.content = content;
+      return this;
+    }
+    
+    /**
+     * 如果content匹配该正则表达式
+     * @param regex
+     * @return
+     */
+    public Rule rContent(String regex) {
+      this.rContent = regex;
+      return this;
+    }
+    
+    /**
+     * 设置微信消息拦截器
+     * @param interceptor
+     * @return
+     */
+    public Rule interceptor(WxCpMessageInterceptor interceptor) {
+      return interceptor(interceptor, (WxCpMessageInterceptor[]) null);
+    }
+    
+    /**
+     * 设置微信消息拦截器
+     * @param interceptor
+     * @param otherInterceptors
+     * @return
+     */
+    public Rule interceptor(WxCpMessageInterceptor interceptor, WxCpMessageInterceptor... otherInterceptors) {
+      this.interceptors.add(interceptor);
+      if (otherInterceptors != null && otherInterceptors.length > 0) {
+        for (WxCpMessageInterceptor i : otherInterceptors) {
+          this.interceptors.add(i);
+        }
+      }
+      return this;
+    }
+    
+    /**
+     * 设置微信消息处理器
+     * @param handler
+     * @return
+     */
+    public Rule handler(WxCpMessageHandler handler) {
+      return handler(handler, (WxCpMessageHandler[]) null);
+    }
+    
+    /**
+     * 设置微信消息处理器
+     * @param handler
+     * @param otherHandlers
+     * @return
+     */
+    public Rule handler(WxCpMessageHandler handler, WxCpMessageHandler... otherHandlers) {
+      this.handlers.add(handler);
+      if (otherHandlers != null && otherHandlers.length > 0) {
+        for (WxCpMessageHandler i : otherHandlers) {
+          this.handlers.add(i);
+        }
+      }
+      return this;
+    }
+    
+    /**
+     * 规则结束,代表如果一个消息匹配该规则,那么它将不再会进入其他规则
+     * @return
+     */
+    public WxCpMessageRouter end() {
+      this.routerBuilder.rules.add(this);
+      return this.routerBuilder;
+    }
+    
+    /**
+     * 规则结束,但是消息还会进入其他规则
+     * @return
+     */
+    public WxCpMessageRouter next() {
+      this.reEnter = true;
+      return end();
+    }
+    
+    protected boolean test(WxCpXmlMessage wxMessage) {
+      return 
+          (this.msgType == null || this.msgType.equals(wxMessage.getMsgType()))
+          &&
+          (this.event == null || this.event.equals(wxMessage.getEvent()))
+          &&
+          (this.eventKey == null || this.eventKey.equals(wxMessage.getEventKey()))
+          &&
+          (this.content == null || this.content.equals(wxMessage.getContent() == null ? null : wxMessage.getContent().trim()))
+          &&
+          (this.rContent == null || Pattern.matches(this.rContent, wxMessage.getContent() == null ? "" : wxMessage.getContent().trim()))
+      ;
+    }
+    
+    /**
+     * 处理微信推送过来的消息
+     * @param wxMessage
+     * @return true 代表继续执行别的router,false 代表停止执行别的router
+     */
+    protected WxXmlOutMessage service(WxCpXmlMessage wxMessage) {
+      Map<String, Object> context = new HashMap<String, Object>();
+      // 如果拦截器不通过
+      for (WxCpMessageInterceptor interceptor : this.interceptors) {
+        if (!interceptor.intercept(wxMessage, context)) {
+          return null;
+        }
+      }
+      
+      // 交给handler处理
+      WxXmlOutMessage res = null;
+      for (WxCpMessageHandler handler : this.handlers) {
+        // 返回最后handler的结果
+        res = handler.handle(wxMessage, context);
+      }
+      return res;
+    }
+    
+  }
+  
+}

+ 183 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpService.java

@@ -0,0 +1,183 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.bean.*;
+import me.chanjar.weixin.enterprise.bean.WxDepartment;
+import me.chanjar.weixin.enterprise.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.enterprise.bean.result.WxUser;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+/**
+ * 微信API的Service
+ */
+public interface WxCpService {
+  
+  /**
+   * <pre>
+   * 验证推送过来的消息的正确性
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=验证消息真实性
+   * </pre>
+   * @param msgSignature
+   * @param timestamp
+   * @param nonce
+   * @param data       微信传输过来的数据,有可能是echoStr,有可能是xml消息
+   * @return
+   */
+  public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
+  
+  /**
+   * <pre>
+   * 获取access_token,本方法线程安全
+   * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+   * 
+   * 另:本service的所有方法都会在access_token过期是调用此方法
+   * 
+   * 程序员在非必要情况下尽量不要主动调用此方法
+
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token
+   * </pre>
+   * @throws me.chanjar.weixin.enterprise.exception.WxErrorException
+   */
+  public void accessTokenRefresh() throws WxErrorException;
+  
+  /**
+   * <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 WxConsts}
+   * @param fileType          文件类型,请看{@link WxConsts}
+   * @param inputStream       输入流
+   * @throws WxErrorException
+   */
+  public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException;
+
+  /**
+   * @see #mediaUpload(String, String, InputStream)
+   * @param mediaType
+   * @param file
+   * @throws WxErrorException
+   */
+  public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 下载多媒体文件
+   * 根据微信文档,视频文件下载不了,会返回null
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * </pre>
+   * @params media_id
+   * @return 保存到本地的临时文件
+   * @throws WxErrorException
+   */
+  public File mediaDownload(String media_id) throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 发送客服消息
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息
+   * </pre>
+   * @param message
+   * @throws WxErrorException
+   */
+  public void messageSend(WxCustomMessage message) throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   * </pre>
+   * @param menu
+   * @throws WxErrorException
+   */
+  public void menuCreate(WxMenu menu) throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   * </pre>
+   * @throws WxErrorException
+   */
+  public void menuDelete() throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   * </pre>
+   * @return
+   * @throws WxErrorException
+   */
+  public WxMenu menuGet() throws WxErrorException;
+
+  /**
+   * <pre>
+   * 分组管理接口 - 创建分组
+   * 最多支持创建500个分组
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=分组管理接口
+   * </pre>
+   * @param name 分组名字(30个字符以内) 
+   * @throws WxErrorException
+   */
+  public WxDepartment departmentCreate(String name) throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 分组管理接口 - 查询所有分组
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=分组管理接口
+   * </pre>
+   * @return
+   * @throws WxErrorException
+   */
+  public List<WxDepartment> departmentGet() throws WxErrorException;
+  
+  /**
+   * <pre>
+   * 分组管理接口 - 修改分组名
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=分组管理接口
+   * 
+   * 如果id为0(未分组),1(黑名单),2(星标组),或者不存在的id,微信会返回系统繁忙的错误
+   * </pre>
+   * @param group 要更新的group,group的id,name必须设置 
+   * @throws WxErrorException
+   */
+  public void departmentUpdate(WxDepartment group) throws WxErrorException;
+
+  /**
+   * <pre>
+   * 部门管理接口 - 删除部门
+   *
+   * </pre>
+   * @param department
+   * @throws WxErrorException
+   */
+  public void departmentDelete(WxDepartment department) throws WxErrorException;
+
+  public void userCreate(WxUser user) throws WxErrorException;
+
+  public void userUpdate(WxUser user) throws WxErrorException;
+
+  public void userDelete(String userid) throws WxErrorException;
+
+  public WxUser userGet(String userid) throws WxErrorException;
+
+  public List<WxUser> userGetByDepartment(String departmentId) throws WxErrorException;
+
+  /**
+   * 注入 {@link WxCpConfigStorage} 的实现
+   * @param wxConfigProvider
+   */
+  public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider);
+}

+ 278 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxCpServiceImpl.java

@@ -0,0 +1,278 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import me.chanjar.weixin.enterprise.bean.*;
+import me.chanjar.weixin.enterprise.util.crypto.WxCryptUtil;
+import me.chanjar.weixin.enterprise.util.http.SimpleGetRequestExecutor;
+import me.chanjar.weixin.enterprise.util.crypto.SHA1;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.ClientProtocolException;
+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 org.apache.http.impl.client.HttpClients;
+
+import me.chanjar.weixin.enterprise.bean.WxDepartment;
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+import me.chanjar.weixin.enterprise.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.enterprise.bean.result.WxUser;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+import me.chanjar.weixin.enterprise.util.fs.FileUtils;
+import me.chanjar.weixin.enterprise.util.http.MediaDownloadRequestExecutor;
+import me.chanjar.weixin.enterprise.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.enterprise.util.http.RequestExecutor;
+import me.chanjar.weixin.enterprise.util.http.SimplePostRequestExecutor;
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.internal.Streams;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+
+public class WxCpServiceImpl implements WxCpService {
+
+  /**
+   * 全局的是否正在刷新Access Token的flag
+   * true: 正在刷新
+   * false: 没有刷新
+   */
+  protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false);
+
+  protected static final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+  protected WxCpConfigStorage wxCpConfigStorage;
+
+  protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>();
+
+  public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
+    try {
+      return SHA1.gen(wxCpConfigStorage.getToken(), timestamp, nonce, data).equals(msgSignature);
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+
+  public void accessTokenRefresh() throws WxErrorException {
+    if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) {
+      try {
+        String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
+            + "&corpid=" + wxCpConfigStorage.getCorpId()
+            + "&corpsecret=" + wxCpConfigStorage.getCorpSecret();
+        try {
+          HttpGet httpGet = new HttpGet(url);
+          CloseableHttpResponse response = httpclient.execute(httpGet);
+          String resultContent = new BasicResponseHandler().handleResponse(response);
+          WxError error = WxError.fromJson(resultContent);
+          if (error.getErrorCode() != 0) {
+            throw new WxErrorException(error);
+          }
+          WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+          wxCpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+        } catch (ClientProtocolException e) {
+          throw new RuntimeException(e);
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      } finally {
+        GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false);
+      }
+    } else {
+      // 每隔100ms检查一下是否刷新完毕了
+      while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) {
+        try {
+          Thread.sleep(100);
+        } catch (InterruptedException e) {
+        }
+      }
+      // 刷新完毕了,就没他什么事儿了
+    }
+  }
+
+  public void messageSend(WxCustomMessage message) throws WxErrorException {
+    String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
+    execute(new SimplePostRequestExecutor(), url, message.toJson());
+  }
+
+  public void menuCreate(WxMenu menu) throws WxErrorException {
+    String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
+    execute(new SimplePostRequestExecutor(), url, menu.toJson());
+  }
+
+  public void menuDelete() throws WxErrorException {
+    String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
+    execute(new SimpleGetRequestExecutor(), url, null);
+  }
+
+  public WxMenu menuGet() throws WxErrorException {
+    String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
+    try {
+      String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
+      return WxMenu.fromJson(resultContent);
+    } catch (WxErrorException e) {
+      // 46003 不存在的菜单数据
+      if (e.getError().getErrorCode() == 46003) {
+        return null;
+      }
+      throw e;
+    }
+  }
+
+  public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
+      throws WxErrorException, IOException {
+    return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
+  }
+
+  public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
+    String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
+    return execute(new MediaUploadRequestExecutor(), url, file);
+  }
+
+  public File mediaDownload(String media_id) throws WxErrorException {
+    String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
+    return execute(new MediaDownloadRequestExecutor(), url, "media_id=" + media_id);
+  }
+
+
+  public WxDepartment departmentCreate(String name) throws WxErrorException {
+    // TODO
+    String url = "https://api.weixin.qq.com/cgi-bin/groups/create";
+    JsonObject json = new JsonObject();
+    JsonObject groupJson = new JsonObject();
+    json.add("group", groupJson);
+    groupJson.addProperty("name", name);
+
+    String responseContent = execute(
+        new SimplePostRequestExecutor(),
+        url,
+        json.toString());
+    return WxDepartment.fromJson(responseContent);
+  }
+
+  public void departmentUpdate(WxDepartment group) throws WxErrorException {
+    // TODO
+    String url = "https://api.weixin.qq.com/cgi-bin/groups/update";
+    execute(new SimplePostRequestExecutor(), url, group.toJson());
+  }
+
+  public List<WxDepartment> departmentGet() throws WxErrorException {
+    // TODO
+    String url = "https://api.weixin.qq.com/cgi-bin/groups/get";
+    String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
+    /*
+     * 操蛋的微信API,创建时返回的是 { group : { id : ..., name : ...} }
+     * 查询时返回的是 { groups : [ { id : ..., name : ..., count : ... }, ... ] }
+     */
+    JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
+    return WxGsonBuilder.INSTANCE.create()
+        .fromJson(tmpJsonElement.getAsJsonObject().get("groups"), new TypeToken<List<WxDepartment>>() {
+        }.getType());
+  }
+
+  public void departmentDelete(WxDepartment department) throws WxErrorException {
+    // TODO
+
+  }
+
+  @Override
+  public void userCreate(WxUser user) throws WxErrorException {
+    // TODO
+  }
+
+  @Override
+  public void userUpdate(WxUser user) throws WxErrorException {
+    // TODO
+  }
+
+  @Override
+  public void userDelete(String userid) throws WxErrorException {
+    // TODO
+  }
+
+  @Override
+  public WxUser userGet(String userid) throws WxErrorException {
+    return null;
+  }
+
+  @Override
+  public List<WxUser> userGetByDepartment(String departmentId) throws WxErrorException {
+    return null;
+  }
+
+  /**
+   * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
+   *
+   * @param executor
+   * @param uri
+   * @param data
+   * @return
+   * @throws WxErrorException
+   */
+  public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
+    if (StringUtils.isBlank(wxCpConfigStorage.getAccessToken())) {
+      accessTokenRefresh();
+    }
+    String accessToken = wxCpConfigStorage.getAccessToken();
+
+    String uriWithAccessToken = uri;
+    uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;
+
+    try {
+      return executor.execute(uriWithAccessToken, data);
+    } catch (WxErrorException e) {
+      WxError error = e.getError();
+      /*
+       * 发生以下情况时尝试刷新access_token
+       * 40001 获取access_token时AppSecret错误,或者access_token无效
+       * 42001 access_token超时
+       */
+      if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) {
+        accessTokenRefresh();
+        return execute(executor, uri, data);
+      }
+      /**
+       * -1 系统繁忙, 1000ms后重试
+       */
+      if (error.getErrorCode() == -1) {
+        if (retryTimes.get() == null) {
+          retryTimes.set(0);
+        }
+        if (retryTimes.get() > 4) {
+          retryTimes.set(0);
+          throw new RuntimeException("微信服务端异常,超出重试次数");
+        }
+        int sleepMillis = 1000 * (1 << retryTimes.get());
+        try {
+          System.out.println("微信系统繁忙," + sleepMillis + "ms后重试");
+          Thread.sleep(sleepMillis);
+          retryTimes.set(retryTimes.get() + 1);
+          return execute(executor, uri, data);
+        } catch (InterruptedException e1) {
+          throw new RuntimeException(e1);
+        }
+      }
+      if (error.getErrorCode() != 0) {
+        throw new WxErrorException(error);
+      }
+      return null;
+    } catch (ClientProtocolException e) {
+      throw new RuntimeException(e);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
+    this.wxCpConfigStorage = wxConfigProvider;
+  }
+
+}

+ 85 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/api/WxInMemoryCpConfigStorage.java

@@ -0,0 +1,85 @@
+package me.chanjar.weixin.enterprise.api;
+
+import me.chanjar.weixin.enterprise.bean.WxAccessToken;
+
+/**
+ * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化
+ * @author Daniel Qian
+ *
+ */
+public class WxInMemoryCpConfigStorage implements WxCpConfigStorage {
+
+  protected String corpId;
+  protected String corpSecret;
+
+  protected String token;
+  protected String accessToken;
+  protected String aesKey;
+  protected String agentId;
+  protected int expiresIn;
+
+  public void updateAccessToken(WxAccessToken accessToken) {
+    updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+  }
+  
+  public void updateAccessToken(String accessToken, int expiresIn) {
+    this.accessToken = accessToken;
+    this.expiresIn = expiresIn;
+  }
+
+  public String getAccessToken() {
+    return this.accessToken;
+  }
+
+  public String getCorpId() {
+    return this.corpId;
+  }
+
+  public String getCorpSecret() {
+    return this.corpSecret;
+  }
+
+  public String getToken() {
+    return this.token;
+  }
+
+  public int getExpiresIn() {
+    return this.expiresIn;
+  }
+
+  public void setCorpId(String corpId) {
+    this.corpId = corpId;
+  }
+
+  public void setCorpSecret(String corpSecret) {
+    this.corpSecret = corpSecret;
+  }
+
+  public void setToken(String token) {
+    this.token = token;
+  }
+
+  public String getAesKey() {
+    return aesKey;
+  }
+
+  public void setAesKey(String aesKey) {
+    this.aesKey = aesKey;
+  }
+
+  public void setAccessToken(String accessToken) {
+    this.accessToken = accessToken;
+  }
+
+  public void setExpiresIn(int expiresIn) {
+    this.expiresIn = expiresIn;
+  }
+
+  public String getAgentId() {
+    return agentId;
+  }
+
+  public void setAgentId(String agentId) {
+    this.agentId = agentId;
+  }
+}

+ 31 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxAccessToken.java

@@ -0,0 +1,31 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+public class WxAccessToken {
+
+  private String accessToken;
+  
+  private int expiresIn = -1;
+
+  public String getAccessToken() {
+    return accessToken;
+  }
+
+  public void setAccessToken(String accessToken) {
+    this.accessToken = accessToken;
+  }
+
+  public int getExpiresIn() {
+    return expiresIn;
+  }
+
+  public void setExpiresIn(int expiresIn) {
+    this.expiresIn = expiresIn;
+  }
+
+  public static WxAccessToken fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxAccessToken.class);
+  }
+  
+}

+ 639 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxCpXmlMessage.java

@@ -0,0 +1,639 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.api.WxCpConfigStorage;
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+import me.chanjar.weixin.enterprise.util.xml.XmlTransformer;
+import me.chanjar.weixin.enterprise.util.crypto.WxCryptUtil;
+import org.apache.commons.io.IOUtils;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.*;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <pre>
+ * 微信推送过来的消息,也是同步回复给用户的消息,xml格式
+ * 相关字段的解释看微信开发者文档:
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收普通消息
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收事件推送
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收语音识别结果
+ * </pre>
+ *
+ * @author Daniel Qian
+ */
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxCpXmlMessage {
+
+  ///////////////////////
+  // 以下都是微信推送过来的消息的xml的element所对应的属性
+  ///////////////////////
+
+  @XmlElement(name = "ToUserName")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String toUserName;
+
+  @XmlElement(name = "FromUserName")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String fromUserName;
+
+  @XmlElement(name = "CreateTime")
+  private Long createTime;
+
+  @XmlElement(name = "MsgType")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String msgType;
+
+  @XmlElement(name = "Content")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String content;
+
+  @XmlElement(name = "MsgId")
+  private Long msgId;
+
+  @XmlElement(name = "PicUrl")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String picUrl;
+
+  @XmlElement(name = "MediaId")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String mediaId;
+
+  @XmlElement(name = "Format")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String format;
+
+  @XmlElement(name = "ThumbMediaId")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String thumbMediaId;
+
+  @XmlElement(name = "Location_X")
+  private Double locationX;
+
+  @XmlElement(name = "Location_Y")
+  private Double locationY;
+
+  @XmlElement(name = "Scale")
+  private Double scale;
+
+  @XmlElement(name = "Label")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String label;
+
+  @XmlElement(name = "Title")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String title;
+
+  @XmlElement(name = "Description")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String description;
+
+  @XmlElement(name = "Url")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String url;
+
+  @XmlElement(name = "Event")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String event;
+
+  @XmlElement(name = "EventKey")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String eventKey;
+
+  @XmlElement(name = "Ticket")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String ticket;
+
+  @XmlElement(name = "Latitude")
+  private Double latitude;
+
+  @XmlElement(name = "Longitude")
+  private Double longitude;
+
+  @XmlElement(name = "Precision")
+  private Double precision;
+
+  @XmlElement(name = "Recognition")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String recognition;
+
+  ///////////////////////////////////////
+  // 群发消息返回的结果
+  ///////////////////////////////////////
+  /**
+   * 群发的结果
+   */
+  @XmlElement(name = "Status")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String status;
+  /**
+   * group_id下粉丝数;或者openid_list中的粉丝数
+   */
+  @XmlElement(name = "TotalCount")
+  private Integer totalCount;
+  /**
+   * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,filterCount = sentCount + errorCount
+   */
+  @XmlElement(name = "FilterCount")
+  private Integer filterCount;
+  /**
+   * 发送成功的粉丝数
+   */
+  @XmlElement(name = "SentCount")
+  private Integer sentCount;
+  /**
+   * 发送失败的粉丝数
+   */
+  @XmlElement(name = "ErrorCount")
+  private Integer errorCount;
+
+  @XmlElement(name = "ScanCodeInfo")
+  private ScanCodeInfo scanCodeInfo = new ScanCodeInfo();
+
+  @XmlElement(name = "SendPicsInfo")
+  private SendPicsInfo sendPicsInfo = new SendPicsInfo();
+
+  @XmlElement(name = "SendLocationInfo")
+  private SendLocationInfo sendLocationInfo = new SendLocationInfo();
+
+  public String getToUserName() {
+    return toUserName;
+  }
+
+  public void setToUserName(String toUserName) {
+    this.toUserName = toUserName;
+  }
+
+  public Long getCreateTime() {
+    return createTime;
+  }
+
+  public void setCreateTime(Long createTime) {
+    this.createTime = createTime;
+  }
+
+  /**
+   * <pre>
+   * 当接受用户消息时,可能会获得以下值:
+   * {@link WxConsts#XML_MSG_TEXT}
+   * {@link WxConsts#XML_MSG_IMAGE}
+   * {@link WxConsts#XML_MSG_VOICE}
+   * {@link WxConsts#XML_MSG_VIDEO}
+   * {@link WxConsts#XML_MSG_LOCATION}
+   * {@link WxConsts#XML_MSG_LINK}
+   * {@link WxConsts#XML_MSG_EVENT}
+   * </pre>
+   *
+   * @return
+   */
+  public String getMsgType() {
+    return msgType;
+  }
+
+  /**
+   * <pre>
+   * 当发送消息的时候使用:
+   * {@link WxConsts#XML_MSG_TEXT}
+   * {@link WxConsts#XML_MSG_IMAGE}
+   * {@link WxConsts#XML_MSG_VOICE}
+   * {@link WxConsts#XML_MSG_VIDEO}
+   * {@link WxConsts#XML_MSG_NEWS}
+   * {@link WxConsts#XML_MSG_MUSIC}
+   * </pre>
+   *
+   * @param msgType
+   */
+  public void setMsgType(String msgType) {
+    this.msgType = msgType;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public Long getMsgId() {
+    return msgId;
+  }
+
+  public void setMsgId(Long msgId) {
+    this.msgId = msgId;
+  }
+
+  public String getPicUrl() {
+    return picUrl;
+  }
+
+  public void setPicUrl(String picUrl) {
+    this.picUrl = picUrl;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public String getFormat() {
+    return format;
+  }
+
+  public void setFormat(String format) {
+    this.format = format;
+  }
+
+  public String getThumbMediaId() {
+    return thumbMediaId;
+  }
+
+  public void setThumbMediaId(String thumbMediaId) {
+    this.thumbMediaId = thumbMediaId;
+  }
+
+  public Double getLocationX() {
+    return locationX;
+  }
+
+  public void setLocationX(Double locationX) {
+    this.locationX = locationX;
+  }
+
+  public Double getLocationY() {
+    return locationY;
+  }
+
+  public void setLocationY(Double locationY) {
+    this.locationY = locationY;
+  }
+
+  public Double getScale() {
+    return scale;
+  }
+
+  public void setScale(Double scale) {
+    this.scale = scale;
+  }
+
+  public String getLabel() {
+    return label;
+  }
+
+  public void setLabel(String label) {
+    this.label = label;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public String getEvent() {
+    return event;
+  }
+
+  public void setEvent(String event) {
+    this.event = event;
+  }
+
+  public String getEventKey() {
+    return eventKey;
+  }
+
+  public void setEventKey(String eventKey) {
+    this.eventKey = eventKey;
+  }
+
+  public String getTicket() {
+    return ticket;
+  }
+
+  public void setTicket(String ticket) {
+    this.ticket = ticket;
+  }
+
+  public Double getLatitude() {
+    return latitude;
+  }
+
+  public void setLatitude(Double latitude) {
+    this.latitude = latitude;
+  }
+
+  public Double getLongitude() {
+    return longitude;
+  }
+
+  public void setLongitude(Double longitude) {
+    this.longitude = longitude;
+  }
+
+  public Double getPrecision() {
+    return precision;
+  }
+
+  public void setPrecision(Double precision) {
+    this.precision = precision;
+  }
+
+  public String getRecognition() {
+    return recognition;
+  }
+
+  public void setRecognition(String recognition) {
+    this.recognition = recognition;
+  }
+
+  public String getFromUserName() {
+    return fromUserName;
+  }
+
+  public void setFromUserName(String fromUserName) {
+    this.fromUserName = fromUserName;
+  }
+
+  public static WxCpXmlMessage fromXml(String xml) {
+    try {
+      return XmlTransformer.fromXml(WxCpXmlMessage.class, xml);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static WxCpXmlMessage fromXml(InputStream is) {
+    try {
+      return XmlTransformer.fromXml(WxCpXmlMessage.class, is);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * 从加密字符串转换
+   *
+   * @param encryptedXml
+   * @param wxCpConfigStorage
+   * @param timestamp
+   * @param nonce
+   * @param msgSignature
+   * @return
+   */
+  public static WxCpXmlMessage fromEncryptedXml(
+      String encryptedXml,
+      WxCpConfigStorage wxCpConfigStorage,
+      String timestamp, String nonce, String msgSignature) {
+    WxCryptUtil cryptUtil = new WxCryptUtil(wxCpConfigStorage);
+    String plainText = cryptUtil.decrypt(msgSignature, timestamp, nonce, encryptedXml);
+    return fromXml(plainText);
+  }
+
+  public static WxCpXmlMessage fromEncryptedXml(
+      InputStream is,
+      WxCpConfigStorage wxCpConfigStorage,
+      String timestamp, String nonce, String msgSignature) {
+    try {
+      return fromEncryptedXml(IOUtils.toString(is, "UTF-8"), wxCpConfigStorage, timestamp, nonce, msgSignature);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Integer getTotalCount() {
+    return totalCount;
+  }
+
+  public void setTotalCount(Integer totalCount) {
+    this.totalCount = totalCount;
+  }
+
+  public Integer getFilterCount() {
+    return filterCount;
+  }
+
+  public void setFilterCount(Integer filterCount) {
+    this.filterCount = filterCount;
+  }
+
+  public Integer getSentCount() {
+    return sentCount;
+  }
+
+  public void setSentCount(Integer sentCount) {
+    this.sentCount = sentCount;
+  }
+
+  public Integer getErrorCount() {
+    return errorCount;
+  }
+
+  public void setErrorCount(Integer errorCount) {
+    this.errorCount = errorCount;
+  }
+
+  public WxCpXmlMessage.ScanCodeInfo getScanCodeInfo() {
+    return scanCodeInfo;
+  }
+
+  public void setScanCodeInfo(WxCpXmlMessage.ScanCodeInfo scanCodeInfo) {
+    this.scanCodeInfo = scanCodeInfo;
+  }
+
+  public WxCpXmlMessage.SendPicsInfo getSendPicsInfo() {
+    return sendPicsInfo;
+  }
+
+  public void setSendPicsInfo(WxCpXmlMessage.SendPicsInfo sendPicsInfo) {
+    this.sendPicsInfo = sendPicsInfo;
+  }
+
+  public WxCpXmlMessage.SendLocationInfo getSendLocationInfo() {
+    return sendLocationInfo;
+  }
+
+  public void setSendLocationInfo(WxCpXmlMessage.SendLocationInfo sendLocationInfo) {
+    this.sendLocationInfo = sendLocationInfo;
+  }
+
+  @XmlRootElement(name = "ScanCodeInfo")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class ScanCodeInfo {
+
+    @XmlElement(name = "ScanType")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String scanType;
+
+    @XmlElement(name = "ScanResult")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String scanResult;
+
+    /**
+     * 扫描类型,一般是qrcode
+     * @return
+     */
+    public String getScanType() {
+
+      return scanType;
+    }
+
+    public void setScanType(String scanType) {
+      this.scanType = scanType;
+    }
+
+    /**
+     * 扫描结果,即二维码对应的字符串信息
+     * @return
+     */
+    public String getScanResult() {
+      return scanResult;
+    }
+
+    public void setScanResult(String scanResult) {
+      this.scanResult = scanResult;
+    }
+
+  }
+
+  @XmlRootElement(name = "SendPicsInfo")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class SendPicsInfo {
+
+    @XmlElement(name = "Count")
+    private Long count;
+
+    @XmlElementWrapper(name="PicList")
+    @XmlElement(name = "item")
+    protected final List<Item> picList = new ArrayList<Item>();
+
+    public Long getCount() {
+      return count;
+    }
+
+    public void setCount(Long count) {
+      this.count = count;
+    }
+
+    public List<Item> getPicList() {
+      return picList;
+    }
+
+    @XmlRootElement(name = "item")
+    @XmlAccessorType(XmlAccessType.FIELD)
+    @XmlType(name = "WxXmlMessage.SendPicsInfo.Item")
+    public static class Item {
+
+      @XmlElement(name = "PicMd5Sum")
+      @XmlJavaTypeAdapter(AdapterCDATA.class)
+      private String PicMd5Sum;
+
+      public String getPicMd5Sum() {
+        return PicMd5Sum;
+      }
+
+      public void setPicMd5Sum(String picMd5Sum) {
+        PicMd5Sum = picMd5Sum;
+      }
+    }
+  }
+
+  @XmlRootElement(name = "SendLocationInfo")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class SendLocationInfo {
+
+    @XmlElement(name = "Location_X")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String locationX;
+
+    @XmlElement(name = "Location_Y")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String locationY;
+
+    @XmlElement(name = "Scale")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String scale;
+
+    @XmlElement(name = "Label")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String label;
+
+    @XmlElement(name = "Poiname")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String poiname;
+
+    public String getLocationX() {
+      return locationX;
+    }
+
+    public void setLocationX(String locationX) {
+      this.locationX = locationX;
+    }
+
+    public String getLocationY() {
+      return locationY;
+    }
+
+    public void setLocationY(String locationY) {
+      this.locationY = locationY;
+    }
+
+    public String getScale() {
+      return scale;
+    }
+
+    public void setScale(String scale) {
+      this.scale = scale;
+    }
+
+    public String getLabel() {
+      return label;
+    }
+
+    public void setLabel(String label) {
+      this.label = label;
+    }
+
+    public String getPoiname() {
+      return poiname;
+    }
+
+    public void setPoiname(String poiname) {
+      this.poiname = poiname;
+    }
+  }
+
+}

+ 193 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxCustomMessage.java

@@ -0,0 +1,193 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.custombuilder.ImageBuilder;
+import me.chanjar.weixin.enterprise.bean.custombuilder.MusicBuilder;
+import me.chanjar.weixin.enterprise.bean.custombuilder.NewsBuilder;
+import me.chanjar.weixin.enterprise.bean.custombuilder.TextBuilder;
+import me.chanjar.weixin.enterprise.bean.custombuilder.VideoBuilder;
+import me.chanjar.weixin.enterprise.bean.custombuilder.VoiceBuilder;
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 客服消息
+ * @author Daniel Qian
+ *
+ */
+public class WxCustomMessage {
+
+  private String toUser;
+  private String msgType;
+  private String content;
+  private String mediaId;
+  private String thumbMediaId;
+  private String title;
+  private String description;
+  private String musicUrl;
+  private String hqMusicUrl;
+  private List<WxArticle> articles = new ArrayList<WxArticle>();
+  
+  public String getToUser() {
+    return toUser;
+  }
+  public void setToUser(String toUser) {
+    this.toUser = toUser;
+  }
+  public String getMsgType() {
+    return msgType;
+  }
+  
+  /**
+   * <pre>
+   * 请使用
+   * {@link WxConsts#CUSTOM_MSG_TEXT}
+   * {@link WxConsts#CUSTOM_MSG_IMAGE}
+   * {@link WxConsts#CUSTOM_MSG_VOICE}
+   * {@link WxConsts#CUSTOM_MSG_MUSIC}
+   * {@link WxConsts#CUSTOM_MSG_VIDEO}
+   * {@link WxConsts#CUSTOM_MSG_NEWS}
+   * </pre>
+   * @param msgType
+   */
+  public void setMsgType(String msgType) {
+    this.msgType = msgType;
+  }
+  public String getContent() {
+    return content;
+  }
+  public void setContent(String content) {
+    this.content = content;
+  }
+  public String getMediaId() {
+    return mediaId;
+  }
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+  public String getThumbMediaId() {
+    return thumbMediaId;
+  }
+  public void setThumbMediaId(String thumbMediaId) {
+    this.thumbMediaId = thumbMediaId;
+  }
+  public String getTitle() {
+    return title;
+  }
+  public void setTitle(String title) {
+    this.title = title;
+  }
+  public String getDescription() {
+    return description;
+  }
+  public void setDescription(String description) {
+    this.description = description;
+  }
+  public String getMusicUrl() {
+    return musicUrl;
+  }
+  public void setMusicUrl(String musicUrl) {
+    this.musicUrl = musicUrl;
+  }
+  public String getHqMusicUrl() {
+    return hqMusicUrl;
+  }
+  public void setHqMusicUrl(String hqMusicUrl) {
+    this.hqMusicUrl = hqMusicUrl;
+  }
+  public List<WxArticle> getArticles() {
+    return articles;
+  }
+  public void setArticles(List<WxArticle> articles) {
+    this.articles = articles;
+  }
+  
+  public String toJson() {
+    return WxGsonBuilder.INSTANCE.create().toJson(this);
+  }
+  
+  public static class WxArticle {
+    
+    private String title;
+    private String description;
+    private String url;
+    private String picUrl;
+    
+    public String getTitle() {
+      return title;
+    }
+    public void setTitle(String title) {
+      this.title = title;
+    }
+    public String getDescription() {
+      return description;
+    }
+    public void setDescription(String description) {
+      this.description = description;
+    }
+    public String getUrl() {
+      return url;
+    }
+    public void setUrl(String url) {
+      this.url = url;
+    }
+    public String getPicUrl() {
+      return picUrl;
+    }
+    public void setPicUrl(String picUrl) {
+      this.picUrl = picUrl;
+    }
+    
+  }
+  
+  /**
+   * 获得文本消息builder
+   * @return
+   */
+  public static TextBuilder TEXT() {
+    return new TextBuilder();
+  }
+
+  /**
+   * 获得图片消息builder
+   * @return
+   */
+  public static ImageBuilder IMAGE() {
+    return new ImageBuilder();
+  }
+
+  /**
+   * 获得语音消息builder
+   * @return
+   */
+  public static VoiceBuilder VOICE() {
+    return new VoiceBuilder();
+  }
+  
+  /**
+   * 获得视频消息builder
+   * @return
+   */
+  public static VideoBuilder VIDEO() {
+    return new VideoBuilder();
+  }
+  
+  /**
+   * 获得音乐消息builder
+   * @return
+   */
+  public static MusicBuilder MUSIC() {
+    return new MusicBuilder();
+  }
+  
+  /**
+   * 获得图文消息builder
+   * @return
+   */
+  public static NewsBuilder NEWS() {
+    return new NewsBuilder();
+  }
+  
+}

+ 46 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxDepartment.java

@@ -0,0 +1,46 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 微信用户分组
+ * @author Daniel Qian
+ *
+ */
+public class WxDepartment {
+
+  private long id = -1;
+  private String name;
+  private long count;
+  public long getId() {
+    return id;
+  }
+  public void setId(long id) {
+    this.id = id;
+  }
+  public String getName() {
+    return name;
+  }
+  public void setName(String name) {
+    this.name = name;
+  }
+  public long getCount() {
+    return count;
+  }
+  public void setCount(long count) {
+    this.count = count;
+  }
+  
+  public static WxDepartment fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxDepartment.class);
+  }
+  
+  public String toJson() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+  @Override
+  public String toString() {
+    return "WxGroup [id=" + id + ", name=" + name + ", count=" + count + "]";
+  }
+  
+}

+ 70 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassGroupMessage.java

@@ -0,0 +1,70 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 分组群发的消息
+ * 
+ * @author Daniel Qian
+ */
+public class WxMassGroupMessage {
+  
+  private long groupId;
+  private String msgtype;
+  private String content;
+  private String mediaId;
+
+  public WxMassGroupMessage() {
+    super();
+  }
+  
+  public String getMsgtype() {
+    return msgtype;
+  }
+
+  /**
+   * <pre>
+   * 请使用
+   * {@link WxConsts#MASS_MSG_IMAGE}
+   * {@link WxConsts#MASS_MSG_NEWS}
+   * {@link WxConsts#MASS_MSG_TEXT}
+   * {@link WxConsts#MASS_MSG_VIDEO}
+   * {@link WxConsts#MASS_MSG_VOICE}
+   * 如果msgtype和media_id不匹配的话,会返回系统繁忙的错误
+   * </pre>
+   * @param msgtype
+   */
+  public void setMsgtype(String msgtype) {
+    this.msgtype = msgtype;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public String toJson() {
+    return WxGsonBuilder.INSTANCE.create().toJson(this);
+  }
+
+  public long getGroupId() {
+    return groupId;
+  }
+
+  public void setGroupId(long groupId) {
+    this.groupId = groupId;
+  }
+
+}

+ 117 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassNews.java

@@ -0,0 +1,117 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 群发时用到的图文消息素材
+ * @author Daniel Qian
+ *
+ */
+public class WxMassNews {
+
+  private List<WxMassNewsArticle> articles = new ArrayList<WxMassNewsArticle>();
+  
+  public List<WxMassNewsArticle> getArticles() {
+    return articles;
+  }
+  
+  public void addArticle(WxMassNewsArticle article) {
+    this.articles.add(article);
+  }
+  
+  public String toJson() {
+    return WxGsonBuilder.INSTANCE.create().toJson(this);
+  }
+  
+  /**
+   * <pre>
+   * 群发图文消息article
+   * 1. thumbMediaId  (必填) 图文消息缩略图的media_id,可以在基础支持-上传多媒体文件接口中获得
+   * 2. author          图文消息的作者
+   * 3. title           (必填) 图文消息的标题
+   * 4. contentSourceUrl 在图文消息页面点击“阅读原文”后的页面链接
+   * 5. content (必填)  图文消息页面的内容,支持HTML标签
+   * 6. digest          图文消息的描述
+   * 7, showCoverPic  是否显示封面,true为显示,false为不显示
+   * </pre>
+   * @author Daniel Qian
+   *
+   */
+  public static class WxMassNewsArticle {
+    /**
+     * (必填) 图文消息缩略图的media_id,可以在基础支持-上传多媒体文件接口中获得
+     */
+    private String thumbMediaId;
+    /**
+     * 图文消息的作者
+     */
+    private String author;
+    /**
+     * (必填) 图文消息的标题
+     */
+    private String title;
+    /**
+     * 在图文消息页面点击“阅读原文”后的页面链接
+     */
+    private String contentSourceUrl;
+    /**
+     * (必填) 图文消息页面的内容,支持HTML标签
+     */
+    private String content;
+    /**
+     * 图文消息的描述
+     */
+    private String digest;
+    /**
+     * 是否显示封面,true为显示,false为不显示
+     */
+    private boolean showCoverPic;
+    
+    public String getThumbMediaId() {
+      return thumbMediaId;
+    }
+    public void setThumbMediaId(String thumbMediaId) {
+      this.thumbMediaId = thumbMediaId;
+    }
+    public String getAuthor() {
+      return author;
+    }
+    public void setAuthor(String author) {
+      this.author = author;
+    }
+    public String getTitle() {
+      return title;
+    }
+    public void setTitle(String title) {
+      this.title = title;
+    }
+    public String getContentSourceUrl() {
+      return contentSourceUrl;
+    }
+    public void setContentSourceUrl(String contentSourceUrl) {
+      this.contentSourceUrl = contentSourceUrl;
+    }
+    public String getContent() {
+      return content;
+    }
+    public void setContent(String content) {
+      this.content = content;
+    }
+    public String getDigest() {
+      return digest;
+    }
+    public void setDigest(String digest) {
+      this.digest = digest;
+    }
+    public boolean isShowCoverPic() {
+      return showCoverPic;
+    }
+    public void setShowCoverPic(boolean showCoverPic) {
+      this.showCoverPic = showCoverPic;
+    }
+    
+  }
+}

+ 79 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassOpenIdsMessage.java

@@ -0,0 +1,79 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * OpenId列表群发的消息
+ * 
+ * @author Daniel Qian
+ */
+public class WxMassOpenIdsMessage {
+  
+  private List<String> toUsers = new ArrayList<String>();
+  private String msgType;
+  private String content;
+  private String mediaId;
+
+  public WxMassOpenIdsMessage() {
+    super();
+  }
+  
+  public String getMsgType() {
+    return msgType;
+  }
+
+  /**
+   * <pre>
+   * 请使用
+   * {@link me.chanjar.weixin.enterprise.api.WxConsts#MASS_MSG_IMAGE}
+   * {@link me.chanjar.weixin.enterprise.api.WxConsts#MASS_MSG_NEWS}
+   * {@link me.chanjar.weixin.enterprise.api.WxConsts#MASS_MSG_TEXT}
+   * {@link me.chanjar.weixin.enterprise.api.WxConsts#MASS_MSG_VIDEO}
+   * {@link me.chanjar.weixin.enterprise.api.WxConsts#MASS_MSG_VOICE}
+   * 如果msgtype和media_id不匹配的话,会返回系统繁忙的错误
+   * </pre>
+   * @param msgType
+   */
+  public void setMsgType(String msgType) {
+    this.msgType = msgType;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public String toJson() {
+    return WxGsonBuilder.INSTANCE.create().toJson(this);
+  }
+
+  /**
+   * OpenId列表,最多支持10,000个
+   * @return
+   */
+  public List<String> getToUsers() {
+    return toUsers;
+  }
+
+  /**
+   * 添加OpenId,最多支持10,000个
+   * @param openId
+   */
+  public void addUser(String openId) {
+    this.toUsers.add(openId);
+  }
+}

+ 43 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMassVideo.java

@@ -0,0 +1,43 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 群发时用到的视频素材
+ * 
+ * @author Daniel Qian
+ */
+public class WxMassVideo {
+
+  private String mediaId;
+  private String title;
+  private String description;
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public String toJson() {
+    return WxGsonBuilder.INSTANCE.create().toJson(this);
+  }
+}

+ 90 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxMenu.java

@@ -0,0 +1,90 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 公众号菜单
+ * @author Daniel Qian
+ *
+ */
+public class WxMenu {
+
+  private List<WxMenuButton> buttons = new ArrayList<WxMenuButton>();
+
+  public List<WxMenuButton> getButtons() {
+    return buttons;
+  }
+
+  public void setButtons(List<WxMenuButton> buttons) {
+    this.buttons = buttons;
+  }
+  
+  public String toJson() {
+    return WxGsonBuilder.create().toJson(this);
+  }
+  
+  public static WxMenu fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMenu.class);
+  }
+  
+  public static WxMenu fromJson(InputStream is) {
+    return WxGsonBuilder.create().fromJson(new InputStreamReader(is), WxMenu.class);
+  }
+  
+  public static class WxMenuButton {
+
+    private String type;
+    private String name;
+    private String key;
+    private String url;
+    
+    private List<WxMenuButton> subButtons = new ArrayList<WxMenuButton>();
+
+    public String getType() {
+      return type;
+    }
+
+    public void setType(String type) {
+      this.type = type;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public void setKey(String key) {
+      this.key = key;
+    }
+
+    public String getUrl() {
+      return url;
+    }
+
+    public void setUrl(String url) {
+      this.url = url;
+    }
+
+    public List<WxMenuButton> getSubButtons() {
+      return subButtons;
+    }
+
+    public void setSubButtons(List<WxMenuButton> subButtons) {
+      this.subButtons = subButtons;
+    }
+    
+  }
+
+}

+ 32 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutImageMessage.java

@@ -0,0 +1,32 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.MediaIdMarshaller;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutImageMessage extends WxXmlOutMessage {
+  
+  @XmlElement(name="Image")
+  @XmlJavaTypeAdapter(MediaIdMarshaller.class)
+  private String mediaId;
+
+  public WxXmlOutImageMessage() {
+    this.msgType = WxConsts.XML_MSG_IMAGE;
+  }
+  
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+  
+}

+ 137 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMessage.java

@@ -0,0 +1,137 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxCpConfigStorage;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.ImageBuilder;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.MusicBuilder;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.NewsBuilder;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.TextBuilder;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.VideoBuilder;
+import me.chanjar.weixin.enterprise.bean.outxmlbuilder.VoiceBuilder;
+import me.chanjar.weixin.enterprise.util.crypto.WxCryptUtil;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+import me.chanjar.weixin.enterprise.util.xml.XmlTransformer;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutMessage {
+
+  @XmlElement(name="ToUserName")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  protected String toUserName;
+  
+  @XmlElement(name="FromUserName")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  protected String fromUserName;
+  
+  @XmlElement(name="CreateTime")
+  protected Long createTime;
+  
+  @XmlElement(name="MsgType")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  protected String msgType;
+
+  public String getToUserName() {
+    return toUserName;
+  }
+
+  public void setToUserName(String toUserName) {
+    this.toUserName = toUserName;
+  }
+
+  public String getFromUserName() {
+    return fromUserName;
+  }
+
+  public void setFromUserName(String fromUserName) {
+    this.fromUserName = fromUserName;
+  }
+
+  public Long getCreateTime() {
+    return createTime;
+  }
+
+  public void setCreateTime(Long createTime) {
+    this.createTime = createTime;
+  }
+
+  public String getMsgType() {
+    return msgType;
+  }
+
+  public void setMsgType(String msgType) {
+    this.msgType = msgType;
+  }
+  
+  public String toXml() {
+    try {
+      return XmlTransformer.toXml((Class)this.getClass(), this);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * 转换成加密的xml格式
+   * @return
+   */
+  public String toEncryptedXml(WxCpConfigStorage wxCpConfigStorage) {
+    String plainXml = toXml();
+    WxCryptUtil pc = new WxCryptUtil(wxCpConfigStorage);
+    return pc.encrypt(plainXml);
+  }
+
+  /**
+   * 获得文本消息builder
+   * @return
+   */
+  public static TextBuilder TEXT() {
+    return new TextBuilder();
+  }
+
+  /**
+   * 获得图片消息builder
+   * @return
+   */
+  public static ImageBuilder IMAGE() {
+    return new ImageBuilder();
+  }
+
+  /**
+   * 获得语音消息builder
+   * @return
+   */
+  public static VoiceBuilder VOICE() {
+    return new VoiceBuilder();
+  }
+  
+  /**
+   * 获得视频消息builder
+   * @return
+   */
+  public static VideoBuilder VIDEO() {
+    return new VideoBuilder();
+  }
+  
+  /**
+   * 获得音乐消息builder
+   * @return
+   */
+  public static MusicBuilder MUSIC() {
+    return new MusicBuilder();
+  }
+  
+  /**
+   * 获得图文消息builder
+   * @return
+   */
+  public static NewsBuilder NEWS() {
+    return new NewsBuilder();
+  }
+}

+ 100 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMewsMessage.java

@@ -0,0 +1,100 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutMewsMessage extends WxXmlOutMessage {
+
+  @XmlElement(name = "ArticleCount")
+  protected int articleCount;
+  
+  @XmlElementWrapper(name="Articles")
+  @XmlElement(name = "item")
+  protected final List<Item> articles = new ArrayList<Item>();
+  
+  public WxXmlOutMewsMessage() {
+    this.msgType = WxConsts.XML_MSG_NEWS;
+  }
+
+  public int getArticleCount() {
+    return articleCount;
+  }
+
+  public void addArticle(Item item) {
+    this.articles.add(item);
+    this.articleCount = this.articles.size();
+  }
+  
+  public List<Item> getArticles() {
+    return articles;
+  }
+  
+  
+  @XmlRootElement(name = "Item")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class Item {
+    
+    @XmlElement(name = "Title")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String Title;
+
+    @XmlElement(name = "Description")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String Description;
+
+    @XmlElement(name="PicUrl")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String PicUrl;
+    
+    @XmlElement(name="Url")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String Url;
+    
+    public String getTitle() {
+      return Title;
+    }
+
+    public void setTitle(String title) {
+      Title = title;
+    }
+
+    public String getDescription() {
+      return Description;
+    }
+
+    public void setDescription(String description) {
+      Description = description;
+    }
+
+    public String getPicUrl() {
+      return PicUrl;
+    }
+
+    public void setPicUrl(String picUrl) {
+      PicUrl = picUrl;
+    }
+
+    public String getUrl() {
+      return Url;
+    }
+
+    public void setUrl(String url) {
+      Url = url;
+    }
+
+  }
+
+
+}

+ 129 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMusicMessage.java

@@ -0,0 +1,129 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutMusicMessage extends WxXmlOutMessage {
+
+  @XmlElement(name = "Music")
+  protected final Music music = new Music();
+
+  public WxXmlOutMusicMessage() {
+    this.msgType = WxConsts.XML_MSG_MUSIC;
+  }
+
+  public String getTitle() {
+    return music.getTitle();
+  }
+
+  public void setTitle(String title) {
+    music.setTitle(title);
+  }
+
+  public String getDescription() {
+    return music.getDescription();
+  }
+
+  public void setDescription(String description) {
+    music.setDescription(description);
+  }
+  
+  public String getThumbMediaId() {
+    return music.getThumbMediaId();
+  }
+
+  public void setThumbMediaId(String thumbMediaId) {
+    music.setThumbMediaId(thumbMediaId);
+  }
+
+  public String getMusicUrl() {
+    return music.getMusicUrl();
+  }
+
+  public void setMusicUrl(String musicUrl) {
+    music.setMusicUrl(musicUrl);
+  }
+
+  public String getHqMusicUrl() {
+    return music.getHqMusicUrl();
+  }
+
+  public void setHqMusicUrl(String hqMusicUrl) {
+    music.setHqMusicUrl(hqMusicUrl);
+  }
+  
+  @XmlRootElement(name = "Music")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  private static class Music {
+    
+    @XmlElement(name = "Title")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String title;
+
+    @XmlElement(name = "Description")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String description;
+
+    @XmlElement(name="ThumbMediaId")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String thumbMediaId;
+    
+    @XmlElement(name="MusicUrl")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String musicUrl;
+    
+    @XmlElement(name="HQMusicUrl")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String hqMusicUrl;
+    
+    public String getTitle() {
+      return title;
+    }
+
+    public void setTitle(String title) {
+      this.title = title;
+    }
+
+    public String getDescription() {
+      return description;
+    }
+
+    public void setDescription(String description) {
+      this.description = description;
+    }
+
+    public String getThumbMediaId() {
+      return thumbMediaId;
+    }
+
+    public void setThumbMediaId(String thumbMediaId) {
+      this.thumbMediaId = thumbMediaId;
+    }
+
+    public String getMusicUrl() {
+      return musicUrl;
+    }
+
+    public void setMusicUrl(String musicUrl) {
+      this.musicUrl = musicUrl;
+    }
+
+    public String getHqMusicUrl() {
+      return hqMusicUrl;
+    }
+
+    public void setHqMusicUrl(String hqMusicUrl) {
+      this.hqMusicUrl = hqMusicUrl;
+    }
+    
+  }
+
+}

+ 33 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutTextMessage.java

@@ -0,0 +1,33 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutTextMessage extends WxXmlOutMessage {
+  
+  @XmlElement(name="Content")
+  @XmlJavaTypeAdapter(AdapterCDATA.class)
+  private String content;
+
+  public WxXmlOutTextMessage() {
+    this.msgType = WxConsts.XML_MSG_TEXT;
+  }
+  
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  
+}

+ 90 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutVideoMessage.java

@@ -0,0 +1,90 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.AdapterCDATA;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutVideoMessage extends WxXmlOutMessage {
+
+  @XmlElement(name = "Video")
+  protected final Video video = new Video();
+
+  public WxXmlOutVideoMessage() {
+    this.msgType = WxConsts.XML_MSG_VIDEO;
+  }
+
+  public String getMediaId() {
+    return video.getMediaId();
+  }
+
+  public void setMediaId(String mediaId) {
+    video.setMediaId(mediaId);
+  }
+
+  public String getTitle() {
+    return video.getTitle();
+  }
+
+  public void setTitle(String title) {
+    video.setTitle(title);
+  }
+
+  public String getDescription() {
+    return video.getDescription();
+  }
+
+  public void setDescription(String description) {
+    video.setDescription(description);
+  }
+  
+
+  @XmlRootElement(name = "Video")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  private static class Video {
+    
+    @XmlElement(name = "MediaId")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String mediaId;
+
+    @XmlElement(name = "Title")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String title;
+
+    @XmlElement(name = "Description")
+    @XmlJavaTypeAdapter(AdapterCDATA.class)
+    private String description;
+
+    public String getMediaId() {
+      return mediaId;
+    }
+
+    public void setMediaId(String mediaId) {
+      this.mediaId = mediaId;
+    }
+
+    public String getTitle() {
+      return title;
+    }
+
+    public void setTitle(String title) {
+      this.title = title;
+    }
+
+    public String getDescription() {
+      return description;
+    }
+
+    public void setDescription(String description) {
+      this.description = description;
+    }
+    
+  }
+
+}

+ 32 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/WxXmlOutVoiceMessage.java

@@ -0,0 +1,32 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.util.xml.MediaIdMarshaller;
+
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WxXmlOutVoiceMessage extends WxXmlOutMessage {
+  
+  @XmlElement(name="Voice")
+  @XmlJavaTypeAdapter(MediaIdMarshaller.class)
+  private String mediaId;
+
+  public WxXmlOutVoiceMessage() {
+    this.msgType = WxConsts.XML_MSG_VOICE;
+  }
+  
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+  
+}

+ 20 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/BaseBuilder.java

@@ -0,0 +1,20 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+public class BaseBuilder<T> {
+  protected String msgType;
+  protected String toUser;
+
+  public T toUser(String toUser) {
+    this.toUser = toUser;
+    return (T) this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = new WxCustomMessage();
+    m.setMsgType(this.msgType);
+    m.setToUser(this.toUser);
+    return m;
+  }
+}

+ 31 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/ImageBuilder.java

@@ -0,0 +1,31 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 获得消息builder
+ * <pre>
+ * 用法: WxCustomMessage m = WxCustomMessage.IMAGE().mediaId(...).toUser(...).build();
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public final class ImageBuilder extends BaseBuilder<ImageBuilder> {
+  private String mediaId;
+
+  public ImageBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_IMAGE;
+  }
+
+  public ImageBuilder mediaId(String media_id) {
+    this.mediaId = media_id;
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setMediaId(this.mediaId);
+    return m;
+  }
+}

+ 64 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/MusicBuilder.java

@@ -0,0 +1,64 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 音乐消息builder
+ * <pre>
+ * 用法: WxCustomMessage m = WxCustomMessage.MUSIC()
+ *                      .musicUrl(...)
+ *                      .hqMusicUrl(...)
+ *                      .title(...)
+ *                      .thumbMediaId(..)
+ *                      .description(..)
+ *                      .toUser(...)
+ *                      .build();
+ * </pre>
+ */
+public final class MusicBuilder extends BaseBuilder<MusicBuilder> {
+  private String title;
+  private String description;
+  private String thumbMediaId;
+  private String musicUrl;
+  private String hqMusicUrl;
+
+  public MusicBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_MUSIC;
+  }
+
+  public MusicBuilder musicUrl(String musicurl) {
+    this.musicUrl = musicurl;
+    return this;
+  }
+
+  public MusicBuilder hqMusicUrl(String hqMusicurl) {
+    this.hqMusicUrl = hqMusicurl;
+    return this;
+  }
+
+  public MusicBuilder title(String title) {
+    this.title = title;
+    return this;
+  }
+
+  public MusicBuilder description(String description) {
+    this.description = description;
+    return this;
+  }
+
+  public MusicBuilder thumbMediaId(String thumb_media_id) {
+    this.thumbMediaId = thumb_media_id;
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setMusicUrl(this.musicUrl);
+    m.setHqMusicUrl(this.hqMusicUrl);
+    m.setTitle(title);
+    m.setDescription(description);
+    m.setThumbMediaId(thumbMediaId);
+    return m;
+  }
+}

+ 36 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/NewsBuilder.java

@@ -0,0 +1,36 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 图文消息builder
+ * <pre>
+ * 用法:
+ * WxCustomMessage m = WxCustomMessage.NEWS().addArticle(article).toUser(...).build();
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public final class NewsBuilder extends BaseBuilder<NewsBuilder> {
+
+  private List<WxCustomMessage.WxArticle> articles = new ArrayList<WxCustomMessage.WxArticle>();
+  
+  public NewsBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_NEWS;
+  }
+
+  public NewsBuilder addArticle(WxCustomMessage.WxArticle article) {
+    this.articles.add(article);
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setArticles(this.articles);
+    return m;
+  }
+}

+ 31 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/TextBuilder.java

@@ -0,0 +1,31 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 文本消息builder
+ * <pre>
+ * 用法: WxCustomMessage m = WxCustomMessage.TEXT().content(...).toUser(...).build();
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public final class TextBuilder extends BaseBuilder<TextBuilder> {
+  private String content;
+
+  public TextBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_TEXT;
+  }
+
+  public TextBuilder content(String content) {
+    this.content = content;
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setContent(this.content);
+    return m;
+  }
+}

+ 58 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/VideoBuilder.java

@@ -0,0 +1,58 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 视频消息builder
+ * <pre>
+ * 用法: WxCustomMessage m = WxCustomMessage.VOICE()
+ *                              .mediaId(...)
+ *                              .title(...)
+ *                              .thumbMediaId(..)
+ *                              .description(..)
+ *                              .toUser(...)
+ *                              .build();
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public final class VideoBuilder extends BaseBuilder<VideoBuilder> {
+  private String mediaId;
+  private String title;
+  private String description;
+  private String thumbMediaId;
+
+  public VideoBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_VIDEO;
+  }
+
+  public VideoBuilder mediaId(String mediaId) {
+    this.mediaId = mediaId;
+    return this;
+  }
+
+  public VideoBuilder title(String title) {
+    this.title = title;
+    return this;
+  }
+
+  public VideoBuilder description(String description) {
+    this.description = description;
+    return this;
+  }
+
+  public VideoBuilder thumbMediaId(String thumb_media_id) {
+    this.thumbMediaId = thumb_media_id;
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setMediaId(this.mediaId);
+    m.setTitle(title);
+    m.setDescription(description);
+    m.setThumbMediaId(thumbMediaId);
+    return m;
+  }
+}

+ 31 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/custombuilder/VoiceBuilder.java

@@ -0,0 +1,31 @@
+package me.chanjar.weixin.enterprise.bean.custombuilder;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+/**
+ * 语音消息builder
+ * <pre>
+ * 用法: WxCustomMessage m = WxCustomMessage.VOICE().mediaId(...).toUser(...).build();
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public final class VoiceBuilder extends BaseBuilder<VoiceBuilder> {
+  private String mediaId;
+
+  public VoiceBuilder() {
+    this.msgType = WxConsts.CUSTOM_MSG_VOICE;
+  }
+
+  public VoiceBuilder mediaId(String media_id) {
+    this.mediaId = media_id;
+    return this;
+  }
+
+  public WxCustomMessage build() {
+    WxCustomMessage m = super.build();
+    m.setMediaId(this.mediaId);
+    return m;
+  }
+}

+ 29 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/BaseBuilder.java

@@ -0,0 +1,29 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+
+public abstract class BaseBuilder<BuilderType, ValueType> {
+  
+  protected String toUserName;
+  
+  protected String fromUserName;
+  
+  public BuilderType toUser(String touser) {
+    this.toUserName = touser;
+    return (BuilderType) this;
+  }
+  
+  public BuilderType fromUser(String fromusername) {
+    this.fromUserName = fromusername;
+    return (BuilderType) this;
+  }
+
+  public abstract ValueType build();
+  
+  public void setCommon(WxXmlOutMessage m) {
+    m.setToUserName(this.toUserName);
+    m.setFromUserName(this.fromUserName);
+    m.setCreateTime(System.currentTimeMillis() / 1000l);
+  }
+  
+}

+ 25 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/ImageBuilder.java

@@ -0,0 +1,25 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutImageMessage;
+
+/**
+ * 图片消息builder
+ * @author Daniel Qian
+ */
+public final class ImageBuilder extends BaseBuilder<ImageBuilder, WxXmlOutImageMessage> {
+
+  private String mediaId;
+
+  public ImageBuilder mediaId(String media_id) {
+    this.mediaId = media_id;
+    return this;
+  }
+
+  public WxXmlOutImageMessage build() {
+    WxXmlOutImageMessage m = new WxXmlOutImageMessage();
+    setCommon(m);
+    m.setMediaId(this.mediaId);
+    return m;
+  }
+  
+}

+ 54 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/MusicBuilder.java

@@ -0,0 +1,54 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMusicMessage;
+
+/**
+ * 音乐消息builder
+ * 
+ * @author Daniel Qian
+ */
+public final class MusicBuilder extends BaseBuilder<MusicBuilder, WxXmlOutMusicMessage> {
+
+  private String title;
+  private String description;
+  private String hqMusicUrl;
+  private String musicUrl;
+  private String thumbMediaId;
+
+  public MusicBuilder title(String title) {
+    this.title = title;
+    return this;
+  }
+
+  public MusicBuilder description(String description) {
+    this.description = description;
+    return this;
+  }
+
+  public MusicBuilder hqMusicUrl(String hqMusicUrl) {
+    this.hqMusicUrl = hqMusicUrl;
+    return this;
+  }
+
+  public MusicBuilder musicUrl(String musicUrl) {
+    this.musicUrl = musicUrl;
+    return this;
+  }
+
+  public MusicBuilder thumbMediaId(String thumbMediaId) {
+    this.thumbMediaId = thumbMediaId;
+    return this;
+  }
+
+  public WxXmlOutMusicMessage build() {
+    WxXmlOutMusicMessage m = new WxXmlOutMusicMessage();
+    setCommon(m);
+    m.setTitle(title);
+    m.setDescription(description);
+    m.setHqMusicUrl(hqMusicUrl);
+    m.setMusicUrl(musicUrl);
+    m.setThumbMediaId(thumbMediaId);
+    return m;
+  }
+
+}

+ 4 - 4
src/main/java/me/chanjar/weixin/bean/outxmlbuilder/NewsBuilder.java

@@ -1,14 +1,14 @@
-package me.chanjar.weixin.bean.outxmlbuilder;
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import me.chanjar.weixin.bean.WxXmlOutMewsMessage;
-import me.chanjar.weixin.bean.WxXmlOutMewsMessage.Item;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMewsMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMewsMessage.Item;
 
 /**
  * 图文消息builder
- * @author chanjarster
+ * @author Daniel Qian
  */
 public final class NewsBuilder extends BaseBuilder<NewsBuilder, WxXmlOutMewsMessage> {
 

+ 24 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/TextBuilder.java

@@ -0,0 +1,24 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutTextMessage;
+
+/**
+ * 文本消息builder
+ * @author Daniel Qian
+ *
+ */
+public final class TextBuilder extends BaseBuilder<TextBuilder, WxXmlOutTextMessage> {
+  private String content;
+
+  public TextBuilder content(String content) {
+    this.content = content;
+    return this;
+  }
+
+  public WxXmlOutTextMessage build() {
+    WxXmlOutTextMessage m = new WxXmlOutTextMessage();
+    setCommon(m);
+    m.setContent(this.content);
+    return m;
+  }
+}

+ 38 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/VideoBuilder.java

@@ -0,0 +1,38 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutVideoMessage;
+
+/**
+ * 视频消息builder
+ * @author Daniel Qian
+ *
+ */
+public final class VideoBuilder extends BaseBuilder<VideoBuilder, WxXmlOutVideoMessage> {
+
+  private String mediaId;
+  private String title;
+  private String description;
+
+  public VideoBuilder title(String title) {
+    this.title = title;
+    return this;
+  }
+  public VideoBuilder description(String description) {
+    this.description = description;
+    return this;
+  }
+  public VideoBuilder mediaId(String mediaId) {
+    this.mediaId = mediaId;
+    return this;
+  }
+  
+  public WxXmlOutVideoMessage build() {
+    WxXmlOutVideoMessage m = new WxXmlOutVideoMessage();
+    setCommon(m);
+    m.setTitle(title);
+    m.setDescription(description);
+    m.setMediaId(mediaId);
+    return m;
+  }
+  
+}

+ 25 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/outxmlbuilder/VoiceBuilder.java

@@ -0,0 +1,25 @@
+package me.chanjar.weixin.enterprise.bean.outxmlbuilder;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutVoiceMessage;
+
+/**
+ * 语音消息builder
+ * @author Daniel Qian
+ */
+public final class VoiceBuilder extends BaseBuilder<VoiceBuilder, WxXmlOutVoiceMessage> {
+
+  private String mediaId;
+
+  public VoiceBuilder mediaId(String mediaId) {
+    this.mediaId = mediaId;
+    return this;
+  }
+  
+  public WxXmlOutVoiceMessage build() {
+    WxXmlOutVoiceMessage m = new WxXmlOutVoiceMessage();
+    setCommon(m);
+    m.setMediaId(mediaId);
+    return m;
+  }
+  
+}

+ 136 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxError.java

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

+ 55 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMassSendResult.java

@@ -0,0 +1,55 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * <pre>
+ * 群发消息一发送就返回的结果
+ * 
+ * 真正的群发消息是否发送成功要看
+ * http://mp.weixin.qq.com/wiki/index.php?title=高级群发接口#.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81.E7.BE.A4.E5.8F.91.E7.BB.93.E6.9E.9C
+ * 
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public class WxMassSendResult {
+
+  private String errorCode;
+  private String errorMsg;
+  private String msgId;
+
+  public String getErrorCode() {
+    return errorCode;
+  }
+
+  public void setErrorCode(String errorCode) {
+    this.errorCode = errorCode;
+  }
+
+  public String getErrorMsg() {
+    return errorMsg;
+  }
+
+  public void setErrorMsg(String errorMsg) {
+    this.errorMsg = errorMsg;
+  }
+
+  public String getMsgId() {
+    return msgId;
+  }
+
+  public void setMsgId(String msgId) {
+    this.msgId = msgId;
+  }
+  
+  public static WxMassSendResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMassSendResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return "WxMassSendResult [errcode=" + errorCode + ", errmsg=" + errorMsg + ", msg_id=" + msgId + "]";
+  }
+  
+}

+ 52 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMassUploadResult.java

@@ -0,0 +1,52 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * <pre>
+ * 上传群发用的素材的结果
+ * 视频和图文消息需要在群发前上传素材
+ * </pre>
+ * @author Daniel Qian
+ *
+ */
+public class WxMassUploadResult {
+
+  private String type;
+  private String mediaId;
+  private long createdAt;
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public long getCreatedAt() {
+    return createdAt;
+  }
+
+  public void setCreatedAt(long createdAt) {
+    this.createdAt = createdAt;
+  }
+
+  public static WxMassUploadResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMassUploadResult.class);
+  }
+
+  @Override
+  public String toString() {
+    return "WxUploadResult [type=" + type + ", media_id=" + mediaId + ", created_at=" + createdAt + "]";
+  }
+
+}

+ 54 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxMediaUploadResult.java

@@ -0,0 +1,54 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+public class WxMediaUploadResult {
+
+  private String type;
+  private String mediaId;
+  private String thumbMediaId;
+  private long createdAt;
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getMediaId() {
+    return mediaId;
+  }
+
+  public void setMediaId(String mediaId) {
+    this.mediaId = mediaId;
+  }
+
+  public long getCreatedAt() {
+    return createdAt;
+  }
+
+  public void setCreatedAt(long createdAt) {
+    this.createdAt = createdAt;
+  }
+
+  public static WxMediaUploadResult fromJson(String json) {
+    return WxGsonBuilder.create().fromJson(json, WxMediaUploadResult.class);
+  }
+
+  public String getThumbMediaId() {
+    return thumbMediaId;
+  }
+
+  public void setThumbMediaId(String thumbMediaId) {
+    this.thumbMediaId = thumbMediaId;
+  }
+
+  @Override
+  public String toString() {
+    return "WxUploadResult [type=" + type + ", media_id=" + mediaId + ", thumb_media_id=" + thumbMediaId
+        + ", created_at=" + createdAt + "]";
+  }
+
+}

+ 46 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxQrCodeTicket.java

@@ -0,0 +1,46 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 换取二维码的Ticket
+ * 
+ * @author Daniel Qian
+ */
+public class WxQrCodeTicket {
+  
+  protected String ticket;
+  protected int expire_seconds = -1;
+  protected String url;
+
+  public String getTicket() {
+    return ticket;
+  }
+
+  public void setTicket(String ticket) {
+    this.ticket = ticket;
+  }
+
+  /**
+   * 如果返回-1说明是永久
+   */
+  public int getExpire_seconds() {
+    return expire_seconds;
+  }
+
+  public void setExpire_seconds(int expire_seconds) {
+    this.expire_seconds = expire_seconds;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public static WxQrCodeTicket fromJson(String json) {
+    return WxGsonBuilder.INSTANCE.create().fromJson(json, WxQrCodeTicket.class);
+  }
+}

+ 95 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxUser.java

@@ -0,0 +1,95 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 微信用户信息
+ * @author Daniel Qian
+ *
+ */
+public class WxUser {
+
+  protected boolean subscribe;
+  protected String openId;
+  protected String nickname;
+  protected String sex;
+  protected String language;
+  protected String city;
+  protected String province;
+  protected String country;
+  protected String headImgUrl;
+  protected long subscribeTime;
+  protected String unionId;
+  
+  public boolean isSubscribe() {
+    return subscribe;
+  }
+  public void setSubscribe(boolean subscribe) {
+    this.subscribe = subscribe;
+  }
+  public String getOpenId() {
+    return openId;
+  }
+  public void setOpenId(String openId) {
+    this.openId = openId;
+  }
+  public String getNickname() {
+    return nickname;
+  }
+  public void setNickname(String nickname) {
+    this.nickname = nickname;
+  }
+  public String getSex() {
+    return sex;
+  }
+  public void setSex(String sex) {
+    this.sex = sex;
+  }
+  public String getLanguage() {
+    return language;
+  }
+  public void setLanguage(String language) {
+    this.language = language;
+  }
+  public String getCity() {
+    return city;
+  }
+  public void setCity(String city) {
+    this.city = city;
+  }
+  public String getProvince() {
+    return province;
+  }
+  public void setProvince(String province) {
+    this.province = province;
+  }
+  public String getCountry() {
+    return country;
+  }
+  public void setCountry(String country) {
+    this.country = country;
+  }
+  public String getHeadImgUrl() {
+    return headImgUrl;
+  }
+  public void setHeadImgUrl(String headImgUrl) {
+    this.headImgUrl = headImgUrl;
+  }
+  public long getSubscribeTime() {
+    return subscribeTime;
+  }
+  public void setSubscribeTime(long subscribeTime) {
+    this.subscribeTime = subscribeTime;
+  }
+  public String getUnionId() {
+    return unionId;
+  }
+  public void setUnionId(String unionId) {
+    this.unionId = unionId;
+  }
+  
+  public static WxUser fromJson(String json) {
+    return WxGsonBuilder.INSTANCE.create().fromJson(json, WxUser.class);
+  }
+  
+}

+ 47 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/bean/result/WxUserList.java

@@ -0,0 +1,47 @@
+package me.chanjar.weixin.enterprise.bean.result;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.util.json.WxGsonBuilder;
+
+/**
+ * 关注者列表
+ * @author Daniel Qian
+ *
+ */
+public class WxUserList {
+
+  protected int total = -1;
+  protected int count = -1;
+  protected List<String> openIds = new ArrayList<String>();
+  protected String nextOpenId;
+  public int getTotal() {
+    return total;
+  }
+  public void setTotal(int total) {
+    this.total = total;
+  }
+  public int getCount() {
+    return count;
+  }
+  public void setCount(int count) {
+    this.count = count;
+  }
+  public List<String> getOpenIds() {
+    return openIds;
+  }
+  public void setOpenIds(List<String> openIds) {
+    this.openIds = openIds;
+  }
+  public String getNextOpenId() {
+    return nextOpenId;
+  }
+  public void setNextOpenId(String nextOpenId) {
+    this.nextOpenId = nextOpenId;
+  }
+  
+  public static WxUserList fromJson(String json) {
+    return WxGsonBuilder.INSTANCE.create().fromJson(json, WxUserList.class);
+  }
+}

+ 21 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/exception/WxErrorException.java

@@ -0,0 +1,21 @@
+package me.chanjar.weixin.enterprise.exception;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+
+public class WxErrorException extends Exception {
+
+  private static final long serialVersionUID = -6357149550353160810L;
+  
+  private WxError error;
+
+  public WxErrorException(WxError error) {
+    super(error.toString());
+    this.error = error;
+  }
+
+  public WxError getError() {
+    return error;
+  }
+
+ 
+}

+ 26 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/ByteGroup.java

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

+ 68 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/PKCS7Encoder.java

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

+ 42 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/SHA1.java

@@ -0,0 +1,42 @@
+package me.chanjar.weixin.enterprise.util.crypto;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Created by Daniel Qian on 14/10/19.
+ */
+public class SHA1 {
+
+  /**
+   * 生成SHA1签名
+   * @param arr
+   * @return
+   */
+  public static String gen(String... arr) throws NoSuchAlgorithmException {
+    Arrays.sort(arr);
+    StringBuilder sb = new StringBuilder();
+    for(String a : arr) {
+      sb.append(a);
+    }
+
+    MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+    sha1.update(sb.toString().getBytes());
+    byte[] output = sha1.digest();
+    return bytesToHex(output);
+  }
+
+
+  protected static String bytesToHex(byte[] b) {
+    char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+    StringBuffer buf = new StringBuffer();
+    for (int j = 0; j < b.length; j++) {
+      buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
+      buf.append(hexDigit[b[j] & 0x0f]);
+    }
+    return buf.toString();
+  }
+
+}

+ 322 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/crypto/WxCryptUtil.java

@@ -0,0 +1,322 @@
+/**
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright 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
+ */
+package me.chanjar.weixin.enterprise.util.crypto;
+
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Random;
+
+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 me.chanjar.weixin.enterprise.api.WxCpConfigStorage;
+import org.apache.commons.codec.binary.Base64;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+public class WxCryptUtil {
+
+  private static final Base64 base64 = new Base64();
+  private static final Charset CHARSET = Charset.forName("utf-8");
+
+  private static final ThreadLocal<DocumentBuilder> builderLocal =
+      new ThreadLocal<DocumentBuilder>() {
+        @Override protected DocumentBuilder initialValue() {
+          try {
+            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
+          } catch (ParserConfigurationException exc) {
+            throw new IllegalArgumentException(exc);
+          }
+        }
+      };
+
+  private byte[] aesKey;
+  private String token;
+  private String corpId;
+
+  /**
+   * 构造函数
+   *
+   * @param wxCpConfigStorage
+   */
+  public WxCryptUtil(WxCpConfigStorage wxCpConfigStorage) {
+    /*
+     * @param token          公众平台上,开发者设置的token
+     * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+     * @param corpId          公众平台appid
+     */
+    String encodingAesKey = wxCpConfigStorage.getAesKey();
+    String token = wxCpConfigStorage.getToken();
+    String corpId = wxCpConfigStorage.getCorpId();
+
+    this.token = token;
+    this.corpId = corpId;
+    this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+
+  }
+
+  /**
+   * 构造函数
+   *
+   * @param token          公众平台上,开发者设置的token
+   * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+   * @param corpId          公众平台appid
+   */
+  public WxCryptUtil(String token, String encodingAesKey, String corpId) {
+    this.token = token;
+    this.corpId = corpId;
+    this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+  }
+
+  /**
+   * 将公众平台回复用户的消息加密打包.
+   * <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 = timeStamp = Long.toString(System.currentTimeMillis() / 1000l);
+    String nonce = genRandomStr();
+
+    try {
+      String signature = SHA1.gen(token, timeStamp, nonce, encryptedXml);
+      String result = generateXml(encryptedXml, signature, timeStamp, nonce);
+      return result;
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * 对明文进行加密.
+   *
+   * @param plainText 需要加密的明文
+   * @return 加密后base64编码的字符串
+   */
+  protected 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 = corpId.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(aesKey, "AES");
+      IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+      cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+
+      // 加密
+      byte[] encrypted = cipher.doFinal(unencrypted);
+
+      // 使用BASE64对加密后的字符串进行编码
+      String base64Encrypted = base64.encodeToString(encrypted);
+
+      return base64Encrypted;
+    } catch (Exception e) {
+      throw new RuntimeException(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);
+
+    try {
+      // 验证安全签名
+      String signature = SHA1.gen(token, timeStamp, nonce, cipherText);
+      if (!signature.equals(msgSignature)) {
+        throw new RuntimeException("加密消息签名校验失败");
+      }
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException(e);
+    }
+
+    // 解密
+    String result = decrypt(cipherText);
+    return result;
+  }
+
+  /**
+   * 对密文进行解密.
+   *
+   * @param cipherText 需要解密的密文
+   * @return 解密得到的明文
+   */
+  public String decrypt(String cipherText) {
+    byte[] original;
+    try {
+      // 设置解密模式为AES的CBC模式
+      Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+      SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
+      IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
+      cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+
+      // 使用BASE64对密文进行解码
+      byte[] encrypted = Base64.decodeBase64(cipherText);
+
+      // 解密
+      original = cipher.doFinal(encrypted);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    String xmlContent, from_appid;
+    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);
+      from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
+          CHARSET);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    // appid不相同的情况
+    if (!from_appid.equals(corpId)) {
+      throw new RuntimeException("AppID不正确");
+    }
+
+    return xmlContent;
+
+  }
+
+  /**
+   * 将一个数字转换成生成4个字节的网络字节序bytes数组
+   *
+   * @param number
+   */
+  private 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数组还原成一个数字
+   *
+   * @param bytesInNetworkOrder
+   */
+  private int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
+    int sourceNumber = 0;
+    for (int i = 0; i < 4; i++) {
+      sourceNumber <<= 8;
+      sourceNumber |= bytesInNetworkOrder[i] & 0xff;
+    }
+    return sourceNumber;
+  }
+
+  /**
+   * 随机生成16位字符串
+   *
+   * @return
+   */
+  private String genRandomStr() {
+    String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    Random random = new Random();
+    StringBuffer sb = new StringBuffer();
+    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 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);
+  }
+
+  static String extractEncryptPart(String xml) {
+    try {
+      DocumentBuilder db = builderLocal.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 RuntimeException(e);
+    }
+  }
+
+}

+ 47 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/fs/FileUtils.java

@@ -0,0 +1,47 @@
+package me.chanjar.weixin.enterprise.util.fs;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class FileUtils {
+
+  /**
+   * 创建临时文件
+   * @param inputStream
+   * @param name  文件名
+   * @param ext   扩展名
+   * @return
+   * @throws IOException
+   */
+  public static File createTmpFile(InputStream inputStream, String name, String ext) throws IOException {
+    FileOutputStream fos = null;
+    try {
+      File tmpFile = File.createTempFile(name, '.' + ext);
+      tmpFile.deleteOnExit();
+      fos = new FileOutputStream(tmpFile);
+      int read = 0;
+      byte[] bytes = new byte[1024 * 100];
+      while ((read = inputStream.read(bytes)) != -1) {
+        fos.write(bytes, 0, read);
+      }
+      fos.flush();
+      return tmpFile;
+    } finally {
+      if (inputStream != null) {
+        try {
+          inputStream.close();
+        } catch (IOException e) {
+        }
+      }
+      if (fos != null) {
+        try {
+          fos.close();
+        } catch (IOException e) {
+        }
+      }
+    }
+  }
+  
+}

+ 27 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/InputStreamResponseHandler.java

@@ -0,0 +1,27 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+
+public class InputStreamResponseHandler implements ResponseHandler<InputStream> {
+
+  public static final ResponseHandler<InputStream> INSTANCE = new InputStreamResponseHandler();
+  
+  public InputStream handleResponse(final HttpResponse response) throws HttpResponseException, IOException {
+    final StatusLine statusLine = response.getStatusLine();
+    final HttpEntity entity = response.getEntity();
+    if (statusLine.getStatusCode() >= 300) {
+      EntityUtils.consume(entity);
+      throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
+    }
+    return entity == null ? null : entity.getContent();
+  }
+
+}

+ 68 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/MediaDownloadRequestExecutor.java

@@ -0,0 +1,68 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.entity.ContentType;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+import me.chanjar.weixin.enterprise.util.fs.FileUtils;
+
+/**
+ * 下载媒体文件请求执行器,请求的参数是String, 返回的结果是File
+ * @author Daniel Qian
+ *
+ */
+public class MediaDownloadRequestExecutor implements RequestExecutor<File, String> {
+
+  @Override
+  public File execute(String uri, String queryParam) throws WxErrorException, ClientProtocolException, IOException {
+    if (queryParam != null) {
+      if (uri.indexOf('?') == -1) {
+        uri += '?';
+      }
+      uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+    }
+    
+    HttpGet httpGet = new HttpGet(uri);
+    CloseableHttpResponse response = httpclient.execute(httpGet);
+
+    Header[] contentTypeHeader = response.getHeaders("Content-Type");
+    if (contentTypeHeader != null && contentTypeHeader.length > 0) {
+      // 下载媒体文件出错
+      if (ContentType.TEXT_PLAIN.getMimeType().equals(contentTypeHeader[0].getValue())) {
+        String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+        throw new WxErrorException(WxError.fromJson(responseContent));
+      }
+    }
+    InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response);
+    
+    // 视频文件不支持下载
+    String fileName = getFileName(response);
+    if (StringUtils.isBlank(fileName)) {
+      return null;
+    }
+    String[] name_ext = fileName.split("\\.");
+    File localFile = FileUtils.createTmpFile(inputStream, name_ext[0], name_ext[1]);
+    return localFile;
+  }
+
+  protected String getFileName(CloseableHttpResponse response) {
+    Header[] contentDispositionHeader = response.getHeaders("Content-disposition");
+    Pattern p = Pattern.compile(".*filename=\"(.*)\"");
+    Matcher m = p.matcher(contentDispositionHeader[0].getValue());
+    m.matches();
+    String fileName = m.group(1);
+    return fileName;
+  }
+  
+}

+ 44 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/MediaUploadRequestExecutor.java

@@ -0,0 +1,44 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+import me.chanjar.weixin.enterprise.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+/**
+ * 上传媒体文件请求执行器,请求的参数是File, 返回的结果是String
+ * @author Daniel Qian
+ *
+ */
+public class MediaUploadRequestExecutor implements RequestExecutor<WxMediaUploadResult, File> {
+
+  @Override
+  public WxMediaUploadResult execute(String uri, File file) throws WxErrorException, ClientProtocolException, IOException {
+    HttpPost httpPost = new HttpPost(uri);
+    if (file != null) {
+      HttpEntity entity = MultipartEntityBuilder
+            .create()
+            .addBinaryBody("media", file)
+            .build();
+      httpPost.setEntity(entity);
+      httpPost.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString());
+    }
+    CloseableHttpResponse response = httpclient.execute(httpPost);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrorCode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return WxMediaUploadResult.fromJson(responseContent);
+  }
+
+}

+ 56 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/QrCodeRequestExecutor.java

@@ -0,0 +1,56 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.util.UUID;
+
+import me.chanjar.weixin.enterprise.bean.result.WxQrCodeTicket;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+import me.chanjar.weixin.enterprise.util.fs.FileUtils;
+import org.apache.http.Header;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.entity.ContentType;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+
+/**
+ * 获得QrCode图片 请求执行器
+ * @author Daniel Qian
+ *
+ */
+public class QrCodeRequestExecutor implements RequestExecutor<File, WxQrCodeTicket> {
+
+  @Override
+  public File execute(String uri, WxQrCodeTicket ticket) throws WxErrorException, ClientProtocolException, IOException {
+    if (ticket != null) {
+      if (uri.indexOf('?') == -1) {
+        uri += '?';
+      }
+      uri += uri.endsWith("?") ? 
+          "ticket=" + URLEncoder.encode(ticket.getTicket(), "UTF-8") 
+          : 
+          "&ticket=" + URLEncoder.encode(ticket.getTicket(), "UTF-8");
+    }
+    
+    HttpGet httpGet = new HttpGet(uri);
+    CloseableHttpResponse response = httpclient.execute(httpGet);
+
+    Header[] contentTypeHeader = response.getHeaders("Content-Type");
+    if (contentTypeHeader != null && contentTypeHeader.length > 0) {
+      // 出错
+      if (ContentType.TEXT_PLAIN.getMimeType().equals(contentTypeHeader[0].getValue())) {
+        String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+        throw new WxErrorException(WxError.fromJson(responseContent));
+      }
+    }
+    InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response);
+    
+    File localFile = FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), "jpg");
+    return localFile;
+  }
+
+}

+ 24 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/RequestExecutor.java

@@ -0,0 +1,24 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+/**
+ * http请求执行器
+ * @author Daniel Qian
+ *
+ * @param <T> 返回值类型
+ * @param <E> 请求参数类型
+ */
+public interface RequestExecutor<T, E> {
+
+  public static final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+  public T execute(String uri, E data) throws WxErrorException, ClientProtocolException, IOException;
+  
+}

+ 37 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/SimpleGetRequestExecutor.java

@@ -0,0 +1,37 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+/**
+ * 简单的GET请求执行器,请求的参数是String, 返回的结果也是String
+ * @author Daniel Qian
+ *
+ */
+public class SimpleGetRequestExecutor implements RequestExecutor<String, String> {
+
+  @Override
+  public String execute(String uri, String queryParam) throws WxErrorException, ClientProtocolException, IOException {
+    if (queryParam != null) {
+      if (uri.indexOf('?') == -1) {
+        uri += '?';
+      }
+      uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+    }
+    HttpGet httpGet = new HttpGet(uri);
+    CloseableHttpResponse response = httpclient.execute(httpGet);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrorCode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return responseContent;
+  }
+
+}

+ 36 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/SimplePostRequestExecutor.java

@@ -0,0 +1,36 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.IOException;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+import org.apache.http.Consts;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+
+/**
+ * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String
+ * @author Daniel Qian
+ *
+ */
+public class SimplePostRequestExecutor implements RequestExecutor<String, String> {
+
+  @Override
+  public String execute(String uri, String postEntity) throws WxErrorException, ClientProtocolException, IOException {
+    HttpPost httpPost = new HttpPost(uri);
+    if (postEntity != null) {
+      StringEntity entity = new StringEntity(postEntity, Consts.UTF_8);
+      httpPost.setEntity(entity);
+    }
+    CloseableHttpResponse response = httpclient.execute(httpPost);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrorCode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return responseContent;
+  }
+
+}

+ 32 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/http/Utf8ResponseHandler.java

@@ -0,0 +1,32 @@
+package me.chanjar.weixin.enterprise.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+
+/**
+ * copy from {@link org.apache.http.impl.client.BasicResponseHandler}
+ * @author Daniel Qian
+ *
+ */
+public class Utf8ResponseHandler implements ResponseHandler<String> {
+
+  public static final ResponseHandler<String> INSTANCE = new Utf8ResponseHandler();
+  
+  public String handleResponse(final HttpResponse response) throws HttpResponseException, IOException {
+    final StatusLine statusLine = response.getStatusLine();
+    final HttpEntity entity = response.getEntity();
+    if (statusLine.getStatusCode() >= 300) {
+      EntityUtils.consume(entity);
+      throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
+    }
+    return entity == null ? null : EntityUtils.toString(entity, Consts.UTF_8);
+  }
+
+}

+ 115 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/GsonHelper.java

@@ -0,0 +1,115 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+
+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 ? false : r.booleanValue();
+	}
+	
+	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;
+	}
+	
+}

+ 36 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxAccessTokenAdapter.java

@@ -0,0 +1,36 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import com.google.gson.*;
+import me.chanjar.weixin.enterprise.bean.WxAccessToken;
+
+import java.lang.reflect.Type;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxAccessTokenAdapter implements JsonDeserializer<WxAccessToken> {
+
+  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;
+  }
+  
+}

+ 87 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxCustomMessageGsonAdapter.java

@@ -0,0 +1,87 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxCustomMessageGsonAdapter implements JsonSerializer<WxCustomMessage> {
+
+  public JsonElement serialize(WxCustomMessage message, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject messageJson = new JsonObject();
+    messageJson.addProperty("touser", message.getToUser());
+    messageJson.addProperty("msgtype", message.getMsgType());
+    
+    if (WxConsts.CUSTOM_MSG_TEXT.equals(message.getMsgType())) {
+      JsonObject text = new JsonObject();
+      text.addProperty("content", message.getContent());
+      messageJson.add("text", text);
+    }
+
+    if (WxConsts.CUSTOM_MSG_IMAGE.equals(message.getMsgType())) {
+      JsonObject image = new JsonObject();
+      image.addProperty("media_id", message.getMediaId());
+      messageJson.add("image", image);
+    }
+
+    if (WxConsts.CUSTOM_MSG_VOICE.equals(message.getMsgType())) {
+      JsonObject voice = new JsonObject();
+      voice.addProperty("media_id", message.getMediaId());
+      messageJson.add("voice", voice);
+    }
+
+    if (WxConsts.CUSTOM_MSG_VIDEO.equals(message.getMsgType())) {
+      JsonObject video = new JsonObject();
+      video.addProperty("media_id", message.getMediaId());
+      video.addProperty("thumb_media_id", message.getThumbMediaId());
+      video.addProperty("title", message.getTitle());
+      video.addProperty("description", message.getDescription());
+      messageJson.add("video", video);
+    }
+
+    if (WxConsts.CUSTOM_MSG_MUSIC.equals(message.getMsgType())) {
+      JsonObject music = new JsonObject();
+      music.addProperty("title", message.getTitle());
+      music.addProperty("description", message.getDescription());
+      music.addProperty("thumb_media_id", message.getThumbMediaId());
+      music.addProperty("musicurl", message.getMusicUrl());
+      music.addProperty("hqmusicurl", message.getHqMusicUrl());
+      messageJson.add("music", music);
+    }
+    
+    if (WxConsts.CUSTOM_MSG_NEWS.equals(message.getMsgType())) {
+      JsonArray articleJsonArray = new JsonArray();
+      for (WxCustomMessage.WxArticle article : message.getArticles()) {
+        JsonObject articleJson = new JsonObject();
+        articleJson.addProperty("title", article.getTitle());
+        articleJson.addProperty("description", article.getDescription());
+        articleJson.addProperty("url", article.getUrl());
+        articleJson.addProperty("picurl", article.getPicUrl());
+        articleJsonArray.add(articleJson);
+      }
+      messageJson.add("articles", articleJsonArray);
+    }
+    
+    return messageJson;
+  }
+
+}

+ 2 - 3
src/main/java/me/chanjar/weixin/util/json/WxErrorAdapter.java

@@ -6,11 +6,10 @@
  * arose from modification of the original source, or other redistribution of this source
  * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
  */
-package me.chanjar.weixin.util.json;
+package me.chanjar.weixin.enterprise.util.json;
 
 import com.google.gson.*;
-import me.chanjar.weixin.bean.WxAccessToken;
-import me.chanjar.weixin.bean.result.WxError;
+import me.chanjar.weixin.enterprise.bean.result.WxError;
 
 import java.lang.reflect.Type;
 

+ 58 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxGroupGsonAdapter.java

@@ -0,0 +1,58 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.bean.WxDepartment;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxGroupGsonAdapter implements JsonSerializer<WxDepartment>, JsonDeserializer<WxDepartment> {
+
+  public JsonElement serialize(WxDepartment group, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject json = new JsonObject();
+    JsonObject groupJson = new JsonObject();
+    groupJson.addProperty("name", group.getName());
+    groupJson.addProperty("id", group.getId());
+    groupJson.addProperty("count", group.getCount());
+    json.add("group", groupJson);
+    return json;
+  }
+
+  public WxDepartment deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxDepartment group = new WxDepartment();
+    JsonObject groupJson = json.getAsJsonObject();
+    if (json.getAsJsonObject().get("group") != null) {
+      groupJson = json.getAsJsonObject().get("group").getAsJsonObject();
+    }
+    if (groupJson.get("name") != null && !groupJson.get("name").isJsonNull()) {
+      group.setName(GsonHelper.getAsString(groupJson.get("name")));
+    }
+    if (groupJson.get("id") != null && !groupJson.get("id").isJsonNull()) {
+      group.setId(GsonHelper.getAsPrimitiveLong(groupJson.get("id")));
+    }
+    if (groupJson.get("count") != null && !groupJson.get("count").isJsonNull()) {
+      group.setCount(GsonHelper.getAsPrimitiveLong(groupJson.get("count")));
+    }
+    return group;
+  }
+  
+}

+ 36 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxGsonBuilder.java

@@ -0,0 +1,36 @@
+package me.chanjar.weixin.enterprise.util.json;
+
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import me.chanjar.weixin.enterprise.bean.*;
+import me.chanjar.weixin.enterprise.bean.result.*;
+
+public class WxGsonBuilder {
+
+  public static final GsonBuilder INSTANCE = new GsonBuilder();
+  
+  static {
+    INSTANCE.disableHtmlEscaping();
+    INSTANCE.registerTypeAdapter(WxCustomMessage.class, new WxCustomMessageGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMenu.class, new WxMenuGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMassNews.class, new WxMassNewsGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMassGroupMessage.class, new WxMassMessageGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMassOpenIdsMessage.class, new WxMassOpenIdsMessageGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxDepartment.class, new WxGroupGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxUser.class, new WxUserGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxUserList.class, new WxUserListGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxAccessToken.class, new WxAccessTokenAdapter());
+    INSTANCE.registerTypeAdapter(WxError.class, new WxErrorAdapter());
+    INSTANCE.registerTypeAdapter(WxMassVideo.class, new WxMassVideoAdapter());
+    INSTANCE.registerTypeAdapter(WxMediaUploadResult.class, new WxMediaUploadResultAdapter());
+    INSTANCE.registerTypeAdapter(WxMassSendResult.class, new WxMassSendResultAdapter());
+    INSTANCE.registerTypeAdapter(WxMassUploadResult.class, new WxMassUploadResultAdapter());
+    INSTANCE.registerTypeAdapter(WxQrCodeTicket.class, new WxQrCodeTicketAdapter());
+  }
+  
+  public static Gson create() {
+    return INSTANCE.create();
+  }
+  
+}

+ 64 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassMessageGsonAdapter.java

@@ -0,0 +1,64 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxMassGroupMessage;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassMessageGsonAdapter implements JsonSerializer<WxMassGroupMessage> {
+
+  public JsonElement serialize(WxMassGroupMessage message, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject messageJson = new JsonObject();
+    
+    JsonObject filter = new JsonObject();
+    filter.addProperty("group_id", message.getGroupId());
+    messageJson.add("filter", filter);
+    
+    if (WxConsts.MASS_MSG_NEWS.equals(message.getMsgtype())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_NEWS, sub);
+    }
+    if (WxConsts.MASS_MSG_TEXT.equals(message.getMsgtype())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("content", message.getContent());
+      messageJson.add(WxConsts.MASS_MSG_TEXT, sub);
+    }
+    if (WxConsts.MASS_MSG_VOICE.equals(message.getMsgtype())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_VOICE, sub);
+    }
+    if (WxConsts.MASS_MSG_IMAGE.equals(message.getMsgtype())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_IMAGE, sub);
+    }
+    if (WxConsts.MASS_MSG_VIDEO.equals(message.getMsgtype())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_VIDEO, sub);
+    }
+    messageJson.addProperty("msgtype", message.getMsgtype());
+    return messageJson;
+  }
+
+}

+ 55 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassNewsGsonAdapter.java

@@ -0,0 +1,55 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.bean.WxMassNews;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassNewsGsonAdapter implements JsonSerializer<WxMassNews> {
+
+  public JsonElement serialize(WxMassNews message, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject newsJson = new JsonObject();
+    
+    JsonArray articleJsonArray = new JsonArray();
+    for (WxMassNews.WxMassNewsArticle article : message.getArticles()) {
+      JsonObject articleJson = new JsonObject();
+      articleJson.addProperty("thumb_media_id", article.getThumbMediaId());
+      articleJson.addProperty("title", article.getTitle());
+      articleJson.addProperty("content", article.getContent());
+
+      if (null != article.getAuthor()) {
+        articleJson.addProperty("author", article.getAuthor());
+      }
+      if (null != article.getContentSourceUrl()) {
+        articleJson.addProperty("content_source_url", article.getContentSourceUrl());
+      }
+      if (null != article.getDigest()) {
+        articleJson.addProperty("digest", article.getDigest());
+      }
+      articleJson.addProperty("show_cover_pic", article.isShowCoverPic() ? "1" : "0");
+      articleJsonArray.add(articleJson);
+    }
+    newsJson.add("articles", articleJsonArray);
+    
+    return newsJson;
+  }
+
+}

+ 68 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassOpenIdsMessageGsonAdapter.java

@@ -0,0 +1,68 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxMassOpenIdsMessage;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassOpenIdsMessageGsonAdapter implements JsonSerializer<WxMassOpenIdsMessage> {
+
+  public JsonElement serialize(WxMassOpenIdsMessage message, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject messageJson = new JsonObject();
+    
+    JsonArray toUsers = new JsonArray();
+    for (String openId : message.getToUsers()) {
+      toUsers.add(new JsonPrimitive(openId));
+    }
+    messageJson.add("touser", toUsers);
+    
+    if (WxConsts.MASS_MSG_NEWS.equals(message.getMsgType())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_NEWS, sub);
+    }
+    if (WxConsts.MASS_MSG_TEXT.equals(message.getMsgType())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("content", message.getContent());
+      messageJson.add(WxConsts.MASS_MSG_TEXT, sub);
+    }
+    if (WxConsts.MASS_MSG_VOICE.equals(message.getMsgType())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_VOICE, sub);
+    }
+    if (WxConsts.MASS_MSG_IMAGE.equals(message.getMsgType())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_IMAGE, sub);
+    }
+    if (WxConsts.MASS_MSG_VIDEO.equals(message.getMsgType())) {
+      JsonObject sub = new JsonObject();
+      sub.addProperty("media_id", message.getMediaId());
+      messageJson.add(WxConsts.MASS_MSG_VIDEO, sub);
+    }
+    messageJson.addProperty("msgtype", message.getMsgType());
+    return messageJson;
+  }
+
+}

+ 39 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassSendResultAdapter.java

@@ -0,0 +1,39 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import com.google.gson.*;
+import me.chanjar.weixin.enterprise.bean.result.WxMassSendResult;
+
+import java.lang.reflect.Type;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassSendResultAdapter implements JsonDeserializer<WxMassSendResult> {
+
+  public WxMassSendResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxMassSendResult sendResult = new WxMassSendResult();
+    JsonObject sendResultJsonObject = json.getAsJsonObject();
+
+    if (sendResultJsonObject.get("errcode") != null && !sendResultJsonObject.get("errcode").isJsonNull()) {
+      sendResult.setErrorCode(GsonHelper.getAsString(sendResultJsonObject.get("errcode")));
+    }
+    if (sendResultJsonObject.get("errmsg") != null && !sendResultJsonObject.get("errmsg").isJsonNull()) {
+      sendResult.setErrorMsg(GsonHelper.getAsString(sendResultJsonObject.get("errmsg")));
+    }
+    if (sendResultJsonObject.get("msg_id") != null && !sendResultJsonObject.get("msg_id").isJsonNull()) {
+      sendResult.setMsgId(GsonHelper.getAsString(sendResultJsonObject.get("msg_id")));
+    }
+    return sendResult;
+  }
+  
+}

+ 39 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassUploadResultAdapter.java

@@ -0,0 +1,39 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import com.google.gson.*;
+import me.chanjar.weixin.enterprise.bean.result.WxMassUploadResult;
+
+import java.lang.reflect.Type;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassUploadResultAdapter implements JsonDeserializer<WxMassUploadResult> {
+
+  public WxMassUploadResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxMassUploadResult uploadResult = new WxMassUploadResult();
+    JsonObject uploadResultJsonObject = json.getAsJsonObject();
+
+    if (uploadResultJsonObject.get("type") != null && !uploadResultJsonObject.get("type").isJsonNull()) {
+      uploadResult.setType(GsonHelper.getAsString(uploadResultJsonObject.get("type")));
+    }
+    if (uploadResultJsonObject.get("media_id") != null && !uploadResultJsonObject.get("media_id").isJsonNull()) {
+      uploadResult.setMediaId(GsonHelper.getAsString(uploadResultJsonObject.get("media_id")));
+    }
+    if (uploadResultJsonObject.get("created_at") != null && !uploadResultJsonObject.get("created_at").isJsonNull()) {
+      uploadResult.setCreatedAt(GsonHelper.getAsPrimitiveLong(uploadResultJsonObject.get("created_at")));
+    }
+    return uploadResult;
+  }
+  
+}

+ 31 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMassVideoAdapter.java

@@ -0,0 +1,31 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import com.google.gson.*;
+import me.chanjar.weixin.enterprise.bean.WxMassVideo;
+
+import java.lang.reflect.Type;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMassVideoAdapter implements JsonSerializer<WxMassVideo> {
+
+  public JsonElement serialize(WxMassVideo message, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject messageJson = new JsonObject();
+    messageJson.addProperty("media_id", message.getMediaId());
+    messageJson.addProperty("description", message.getDescription());
+    messageJson.addProperty("title", message.getTitle());
+    return messageJson;
+  }
+
+}

+ 42 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMediaUploadResultAdapter.java

@@ -0,0 +1,42 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import com.google.gson.*;
+import me.chanjar.weixin.enterprise.bean.result.WxMediaUploadResult;
+
+import java.lang.reflect.Type;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMediaUploadResultAdapter implements JsonDeserializer<WxMediaUploadResult> {
+
+  public WxMediaUploadResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxMediaUploadResult uploadResult = new WxMediaUploadResult();
+    JsonObject uploadResultJsonObject = json.getAsJsonObject();
+
+    if (uploadResultJsonObject.get("type") != null && !uploadResultJsonObject.get("type").isJsonNull()) {
+      uploadResult.setType(GsonHelper.getAsString(uploadResultJsonObject.get("type")));
+    }
+    if (uploadResultJsonObject.get("media_id") != null && !uploadResultJsonObject.get("media_id").isJsonNull()) {
+      uploadResult.setMediaId(GsonHelper.getAsString(uploadResultJsonObject.get("media_id")));
+    }
+    if (uploadResultJsonObject.get("thumb_media_id") != null && !uploadResultJsonObject.get("thumb_media_id").isJsonNull()) {
+      uploadResult.setThumbMediaId(GsonHelper.getAsString(uploadResultJsonObject.get("thumb_media_id")));
+    }
+    if (uploadResultJsonObject.get("created_at") != null && !uploadResultJsonObject.get("created_at").isJsonNull()) {
+      uploadResult.setCreatedAt(GsonHelper.getAsPrimitiveLong(uploadResultJsonObject.get("created_at")));
+    }
+    return uploadResult;
+  }
+  
+}

+ 94 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxMenuGsonAdapter.java

@@ -0,0 +1,94 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.bean.WxMenu;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxMenuGsonAdapter implements JsonSerializer<WxMenu>, JsonDeserializer<WxMenu> {
+
+  public JsonElement serialize(WxMenu menu, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject json = new JsonObject();
+
+    JsonArray buttonArray = new JsonArray();
+    for (WxMenu.WxMenuButton button : menu.getButtons()) {
+      JsonObject buttonJson = convertToJson(button);
+      buttonArray.add(buttonJson);
+    }
+    json.add("button", buttonArray);
+    
+    return json;
+  }
+
+  protected JsonObject convertToJson(WxMenu.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());
+    if (button.getSubButtons() != null && button.getSubButtons().size() > 0) {
+      JsonArray buttonArray = new JsonArray();
+      for (WxMenu.WxMenuButton sub_button : button.getSubButtons()) {
+        buttonArray.add(convertToJson(sub_button));
+      }
+      buttonJson.add("sub_button", buttonArray);
+    }
+    return buttonJson;
+  }
+
+  public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    /*
+     * 操蛋的微信
+     * 创建菜单时是 { button : ... }
+     * 查询菜单时是 { menu : { button : ... } }
+     */
+    WxMenu menu = new WxMenu();
+    JsonObject menuJson = json.getAsJsonObject().get("menu").getAsJsonObject();
+    JsonArray buttonsJson = menuJson.get("button").getAsJsonArray();
+    for (int i = 0; i < buttonsJson.size(); i++) {
+      JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject();
+      WxMenu.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 WxMenu.WxMenuButton convertFromJson(JsonObject json) {
+    WxMenu.WxMenuButton button = new WxMenu.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"));
+    return button;
+  }
+
+}

+ 2 - 3
src/main/java/me/chanjar/weixin/util/json/WxQrCodeTicketAdapter.java

@@ -6,11 +6,10 @@
  * arose from modification of the original source, or other redistribution of this source
  * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
  */
-package me.chanjar.weixin.util.json;
+package me.chanjar.weixin.enterprise.util.json;
 
 import com.google.gson.*;
-import me.chanjar.weixin.bean.result.WxMassUploadResult;
-import me.chanjar.weixin.bean.result.WxQrCodeTicket;
+import me.chanjar.weixin.enterprise.bean.result.WxQrCodeTicket;
 
 import java.lang.reflect.Type;
 

+ 52 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxUserGsonAdapter.java

@@ -0,0 +1,52 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.bean.result.WxUser;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxUserGsonAdapter implements JsonDeserializer<WxUser> {
+
+  public WxUser deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    JsonObject o = json.getAsJsonObject();
+    WxUser wxUser = new WxUser();
+    wxUser.setSubscribe(new Integer(0).equals(GsonHelper.getInteger(o, "subscribe")) ? false : true);
+    wxUser.setCity(GsonHelper.getString(o, "city"));
+    wxUser.setCountry(GsonHelper.getString(o, "country"));
+    wxUser.setHeadImgUrl(GsonHelper.getString(o, "headimgurl"));
+    wxUser.setLanguage(GsonHelper.getString(o, "language"));
+    wxUser.setNickname(GsonHelper.getString(o, "nickname"));
+    wxUser.setOpenId(GsonHelper.getString(o, "openid"));
+    wxUser.setProvince(GsonHelper.getString(o, "province"));
+    wxUser.setSubscribeTime(GsonHelper.getLong(o, "subscribe_time"));
+    wxUser.setUnionId(GsonHelper.getString(o, "unionid"));
+    Integer sex = GsonHelper.getInteger(o, "sex");
+    if(new Integer(1).equals(sex)) {
+      wxUser.setSex("男");
+    } else if (new Integer(2).equals(sex)) {
+      wxUser.setSex("女");
+    } else {
+      wxUser.setSex("未知");
+    }
+    return wxUser;
+  }
+
+}

+ 42 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/json/WxUserListGsonAdapter.java

@@ -0,0 +1,42 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package me.chanjar.weixin.enterprise.util.json;
+
+import java.lang.reflect.Type;
+
+import me.chanjar.weixin.enterprise.bean.result.WxUserList;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class WxUserListGsonAdapter implements JsonDeserializer<WxUserList> {
+
+  public WxUserList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    JsonObject o = json.getAsJsonObject();
+    WxUserList wxUserList = new WxUserList();
+    wxUserList.setTotal(GsonHelper.getInteger(o, "total"));
+    wxUserList.setCount(GsonHelper.getInteger(o, "count"));
+    wxUserList.setNextOpenId(GsonHelper.getString(o, "next_openid"));
+    JsonArray data = o.get("data").getAsJsonObject().get("openid").getAsJsonArray();
+    for (int i = 0; i < data.size(); i++) {
+      wxUserList.getOpenIds().add(GsonHelper.getAsString(data.get(i)));
+    }
+    return wxUserList;
+  }
+
+}

+ 24 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/AdapterCDATA.java

@@ -0,0 +1,24 @@
+package me.chanjar.weixin.enterprise.util.xml;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+/**
+ * 
+ * http://stackoverflow.com/questions/14193944/jaxb-marshalling-unmarshalling-with-cdata
+ * 
+ * @author Daniel Qian
+ *
+ */
+public class AdapterCDATA extends XmlAdapter<String, String> {
+
+    @Override
+    public String marshal(String arg0) throws Exception {
+        return "<![CDATA[" + arg0 + "]]>";
+    }
+    
+    @Override
+    public String unmarshal(String arg0) throws Exception {
+        return arg0;
+    }
+    
+}

+ 21 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/MediaIdMarshaller.java

@@ -0,0 +1,21 @@
+package me.chanjar.weixin.enterprise.util.xml;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+/**
+ * @author Daniel Qian
+ */
+public class MediaIdMarshaller extends XmlAdapter<String, String> {
+
+  @Override
+  public String marshal(String arg0) throws Exception {
+    return "<MediaId><![CDATA[" + arg0 + "]]></MediaId>";
+  }
+
+  @Override
+  public String unmarshal(String arg0) throws Exception {
+    // do nothing
+    return arg0;
+  }
+
+}

+ 94 - 0
weixin-java-enterprise/src/main/java/me/chanjar/weixin/enterprise/util/xml/XmlTransformer.java

@@ -0,0 +1,94 @@
+package me.chanjar.weixin.enterprise.util.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import javax.xml.bind.*;
+
+import me.chanjar.weixin.enterprise.bean.*;
+import me.chanjar.weixin.enterprise.bean.*;
+import org.xml.sax.InputSource;
+
+import com.sun.xml.bind.marshaller.CharacterEscapeHandler;
+
+public class XmlTransformer {
+
+  protected static final JAXBContext JAXB_CONTEXT = initJAXBContext();
+
+  /**
+   * xml -> pojo
+   *
+   * @param clazz
+   * @param xml
+   * @return
+   * @throws JAXBException
+   */
+  @SuppressWarnings("unchecked")
+  public static <T> T fromXml(Class<T> clazz, String xml) throws JAXBException {
+    Unmarshaller um = JAXB_CONTEXT.createUnmarshaller();
+    T object = (T) um.unmarshal(new StringReader(xml));
+    return object;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <T> T fromXml(Class<T> clazz, InputStream is) throws JAXBException {
+    Unmarshaller um = JAXB_CONTEXT.createUnmarshaller();
+    InputSource inputSource = new InputSource(is);
+    inputSource.setEncoding("utf-8");
+    T object = (T) um.unmarshal(inputSource);
+    return object;
+  }
+
+  /**
+   * pojo -> xml
+   *
+   * @param clazz
+   * @param object
+   * @return
+   * @throws JAXBException
+   */
+  public static <T> String toXml(Class<T> clazz, T object) throws JAXBException {
+    StringWriter stringWriter = new StringWriter();
+    toXml(clazz, object, stringWriter);
+    return stringWriter.getBuffer().toString();
+  }
+
+  public static <T> void toXml(Class<T> clazz, T object, Writer writer) throws JAXBException {
+    Marshaller m = JAXB_CONTEXT.createMarshaller();
+    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+    m.setProperty(CharacterEscapeHandler.class.getName(), CHAR_ESCAPE_HANDLER);
+    m.marshal(object, writer);
+  }
+
+  protected static final CharacterEscapeHandler CHAR_ESCAPE_HANDLER = new CharacterUnescapeHandler();
+
+  protected static class CharacterUnescapeHandler implements CharacterEscapeHandler {
+    public void escape(char[] ac, int i, int j, boolean flag, Writer writer) throws IOException {
+      writer.write(ac, i, j);
+    }
+  }
+
+  private static JAXBContext initJAXBContext() {
+    /*
+     * JAXBContext对象是线程安全的,根据官方文档的建议将对象作为全局实例
+     * https://jaxb.java.net/guide/Performance_and_thread_safety.html
+     */
+    try {
+      return JAXBContext.newInstance(
+          WxXmlOutMessage.class,
+          WxXmlOutImageMessage.class,
+          WxXmlOutMewsMessage.class,
+          WxXmlOutMusicMessage.class,
+          WxXmlOutTextMessage.class,
+          WxXmlOutVideoMessage.class,
+          WxXmlOutVoiceMessage.class,
+          WxCpXmlMessage.class);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+}

+ 61 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/ApiTestModule.java

@@ -0,0 +1,61 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.io.InputStream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import com.google.inject.Binder;
+import com.google.inject.Module;
+import org.xml.sax.InputSource;
+
+public class ApiTestModule implements Module {
+
+  @Override
+  public void configure(Binder binder) {
+    try {
+      InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml");
+      WxXmlCpConfigStorage config = fromXml(WxXmlCpConfigStorage.class, is1);
+      WxCpServiceImpl wxService = new WxCpServiceImpl();
+      wxService.setWxCpConfigStorage(config);
+
+      binder.bind(WxCpServiceImpl.class).toInstance(wxService);
+      binder.bind(WxCpConfigStorage.class).toInstance(config);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static <T> T fromXml(Class<T> clazz, InputStream is) throws JAXBException {
+    Unmarshaller um = JAXBContext.newInstance(clazz).createUnmarshaller();
+    InputSource inputSource = new InputSource(is);
+    inputSource.setEncoding("utf-8");
+    T object = (T) um.unmarshal(inputSource);
+    return object;
+  }
+
+  @XmlRootElement(name = "xml")
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class WxXmlCpConfigStorage extends WxInMemoryCpConfigStorage {
+    
+    protected String openId;
+
+    public String getOpenId() {
+      return openId;
+    }
+    public void setOpenId(String openId) {
+      this.openId = openId;
+    }
+    @Override
+    public String toString() {
+      return "SimpleWxConfigProvider [corpId=" + corpId + ", corpSecret=" + corpSecret + ", accessToken=" + accessToken
+          + ", expiresIn=" + expiresIn + ", token=" + token + ", openId=" + openId + "]";
+    }
+     
+  }
+  
+}

+ 35 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxBaseAPITest.java

@@ -0,0 +1,35 @@
+package me.chanjar.weixin.enterprise.api;
+
+import org.apache.commons.lang3.StringUtils;
+import org.testng.Assert;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+import com.google.inject.Inject;
+
+/**
+ * 基础API测试
+ * @author Daniel Qian
+ *
+ */
+@Test(groups = "baseAPI")
+@Guice(modules = ApiTestModule.class)
+public class WxBaseAPITest {
+
+  @Inject
+  protected WxCpServiceImpl wxService;
+
+  public void testRefreshAccessToken() throws WxErrorException {
+    WxCpConfigStorage configStorage = wxService.wxCpConfigStorage;
+    String before = configStorage.getAccessToken();
+    wxService.accessTokenRefresh();
+
+    String after = configStorage.getAccessToken();
+
+    Assert.assertNotEquals(before, after);
+    Assert.assertTrue(StringUtils.isNotBlank(after));
+  }
+
+}

+ 159 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxCpMessageRouterTest.java

@@ -0,0 +1,159 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.Map;
+
+import me.chanjar.weixin.enterprise.bean.WxCpXmlMessage;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+
+/**
+ * 测试消息路由器
+ * @author Daniel Qian
+ *
+ */
+@Test
+public class WxCpMessageRouterTest {
+  
+  @Test(enabled = false)
+  public void prepare(boolean async, StringBuffer sb, WxCpMessageRouter router) {
+    router
+      .rule()
+        .async(async)
+        .msgType(WxConsts.XML_MSG_TEXT).event(WxConsts.EVT_CLICK).eventKey("KEY_1").content("CONTENT_1")
+        .handler(new WxEchoCpMessageHandler(sb, "COMBINE_4"))
+      .end()
+      .rule()
+        .async(async)
+        .msgType(WxConsts.XML_MSG_TEXT).event(WxConsts.EVT_CLICK).eventKey("KEY_1")
+        .handler(new WxEchoCpMessageHandler(sb, "COMBINE_3"))
+      .end()
+      .rule()
+        .async(async)
+        .msgType(WxConsts.XML_MSG_TEXT).event(WxConsts.EVT_CLICK)
+        .handler(new WxEchoCpMessageHandler(sb, "COMBINE_2"))
+      .end()
+      .rule().async(async).msgType(WxConsts.XML_MSG_TEXT).handler(new WxEchoCpMessageHandler(sb, WxConsts.XML_MSG_TEXT)).end()
+      .rule().async(async).event(WxConsts.EVT_CLICK).handler(new WxEchoCpMessageHandler(sb, WxConsts.EVT_CLICK)).end()
+      .rule().async(async).eventKey("KEY_1").handler(new WxEchoCpMessageHandler(sb, "KEY_1")).end()
+      .rule().async(async).content("CONTENT_1").handler(new WxEchoCpMessageHandler(sb, "CONTENT_1")).end()
+      .rule().async(async).rContent(".*bc.*").handler(new WxEchoCpMessageHandler(sb, "abcd")).end()
+      .rule().async(async).handler(new WxEchoCpMessageHandler(sb, "ALL")).end();
+    ;
+  }
+  
+  @Test(dataProvider="messages-1")
+  public void testSync(WxCpXmlMessage message, String expected) {
+    StringBuffer sb = new StringBuffer();
+    WxCpMessageRouter router = new WxCpMessageRouter();
+    prepare(false, sb, router);
+    router.route(message);
+    Assert.assertEquals(sb.toString(), expected);
+  }
+  
+  @Test(dataProvider="messages-1")
+  public void testAsync(WxCpXmlMessage message, String expected) throws InterruptedException {
+    StringBuffer sb = new StringBuffer();
+    WxCpMessageRouter router = new WxCpMessageRouter();
+    prepare(true,  sb, router);
+    router.route(message);
+    Thread.sleep(500l);
+    Assert.assertEquals(sb.toString(), expected);
+  }
+  
+  public void testConcurrency() throws InterruptedException {
+    final WxCpMessageRouter router = new WxCpMessageRouter();
+    router.rule().handler(new WxCpMessageHandler() {
+      @Override
+      public WxXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context) {
+        return null;
+      }
+    }).end();
+    
+    final WxCpXmlMessage m = new WxCpXmlMessage();
+    Runnable r = new Runnable() {
+      @Override
+      public void run() {
+        router.route(m);
+        try {
+          Thread.sleep(1000l);
+        } catch (InterruptedException e) {
+        }
+      }
+    };
+    for (int i = 0; i < 10; i++) {
+      new Thread(r).start();
+    }
+    
+    Thread.sleep(1000l * 2);
+  }
+  @DataProvider(name="messages-1")
+  public Object[][] messages2() {
+    WxCpXmlMessage message1 = new WxCpXmlMessage();
+    message1.setMsgType(WxConsts.XML_MSG_TEXT);
+  
+    WxCpXmlMessage message2 = new WxCpXmlMessage();
+    message2.setEvent(WxConsts.EVT_CLICK);
+    
+    WxCpXmlMessage message3 = new WxCpXmlMessage();
+    message3.setEventKey("KEY_1");
+    
+    WxCpXmlMessage message4 = new WxCpXmlMessage();
+    message4.setContent("CONTENT_1");
+    
+    WxCpXmlMessage message5 = new WxCpXmlMessage();
+    message5.setContent("BLA");
+    
+    WxCpXmlMessage message6 =  new WxCpXmlMessage();
+    message6.setContent("abcd");
+    
+    WxCpXmlMessage c2 = new WxCpXmlMessage();
+    c2.setMsgType(WxConsts.XML_MSG_TEXT);
+    c2.setEvent(WxConsts.EVT_CLICK);
+    
+    WxCpXmlMessage c3 = new WxCpXmlMessage();
+    c3.setMsgType(WxConsts.XML_MSG_TEXT);
+    c3.setEvent(WxConsts.EVT_CLICK);
+    c3.setEventKey("KEY_1");
+    
+    WxCpXmlMessage c4 = new WxCpXmlMessage();
+    c4.setMsgType(WxConsts.XML_MSG_TEXT);
+    c4.setEvent(WxConsts.EVT_CLICK);
+    c4.setEventKey("KEY_1");
+    c4.setContent("CONTENT_1");
+    
+    return new Object[][] {
+        new Object[] { message1, WxConsts.XML_MSG_TEXT + "," },
+        new Object[] { message2, WxConsts.EVT_CLICK + "," }, 
+        new Object[] { message3, "KEY_1," },
+        new Object[] { message4, "CONTENT_1," },
+        new Object[] { message5, "ALL," },
+        new Object[] { message6, "abcd," },
+        new Object[] { c2, "COMBINE_2," },
+        new Object[] { c3, "COMBINE_3," },
+        new Object[] { c4, "COMBINE_4," }
+    };
+    
+  }
+
+  public static class WxEchoCpMessageHandler implements WxCpMessageHandler {
+
+    private StringBuffer sb;
+    private String echoStr;
+
+    public WxEchoCpMessageHandler(StringBuffer sb, String echoStr) {
+      this.sb = sb;
+      this.echoStr = echoStr;
+    }
+
+    @Override
+    public WxXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context) {
+      sb.append(this.echoStr).append(',');
+      return null;
+    }
+
+  }
+
+}

+ 33 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxCustomMessageAPITest.java

@@ -0,0 +1,33 @@
+package me.chanjar.weixin.enterprise.api;
+
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+import com.google.inject.Inject;
+
+/***
+ * 测试发送客服消息
+ * @author Daniel Qian
+ *
+ */
+@Test(groups="customMessageAPI", dependsOnGroups = "baseAPI")
+@Guice(modules = ApiTestModule.class)
+public class WxCustomMessageAPITest {
+
+  @Inject
+  protected WxCpServiceImpl wxService;
+
+  public void testSendCustomMessage() throws WxErrorException {
+    ApiTestModule.WxXmlCpConfigStorage configStorage = (ApiTestModule.WxXmlCpConfigStorage) wxService.wxCpConfigStorage;
+    WxCustomMessage message = new WxCustomMessage();
+    message.setMsgType(WxConsts.CUSTOM_MSG_TEXT);
+    message.setToUser(configStorage.getOpenId());
+    message.setContent("欢迎欢迎,热烈欢迎\n换行测试\n超链接:<a href=\"http://www.baidu.com\">Hello World</a>");
+
+    wxService.messageSend(message);
+  }
+
+}

+ 50 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxGroupAPITest.java

@@ -0,0 +1,50 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.util.List;
+
+import me.chanjar.weixin.enterprise.bean.WxDepartment;
+import org.testng.Assert;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+import com.google.inject.Inject;
+
+/**
+ * 测试分组接口
+ * 
+ * @author Daniel Qian
+ */
+@Test(groups = "groupAPI", dependsOnGroups = "baseAPI")
+@Guice(modules = ApiTestModule.class)
+public class WxGroupAPITest {
+
+  @Inject
+  protected WxCpServiceImpl wxService;
+
+  protected WxDepartment group;
+  
+  public void testGroupCreate() throws WxErrorException {
+    WxDepartment res = wxService.departmentCreate("测试分组1");
+    Assert.assertEquals(res.getName(), "测试分组1");
+  }
+
+  @Test(dependsOnMethods="testGroupCreate")
+  public void testGroupGet() throws WxErrorException {
+    List<WxDepartment> groupList = wxService.departmentGet();
+    Assert.assertNotNull(groupList);
+    Assert.assertTrue(groupList.size() > 0);
+    for (WxDepartment g : groupList) {
+      group = g;
+      Assert.assertNotNull(g.getName());
+    }
+  }
+  
+  @Test(dependsOnMethods={"testGroupGet", "testGroupCreate"})
+  public void getGroupUpdate() throws WxErrorException {
+    group.setName("分组改名");
+    wxService.departmentUpdate(group);
+  }
+
+}

+ 72 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxMediaAPITest.java

@@ -0,0 +1,72 @@
+package me.chanjar.weixin.enterprise.api;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+import com.google.inject.Inject;
+
+/**
+ * 测试多媒体文件上传下载
+ * @author Daniel Qian
+ *
+ */
+@Test(groups="mediaAPI", dependsOnGroups="baseAPI")
+@Guice(modules = ApiTestModule.class)
+public class WxMediaAPITest {
+
+  @Inject
+  protected WxCpServiceImpl wxService;
+
+  private List<String> media_ids = new ArrayList<String>();
+  
+  @Test(dataProvider="uploadMedia")
+  public void testUploadMedia(String mediaType, String fileType, String fileName) throws WxErrorException, IOException {
+    InputStream inputStream = ClassLoader.getSystemResourceAsStream(fileName);
+    WxMediaUploadResult res = wxService.mediaUpload(mediaType, fileType, inputStream);
+    Assert.assertNotNull(res.getType());
+    Assert.assertNotNull(res.getCreatedAt());
+    Assert.assertTrue(res.getMediaId() != null || res.getThumbMediaId() != null);
+    
+    if (res.getMediaId() != null) {
+      media_ids.add(res.getMediaId());
+    }
+    if (res.getThumbMediaId() != null) {
+      media_ids.add(res.getThumbMediaId());
+    }
+  }
+  
+  @DataProvider
+  public Object[][] uploadMedia() {
+    return new Object[][] {
+        new Object[] { WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, "mm.jpeg" },
+        new Object[] { WxConsts.MEDIA_VOICE, WxConsts.FILE_MP3, "mm.mp3" },
+        new Object[] { WxConsts.MEDIA_VIDEO, WxConsts.FILE_MP4, "mm.mp4" },
+        new Object[] { WxConsts.MEDIA_THUMB, WxConsts.FILE_JPG, "mm.jpeg" }
+    };
+  }
+  
+  @Test(dependsOnMethods = { "testUploadMedia" }, dataProvider="downloadMedia")
+  public void testDownloadMedia(String media_id) throws WxErrorException {
+    wxService.mediaDownload(media_id);
+  }
+  
+  @DataProvider
+  public Object[][] downloadMedia() {
+    Object[][] params = new Object[this.media_ids.size()][];
+    for (int i = 0; i < this.media_ids.size(); i++) {
+      params[i] = new Object[] { this.media_ids.get(i) };
+    }
+    return params;
+  }
+  
+}

+ 91 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/api/WxMenuAPITest.java

@@ -0,0 +1,91 @@
+package me.chanjar.weixin.enterprise.api;
+
+import javax.xml.bind.JAXBException;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.google.inject.Inject;
+
+import me.chanjar.weixin.enterprise.bean.WxMenu;
+import me.chanjar.weixin.enterprise.bean.WxMenu.WxMenuButton;
+import me.chanjar.weixin.enterprise.exception.WxErrorException;
+
+/**
+ * 测试菜单
+ * @author Daniel Qian
+ *
+ */
+@Test(groups="menuAPI", dependsOnGroups="baseAPI")
+@Guice(modules = ApiTestModule.class)
+public class WxMenuAPITest {
+
+  @Inject
+  protected WxCpServiceImpl wxService;
+  
+  @Test(dataProvider = "menu")
+  public void testCreateMenu(WxMenu wxMenu) throws WxErrorException {
+    wxService.menuCreate(wxMenu);
+  }
+  
+  @Test(dependsOnMethods = { "testCreateMenu"})
+  public void testGetMenu() throws WxErrorException {
+    Assert.assertNotNull(wxService.menuGet());
+  }
+  
+  @Test(dependsOnMethods = { "testGetMenu"})
+  public void testDeleteMenu() throws WxErrorException {
+    wxService.menuDelete();
+  }
+  
+  @DataProvider(name="menu")
+  public Object[][] getMenu() throws JAXBException {
+    WxMenu menu = new WxMenu();
+    WxMenuButton button1 = new WxMenuButton();
+    button1.setType(WxConsts.BUTTON_CLICK);
+    button1.setName("今日歌曲");
+    button1.setKey("V1001_TODAY_MUSIC");
+    
+    WxMenuButton button2 = new WxMenuButton();
+    button2.setType(WxConsts.BUTTON_CLICK);
+    button2.setName("歌手简介");
+    button2.setKey("V1001_TODAY_SINGER");
+    
+    WxMenuButton button3 = new WxMenuButton();
+    button3.setName("菜单");
+    
+    menu.getButtons().add(button1);
+    menu.getButtons().add(button2);
+    menu.getButtons().add(button3);
+    
+    WxMenuButton button31 = new WxMenuButton();
+    button31.setType(WxConsts.BUTTON_VIEW);
+    button31.setName("搜索");
+    button31.setUrl("http://www.soso.com/");
+    
+    WxMenuButton button32 = new WxMenuButton();
+    button32.setType(WxConsts.BUTTON_VIEW);
+    button32.setName("视频");
+    button32.setUrl("http://v.qq.com/");
+    
+    WxMenuButton button33 = new WxMenuButton();
+    button33.setType(WxConsts.BUTTON_CLICK);
+    button33.setName("赞一下我们");
+    button33.setKey("V1001_GOOD");
+    
+    button3.getSubButtons().add(button31);
+    button3.getSubButtons().add(button32);
+    button3.getSubButtons().add(button33);
+    
+    return new Object[][] {
+        new Object[] {
+            menu
+        }
+    };
+  
+  }
+  
+  
+}

+ 19 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxAccessTokenTest.java

@@ -0,0 +1,19 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxAccessToken;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxAccessTokenTest {
+
+  public void testFromJson() {
+
+    String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}";
+    WxAccessToken wxError = WxAccessToken.fromJson(json);
+    Assert.assertEquals(wxError.getAccessToken(), "ACCESS_TOKEN");
+    Assert.assertTrue(wxError.getExpiresIn() == 7200);
+
+  }
+  
+}

+ 91 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxCpXmlMessageTest.java

@@ -0,0 +1,91 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxCpXmlMessageTest {
+
+  public void testFromXml() {
+
+    String xml = "<xml>"
+                + "<ToUserName><![CDATA[toUser]]></ToUserName>"
+                + "<FromUserName><![CDATA[fromUser]]></FromUserName> "
+                + "<CreateTime>1348831860</CreateTime>"
+                + "<MsgType><![CDATA[text]]></MsgType>"
+                + "<Content><![CDATA[this is a test]]></Content>"
+                + "<MsgId>1234567890123456</MsgId>"
+                + "<PicUrl><![CDATA[this is a url]]></PicUrl>"
+                + "<MediaId><![CDATA[media_id]]></MediaId>"
+                + "<Format><![CDATA[Format]]></Format>"
+                + "<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>"
+                + "<Location_X>23.134521</Location_X>"
+                + "<Location_Y>113.358803</Location_Y>"
+                + "<Scale>20</Scale>"
+                + "<Label><![CDATA[位置信息]]></Label>"
+                + "<Description><![CDATA[公众平台官网链接]]></Description>"
+                + "<Url><![CDATA[url]]></Url>"
+                + "<Title><![CDATA[公众平台官网链接]]></Title>"
+                + "<Event><![CDATA[subscribe]]></Event>"
+                + "<EventKey><![CDATA[qrscene_123123]]></EventKey>"
+                + "<Ticket><![CDATA[TICKET]]></Ticket>"
+                + "<Latitude>23.137466</Latitude>"
+                + "<Longitude>113.352425</Longitude>"
+                + "<Precision>119.385040</Precision>"
+                + "<ScanCodeInfo>"
+                + " <ScanType><![CDATA[qrcode]]></ScanType>"
+                + " <ScanResult><![CDATA[1]]></ScanResult>"
+                + "</ScanCodeInfo>"
+                + "<SendPicsInfo>"
+                + " <Count>1</Count>\n"
+                + " <PicList>"
+                + "  <item>"
+                + "   <PicMd5Sum><![CDATA[1b5f7c23b5bf75682a53e7b6d163e185]]></PicMd5Sum>"
+                + "  </item>"
+                + " </PicList>"
+                + "</SendPicsInfo>"
+                + "<SendLocationInfo>"
+                + "  <Location_X><![CDATA[23]]></Location_X>\n"
+                + "  <Location_Y><![CDATA[113]]></Location_Y>\n"
+                + "  <Scale><![CDATA[15]]></Scale>\n"
+                + "  <Label><![CDATA[ 广州市海珠区客村艺苑路 106号]]></Label>\n"
+                + "  <Poiname><![CDATA[wo de poi]]></Poiname>\n"
+                + "</SendLocationInfo>"
+                + "</xml>";
+    WxCpXmlMessage wxMessage = WxCpXmlMessage.fromXml(xml);
+    Assert.assertEquals(wxMessage.getToUserName(), "toUser");
+    Assert.assertEquals(wxMessage.getFromUserName(), "fromUser");
+    Assert.assertEquals(wxMessage.getCreateTime(), new Long(1348831860l));
+    Assert.assertEquals(wxMessage.getMsgType(), WxConsts.XML_MSG_TEXT);
+    Assert.assertEquals(wxMessage.getContent(), "this is a test");
+    Assert.assertEquals(wxMessage.getMsgId(), new Long(1234567890123456l));
+    Assert.assertEquals(wxMessage.getPicUrl(), "this is a url");
+    Assert.assertEquals(wxMessage.getMediaId(), "media_id");
+    Assert.assertEquals(wxMessage.getFormat(), "Format");
+    Assert.assertEquals(wxMessage.getThumbMediaId(), "thumb_media_id");
+    Assert.assertEquals(wxMessage.getLocationX(), new Double(23.134521d));
+    Assert.assertEquals(wxMessage.getLocationY(), new Double(113.358803d));
+    Assert.assertEquals(wxMessage.getScale(), new Double(20));
+    Assert.assertEquals(wxMessage.getLabel(), "位置信息");
+    Assert.assertEquals(wxMessage.getDescription(), "公众平台官网链接");
+    Assert.assertEquals(wxMessage.getUrl(), "url");
+    Assert.assertEquals(wxMessage.getTitle(), "公众平台官网链接");
+    Assert.assertEquals(wxMessage.getEvent(), "subscribe");
+    Assert.assertEquals(wxMessage.getEventKey(), "qrscene_123123");
+    Assert.assertEquals(wxMessage.getTicket(), "TICKET");
+    Assert.assertEquals(wxMessage.getLatitude(), new Double(23.137466));
+    Assert.assertEquals(wxMessage.getLongitude(), new Double(113.352425));
+    Assert.assertEquals(wxMessage.getPrecision(), new Double(119.385040));
+    Assert.assertEquals(wxMessage.getScanCodeInfo().getScanType(), "qrcode");
+    Assert.assertEquals(wxMessage.getScanCodeInfo().getScanResult(), "1");
+    Assert.assertEquals(wxMessage.getSendPicsInfo().getCount(), new Long(1l));
+    Assert.assertEquals(wxMessage.getSendPicsInfo().getPicList().get(0).getPicMd5Sum(), "1b5f7c23b5bf75682a53e7b6d163e185");
+    Assert.assertEquals(wxMessage.getSendLocationInfo().getLocationX(), "23");
+    Assert.assertEquals(wxMessage.getSendLocationInfo().getLocationY(), "113");
+    Assert.assertEquals(wxMessage.getSendLocationInfo().getScale(), "15");
+    Assert.assertEquals(wxMessage.getSendLocationInfo().getLabel(), " 广州市海珠区客村艺苑路 106号");
+    Assert.assertEquals(wxMessage.getSendLocationInfo().getPoiname(), "wo de poi");
+  }
+  
+}

+ 133 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxCustomMessageTest.java

@@ -0,0 +1,133 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.api.WxConsts;
+import me.chanjar.weixin.enterprise.bean.WxCustomMessage.WxArticle;
+
+@Test
+public class WxCustomMessageTest {
+
+  public void testTextReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_TEXT);
+    reply.setContent("sfsfdsdf");
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}");
+  }
+  
+  public void testTextBuild() {
+    WxCustomMessage reply = WxCustomMessage.TEXT().toUser("OPENID").content("sfsfdsdf").build();
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}");
+  }
+  
+  public void testImageReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_IMAGE);
+    reply.setMediaId("MEDIA_ID");
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}");
+  }
+  
+  public void testImageBuild() {
+    WxCustomMessage reply = WxCustomMessage.IMAGE().toUser("OPENID").mediaId("MEDIA_ID").build();
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}");
+  }
+  
+  public void testVoiceReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_VOICE);
+    reply.setMediaId("MEDIA_ID");
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"voice\",\"voice\":{\"media_id\":\"MEDIA_ID\"}}");
+  }
+  
+  public void testVoiceBuild() {
+    WxCustomMessage reply = WxCustomMessage.VOICE().toUser("OPENID").mediaId("MEDIA_ID").build();
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"voice\",\"voice\":{\"media_id\":\"MEDIA_ID\"}}");
+  }
+  
+  public void testVideoReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_VIDEO);
+    reply.setMediaId("MEDIA_ID");
+    reply.setThumbMediaId("MEDIA_ID");
+    reply.setTitle("TITLE");
+    reply.setDescription("DESCRIPTION");
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"video\",\"video\":{\"media_id\":\"MEDIA_ID\",\"thumb_media_id\":\"MEDIA_ID\",\"title\":\"TITLE\",\"description\":\"DESCRIPTION\"}}");
+  }
+  
+  public void testVideoBuild() {
+    WxCustomMessage reply = WxCustomMessage.VIDEO().toUser("OPENID").title("TITLE").mediaId("MEDIA_ID").thumbMediaId("MEDIA_ID").description("DESCRIPTION").build();
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"video\",\"video\":{\"media_id\":\"MEDIA_ID\",\"thumb_media_id\":\"MEDIA_ID\",\"title\":\"TITLE\",\"description\":\"DESCRIPTION\"}}");
+  }
+  
+  public void testMusicReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_MUSIC);
+    reply.setThumbMediaId("MEDIA_ID");
+    reply.setDescription("DESCRIPTION");
+    reply.setTitle("TITLE");
+    reply.setMusicUrl("MUSIC_URL");
+    reply.setHqMusicUrl("HQ_MUSIC_URL");
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"music\",\"music\":{\"title\":\"TITLE\",\"description\":\"DESCRIPTION\",\"thumb_media_id\":\"MEDIA_ID\",\"musicurl\":\"MUSIC_URL\",\"hqmusicurl\":\"HQ_MUSIC_URL\"}}");
+  }
+  
+  public void testMusicBuild() {
+    WxCustomMessage reply = WxCustomMessage.MUSIC()
+          .toUser("OPENID")
+          .title("TITLE")
+          .thumbMediaId("MEDIA_ID")
+          .description("DESCRIPTION")
+          .musicUrl("MUSIC_URL")
+          .hqMusicUrl("HQ_MUSIC_URL")
+          .build();
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"music\",\"music\":{\"title\":\"TITLE\",\"description\":\"DESCRIPTION\",\"thumb_media_id\":\"MEDIA_ID\",\"musicurl\":\"MUSIC_URL\",\"hqmusicurl\":\"HQ_MUSIC_URL\"}}");
+  }
+  
+  public void testNewsReply() {
+    WxCustomMessage reply = new WxCustomMessage();
+    reply.setToUser("OPENID");
+    reply.setMsgType(WxConsts.CUSTOM_MSG_NEWS);
+    
+    WxArticle article1 = new WxArticle();
+    article1.setUrl("URL");
+    article1.setPicUrl("PIC_URL");
+    article1.setDescription("Is Really A Happy Day");
+    article1.setTitle("Happy Day");
+    reply.getArticles().add(article1);
+    
+    WxArticle article2 = new WxArticle();
+    article2.setUrl("URL");
+    article2.setPicUrl("PIC_URL");
+    article2.setDescription("Is Really A Happy Day");
+    article2.setTitle("Happy Day");
+    reply.getArticles().add(article2);
+
+    
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"news\",\"articles\":[{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"},{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"}]}");
+  }
+  
+  public void testNewsBuild() {
+    WxArticle article1 = new WxArticle();
+    article1.setUrl("URL");
+    article1.setPicUrl("PIC_URL");
+    article1.setDescription("Is Really A Happy Day");
+    article1.setTitle("Happy Day");
+    
+    WxArticle article2 = new WxArticle();
+    article2.setUrl("URL");
+    article2.setPicUrl("PIC_URL");
+    article2.setDescription("Is Really A Happy Day");
+    article2.setTitle("Happy Day");
+
+    WxCustomMessage reply = WxCustomMessage.NEWS().toUser("OPENID").addArticle(article1).addArticle(article2).build();
+    
+    Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"news\",\"articles\":[{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"},{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"}]}");
+  }
+  
+}

+ 38 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxErrorTest.java

@@ -0,0 +1,38 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.bean.result.WxError;
+
+@Test
+public class WxErrorTest {
+
+  public void testFromJson() {
+
+    String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\" }";
+    WxError wxError = WxError.fromJson(json);
+    Assert.assertTrue(wxError.getErrorCode() == 40003);
+    Assert.assertEquals(wxError.getErrorMsg(), "invalid openid");
+
+  }
+  
+  public void testFromBadJson1() {
+
+    String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\", \"media_id\": \"12323423dsfafsf232f\" }";
+    WxError wxError = WxError.fromJson(json);
+    Assert.assertTrue(wxError.getErrorCode() == 40003);
+    Assert.assertEquals(wxError.getErrorMsg(), "invalid openid");
+
+  }
+  
+  public void testFromBadJson2() {
+
+    String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}";
+    WxError wxError = WxError.fromJson(json);
+    Assert.assertTrue(wxError.getErrorCode() == 0);
+    Assert.assertEquals(wxError.getErrorMsg(), null);
+
+  }
+  
+}

+ 112 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxMenuTest.java

@@ -0,0 +1,112 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxMenu;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import me.chanjar.weixin.enterprise.bean.WxMenu.WxMenuButton;
+
+@Test
+public class WxMenuTest {
+
+  @Test(dataProvider="wxReturnMenu")
+  public void testFromJson(String json) {
+    WxMenu menu = WxMenu.fromJson(json);
+    Assert.assertEquals(menu.getButtons().size(), 3);
+  }
+  
+  @Test(dataProvider="wxPushMenu")
+  public void testToJson(String json) {
+    WxMenu menu = new WxMenu();
+    WxMenuButton button1 = new WxMenuButton();
+    button1.setType("click");
+    button1.setName("今日歌曲");
+    button1.setKey("V1001_TODAY_MUSIC");
+    
+    WxMenuButton button2 = new WxMenuButton();
+    button2.setType("click");
+    button2.setName("歌手简介");
+    button2.setKey("V1001_TODAY_SINGER");
+    
+    WxMenuButton button3 = new WxMenuButton();
+    button3.setName("菜单");
+    
+    menu.getButtons().add(button1);
+    menu.getButtons().add(button2);
+    menu.getButtons().add(button3);
+    
+    WxMenuButton button31 = new WxMenuButton();
+    button31.setType("view");
+    button31.setName("搜索");
+    button31.setUrl("http://www.soso.com/");
+    
+    WxMenuButton button32 = new WxMenuButton();
+    button32.setType("view");
+    button32.setName("视频");
+    button32.setUrl("http://v.qq.com/");
+    
+    WxMenuButton button33 = new WxMenuButton();
+    button33.setType("click");
+    button33.setName("赞一下我们");
+    button33.setKey("V1001_GOOD");
+    
+    button3.getSubButtons().add(button31);
+    button3.getSubButtons().add(button32);
+    button3.getSubButtons().add(button33);
+    
+    Assert.assertEquals(menu.toJson(), json);
+  }
+  
+  @DataProvider
+  public Object[][] wxReturnMenu() {
+    Object[][]  res = menuJson();
+    String json = "{ \"menu\" : " + res[0][0] + " }";
+    return new Object[][] {
+        new Object[] { json }
+    };
+  }
+  
+  @DataProvider(name="wxPushMenu")
+  public Object[][] menuJson() {
+    String json = 
+        "{"
+            +"\"button\":["
+              +"{"
+                +"\"type\":\"click\","
+                +"\"name\":\"今日歌曲\","
+                +"\"key\":\"V1001_TODAY_MUSIC\""
+              +"},"
+              +"{"
+                +"\"type\":\"click\","
+                +"\"name\":\"歌手简介\","
+                +"\"key\":\"V1001_TODAY_SINGER\""
+              +"},"
+              +"{"
+                +"\"name\":\"菜单\","
+                +"\"sub_button\":["
+                  +"{"
+                    +"\"type\":\"view\","
+                    +"\"name\":\"搜索\","
+                    +"\"url\":\"http://www.soso.com/\""
+                  +"},"
+                  +"{"
+                    +"\"type\":\"view\","
+                    +"\"name\":\"视频\","
+                    +"\"url\":\"http://v.qq.com/\""
+                  +"},"
+                  +"{"
+                  +"\"type\":\"click\","
+                  +"\"name\":\"赞一下我们\","
+                  +"\"key\":\"V1001_GOOD\""
+                  +"}"
+                +"]"
+              +"}"
+            +"]"
+        +"}";
+    return new Object[][] {
+        new Object[] { json }
+    };
+  }
+  
+}

+ 52 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutImageMessageTest.java

@@ -0,0 +1,52 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutImageMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxXmlOutImageMessageTest {
+
+  public void test() {
+    WxXmlOutImageMessage m = new WxXmlOutImageMessage();
+    m.setMediaId("ddfefesfsdfef");
+    m.setCreateTime(1122l);
+    m.setFromUserName("from");
+    m.setToUserName("to");
+    
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[to]]></ToUserName>"
+        + "<FromUserName><![CDATA[from]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[image]]></MsgType>"
+        + "<Image><MediaId><![CDATA[ddfefesfsdfef]]></MediaId></Image>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
+  }
+  
+  public void testBuild() {
+    WxXmlOutImageMessage m = WxXmlOutMessage.IMAGE().mediaId("ddfefesfsdfef").fromUser("from").toUser("to").build();
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[to]]></ToUserName>"
+        + "<FromUserName><![CDATA[from]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[image]]></MsgType>"
+        + "<Image><MediaId><![CDATA[ddfefesfsdfef]]></MediaId></Image>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(
+              m
+                .toXml()
+                .replaceAll("\\s", "")
+                .replaceAll("<CreateTime>.*?</CreateTime>", ""), 
+              expected
+                .replaceAll("\\s", "")
+                .replaceAll("<CreateTime>.*?</CreateTime>", "")
+              );
+    
+  }
+}

+ 76 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutMusicMessageTest.java

@@ -0,0 +1,76 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMusicMessage;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxXmlOutMusicMessageTest {
+
+  public void test() {
+    WxXmlOutMusicMessage m = new WxXmlOutMusicMessage();
+    m.setTitle("title");
+    m.setDescription("ddfff");
+    m.setHqMusicUrl("hQMusicUrl");
+    m.setMusicUrl("musicUrl");
+    m.setThumbMediaId("thumbMediaId");
+    m.setCreateTime(1122l);
+    m.setFromUserName("fromUser");
+    m.setToUserName("toUser");
+    
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[toUser]]></ToUserName>"
+        + "<FromUserName><![CDATA[fromUser]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[music]]></MsgType>"
+        + "<Music>"
+        + "        <Title><![CDATA[title]]></Title>"
+        + "        <Description><![CDATA[ddfff]]></Description>"
+        + "        <ThumbMediaId><![CDATA[thumbMediaId]]></ThumbMediaId>"
+        + "        <MusicUrl><![CDATA[musicUrl]]></MusicUrl>"
+        + "        <HQMusicUrl><![CDATA[hQMusicUrl]]></HQMusicUrl>"
+        + "    </Music>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
+  }
+  
+  public void testBuild() {
+    WxXmlOutMusicMessage m = WxXmlOutMessage.MUSIC()
+          .fromUser("fromUser")
+          .toUser("toUser")
+          .title("title")
+          .description("ddfff")
+          .hqMusicUrl("hQMusicUrl")
+          .musicUrl("musicUrl")
+          .thumbMediaId("thumbMediaId")
+          .build();
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[toUser]]></ToUserName>"
+        + "<FromUserName><![CDATA[fromUser]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[music]]></MsgType>"
+        + "<Music>"
+        + "        <Title><![CDATA[title]]></Title>"
+        + "        <Description><![CDATA[ddfff]]></Description>"
+        + "        <ThumbMediaId><![CDATA[thumbMediaId]]></ThumbMediaId>"
+        + "        <MusicUrl><![CDATA[musicUrl]]></MusicUrl>"
+        + "        <HQMusicUrl><![CDATA[hQMusicUrl]]></HQMusicUrl>"
+        + "    </Music>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(
+        m
+        .toXml()
+        .replaceAll("\\s", "")
+        .replaceAll("<CreateTime>.*?</CreateTime>", ""), 
+        expected
+        .replaceAll("\\s", "")
+        .replaceAll("<CreateTime>.*?</CreateTime>", "")
+        );
+  }
+  
+}

+ 97 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutNewsMessageTest.java

@@ -0,0 +1,97 @@
+package me.chanjar.weixin.enterprise.bean;
+
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMessage;
+import me.chanjar.weixin.enterprise.bean.WxXmlOutMewsMessage;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxXmlOutNewsMessageTest {
+
+  public void test() {
+    WxXmlOutMewsMessage m = new WxXmlOutMewsMessage();
+    m.setCreateTime(1122l);
+    m.setFromUserName("fromUser");
+    m.setToUserName("toUser");
+    
+    WxXmlOutMewsMessage.Item item = new WxXmlOutMewsMessage.Item();
+    item.setDescription("description");
+    item.setPicUrl("picUrl");
+    item.setTitle("title");
+    item.setUrl("url");
+    m.addArticle(item);
+    m.addArticle(item);
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[toUser]]></ToUserName>"
+        + "<FromUserName><![CDATA[fromUser]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[news]]></MsgType>"
+        + "    <ArticleCount>2</ArticleCount>"
+        + "    <Articles>"
+        + "        <item>"
+        + "            <Title><![CDATA[title]]></Title>"
+        + "            <Description><![CDATA[description]]></Description>"
+        + "            <PicUrl><![CDATA[picUrl]]></PicUrl>"
+        + "            <Url><![CDATA[url]]></Url>"
+        + "        </item>"
+        + "        <item>"
+        + "            <Title><![CDATA[title]]></Title>"
+        + "            <Description><![CDATA[description]]></Description>"
+        + "            <PicUrl><![CDATA[picUrl]]></PicUrl>"
+        + "            <Url><![CDATA[url]]></Url>"
+        + "        </item>"
+        + "    </Articles>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
+  }
+  
+  public void testBuild() {
+    WxXmlOutMewsMessage.Item item = new WxXmlOutMewsMessage.Item();
+    item.setDescription("description");
+    item.setPicUrl("picUrl");
+    item.setTitle("title");
+    item.setUrl("url");
+    
+    WxXmlOutMewsMessage m = WxXmlOutMessage.NEWS()
+          .fromUser("fromUser")
+          .toUser("toUser")
+          .addArticle(item)
+          .addArticle(item)
+          .build();
+    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" 
+        + "<xml>"
+        + "<ToUserName><![CDATA[toUser]]></ToUserName>"
+        + "<FromUserName><![CDATA[fromUser]]></FromUserName>"
+        + "<CreateTime>1122</CreateTime>"
+        + "<MsgType><![CDATA[news]]></MsgType>"
+        + "    <ArticleCount>2</ArticleCount>"
+        + "    <Articles>"
+        + "        <item>"
+        + "            <Title><![CDATA[title]]></Title>"
+        + "            <Description><![CDATA[description]]></Description>"
+        + "            <PicUrl><![CDATA[picUrl]]></PicUrl>"
+        + "            <Url><![CDATA[url]]></Url>"
+        + "        </item>"
+        + "        <item>"
+        + "            <Title><![CDATA[title]]></Title>"
+        + "            <Description><![CDATA[description]]></Description>"
+        + "            <PicUrl><![CDATA[picUrl]]></PicUrl>"
+        + "            <Url><![CDATA[url]]></Url>"
+        + "        </item>"
+        + "    </Articles>"
+        + "</xml>";
+    System.out.println(m.toXml());
+    Assert.assertEquals(
+        m
+        .toXml()
+        .replaceAll("\\s", "")
+        .replaceAll("<CreateTime>.*?</CreateTime>", ""), 
+        expected
+        .replaceAll("\\s", "")
+        .replaceAll("<CreateTime>.*?</CreateTime>", "")
+        );
+  }
+  
+}

+ 0 - 0
weixin-java-enterprise/src/test/java/me/chanjar/weixin/enterprise/bean/WxXmlOutTextMessageTest.java


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