| @@ -1,7 +1,48 @@ | |||||
| weixin-java | |||||
| weixin-tools | |||||
| =========== | =========== | ||||
| 微信java开发工具集 | |||||
| 微信java开发工具,本项目提供了两个主要特性:微信消息路由器、微信Java API | |||||
| ## 执行测试 | |||||
| 将 ''src/test/resources/test-config.sample.xml'' 改成 ''test-config.xml'' 并设置appId, secret, 一个过期的accessToken | |||||
| ## 微信消息路由器 | |||||
| 你可以使用``WxMessageRouter``来对微信推送过来的消息、事件进行路由,交给特定的``WxMessageHandler``处理。 | |||||
| 使用方法: | |||||
| ```java | |||||
| 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); | |||||
| ``` | |||||
| 说明: | |||||
| 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理 | |||||
| 2. 默认情况下消息只会被处理一次,除非使用 {@link Rule#next()} | |||||
| 3. 规则的结束必须用{@link Rule#end()}或者{@link Rule#next()},否则不会生效 | |||||
| ## 微信Java API | |||||
| 使用``WxService``可以调用微信API。目前实现了以下功能,其余功能以后陆续补充: | |||||
| 1. 发送客服消息 | |||||
| 1. 创建自定义菜单 | |||||
| 1. 删除自定义菜单 | |||||
| 1. 查询自定义菜单 | |||||
| 1. 刷新access_token | |||||
| ## 如何执行单元测试 | |||||
| 将 ``src/test/resources/test-config.sample.xml`` 改成 ``test-config.xml`` 设置appId, secret, accessToken(可选), openId | |||||
| ```bash | |||||
| mvn clean test | |||||
| ``` | |||||
| @@ -4,7 +4,7 @@ | |||||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | ||||
| <modelVersion>4.0.0</modelVersion> | <modelVersion>4.0.0</modelVersion> | ||||
| <groupId>chanjarster.weixin</groupId> | <groupId>chanjarster.weixin</groupId> | ||||
| <artifactId>weixin-java</artifactId> | |||||
| <artifactId>weixin-toolset</artifactId> | |||||
| <version>1.0.0-SNAPSHOT</version> | <version>1.0.0-SNAPSHOT</version> | ||||
| <name>WeiXin Java Toolset</name> | <name>WeiXin Java Toolset</name> | ||||
| <url>https://github.com/chanjarster/weixin-java</url> | <url>https://github.com/chanjarster/weixin-java</url> | ||||
| @@ -10,6 +10,14 @@ public class WxConsts { | |||||
| public static final String MSG_NEWS = "news"; | public static final String MSG_NEWS = "news"; | ||||
| public static final String MSG_LOCATION = "location"; | public static final String MSG_LOCATION = "location"; | ||||
| public static final String MSG_LINK = "link"; | public static final String MSG_LINK = "link"; | ||||
| public static final String MSG_EVENT = "event"; | |||||
| 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 = "LOCATION"; | |||||
| public static final String EVT_VIEW = "VIEW"; | |||||
| } | } | ||||
| @@ -5,7 +5,7 @@ import java.util.Map; | |||||
| import chanjarster.weixin.bean.WxXmlMessage; | import chanjarster.weixin.bean.WxXmlMessage; | ||||
| /** | /** | ||||
| * 处理微信推送消息的处理器 | |||||
| * 处理微信推送消息的处理器接口 | |||||
| * @author chanjarster | * @author chanjarster | ||||
| * | * | ||||
| */ | */ | ||||
| @@ -5,13 +5,17 @@ import java.util.HashMap; | |||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| import chanjarster.weixin.api.WxMessageRouterTest.WxEchoMessageHandler; | |||||
| import chanjarster.weixin.bean.WxXmlMessage; | import chanjarster.weixin.bean.WxXmlMessage; | ||||
| /** | /** | ||||
| * <pre> | * <pre> | ||||
| * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理 | * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理 | ||||
| * | * | ||||
| * 说明: | |||||
| * 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理 | |||||
| * 2. 默认情况下消息只会被处理一次,除非使用 {@link Rule#next()} | |||||
| * 3. 规则的结束必须用{@link Rule#end()}或者{@link Rule#next()},否则不会生效 | |||||
| * | |||||
| * 使用方法: | * 使用方法: | ||||
| * WxMessageRouter router = new WxMessageRouter(); | * WxMessageRouter router = new WxMessageRouter(); | ||||
| * router | * router | ||||
| @@ -27,9 +31,6 @@ import chanjarster.weixin.bean.WxXmlMessage; | |||||
| * // 将WxXmlMessage交给消息路由器 | * // 将WxXmlMessage交给消息路由器 | ||||
| * router.route(message); | * router.route(message); | ||||
| * | * | ||||
| * 说明: | |||||
| * 1. 配置路由规则时要按照从细到粗的原则 | |||||
| * 2. 默认情况下消息只会被处理一次,除非使用 {@link Rule#reEnter()} | |||||
| * </pre> | * </pre> | ||||
| * @author qianjia | * @author qianjia | ||||
| * | * | ||||
| @@ -52,9 +53,11 @@ public class WxMessageRouter { | |||||
| */ | */ | ||||
| public void route(WxXmlMessage wxMessage) { | public void route(WxXmlMessage wxMessage) { | ||||
| for (Rule rule : rules) { | for (Rule rule : rules) { | ||||
| boolean doNext = rule.service(wxMessage); | |||||
| if (!doNext) { | |||||
| break; | |||||
| if (rule.test(wxMessage)) { | |||||
| boolean reEnter = rule.service(wxMessage); | |||||
| if (!reEnter) { | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -122,16 +125,16 @@ public class WxMessageRouter { | |||||
| } | } | ||||
| /** | /** | ||||
| * 将消息交给后面的Rule处理 | |||||
| * 设置微信消息拦截器 | |||||
| * @param interceptor | |||||
| * @return | * @return | ||||
| */ | */ | ||||
| public Rule reEnter() { | |||||
| this.reEnter = true; | |||||
| return this; | |||||
| public Rule interceptor(WxMessageInterceptor interceptor) { | |||||
| return interceptor(interceptor, (WxMessageInterceptor[]) null); | |||||
| } | } | ||||
| /** | /** | ||||
| * 添加interceptor | |||||
| * 设置微信消息拦截器 | |||||
| * @param interceptor | * @param interceptor | ||||
| * @param otherInterceptors | * @param otherInterceptors | ||||
| * @return | * @return | ||||
| @@ -147,10 +150,20 @@ public class WxMessageRouter { | |||||
| } | } | ||||
| /** | /** | ||||
| * 添加handler | |||||
| * 设置微信消息处理器 | |||||
| * @param handler | * @param handler | ||||
| * @return | * @return | ||||
| */ | */ | ||||
| public Rule handler(WxMessageHandler handler) { | |||||
| return handler(handler, (WxMessageHandler[]) null); | |||||
| } | |||||
| /** | |||||
| * 设置微信消息处理器 | |||||
| * @param handler | |||||
| * @param otherHandlers | |||||
| * @return | |||||
| */ | |||||
| public Rule handler(WxMessageHandler handler, WxMessageHandler... otherHandlers) { | public Rule handler(WxMessageHandler handler, WxMessageHandler... otherHandlers) { | ||||
| this.handlers.add(handler); | this.handlers.add(handler); | ||||
| if (otherHandlers != null && otherHandlers.length > 0) { | if (otherHandlers != null && otherHandlers.length > 0) { | ||||
| @@ -162,7 +175,7 @@ public class WxMessageRouter { | |||||
| } | } | ||||
| /** | /** | ||||
| * 规则结束 | |||||
| * 规则结束,代表如果一个消息匹配该规则,那么它将不再会进入其他规则 | |||||
| * @return | * @return | ||||
| */ | */ | ||||
| public WxMessageRouter end() { | public WxMessageRouter end() { | ||||
| @@ -170,6 +183,15 @@ public class WxMessageRouter { | |||||
| return this.routerBuilder; | return this.routerBuilder; | ||||
| } | } | ||||
| /** | |||||
| * 规则结束,但是消息还会进入其他规则 | |||||
| * @return | |||||
| */ | |||||
| public WxMessageRouter next() { | |||||
| this.reEnter = true; | |||||
| return end(); | |||||
| } | |||||
| protected boolean test(WxXmlMessage wxMessage) { | protected boolean test(WxXmlMessage wxMessage) { | ||||
| return | return | ||||
| (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) | (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) | ||||
| @@ -188,11 +210,6 @@ public class WxMessageRouter { | |||||
| * @return true 代表继续执行别的router,false 代表停止执行别的router | * @return true 代表继续执行别的router,false 代表停止执行别的router | ||||
| */ | */ | ||||
| protected boolean service(WxXmlMessage wxMessage) { | protected boolean service(WxXmlMessage wxMessage) { | ||||
| // 如果不匹配本规则,那么接着执行后面的Rule | |||||
| if (!test(wxMessage)) { | |||||
| return true; | |||||
| } | |||||
| Map<String, Object> context = new HashMap<String, Object>(); | Map<String, Object> context = new HashMap<String, Object>(); | ||||
| // 如果拦截器不通过 | // 如果拦截器不通过 | ||||
| for (WxMessageInterceptor interceptor : this.interceptors) { | for (WxMessageInterceptor interceptor : this.interceptors) { | ||||
| @@ -20,7 +20,7 @@ import chanjarster.weixin.bean.WxCustomMessage; | |||||
| import chanjarster.weixin.bean.WxError; | import chanjarster.weixin.bean.WxError; | ||||
| import chanjarster.weixin.bean.WxMenu; | import chanjarster.weixin.bean.WxMenu; | ||||
| import chanjarster.weixin.exception.WxErrorException; | import chanjarster.weixin.exception.WxErrorException; | ||||
| import chanjarster.weixin.util.Utf8StringResponseHandler; | |||||
| import chanjarster.weixin.util.Utf8ResponseHandler; | |||||
| public class WxServiceImpl implements WxService { | public class WxServiceImpl implements WxService { | ||||
| @@ -133,7 +133,7 @@ public class WxServiceImpl implements WxService { | |||||
| httpPost.setEntity(entity); | httpPost.setEntity(entity); | ||||
| } | } | ||||
| CloseableHttpResponse response = httpclient.execute(httpPost); | CloseableHttpResponse response = httpclient.execute(httpPost); | ||||
| resultContent = Utf8StringResponseHandler.INSTANCE.handleResponse(response); | |||||
| resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||||
| } else if ("GET".equals(method)) { | } else if ("GET".equals(method)) { | ||||
| if (data != null) { | if (data != null) { | ||||
| uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + data; | uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + data; | ||||
| @@ -141,7 +141,7 @@ public class WxServiceImpl implements WxService { | |||||
| HttpGet httpGet = new HttpGet(uriWithAccessToken); | HttpGet httpGet = new HttpGet(uriWithAccessToken); | ||||
| CloseableHttpResponse response = httpclient.execute(httpGet); | CloseableHttpResponse response = httpclient.execute(httpGet); | ||||
| response.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); | response.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); | ||||
| resultContent = Utf8StringResponseHandler.INSTANCE.handleResponse(response); | |||||
| resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||||
| } | } | ||||
| WxError error = WxError.fromJson(resultContent); | WxError error = WxError.fromJson(resultContent); | ||||
| @@ -15,9 +15,9 @@ import org.apache.http.util.EntityUtils; | |||||
| * @author chanjarster | * @author chanjarster | ||||
| * | * | ||||
| */ | */ | ||||
| public class Utf8StringResponseHandler implements ResponseHandler<String> { | |||||
| public class Utf8ResponseHandler implements ResponseHandler<String> { | |||||
| public static final ResponseHandler<String> INSTANCE = new Utf8StringResponseHandler(); | |||||
| public static final ResponseHandler<String> INSTANCE = new Utf8ResponseHandler(); | |||||
| public String handleResponse(final HttpResponse response) throws HttpResponseException, IOException { | public String handleResponse(final HttpResponse response) throws HttpResponseException, IOException { | ||||
| final StatusLine statusLine = response.getStatusLine(); | final StatusLine statusLine = response.getStatusLine(); | ||||
| @@ -3,30 +3,97 @@ package chanjarster.weixin.api; | |||||
| import java.util.Map; | import java.util.Map; | ||||
| import org.testng.Assert; | import org.testng.Assert; | ||||
| import org.testng.annotations.BeforeMethod; | |||||
| import org.testng.annotations.DataProvider; | |||||
| import org.testng.annotations.Test; | import org.testng.annotations.Test; | ||||
| import chanjarster.weixin.bean.WxXmlMessage; | import chanjarster.weixin.bean.WxXmlMessage; | ||||
| @Test | @Test | ||||
| public class WxMessageRouterTest { | public class WxMessageRouterTest { | ||||
| public void testSimple() { | |||||
| StringBuilder sb = new StringBuilder(); | |||||
| WxMessageRouter router = new WxMessageRouter(); | |||||
| protected StringBuilder sb; | |||||
| protected WxMessageRouter router; | |||||
| @BeforeMethod | |||||
| public void prepare() { | |||||
| this.sb = new StringBuilder(); | |||||
| this.router = new WxMessageRouter(); | |||||
| router | router | ||||
| .rule() | |||||
| .msgType(WxConsts.MSG_TEXT).event(WxConsts.EVT_CLICK).eventKey("KEY_1").content("CONTENT_1") | |||||
| .handler(new WxEchoMessageHandler(sb, "COMBINE_4")) | |||||
| .end() | |||||
| .rule() | |||||
| .msgType(WxConsts.MSG_TEXT).event(WxConsts.EVT_CLICK).eventKey("KEY_1") | |||||
| .handler(new WxEchoMessageHandler(sb, "COMBINE_3")) | |||||
| .end() | |||||
| .rule() | |||||
| .msgType(WxConsts.MSG_TEXT).event(WxConsts.EVT_CLICK) | |||||
| .handler(new WxEchoMessageHandler(sb, "COMBINE_2")) | |||||
| .end() | |||||
| .rule().msgType(WxConsts.MSG_TEXT).handler(new WxEchoMessageHandler(sb, WxConsts.MSG_TEXT)).end() | .rule().msgType(WxConsts.MSG_TEXT).handler(new WxEchoMessageHandler(sb, WxConsts.MSG_TEXT)).end() | ||||
| .rule().msgType(WxConsts.MSG_IMAGE).handler(new WxEchoMessageHandler(sb, WxConsts.MSG_IMAGE)).end() | |||||
| .rule().event(WxConsts.EVT_CLICK).handler(new WxEchoMessageHandler(sb, WxConsts.EVT_CLICK)).end() | |||||
| .rule().eventKey("KEY_1").handler(new WxEchoMessageHandler(sb, "KEY_1")).end() | |||||
| .rule().content("CONTENT_1").handler(new WxEchoMessageHandler(sb, "CONTENT_1")).end() | |||||
| .rule().handler(new WxEchoMessageHandler(sb, "ALL")).end(); | |||||
| ; | ; | ||||
| WxXmlMessage message = new WxXmlMessage(); | |||||
| message.setMsgType(WxConsts.MSG_TEXT); | |||||
| } | |||||
| @Test(dataProvider="messages-1") | |||||
| public void testSimple(WxXmlMessage message, String expected) { | |||||
| router.route(message); | router.route(message); | ||||
| Assert.assertEquals(sb.toString(), WxConsts.MSG_TEXT + ","); | |||||
| Assert.assertEquals(sb.toString(), expected); | |||||
| } | |||||
| @Test() | |||||
| public void test() { | |||||
| } | } | ||||
| @DataProvider(name="messages-1") | |||||
| public Object[][] messages2() { | |||||
| WxXmlMessage message1 = new WxXmlMessage(); | |||||
| message1.setMsgType(WxConsts.MSG_TEXT); | |||||
| WxXmlMessage message2 = new WxXmlMessage(); | |||||
| message2.setEvent(WxConsts.EVT_CLICK); | |||||
| WxXmlMessage message3 = new WxXmlMessage(); | |||||
| message3.setEventKey("KEY_1"); | |||||
| WxXmlMessage message4 = new WxXmlMessage(); | |||||
| message4.setContent("CONTENT_1"); | |||||
| WxXmlMessage message5 = new WxXmlMessage(); | |||||
| message5.setContent("BLA"); | |||||
| WxXmlMessage c2 = new WxXmlMessage(); | |||||
| c2.setMsgType(WxConsts.MSG_TEXT); | |||||
| c2.setEvent(WxConsts.EVT_CLICK); | |||||
| WxXmlMessage c3 = new WxXmlMessage(); | |||||
| c3.setMsgType(WxConsts.MSG_TEXT); | |||||
| c3.setEvent(WxConsts.EVT_CLICK); | |||||
| c3.setEventKey("KEY_1"); | |||||
| WxXmlMessage c4 = new WxXmlMessage(); | |||||
| c4.setMsgType(WxConsts.MSG_TEXT); | |||||
| c4.setEvent(WxConsts.EVT_CLICK); | |||||
| c4.setEventKey("KEY_1"); | |||||
| c4.setContent("CONTENT_1"); | |||||
| return new Object[][] { | |||||
| new Object[] { message1, WxConsts.MSG_TEXT + "," }, | |||||
| new Object[] { message2, WxConsts.EVT_CLICK + "," }, | |||||
| new Object[] { message3, "KEY_1," }, | |||||
| new Object[] { message4, "CONTENT_1," }, | |||||
| new Object[] { message5, "ALL," }, | |||||
| new Object[] { c2, "COMBINE_2," }, | |||||
| new Object[] { c3, "COMBINE_3," }, | |||||
| new Object[] { c4, "COMBINE_4," } | |||||
| }; | |||||
| } | |||||
| public static class WxEchoMessageHandler implements WxMessageHandler { | public static class WxEchoMessageHandler implements WxMessageHandler { | ||||
| @@ -43,18 +43,18 @@ public class WxServiceTest { | |||||
| Assert.assertTrue(StringUtils.isNotBlank(after)); | Assert.assertTrue(StringUtils.isNotBlank(after)); | ||||
| } | } | ||||
| @Test(dependsOnMethods = "testRefreshAccessToken", enabled = false) | |||||
| @Test(dependsOnMethods = "testRefreshAccessToken") | |||||
| public void sendCustomMessage() throws WxErrorException { | public void sendCustomMessage() throws WxErrorException { | ||||
| WxXmlConfigStorage configProvider = (WxXmlConfigStorage) wxService.wxConfigProvider; | WxXmlConfigStorage configProvider = (WxXmlConfigStorage) wxService.wxConfigProvider; | ||||
| WxCustomMessage message = new WxCustomMessage(); | WxCustomMessage message = new WxCustomMessage(); | ||||
| message.setMsgtype(WxConsts.MSG_TEXT); | message.setMsgtype(WxConsts.MSG_TEXT); | ||||
| message.setTouser(configProvider.getOpenId()); | message.setTouser(configProvider.getOpenId()); | ||||
| message.setContent("欢迎使用教务系统微信公众号\n下面\n<a href=\"http://www.baidu.com\">Hello World</a>"); | |||||
| message.setContent("欢迎欢迎,热烈欢迎\n换行测试\n超链接:<a href=\"http://www.baidu.com\">Hello World</a>"); | |||||
| wxService.sendCustomMessage(message); | wxService.sendCustomMessage(message); | ||||
| } | } | ||||
| @Test(dataProvider = "menu", enabled = true, dependsOnMethods = "testRefreshAccessToken") | |||||
| @Test(dataProvider = "menu", dependsOnMethods = "testRefreshAccessToken") | |||||
| public void testCreateMenu(WxMenu wxMenu) throws WxErrorException { | public void testCreateMenu(WxMenu wxMenu) throws WxErrorException { | ||||
| wxService.createMenu(wxMenu); | wxService.createMenu(wxMenu); | ||||
| } | } | ||||
| @@ -64,7 +64,7 @@ public class WxServiceTest { | |||||
| Assert.assertNotNull(wxService.getMenu()); | Assert.assertNotNull(wxService.getMenu()); | ||||
| } | } | ||||
| @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }, enabled = false) | |||||
| @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }) | |||||
| public void testDeleteMenu() throws WxErrorException { | public void testDeleteMenu() throws WxErrorException { | ||||
| wxService.deleteMenu(); | wxService.deleteMenu(); | ||||
| } | } | ||||