From 9f4a7a779669fee630c1f1d8a9fe7664df196e5f Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Sun, 25 Jun 2017 18:35:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=87=8D=E6=9E=84=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E5=8F=B7=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=8D=87=E7=BA=A7=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=90=8E=E5=87=BA=E7=8E=B0=E7=9A=84=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E9=97=AE=E9=A2=98=E5=92=8C=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/json/WxMenuGsonAdapter.java | 7 +- .../weixin/cp/api/WxCpMediaService.java | 46 ++++ .../weixin/cp/api/WxCpMenuService.java | 80 ++++++ .../weixin/cp/api/WxCpOAuth2Service.java | 67 +++++ .../weixin/cp/api/WxCpOauth2Service.java | 11 - .../me/chanjar/weixin/cp/api/WxCpService.java | 253 ++++++------------ .../chanjar/weixin/cp/api/WxCpTagService.java | 55 ++++ .../cp/api/impl/AbstractWxCpServiceImpl.java | 167 +++--------- .../cp/api/impl/WxCpMediaServiceImpl.java | 50 ++++ .../cp/api/impl/WxCpMenuServiceImpl.java | 64 +++++ .../cp/api/impl/WxCpOAuth2ServiceImpl.java | 66 +++++ .../impl/WxCpServiceApacheHttpClientImpl.java | 3 +- .../cp/api/impl/WxCpTagServiceImpl.java | 116 ++++++++ .../bean/WxCpTagAddOrRemoveUsersResult.java | 85 ++++++ .../me/chanjar/weixin/cp/bean/WxCpUser.java | 39 ++- .../weixin/cp/util/json/WxCpGsonBuilder.java | 2 + .../cp/util/json/WxCpMenuGsonAdapter.java | 24 ++ .../cp/util/json/WxCpUserGsonAdapter.java | 4 +- .../chanjar/weixin/cp/api/ApiTestModule.java | 2 +- .../weixin/cp/api/WxCpMediaAPITest.java | 77 ------ .../chanjar/weixin/cp/api/WxCpTagAPITest.java | 68 ----- .../cp/api/impl/WxCpMediaServiceImplTest.java | 78 ++++++ .../WxCpMenuServiceImplTest.java} | 60 ++--- .../cp/api/impl/WxCpTagServiceImplTest.java | 75 ++++++ .../cp/api/impl/WxCpUserServiceImplTest.java | 20 +- .../weixin/cp/demo/WxCpDemoServer.java | 9 +- .../weixin/cp/demo/WxCpOAuth2Servlet.java | 2 +- weixin-java-cp/src/test/resources/mm.amr | Bin 0 -> 38982 bytes weixin-java-cp/src/test/resources/testng.xml | 5 - 29 files changed, 1022 insertions(+), 513 deletions(-) create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java delete mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOauth2Service.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMenuGsonAdapter.java delete mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMediaAPITest.java delete mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTagAPITest.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java rename weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/{WxMenuAPITest.java => impl/WxCpMenuServiceImplTest.java} (62%) create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImplTest.java create mode 100644 weixin-java-cp/src/test/resources/mm.amr diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java index 526b5f29..c506c1ed 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java @@ -90,9 +90,14 @@ public class WxMenuGsonAdapter implements JsonSerializer, JsonDeserializ * 操蛋的微信 * 创建菜单时是 { button : ... } * 查询菜单时是 { menu : { button : ... } } + * 现在企业号升级为企业微信后,没有此问题,因此需要单独处理 */ - WxMenu menu = new WxMenu(); JsonArray buttonsJson = json.getAsJsonObject().get("menu").getAsJsonObject().get("button").getAsJsonArray(); + return this.buildMenuFromJson(buttonsJson); + } + + protected WxMenu buildMenuFromJson(JsonArray buttonsJson) { + WxMenu menu = new WxMenu(); for (int i = 0; i < buttonsJson.size(); i++) { JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject(); WxMenuButton button = convertFromJson(buttonJson); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java index ba0241a6..f813f1bf 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java @@ -1,11 +1,57 @@ package me.chanjar.weixin.cp.api; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.exception.WxErrorException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + /** *
+ *  媒体管理接口
  *  Created by BinaryWang on 2017/6/24.
  * 
* * @author Binary Wang */ public interface WxCpMediaService { + + /** + *
+   * 上传多媒体文件
+   * 上传的多媒体文件有格式和大小限制,如下:
+   *   图片(image): 1M,支持JPG格式
+   *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
+   *   视频(video):10MB,支持MP4格式
+   *   缩略图(thumb):64KB,支持JPG格式
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * 
+ * + * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param inputStream 输入流 + */ + WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputStream) + throws WxErrorException, IOException; + + /** + * @param mediaType 媒体类型 + * @param file 文件对象 + * @see #upload(String, String, InputStream) + */ + WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException; + + /** + *
+   * 下载多媒体文件
+   * 根据微信文档,视频文件下载不了,会返回null
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * 
+ * + * @param mediaId 媒体id + * @return 保存到本地的临时文件 + */ + File download(String mediaId) throws WxErrorException; + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java index f6a45e43..012efefb 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java @@ -1,11 +1,91 @@ package me.chanjar.weixin.cp.api; +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.exception.WxErrorException; + /** *
+ *  菜单管理相关接口
  *  Created by BinaryWang on 2017/6/24.
  * 
* * @author Binary Wang */ public interface WxCpMenuService { + /** + *
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * 
+ * + * @param menu 菜单对象 + * @see #create(Integer, WxMenu) + */ + void create(WxMenu menu) throws WxErrorException; + + /** + *
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * 
+ * + * @param agentId 企业号应用的id + * @param menu 菜单对象 + * @see #create(me.chanjar.weixin.common.bean.menu.WxMenu) + */ + void create(Integer agentId, WxMenu menu) throws WxErrorException; + + /** + *
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * 
+ * + * @see #delete(Integer) + */ + void delete() throws WxErrorException; + + /** + *
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * 
+ * + * @param agentId 企业号应用的id + * @see #delete() + */ + void delete(Integer agentId) throws WxErrorException; + + /** + *
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * 
+ * + * @see #get(Integer) + */ + WxMenu get() throws WxErrorException; + + /** + *
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * 
+ * + * @param agentId 企业号应用的id + * @see #get() + */ + WxMenu get(Integer agentId) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java new file mode 100644 index 00000000..33124895 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java @@ -0,0 +1,67 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.exception.WxErrorException; + +/** + *
+ * OAuth2相关管理接口
+ *  Created by BinaryWang on 2017/6/24.
+ * 
+ * + * @author Binary Wang + */ +public interface WxCpOAuth2Service { + /** + *
+   * 构造oauth2授权的url连接
+   * 
+ * + * @param state 状态码 + * @return url + */ + String buildAuthorizationUrl(String state); + + /** + *
+   * 构造oauth2授权的url连接
+   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
+   * 
+ * + * @param redirectUri 跳转链接地址 + * @param state 状态码 + * @return url + */ + String buildAuthorizationUrl(String redirectUri, String state); + + /** + *
+   * 用oauth2获取用户信息
+   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+   *
+   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * 
+ * + * @param code 微信oauth授权返回的代码 + * @return [userid, deviceid] + * @see #getUserInfo(Integer, String) + */ + String[] getUserInfo(String code) throws WxErrorException; + + /** + *
+   * 用oauth2获取用户信息
+   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+   *
+   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+   * 
+ * + * @param agentId 企业号应用的id + * @param code 微信oauth授权返回的代码 + * @return [userid, deviceid] + * @see #getUserInfo(String) + */ + String[] getUserInfo(Integer agentId, String code) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOauth2Service.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOauth2Service.java deleted file mode 100644 index af86f1ed..00000000 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOauth2Service.java +++ /dev/null @@ -1,11 +0,0 @@ -package me.chanjar.weixin.cp.api; - -/** - *
- *  Created by BinaryWang on 2017/6/24.
- * 
- * - * @author Binary Wang - */ -public interface WxCpOauth2Service { -} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index 52692090..880a92b0 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -8,6 +8,7 @@ import me.chanjar.weixin.common.session.WxSession; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.cp.bean.*; import me.chanjar.weixin.cp.config.WxCpConfigStorage; @@ -85,305 +86,208 @@ public interface WxCpService { WxJsapiSignature createJsapiSignature(String url) throws WxErrorException; /** - *
-   * 上传多媒体文件
-   * 上传的多媒体文件有格式和大小限制,如下:
-   *   图片(image): 1M,支持JPG格式
-   *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
-   *   视频(video):10MB,支持MP4格式
-   *   缩略图(thumb):64KB,支持JPG格式
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
-   * 
- * - * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} - * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts} - * @param inputStream 输入流 + * @deprecated 请使用 {@link WxCpMenuService#create(WxMenu)} */ - WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) - throws WxErrorException, IOException; + @Deprecated + void menuCreate(WxMenu menu) throws WxErrorException; /** - * @param mediaType 媒体类型 - * @param file 文件对象 - * @see #mediaUpload(String, String, InputStream) + * @deprecated 请使用 {@link WxCpMenuService#create(Integer, WxMenu)} */ - WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException; + @Deprecated + void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException; /** - *
-   * 下载多媒体文件
-   * 根据微信文档,视频文件下载不了,会返回null
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
-   * 
- * - * @param mediaId 媒体id - * @return 保存到本地的临时文件 + * @deprecated 请使用 {@link WxCpMenuService#delete()} } */ - File mediaDownload(String mediaId) throws WxErrorException; + @Deprecated + void menuDelete() throws WxErrorException; /** - *
-   * 发送消息
-   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
-   * 
- * - * @param message 要发送的消息对象 + * @deprecated 请使用 {@link WxCpMenuService#delete(Integer)} */ - WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException; + @Deprecated + void menuDelete(Integer agentId) throws WxErrorException; /** - *
-   * 自定义菜单创建接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
-   *
-   * 注意: 这个方法使用WxCpConfigStorage里的agentId
-   * 
- * - * @param menu 菜单对象 - * @see #menuCreate(Integer, WxMenu) + * @deprecated 请使用 {@link WxCpMenuService#get() } */ - void menuCreate(WxMenu menu) throws WxErrorException; + @Deprecated + WxMenu menuGet() throws WxErrorException; /** - *
-   * 自定义菜单创建接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
-   *
-   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
-   * 
- * - * @param agentId 企业号应用的id - * @param menu 菜单对象 - * @see #menuCreate(me.chanjar.weixin.common.bean.menu.WxMenu) + * @deprecated 请使用 {@link WxCpMenuService#get(Integer)} */ - void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException; + @Deprecated + WxMenu menuGet(Integer agentId) throws WxErrorException; /** *
-   * 自定义菜单删除接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
-   *
-   * 注意: 这个方法使用WxCpConfigStorage里的agentId
+   * 发送消息
+   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
    * 
* - * @see #menuDelete(Integer) + * @param message 要发送的消息对象 */ - void menuDelete() throws WxErrorException; + WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException; /** - *
-   * 自定义菜单删除接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
-   *
-   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
-   * 
- * - * @param agentId 企业号应用的id - * @see #menuDelete() + * @deprecated 请使用 {@link WxCpDepartmentService#create(WxCpDepart)} */ - void menuDelete(Integer agentId) throws WxErrorException; + @Deprecated + Integer departCreate(WxCpDepart depart) throws WxErrorException; /** - *
-   * 自定义菜单查询接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
-   *
-   * 注意: 这个方法使用WxCpConfigStorage里的agentId
-   * 
- * - * @see #menuGet(Integer) + * @deprecated 请使用 {@link WxCpDepartmentService#update(WxCpDepart)} */ - WxMenu menuGet() throws WxErrorException; + @Deprecated + void departUpdate(WxCpDepart group) throws WxErrorException; /** - *
-   * 自定义菜单查询接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
-   *
-   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
-   * 
- * - * @param agentId 企业号应用的id - * @see #menuGet() + * @deprecated 请使用 {@link WxCpDepartmentService#delete(Integer)} */ - WxMenu menuGet(Integer agentId) throws WxErrorException; + @Deprecated + void departDelete(Integer departId) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpDepartmentService#create(WxCpDepart depart) } + * @deprecated 请使用 {@link WxCpDepartmentService#listAll() } */ @Deprecated - Integer departCreate(WxCpDepart depart) throws WxErrorException; + List departGet() throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpDepartmentService#update(WxCpDepart group) } + * @deprecated 请使用 {@link WxCpMediaService#upload(String, String, InputStream)} */ @Deprecated - void departUpdate(WxCpDepart group) throws WxErrorException; + WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) + throws WxErrorException, IOException; /** - * @deprecated 请使用 {@link WxCpDepartmentService#delete(Integer departId) } + * @deprecated 请使用 {@link WxCpMediaService#upload(String, File)} */ @Deprecated - void departDelete(Integer departId) throws WxErrorException; + WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpDepartmentService#listAll() } + * @deprecated 请使用 {@link WxCpMediaService#download(String)} */ @Deprecated - List departGet() throws WxErrorException; + File mediaDownload(String mediaId) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#authenticate(String userId) } + * @deprecated 请使用 {@link WxCpUserService#authenticate(String)} */ @Deprecated void userAuthenticated(String userId) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#create(WxCpUser user) } + * @deprecated 请使用 {@link WxCpUserService#create(WxCpUser)} */ @Deprecated void userCreate(WxCpUser user) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#update(WxCpUser user)} + * @deprecated 请使用 {@link WxCpUserService#update(WxCpUser)} */ @Deprecated void userUpdate(WxCpUser user) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#delete(String... userids) } + * @deprecated 请使用 {@link WxCpUserService#delete(String...)} */ @Deprecated void userDelete(String userid) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#delete(String[] userids) } + * @deprecated 请使用 {@link WxCpUserService#delete(String...)} */ @Deprecated void userDelete(String[] userids) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#getById(String userid) } + * @deprecated 请使用 {@link WxCpUserService#getById(String)} */ @Deprecated WxCpUser userGet(String userid) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#listByDepartment(Integer departId, Boolean fetchChild, Integer status) } + * @deprecated 请使用 {@link WxCpUserService#listByDepartment(Integer, Boolean, Integer)} */ @Deprecated List userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException; /** - * @deprecated 请使用 {@link WxCpUserService#listSimpleByDepartment(Integer departId, Boolean fetchChild, Integer status) } + * @deprecated 请使用 {@link WxCpUserService#listSimpleByDepartment(Integer, Boolean, Integer)} */ @Deprecated List departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException; /** - * 创建标签 - * - * @param tagName 标签名 + * @deprecated 请使用 {@link WxCpTagService#create(String)} */ + @Deprecated String tagCreate(String tagName) throws WxErrorException; /** - * 更新标签 - * - * @param tagId 标签id - * @param tagName 标签名 + * @deprecated 请使用 {@link WxCpTagService#update(String, String)} */ + @Deprecated void tagUpdate(String tagId, String tagName) throws WxErrorException; /** - * 删除标签 - * - * @param tagId 标签id + * @deprecated 请使用 {@link WxCpTagService#delete(String)} */ + @Deprecated void tagDelete(String tagId) throws WxErrorException; /** - * 获得标签列表 + * @deprecated 请使用 {@link WxCpTagService#listAll()} */ + @Deprecated List tagGet() throws WxErrorException; /** - * 获取标签成员 - * - * @param tagId 标签ID + * @deprecated 请使用 {@link WxCpTagService#listUsersByTagId(String)} */ + @Deprecated List tagGetUsers(String tagId) throws WxErrorException; /** - * 增加标签成员 - * - * @param tagId 标签id - * @param userIds 用户ID 列表 + * @deprecated 请使用 {@link WxCpTagService#addUsers2Tag(String, List, List)} */ + @Deprecated void tagAddUsers(String tagId, List userIds, List partyIds) throws WxErrorException; /** - *
-   * 构造oauth2授权的url连接
-   * 
- * - * @param state 状态码 - * @return url + * @deprecated 请使用 {@link WxCpTagService#removeUsersFromTag(String, List)} + */ + @Deprecated + void tagRemoveUsers(String tagId, List userIds) throws WxErrorException; + + /** + * @deprecated 请使用 {@link WxCpOAuth2Service#buildAuthorizationUrl(String)} */ + @Deprecated String oauth2buildAuthorizationUrl(String state); /** - *
-   * 构造oauth2授权的url连接
-   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
-   * 
- * - * @param redirectUri 跳转链接地址 - * @param state 状态码 - * @return url + * @deprecated 请使用 {@link WxCpOAuth2Service#buildAuthorizationUrl(String, String)} */ + @Deprecated String oauth2buildAuthorizationUrl(String redirectUri, String state); /** - *
-   * 用oauth2获取用户信息
-   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
-   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
-   *
-   * 注意: 这个方法使用WxCpConfigStorage里的agentId
-   * 
- * - * @param code 微信oauth授权返回的代码 - * @return [userid, deviceid] - * @see #oauth2getUserInfo(Integer, String) + * @deprecated 请使用 {@link WxCpOAuth2Service#getUserInfo(String)} */ + @Deprecated String[] oauth2getUserInfo(String code) throws WxErrorException; /** - *
-   * 用oauth2获取用户信息
-   * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
-   * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
-   *
-   * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
-   * 
- * - * @param agentId 企业号应用的id - * @param code 微信oauth授权返回的代码 - * @return [userid, deviceid] - * @see #oauth2getUserInfo(String) + * @deprecated 请使用 {@link WxCpOAuth2Service#getUserInfo(Integer, String)} */ + @Deprecated String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException; - - /** - * 移除标签成员 - * - * @param tagId 标签id - * @param userIds 用户id列表 - */ - void tagRemoveUsers(String tagId, List userIds) throws WxErrorException; - /** *
    * 邀请成员关注
@@ -538,7 +442,7 @@ public interface WxCpService {
   /**
    * 获取Oauth2相关接口的服务类对象
    */
-  WxCpOauth2Service getOauth2Service();
+  WxCpOAuth2Service getOauth2Service();
 
   /**
    * 获取标签相关接口的服务类对象
@@ -549,4 +453,9 @@ public interface WxCpService {
    * 获取用户相关接口的服务类对象
    */
   WxCpUserService getUserService();
+
+  /**
+   * http请求对象
+   */
+  RequestHttp getRequestHttp();
 }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java
index d84f4c95..c96318b8 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java
@@ -1,11 +1,66 @@
 package me.chanjar.weixin.cp.api;
 
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpTag;
+import me.chanjar.weixin.cp.bean.WxCpTagAddOrRemoveUsersResult;
+import me.chanjar.weixin.cp.bean.WxCpUser;
+
+import java.util.List;
+
 /**
  * 
+ *  标签管理接口
  *  Created by BinaryWang on 2017/6/24.
  * 
* * @author Binary Wang */ public interface WxCpTagService { + /** + * 创建标签 + * + * @param tagName 标签名 + */ + String create(String tagName) throws WxErrorException; + + /** + * 更新标签 + * + * @param tagId 标签id + * @param tagName 标签名 + */ + void update(String tagId, String tagName) throws WxErrorException; + + /** + * 删除标签 + * + * @param tagId 标签id + */ + void delete(String tagId) throws WxErrorException; + + /** + * 获得标签列表 + */ + List listAll() throws WxErrorException; + + /** + * 获取标签成员 + * + * @param tagId 标签ID + */ + List listUsersByTagId(String tagId) throws WxErrorException; + + /** + * 增加标签成员 + * @param tagId 标签id + * @param userIds 用户ID 列表 + */ + WxCpTagAddOrRemoveUsersResult addUsers2Tag(String tagId, List userIds, List partyIds) throws WxErrorException; + + /** + * 移除标签成员 + * @param tagId 标签id + * @param userIds 用户id列表 + */ + WxCpTagAddOrRemoveUsersResult removeUsersFromTag(String tagId, List userIds) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java index ac6fe8b2..6cff2648 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java @@ -1,7 +1,9 @@ package me.chanjar.weixin.cp.api.impl; -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.bean.result.WxError; @@ -12,13 +14,13 @@ import me.chanjar.weixin.common.session.WxSession; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.RandomUtils; import me.chanjar.weixin.common.util.crypto.SHA1; -import me.chanjar.weixin.common.util.fs.FileUtils; -import me.chanjar.weixin.common.util.http.*; -import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; +import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; import me.chanjar.weixin.cp.api.*; import me.chanjar.weixin.cp.bean.*; import me.chanjar.weixin.cp.config.WxCpConfigStorage; -import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,17 +29,16 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.UUID; public abstract class AbstractWxCpServiceImpl implements WxCpService, RequestHttp { protected final Logger log = LoggerFactory.getLogger(this.getClass()); private WxCpUserService userService = new WxCpUserServiceImpl(this); private WxCpDepartmentService departmentService = new WxCpDepartmentServiceImpl(this); - private WxCpMediaService mediaService; - private WxCpMenuService menuService; - private WxCpOauth2Service oauth2Service; - private WxCpTagService tagService; + private WxCpMediaService mediaService = new WxCpMediaServiceImpl(this); + private WxCpMenuService menuService = new WxCpMenuServiceImpl(this); + private WxCpOAuth2Service oauth2Service = new WxCpOAuth2ServiceImpl(this); + private WxCpTagService tagService = new WxCpTagServiceImpl(this); /** * 全局的是否正在刷新access token的锁 @@ -136,74 +137,56 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ @Override @Deprecated public void menuCreate(WxMenu menu) throws WxErrorException { - menuCreate(this.configStorage.getAgentId(), menu); + this.getMenuService().create(menu); } @Override @Deprecated public void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid=" - + agentId; - post(url, menu.toJson()); + this.getMenuService().create(agentId, menu); } @Override @Deprecated public void menuDelete() throws WxErrorException { - menuDelete(this.configStorage.getAgentId()); + this.getMenuService().delete(); } @Override @Deprecated public void menuDelete(Integer agentId) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId; - get(url, null); + this.getMenuService().delete(agentId); } @Override @Deprecated public WxMenu menuGet() throws WxErrorException { - return menuGet(this.configStorage.getAgentId()); + return this.getMenuService().get(); } @Override @Deprecated public WxMenu menuGet(Integer agentId) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId; - try { - String resultContent = get(url, null); - return WxMenu.fromJson(resultContent); - } catch (WxErrorException e) { - // 46003 不存在的菜单数据 - if (e.getError().getErrorCode() == 46003) { - return null; - } - throw e; - } + return this.getMenuService().get(agentId); } @Override @Deprecated public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException { - return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); + return this.getMediaService().upload(mediaType, fileType, inputStream); } @Override @Deprecated public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType; - return execute(MediaUploadRequestExecutor.create(this), url, file); + return this.getMediaService().upload(mediaType, file); } @Override @Deprecated public File mediaDownload(String mediaId) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"; - return execute( - MediaDownloadRequestExecutor.create(this, - this.configStorage.getTmpDirFile()), - url, "media_id=" + mediaId); + return this.getMediaService().download(mediaId); } @Override @@ -245,148 +228,79 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ @Override @Deprecated public List userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException { - return this.getUserService().listByDepartment(departId, fetchChild, status); + return this.getUserService().listByDepartment(departId, fetchChild, status); } @Override @Deprecated public List departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException { - return this.getUserService().listSimpleByDepartment(departId, fetchChild, status); + return this.getUserService().listSimpleByDepartment(departId, fetchChild, status); } @Override @Deprecated public String tagCreate(String tagName) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/create"; - JsonObject o = new JsonObject(); - o.addProperty("tagname", tagName); - String responseContent = post(url, o.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return tmpJsonElement.getAsJsonObject().get("tagid").getAsString(); + return this.getTagService().create(tagName); } @Override @Deprecated public void tagUpdate(String tagId, String tagName) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/update"; - JsonObject o = new JsonObject(); - o.addProperty("tagid", tagId); - o.addProperty("tagname", tagName); - post(url, o.toString()); + this.getTagService().update(tagId, tagName); } @Override @Deprecated public void tagDelete(String tagId) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?tagid=" + tagId; - get(url, null); + this.getTagService().delete(tagId); } @Override @Deprecated public List tagGet() throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/list"; - String responseContent = get(url, null); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return WxCpGsonBuilder.INSTANCE.create() - .fromJson( - tmpJsonElement.getAsJsonObject().get("taglist"), - new TypeToken>() { - }.getType() - ); + return this.getTagService().listAll(); } @Override @Deprecated public List tagGetUsers(String tagId) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?tagid=" + tagId; - String responseContent = get(url, null); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return WxCpGsonBuilder.INSTANCE.create() - .fromJson( - tmpJsonElement.getAsJsonObject().get("userlist"), - new TypeToken>() { - }.getType() - ); + return this.getTagService().listUsersByTagId(tagId); } @Override @Deprecated public void tagAddUsers(String tagId, List userIds, List partyIds) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers"; - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("tagid", tagId); - if (userIds != null) { - JsonArray jsonArray = new JsonArray(); - for (String userId : userIds) { - jsonArray.add(new JsonPrimitive(userId)); - } - jsonObject.add("userlist", jsonArray); - } - if (partyIds != null) { - JsonArray jsonArray = new JsonArray(); - for (String userId : partyIds) { - jsonArray.add(new JsonPrimitive(userId)); - } - jsonObject.add("partylist", jsonArray); - } - post(url, jsonObject.toString()); + this.getTagService().addUsers2Tag(tagId, userIds, partyIds); } @Override @Deprecated public void tagRemoveUsers(String tagId, List userIds) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers"; - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("tagid", tagId); - JsonArray jsonArray = new JsonArray(); - for (String userId : userIds) { - jsonArray.add(new JsonPrimitive(userId)); - } - jsonObject.add("userlist", jsonArray); - post(url, jsonObject.toString()); + this.getTagService().removeUsersFromTag(tagId, userIds); } @Override @Deprecated public String oauth2buildAuthorizationUrl(String state) { - return this.oauth2buildAuthorizationUrl( - this.configStorage.getOauth2redirectUri(), - state - ); + return this.getOauth2Service().buildAuthorizationUrl(state); } @Override @Deprecated public String oauth2buildAuthorizationUrl(String redirectUri, String state) { - String url = "https://open.weixin.qq.com/connect/oauth2/authorize?"; - url += "appid=" + this.configStorage.getCorpId(); - url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectUri); - url += "&response_type=code"; - url += "&scope=snsapi_base"; - if (state != null) { - url += "&state=" + state; - } - url += "#wechat_redirect"; - return url; + return this.getOauth2Service().buildAuthorizationUrl(redirectUri, state); } @Override @Deprecated public String[] oauth2getUserInfo(String code) throws WxErrorException { - return oauth2getUserInfo(this.configStorage.getAgentId(), code); + return this.getOauth2Service().getUserInfo(code); } @Override @Deprecated public String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException { - String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?" - + "code=" + code - + "&agentid=" + agentId; - String responseText = get(url, null); - JsonElement je = new JsonParser().parse(responseText); - JsonObject jo = je.getAsJsonObject(); - return new String[]{GsonHelper.getString(jo, "UserId"), GsonHelper.getString(jo, "DeviceId"), GsonHelper.getString(jo, "OpenId")}; + return this.getOauth2Service().getUserInfo(agentId, code); } @Override @@ -473,7 +387,7 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ try { T result = executor.execute(uriWithAccessToken, data); - this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, result); + this.log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, data, result); return result; } catch (WxErrorException e) { WxError error = e.getError(); @@ -490,12 +404,12 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ } if (error.getErrorCode() != 0) { - this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, error); + this.log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, data, error); throw new WxErrorException(error); } return null; } catch (IOException e) { - this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uriWithAccessToken, data, e.getMessage()); + this.log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, data, e.getMessage()); throw new RuntimeException(e); } } @@ -584,7 +498,7 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ } @Override - public WxCpOauth2Service getOauth2Service() { + public WxCpOAuth2Service getOauth2Service() { return oauth2Service; } @@ -621,4 +535,9 @@ public abstract class AbstractWxCpServiceImpl implements WxCpService, Requ public List departGet() throws WxErrorException { return this.getDepartmentService().listAll(); } + + @Override + public RequestHttp getRequestHttp() { + return this; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java new file mode 100644 index 00000000..1364550a --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java @@ -0,0 +1,50 @@ +package me.chanjar.weixin.cp.api.impl; + +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.common.util.fs.FileUtils; +import me.chanjar.weixin.common.util.http.MediaDownloadRequestExecutor; +import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; +import me.chanjar.weixin.cp.api.WxCpMediaService; +import me.chanjar.weixin.cp.api.WxCpService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +/** + *
+ * 媒体管理接口
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +public class WxCpMediaServiceImpl implements WxCpMediaService { + private WxCpService mainService; + + public WxCpMediaServiceImpl(WxCpService mainService) { + this.mainService = mainService; + } + + @Override + public WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputStream) + throws WxErrorException, IOException { + return this.upload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); + } + + @Override + public WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType; + return this.mainService.execute(MediaUploadRequestExecutor.create(this.mainService.getRequestHttp()), url, file); + } + + @Override + public File download(String mediaId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"; + return this.mainService.execute( + MediaDownloadRequestExecutor.create(this.mainService.getRequestHttp(), + this.mainService.getWxCpConfigStorage().getTmpDirFile()), + url, "media_id=" + mediaId); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java new file mode 100644 index 00000000..7c3937d8 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java @@ -0,0 +1,64 @@ +package me.chanjar.weixin.cp.api.impl; + +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpMenuService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + *
+ * 菜单管理相关接口
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +public class WxCpMenuServiceImpl implements WxCpMenuService { + private WxCpService mainService; + + public WxCpMenuServiceImpl(WxCpService mainService) { + this.mainService = mainService; + } + + @Override + public void create(WxMenu menu) throws WxErrorException { + this.create(this.mainService.getWxCpConfigStorage().getAgentId(), menu); + } + + @Override + public void create(Integer agentId, WxMenu menu) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid=" + agentId; + this.mainService.post(url, menu.toJson()); + } + + @Override + public void delete() throws WxErrorException { + this.delete(this.mainService.getWxCpConfigStorage().getAgentId()); + } + + @Override + public void delete(Integer agentId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId; + this.mainService.get(url, null); + } + + @Override + public WxMenu get() throws WxErrorException { + return this.get(this.mainService.getWxCpConfigStorage().getAgentId()); + } + + @Override + public WxMenu get(Integer agentId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId; + try { + String resultContent = this.mainService.get(url, null); + return WxCpGsonBuilder.create().fromJson(resultContent, WxMenu.class); + } catch (WxErrorException e) { + // 46003 不存在的菜单数据 + if (e.getError().getErrorCode() == 46003) { + return null; + } + throw e; + } + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java new file mode 100644 index 00000000..a317bfb4 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java @@ -0,0 +1,66 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.common.util.http.URIUtil; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.cp.api.WxCpOAuth2Service; +import me.chanjar.weixin.cp.api.WxCpService; + +/** + *
+ *
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +public class WxCpOAuth2ServiceImpl implements WxCpOAuth2Service { + private WxCpService mainService; + + public WxCpOAuth2ServiceImpl(WxCpService mainService) { + this.mainService = mainService; + } + + @Override + public String buildAuthorizationUrl(String state) { + return this.buildAuthorizationUrl( + this.mainService.getWxCpConfigStorage().getOauth2redirectUri(), + state + ); + } + + @Override + public String buildAuthorizationUrl(String redirectUri, String state) { + String url = "https://open.weixin.qq.com/connect/oauth2/authorize?"; + url += "appid=" + this.mainService.getWxCpConfigStorage().getCorpId(); + url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectUri); + url += "&response_type=code"; + url += "&scope=snsapi_base"; + if (state != null) { + url += "&state=" + state; + } + url += "#wechat_redirect"; + return url; + } + + @Override + public String[] getUserInfo(String code) throws WxErrorException { + return getUserInfo(this.mainService.getWxCpConfigStorage().getAgentId(), code); + } + + @Override + public String[] getUserInfo(Integer agentId, String code) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?" + + "code=" + code + + "&agentid=" + agentId; + String responseText = this.mainService.get(url, null); + JsonElement je = new JsonParser().parse(responseText); + JsonObject jo = je.getAsJsonObject(); + return new String[]{GsonHelper.getString(jo, "UserId"), + GsonHelper.getString(jo, "DeviceId"), + GsonHelper.getString(jo, "OpenId")}; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java index 9c87ccbd..d9388f03 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java @@ -7,7 +7,6 @@ import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.util.http.HttpType; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; -import me.chanjar.weixin.cp.bean.WxCpDepart; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; @@ -17,7 +16,6 @@ import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; import java.io.IOException; -import java.util.List; public class WxCpServiceApacheHttpClientImpl extends AbstractWxCpServiceImpl { protected CloseableHttpClient httpClient; @@ -103,4 +101,5 @@ public class WxCpServiceApacheHttpClientImpl extends AbstractWxCpServiceImpl + * 标签管理接口 + * Created by Binary Wang on 2017-6-25. + * @author Binary Wang + *
+ */ +public class WxCpTagServiceImpl implements WxCpTagService { + private WxCpService mainService; + + public WxCpTagServiceImpl(WxCpService mainService) { + this.mainService = mainService; + } + + @Override + public String create(String tagName) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/create"; + JsonObject o = new JsonObject(); + o.addProperty("tagname", tagName); + String responseContent = this.mainService.post(url, o.toString()); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return tmpJsonElement.getAsJsonObject().get("tagid").getAsString(); + } + + @Override + public void update(String tagId, String tagName) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/update"; + JsonObject o = new JsonObject(); + o.addProperty("tagid", tagId); + o.addProperty("tagname", tagName); + this.mainService.post(url, o.toString()); + } + + @Override + public void delete(String tagId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?tagid=" + tagId; + this.mainService.get(url, null); + } + + @Override + public List listAll() throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/list"; + String responseContent = this.mainService.get(url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("taglist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List listUsersByTagId(String tagId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?tagid=" + tagId; + String responseContent = this.mainService.get(url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("userlist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public WxCpTagAddOrRemoveUsersResult addUsers2Tag(String tagId, List userIds, List partyIds) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("tagid", tagId); + if (userIds != null) { + JsonArray jsonArray = new JsonArray(); + for (String userId : userIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("userlist", jsonArray); + } + if (partyIds != null) { + JsonArray jsonArray = new JsonArray(); + for (String userId : partyIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("partylist", jsonArray); + } + + return WxCpTagAddOrRemoveUsersResult.fromJson(this.mainService.post(url, jsonObject.toString())); + } + + @Override + public WxCpTagAddOrRemoveUsersResult removeUsersFromTag(String tagId, List userIds) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("tagid", tagId); + JsonArray jsonArray = new JsonArray(); + for (String userId : userIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("userlist", jsonArray); + + return WxCpTagAddOrRemoveUsersResult.fromJson(this.mainService.post(url, jsonObject.toString())); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java new file mode 100644 index 00000000..c216ae46 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java @@ -0,0 +1,85 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.common.base.Splitter; +import com.google.gson.annotations.SerializedName; +import me.chanjar.weixin.common.util.ToStringUtils; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.List; + +/** + *
+ * 为标签添加或移除用户结果对象类
+ * Created by Binary Wang on 2017-6-22.
+ * @author Binary Wang
+ * 
+ */ +public class WxCpTagAddOrRemoveUsersResult { + @Override + public String toString() { + return ToStringUtils.toSimpleString(this); + } + + public static WxCpTagAddOrRemoveUsersResult fromJson(String json) { + return WxCpGsonBuilder.INSTANCE.create().fromJson(json, WxCpTagAddOrRemoveUsersResult.class); + } + + @SerializedName("errcode") + private Integer errCode; + + @SerializedName("errmsg") + private String errMsg; + + @SerializedName("invalidlist") + private String invalidUsers; + + @SerializedName("invalidparty") + private String[] invalidParty; + + public Integer getErrCode() { + return this.errCode; + } + + public void setErrCode(Integer errCode) { + this.errCode = errCode; + } + + public String getErrMsg() { + return this.errMsg; + } + + public void setErrMsg(String errMsg) { + this.errMsg = errMsg; + } + + public String getInvalidUser() { + return this.invalidUsers; + } + + public void setInvalidUser(String invalidUser) { + this.invalidUsers = invalidUser; + } + + public String[] getInvalidParty() { + return this.invalidParty; + } + + public void setInvalidParty(String[] invalidParty) { + this.invalidParty = invalidParty; + } + + public List getInvalidUserList() { + return this.content2List(this.invalidUsers); + } + + private List content2List(String content) { + if(StringUtils.isBlank(content)){ + return Collections.emptyList(); + } + + return Splitter.on("|").splitToList(content); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java index fcf84de9..647a4e6b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpUser.java @@ -13,13 +13,45 @@ import java.util.List; */ public class WxCpUser implements Serializable { + public enum Gender { + MALE("男", "1"), + FEMAIL("女", "2"); + + private String genderName; + private String code; + + Gender(String genderName, String code) { + this.genderName = genderName; + this.code = code; + } + + public String getGenderName() { + return this.genderName; + } + + public String getCode() { + return this.code; + } + + public static Gender fromCode(String code) { + if ("1".equals(code)) { + return Gender.MALE; + } + if ("2".equals(code)) { + return Gender.FEMAIL; + } + + return null; + } + } + private static final long serialVersionUID = -5696099236344075582L; private String userId; private String name; private Integer[] departIds; private String position; private String mobile; - private String gender; + private Gender gender; private String email; private String avatar; private Integer status; @@ -58,11 +90,11 @@ public class WxCpUser implements Serializable { this.departIds = departIds; } - public String getGender() { + public Gender getGender() { return this.gender; } - public void setGender(String gender) { + public void setGender(Gender gender) { this.gender = gender; } @@ -159,7 +191,6 @@ public class WxCpUser implements Serializable { } public static class Attr { - private String name; private String value; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpGsonBuilder.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpGsonBuilder.java index 32358648..5efee45c 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpGsonBuilder.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpGsonBuilder.java @@ -2,6 +2,7 @@ package me.chanjar.weixin.cp.util.json; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.util.json.WxErrorAdapter; import me.chanjar.weixin.cp.bean.WxCpDepart; @@ -19,6 +20,7 @@ public class WxCpGsonBuilder { INSTANCE.registerTypeAdapter(WxCpDepart.class, new WxCpDepartGsonAdapter()); INSTANCE.registerTypeAdapter(WxCpUser.class, new WxCpUserGsonAdapter()); INSTANCE.registerTypeAdapter(WxError.class, new WxErrorAdapter()); + INSTANCE.registerTypeAdapter(WxMenu.class, new WxCpMenuGsonAdapter()); INSTANCE.registerTypeAdapter(WxCpTag.class, new WxCpTagGsonAdapter()); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMenuGsonAdapter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMenuGsonAdapter.java new file mode 100644 index 00000000..c7cb05cc --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMenuGsonAdapter.java @@ -0,0 +1,24 @@ +package me.chanjar.weixin.cp.util.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.util.json.WxMenuGsonAdapter; + +import java.lang.reflect.Type; + +/** + *
+ * 企业号菜单json转换适配器
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +public class WxCpMenuGsonAdapter extends WxMenuGsonAdapter { + + @Override + public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return this.buildMenuFromJson(json.getAsJsonObject().get("button").getAsJsonArray()); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java index 00cdea57..6531d07b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpUserGsonAdapter.java @@ -39,7 +39,7 @@ public class WxCpUserGsonAdapter implements JsonDeserializer, JsonSeri user.setName(GsonHelper.getString(o, "name")); user.setPosition(GsonHelper.getString(o, "position")); user.setMobile(GsonHelper.getString(o, "mobile")); - user.setGender(GsonHelper.getString(o, "gender")); + user.setGender(WxCpUser.Gender.fromCode(GsonHelper.getString(o, "gender"))); user.setEmail(GsonHelper.getString(o, "email")); user.setAvatar(GsonHelper.getString(o, "avatar")); user.setStatus(GsonHelper.getInteger(o, "status")); @@ -85,7 +85,7 @@ public class WxCpUserGsonAdapter implements JsonDeserializer, JsonSeri o.addProperty("mobile", user.getMobile()); } if (user.getGender() != null) { - o.addProperty("gender", user.getGender()); + o.addProperty("gender", user.getGender().getCode()); } if (user.getEmail() != null) { o.addProperty("email", user.getEmail()); diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/ApiTestModule.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/ApiTestModule.java index ca4bc636..179086f7 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/ApiTestModule.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/ApiTestModule.java @@ -31,7 +31,7 @@ public class ApiTestModule implements Module { wxService.setWxCpConfigStorage(config); binder.bind(WxCpService.class).toInstance(wxService); - binder.bind(WxCpConfigStorage.class).toInstance(config); + binder.bind(WxXmlCpInMemoryConfigStorage.class).toInstance(config); } catch (IOException e) { e.printStackTrace(); } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMediaAPITest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMediaAPITest.java deleted file mode 100644 index f10a8b7d..00000000 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMediaAPITest.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.chanjar.weixin.cp.api; - -import com.google.inject.Inject; -import me.chanjar.weixin.common.api.WxConsts; -import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; -import me.chanjar.weixin.common.exception.WxErrorException; -import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Guice; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -/** - * 测试多媒体文件上传下载 - * - * @author Daniel Qian - */ -//@Test(groups="mediaAPI", dependsOnGroups="baseAPI") -@Test -@Guice(modules = ApiTestModule.class) -public class WxCpMediaAPITest { - - @Inject - protected WxCpServiceImpl wxService; - - private List media_ids = new ArrayList<>(); - - @Test(dataProvider = "uploadMedia") - public void testUploadMedia(String mediaType, String fileType, String fileName) throws WxErrorException, IOException { - try (InputStream inputStream = ClassLoader - .getSystemResourceAsStream(fileName);) { - WxMediaUploadResult res = this.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) { - this.media_ids.add(res.getMediaId()); - } - if (res.getThumbMediaId() != null) { - this.media_ids.add(res.getThumbMediaId()); - } - } - } - - @DataProvider - public Object[][] uploadMedia() { - return new Object[][]{ - new Object[]{WxConsts.MEDIA_IMAGE, TestConstants.FILE_JPG, "mm.jpeg"}, - new Object[]{WxConsts.MEDIA_VOICE, TestConstants.FILE_MP3, "mm.mp3"}, - new Object[]{WxConsts.MEDIA_VIDEO, TestConstants.FILE_MP4, "mm.mp4"}, - new Object[]{WxConsts.MEDIA_FILE, TestConstants.FILE_JPG, "mm.jpeg"} - }; - } - - @Test(dependsOnMethods = {"testUploadMedia"}, dataProvider = "downloadMedia") - public void testDownloadMedia(String media_id) throws WxErrorException { - this.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; - } - -} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTagAPITest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTagAPITest.java deleted file mode 100644 index cb743f3d..00000000 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTagAPITest.java +++ /dev/null @@ -1,68 +0,0 @@ -package me.chanjar.weixin.cp.api; - -import com.google.inject.Inject; -import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; -import me.chanjar.weixin.cp.bean.WxCpTag; -import me.chanjar.weixin.cp.bean.WxCpUser; -import me.chanjar.weixin.cp.config.WxCpConfigStorage; -import org.testng.Assert; -import org.testng.annotations.Guice; -import org.testng.annotations.Test; - -import java.util.ArrayList; -import java.util.List; - -@Test(groups = "departAPI", dependsOnGroups = "baseAPI") -@Guice(modules = ApiTestModule.class) -public class WxCpTagAPITest { - - @Inject - protected WxCpServiceImpl wxService; - - @Inject - protected WxCpConfigStorage configStorage; - - protected String tagId; - - public void testTagCreate() throws Exception { - this.tagId = this.wxService.tagCreate("测试标签4"); - System.out.println(this.tagId); - } - - @Test(dependsOnMethods = "testTagCreate") - public void testTagUpdate() throws Exception { - this.wxService.tagUpdate(this.tagId, "测试标签-改名"); - } - - @Test(dependsOnMethods = "testTagUpdate") - public void testTagGet() throws Exception { - List tags = this.wxService.tagGet(); - Assert.assertNotEquals(tags.size(), 0); - } - - @Test(dependsOnMethods = "testTagGet") - public void testTagAddUsers() throws Exception { - List userIds = new ArrayList<>(); - userIds.add(((ApiTestModule.WxXmlCpInMemoryConfigStorage) this.configStorage).getUserId()); - this.wxService.tagAddUsers(this.tagId, userIds, null); - } - - @Test(dependsOnMethods = "testTagAddUsers") - public void testTagGetUsers() throws Exception { - List users = this.wxService.tagGetUsers(this.tagId); - Assert.assertNotEquals(users.size(), 0); - } - - @Test(dependsOnMethods = "testTagGetUsers") - public void testTagRemoveUsers() throws Exception { - List userIds = new ArrayList<>(); - userIds.add(((ApiTestModule.WxXmlCpInMemoryConfigStorage) this.configStorage).getUserId()); - this.wxService.tagRemoveUsers(this.tagId, userIds); - } - - @Test(dependsOnMethods = "testTagRemoveUsers") - public void testTagDelete() throws Exception { - this.wxService.tagDelete(this.tagId); - } - -} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java new file mode 100644 index 00000000..4bdb7341 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java @@ -0,0 +1,78 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.inject.Inject; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.cp.api.ApiTestModule; +import me.chanjar.weixin.cp.api.TestConstants; +import me.chanjar.weixin.cp.api.WxCpService; +import org.testng.annotations.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.testng.Assert.*; + +/** + *
+ *
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +@Guice(modules = ApiTestModule.class) +public class WxCpMediaServiceImplTest { + @Inject + private WxCpService wxService; + + private List mediaIds = new ArrayList<>(); + + @DataProvider + public Object[][] mediaData() { + return new Object[][]{ + new Object[]{WxConsts.MEDIA_IMAGE, TestConstants.FILE_JPG, "mm.jpeg"}, + new Object[]{WxConsts.MEDIA_VOICE, TestConstants.FILE_MP3, "mm.mp3"}, + new Object[]{WxConsts.MEDIA_VOICE, TestConstants.FILE_AMR, "mm.amr"},//{"errcode":301017,"errmsg":"voice file only support amr like myvoice.amr"} + new Object[]{WxConsts.MEDIA_VIDEO, TestConstants.FILE_MP4, "mm.mp4"}, + new Object[]{WxConsts.MEDIA_FILE, TestConstants.FILE_JPG, "mm.jpeg"} + }; + } + + @Test(dataProvider = "mediaData") + public void testUploadMedia(String mediaType, String fileType, String fileName) throws WxErrorException, IOException { + try (InputStream inputStream = ClassLoader.getSystemResourceAsStream(fileName)) { + WxMediaUploadResult res = this.wxService.getMediaService().upload(mediaType, fileType, inputStream); + assertNotNull(res.getType()); + assertNotNull(res.getCreatedAt()); + assertTrue(res.getMediaId() != null || res.getThumbMediaId() != null); + + if (res.getMediaId() != null) { + this.mediaIds.add(res.getMediaId()); + } + if (res.getThumbMediaId() != null) { + this.mediaIds.add(res.getThumbMediaId()); + } + } + } + + @DataProvider + public Object[][] downloadMedia() { + Object[][] params = new Object[this.mediaIds.size()][]; + for (int i = 0; i < this.mediaIds.size(); i++) { + params[i] = new Object[]{this.mediaIds.get(i)}; + } + return params; + } + + @Test(dependsOnMethods = {"testUploadMedia"}, dataProvider = "downloadMedia") + public void testDownloadMedia(String media_id) throws WxErrorException { + File file = this.wxService.getMediaService().download(media_id); + assertNotNull(file); + System.out.println(file); + } + +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxMenuAPITest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImplTest.java similarity index 62% rename from weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxMenuAPITest.java rename to weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImplTest.java index 643ba330..def64198 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxMenuAPITest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImplTest.java @@ -1,45 +1,29 @@ -package me.chanjar.weixin.cp.api; +package me.chanjar.weixin.cp.api.impl; import com.google.inject.Inject; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.bean.menu.WxMenuButton; -import me.chanjar.weixin.common.exception.WxErrorException; -import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Guice; -import org.testng.annotations.Test; +import me.chanjar.weixin.cp.api.ApiTestModule; +import me.chanjar.weixin.cp.api.WxCpService; +import org.testng.annotations.*; + +import static org.testng.Assert.*; /** - * 测试菜单 + *
  *
- * @author Daniel Qian
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
*/ -@Test(groups = "menuAPI", dependsOnGroups = "baseAPI") @Guice(modules = ApiTestModule.class) -public class WxMenuAPITest { - +public class WxCpMenuServiceImplTest { @Inject - protected WxCpServiceImpl wxService; - - @Test(dataProvider = "menu") - public void testCreateMenu(WxMenu wxMenu) throws WxErrorException { - this.wxService.menuCreate(wxMenu); - } + protected WxCpService wxService; - @Test(dependsOnMethods = {"testCreateMenu"}) - public void testGetMenu() throws WxErrorException { - Assert.assertNotNull(this.wxService.menuGet()); - } - - @Test(dependsOnMethods = {"testGetMenu"}) - public void testDeleteMenu() throws WxErrorException { - this.wxService.menuDelete(); - } - - @DataProvider(name = "menu") - public Object[][] getMenu() { + @DataProvider + public Object[][] menuData() { WxMenu menu = new WxMenu(); WxMenuButton button1 = new WxMenuButton(); button1.setType(WxConsts.BUTTON_CLICK); @@ -85,5 +69,21 @@ public class WxMenuAPITest { } + @Test(dataProvider = "menuData") + public void testCreate(WxMenu wxMenu) throws Exception { + this.wxService.getMenuService().create(wxMenu); + } + + @Test(dependsOnMethods = "testCreate") + public void testGet() throws Exception { + WxMenu menu = this.wxService.getMenuService().get(); + assertNotNull(menu); + System.out.println(menu.toJson()); + } + + @Test(dependsOnMethods = {"testGet", "testCreate"}) + public void testDelete() throws Exception { + this.wxService.getMenuService().delete(); + } } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImplTest.java new file mode 100644 index 00000000..94ac3b09 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImplTest.java @@ -0,0 +1,75 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.common.base.Splitter; +import com.google.inject.Inject; +import me.chanjar.weixin.cp.api.ApiTestModule; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpTag; +import me.chanjar.weixin.cp.bean.WxCpTagAddOrRemoveUsersResult; +import me.chanjar.weixin.cp.bean.WxCpUser; +import org.testng.annotations.*; + +import java.util.List; + +import static org.testng.Assert.*; + +/** + *
+ *
+ * Created by Binary Wang on 2017-6-25.
+ * @author Binary Wang
+ * 
+ */ +@Guice(modules = ApiTestModule.class) +public class WxCpTagServiceImplTest { + @Inject + protected WxCpService wxService; + + @Inject + protected ApiTestModule.WxXmlCpInMemoryConfigStorage configStorage; + + protected String tagId; + + @Test + public void testCreate() throws Exception { + this.tagId = this.wxService.getTagService().create("测试标签" + System.currentTimeMillis()); + System.out.println(this.tagId); + } + + @Test(dependsOnMethods = "testCreate") + public void testUpdate() throws Exception { + this.wxService.getTagService().update(this.tagId, "测试标签-改名" + System.currentTimeMillis()); + } + + @Test(dependsOnMethods = {"testUpdate", "testCreate"}) + public void testListAll() throws Exception { + List tags = this.wxService.getTagService().listAll(); + assertNotEquals(tags.size(), 0); + } + + @Test(dependsOnMethods = {"testListAll", "testUpdate", "testCreate"}) + public void testAddUsers2Tag() throws Exception { + List userIds = Splitter.on("|").splitToList(this.configStorage.getUserId()); + WxCpTagAddOrRemoveUsersResult result = this.wxService.getTagService().addUsers2Tag(this.tagId, userIds, null); + assertEquals(result.getErrCode(), Integer.valueOf(0)); + } + + @Test(dependsOnMethods = {"testAddUsers2Tag", "testListAll", "testUpdate", "testCreate"}) + public void testListUsersByTagId() throws Exception { + List users = this.wxService.getTagService().listUsersByTagId(this.tagId); + assertNotEquals(users.size(), 0); + } + + @Test(dependsOnMethods = {"testListUsersByTagId", "testAddUsers2Tag", "testListAll", "testUpdate", "testCreate"}) + public void testRemoveUsersFromTag() throws Exception { + List userIds = Splitter.on("|").splitToList(this.configStorage.getUserId()); + WxCpTagAddOrRemoveUsersResult result = this.wxService.getTagService().removeUsersFromTag(this.tagId, userIds); + assertEquals(result.getErrCode(), Integer.valueOf(0)); + } + + @Test(dependsOnMethods = {"testRemoveUsersFromTag", "testListUsersByTagId", "testAddUsers2Tag", "testListAll", "testUpdate", "testCreate"}) + public void testDelete() throws Exception { + this.wxService.getTagService().delete(this.tagId); + } + +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImplTest.java index d576b866..a01f0343 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImplTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImplTest.java @@ -6,9 +6,7 @@ import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpUser; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.testng.Assert; -import org.testng.annotations.Guice; -import org.testng.annotations.Test; +import org.testng.annotations.*; import java.util.List; @@ -25,19 +23,21 @@ import static org.testng.Assert.*; public class WxCpUserServiceImplTest { @Inject private WxCpService wxCpService; + private String userId = "someone" + System.currentTimeMillis(); @Test public void testAuthenticate() throws Exception { + this.wxCpService.getUserService().authenticate("abc"); } @Test public void testCreate() throws Exception { WxCpUser user = new WxCpUser(); - user.setUserId("some.woman"); + user.setUserId(userId); user.setName("Some Woman"); - user.setDepartIds(new Integer[]{9, 8}); + user.setDepartIds(new Integer[]{2}); user.setEmail("none@none.com"); - user.setGender("女"); + user.setGender(WxCpUser.Gender.FEMAIL); user.setMobile("13560084979"); user.setPosition("woman"); user.setTelephone("3300393"); @@ -48,20 +48,20 @@ public class WxCpUserServiceImplTest { @Test(dependsOnMethods = "testCreate") public void testUpdate() throws Exception { WxCpUser user = new WxCpUser(); - user.setUserId("some.woman"); + user.setUserId(userId); user.setName("Some Woman"); user.addExtAttr("爱好", "table2"); this.wxCpService.getUserService().update(user); } - @Test + @Test(dependsOnMethods = {"testCreate", "testUpdate"}) public void testDelete() throws Exception { - this.wxCpService.getUserService().delete("some.woman"); + this.wxCpService.getUserService().delete(userId); } @Test(dependsOnMethods = "testUpdate") public void testGetById() throws Exception { - WxCpUser user = this.wxCpService.getUserService().getById("some.woman"); + WxCpUser user = this.wxCpService.getUserService().getById(userId); assertNotNull(user); } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpDemoServer.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpDemoServer.java index e9d78768..a9ab309c 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpDemoServer.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpDemoServer.java @@ -1,14 +1,14 @@ package me.chanjar.weixin.cp.demo; import me.chanjar.weixin.common.session.WxSessionManager; -import me.chanjar.weixin.cp.config.WxCpConfigStorage; -import me.chanjar.weixin.cp.message.WxCpMessageHandler; -import me.chanjar.weixin.cp.message.WxCpMessageRouter; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; import me.chanjar.weixin.cp.bean.WxCpXmlMessage; import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; import me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessage; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.message.WxCpMessageHandler; +import me.chanjar.weixin.cp.message.WxCpMessageRouter; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -69,8 +69,7 @@ public class WxCpDemoServer { Map context, WxCpService wxService, WxSessionManager sessionManager) { String href = "测试oauth2"; return WxCpXmlOutMessage.TEXT().content(href) .fromUser(wxMessage.getToUserName()) diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpOAuth2Servlet.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpOAuth2Servlet.java index d65e2427..37c2b7a1 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpOAuth2Servlet.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpOAuth2Servlet.java @@ -30,7 +30,7 @@ public class WxCpOAuth2Servlet extends HttpServlet { response.getWriter().println("

code

"); response.getWriter().println(code); - String[] res = this.wxCpService.oauth2getUserInfo(code); + String[] res = this.wxCpService.getOauth2Service().getUserInfo(code); response.getWriter().println("

result

"); response.getWriter().println(Arrays.toString(res)); } catch (WxErrorException e) { diff --git a/weixin-java-cp/src/test/resources/mm.amr b/weixin-java-cp/src/test/resources/mm.amr new file mode 100644 index 0000000000000000000000000000000000000000..a4df01a0d9e7a0cdbe3e3fb6dd059767d60fedff GIT binary patch literal 38982 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�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 - - - - -