微信有两种模式,编辑模式和开发者模式,有些功能是互斥的,不可以同时使用,微信开发需要在开发者模式下进行(开发者模式下仍然可以去微信的网页上群发消息)。下面介绍的功能能满足大部分的需求,响应文本消息,图文消息,创建菜单,响应菜单消息等。
我们给微信提供服务有两种消息模式,被动和主动
- 被动: 例如用户输入文本,点击菜单,微信服务器会访问我们的 Web 服务对应的 URL,我们返回对应的消息给微信服务器
- 主动: 例如创建菜单,群发消息,这种模式需要我们主动去触发,给微信服务器发送消息,可以是执行某个定时任务触发,或者我们访问某个 URL 然后在其响应的代码里触发
几个关键点
- 微信服务器和我们的服务器绑定验证时使用 GET 发送一个带有
echostr
参数的请求
- 其他消息使用的是 POST,常用的消息有
- 事件消息 event: subscribe, unsubscribe, location 等
- 文本消息 text: 可以回复文本,链接,图文
- 点击菜单回复的 click
- 点击菜单跳转为 view
- 微信服务器访问我们的服务器的 URL 只有一个,就是在配置页中配置的 URL
- 使用 app_id + app_secret 从微信服务器获取 access_token,有效时间是 7200 秒
- 微信的接口:用于我们主动的从微信服务器获取信息或者主动的向微信服务器写入信息
准备外网能访问的域名
- 使用 ngrok.cc 让本地服务能在外网访问,这样微信服务器才能访问到我们的 Web 服务器
准备公众号
- 访问 https://mp.weixin.qq.com 注册开发者账号
- 登陆后到最下面
开发 > 基本配置
中启用开发者模式,并点击 修改配置
配置我们自己给微信提供服务的 Web 地址、Token 等,然后使用 appID
和 appsecret
就可以进行开发了
- 如果我们申请的是
个人订阅号
,很多功能接口都没有,例如自定义菜单都没有,为了使用所有的功能进行开发练习,可以使用微信提供的 公众平台测试帐号
:开发 > 开发者工具 > 公众平台测试帐号 > 进入
,就可以看到 appID
和 appsecret
,也需要在这里配置给微信提供服务的 Web 地址,Token 等,然后在页面中部找到 请用微信扫描关注测试公众号
,扫描关注即可
- 查看公众号接口权限说明:
开发 > 开发者工具 > 开发者文档 > 进入 > 公众号接口权限说明
个人订阅号
是不能进行认证的
Gradle 依赖
1
| compile("com.github.sd4324530:fastweixin:1.3.11")
|
Fastweixin 的 git: https://git.oschina.net/pyinjava/fastweixin
给微信提供服务的 Controller
1
| public class WeixinController extends WeixinControllerSupport
|
消息响应
在 WeixinController 中重载下面几个函数
响应订阅消息
1
| protected BaseMsg handleSubscribe(BaseEvent event)
|
响应文本消息
1
| protected BaseMsg handleTextMsg(TextReqMsg msg)
|
响应点击菜单消息
1
| protected BaseMsg handleMenuClickEvent(MenuEvent event)
|
创建消息
创建文本消息
1
| new TextMsg("你好: <a href=\"http://www.baidu.com\">百度</a>");
|
创建图文消息(单图文,多图文都可以)
1 2 3 4
| String picUrl = "http://image.17car.com.cn/image/20120810/20120810092133_13289.jpg"; String url = "http://news.17car.com.cn/saishi/20120810/336283.html"; String description = "700 马力道路赛车 DDMWorks 打造最强 Atom"; NewsMsg msg = new NewsMsg(Arrays.asList(new Article("Atom", description, picUrl, url), new Article("Atom", description, picUrl, url)));
|
创建菜单
- 微信不会访问这个URL,需要我们自己访问创建菜单的 URL,然后才能向微信发送创建菜单信息。
- 微信只能保证菜单 24 小时之类生效,想要马上看到菜单效果,先取消关注,然后再次关注就可以了
CLICK
类型的菜单会发送消息到我们的服务器,handleMenuClickEvent() 进行响应,根据 key 来判断是哪个菜单被点击
VIEW
类型的菜单不回发送消息到我们的服务器,而是直接跳转到对应的 URL
- 菜单分一级菜单和二级菜单
- 最多有 3 个一级菜单,5 个二级菜单
- 一级菜单最多 4 个汉字,二级菜单最多 7 个汉字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @GetMapping("/create-menu") @ResponseBody public String createMenu() { MenuButton main1 = new MenuButton(); main1.setType(MenuType.CLICK); main1.setKey("main1"); main1.setName("主菜单一");
MenuButton main2 = new MenuButton(); main2.setType(MenuType.VIEW); main2.setName("主菜单二"); main2.setUrl("http://www.baidu.com");
MenuButton main3 = new MenuButton(); main3.setType(MenuType.CLICK); main3.setName("真题");
MenuButton sub1 = new MenuButton(); sub1.setType(MenuType.CLICK); sub1.setName("2016 语文"); sub1.setKey("sub1");
MenuButton sub2 = new MenuButton(); sub2.setType(MenuType.CLICK); sub2.setName("2016 数学"); sub2.setKey("sub2"); main3.setSubButton(Arrays.asList(sub1, sub2));
Menu menu = new Menu(); menu.setButton(Arrays.asList(main1, main2, main3));
ApiConfig config = new ApiConfig(APP_ID, APP_SECRET); MenuAPI menuAPI = new MenuAPI(config); ResultType resultType = menuAPI.createMenu(menu); return resultType.toString(); }
|
使用 Json 字符串创建菜单
上面的程序创建菜单太麻烦了,可以使用 Json 字符串,然后反序列化为菜单对象,下面使用 Jackson 来实现。
Jackson 依赖: compile("com.fasterxml.jackson.core:jackson-databind:2.7.4")
菜单的 Json 字符串可以放在文件,数据库中等,方便修改,而且比使用对象的方式更直观,例如下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "button": [{ "type": "CLICK", "name": "主菜单一", "key": "main1" }, { "type": "VIEW", "name": "主菜单二", "url": "http://www.baidu.com" }, { "type": "CLICK", "name": "真题", "subButton": [{ "type": "CLICK", "name": "2016 语文", "key": "sub1" }, { "type": "CLICK", "name": "2016 数学", "key": "sub2" }] }] }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @GetMapping("/create-menu") @ResponseBody public String createMenu() throws Exception { String json = "{\n" + " \"button\": [{\n" + " \"type\": \"CLICK\",\n" + " \"name\": \"今日歌曲\",\n" + " \"key\": \"V1001_TODAY_MUSIC\"\n" + " }, {\n" + " \"name\": \"菜单\",\n" + " \"subButton\": [{\n" + " \"type\": \"VIEW\",\n" + " \"name\": \"搜索\",\n" + " \"url\": \"http://www.soso.com/\"\n" + " }, {\n" + " \"type\": \"VIEW\",\n" + " \"name\": \"视频\",\n" + " \"url\": \"http://v.qq.com/\"\n" + " }, {\n" + " \"type\": \"CLICK\",\n" + " \"name\": \"赞一下我们\",\n" + " \"key\": \"V1001_GOOD\"\n" + " }]\n" + " }]\n" + "}";
ObjectMapper mapper = new ObjectMapper(); Menu menu = mapper.readValue(json, Menu.class); ApiConfig config = new ApiConfig(APP_ID, APP_SECRET); MenuAPI menuAPI = new MenuAPI(config); ResultType resultType = menuAPI.createMenu(menu); return resultType.toString(); }
|
示例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| package com.xtuer.controller;
import com.github.sd4324530.fastweixin.api.MenuAPI; import com.github.sd4324530.fastweixin.api.config.ApiConfig; import com.github.sd4324530.fastweixin.api.entity.Menu; import com.github.sd4324530.fastweixin.api.entity.MenuButton; import com.github.sd4324530.fastweixin.api.enums.MenuType; import com.github.sd4324530.fastweixin.api.enums.ResultType; import com.github.sd4324530.fastweixin.message.Article; import com.github.sd4324530.fastweixin.message.BaseMsg; import com.github.sd4324530.fastweixin.message.NewsMsg; import com.github.sd4324530.fastweixin.message.TextMsg; import com.github.sd4324530.fastweixin.message.req.MenuEvent; import com.github.sd4324530.fastweixin.message.req.TextReqMsg; import com.github.sd4324530.fastweixin.servlet.WeixinControllerSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RestController @RequestMapping("/weixin") public class WeixinController extends WeixinControllerSupport { private static final Logger LOG = LoggerFactory.getLogger(WeixinController.class); private static final String TOKEN = "xxxxxx"; private static final String APP_ID = "yyyyyy"; private static final String APP_SECRET = "zzzzzz";
@Override protected BaseMsg handleTextMsg(TextReqMsg msg) { String content = msg.getContent();
switch (content.toUpperCase()) { case "URL": return new TextMsg("你好: <a href=\"http://www.baidu.com\">百度</a>"); case "ATOM": String picUrl = "http://image.17car.com.cn/image/20120810/20120810092133_13289.jpg"; String url = "http://news.17car.com.cn/saishi/20120810/336283.html"; String description = "700 马力道路赛车 DDMWorks 打造最强 Atom"; return new NewsMsg(Arrays.asList(new Article("Atom", description, picUrl, url), new Article("Atom", description, picUrl, url))); default: return new TextMsg("不识别的命令, 您输入的内容是: " + content); } }
@Override protected BaseMsg handleMenuClickEvent(MenuEvent event) { String key = event.getEventKey(); switch (key.toUpperCase()) { case "MAIN1": return new TextMsg("点击按钮"); case "SUB1": return new TextMsg("2016 语文"); case "SUB2": return new TextMsg("2016 数学"); default: return new TextMsg("不识别的菜单命令"); } }
@Override protected String getToken() { return TOKEN; }
@GetMapping("/access-token") @ResponseBody public String getAccessToken() { ApiConfig config = new ApiConfig(APP_ID, APP_SECRET); return config.getAccessToken(); }
@GetMapping("/create-menu") @ResponseBody public String createMenu() { MenuButton main1 = new MenuButton(); main1.setType(MenuType.CLICK); main1.setKey("main1"); main1.setName("主菜单一");
MenuButton main2 = new MenuButton(); main2.setType(MenuType.VIEW); main2.setName("主菜单二"); main2.setUrl("http://www.baidu.com");
MenuButton main3 = new MenuButton(); main3.setType(MenuType.CLICK); main3.setName("真题");
MenuButton sub1 = new MenuButton(); sub1.setType(MenuType.CLICK); sub1.setName("2016 语文"); sub1.setKey("sub1");
MenuButton sub2 = new MenuButton(); sub2.setType(MenuType.CLICK); sub2.setName("2016 数学"); sub2.setKey("sub2"); main3.setSubButton(Arrays.asList(sub1, sub2));
Menu menu = new Menu(); menu.setButton(Arrays.asList(main1, main2, main3));
ApiConfig config = new ApiConfig(APP_ID, APP_SECRET); MenuAPI menuAPI = new MenuAPI(config); ResultType resultType = menuAPI.createMenu(menu); return resultType.toString(); } }
|