微信开发公众平台自定义菜单需要花钱认证才能实现,不想花钱只能玩测试账号了,不过这并不影响开发。我的开发都是基于柳峰老师的微信公众平台应用开发做的。
只要我们使用公众平台测试账号就可以开发自定义菜单了,比较方便,测试账号开放了很多接口,很方便。
在开发自定义菜单的时候可以参考微信公众平台开发者文档的自定义菜单创建。
一、自定义菜单
1、自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
2、一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。
3、创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则
可以看到创建后的效果。
自定义菜单接口可实现多种类型按钮,如下:
1、click:点击推事件
用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
2、view:跳转URL
用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
3、scancode_push:扫码推事件
用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
4、scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框
用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
5、pic_sysphoto:弹出系统拍照发图
用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
6、pic_photo_or_album:弹出拍照或者相册发图
用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
7、pic_weixin:弹出微信相册发图器
用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
8、location_select:弹出地理位置选择器
用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
9、media_id:下发消息(除文本消息)
用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
10、view_limited:跳转图文消息URL
用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id
二、访问自定义菜单接口
1:获取access_token
自定义菜单的创建、查询和删除需要调用公众平台开放的自定义菜单接口,而调用该接口需要先获取access_token(接口访问凭证),这些接口全都是基于https协议的,因此我们先要解决如何在Java程序中发送https请求的问题。
获取接口访问凭证access_token:
获取access_token是通过GET方式访问如下链接:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
链接中有三个参数,分别是grant_type、appid和secret。根据图中的参数说明,grant_type传固定值client_credential,而appid和secret就是申请完自定义菜单后微信分配给我们的。
请求发送成功后,微信服务器会返回一个json串,包含access_token和expires_in两个元素。其中,access_token就是我们最终需要的凭证,而expires_in是凭证的有效期,单位是秒,7200秒也就是2个小时。这就意味着,不是每次访问特殊接口,都需要重新获取一次access_token,只要access_token还在有效期内,就一直可以使用。
2:菜单创建
创建菜单要调用菜单接口
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
其实就是向地址https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN以POST方式提交一个JSON格式的菜单字符串。
三、封装通用请求方法(自定义信任管理器)
创建菜单需要两个接口,一个是获取access_token接口,一个是自定义菜单接口,都是https请求。
1:创建证书信任管理器
对于https请求,我们需要一个证书信任管理器,这个管理器类需要自己定义,但需要实现X509TrustManager接口,代码如下:
package org.liufeng.weixin.util; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; /** * 证书信任管理器(用于https请求) * * @author liufeng * @date 2013-08-08 */ public class MyX509TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } }
证书管理器的作用就是信任指定所有证书。
2:创建通用https请求
通用https请求应该
1)支持HTTPS请求;
2)支持GET、POST两种方式;
3)支持参数提交,也支持无参数的情况;
package org.liufeng.weixin.util; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ConnectException; import java.net.URL; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import net.sf.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 公众平台通用接口工具类 * * @author liuyq * @date 2013-08-09 */ public class WeixinUtil { private static Logger log = LoggerFactory.getLogger(WeixinUtil.class); /** * 发起https请求并获取结果 * * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */ public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) { JSONObject jsonObject = null; StringBuffer buffer = new StringBuffer(); try { // 创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); // 从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactory ssf = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection(); httpUrlConn.setSSLSocketFactory(ssf); httpUrlConn.setDoOutput(true); httpUrlConn.setDoInput(true); httpUrlConn.setUseCaches(false); // 设置请求方式(GET/POST) httpUrlConn.setRequestMethod(requestMethod); if ("GET".equalsIgnoreCase(requestMethod)) httpUrlConn.connect(); // 当有数据需要提交时 if (null != outputStr) { OutputStream outputStream = httpUrlConn.getOutputStream(); // 注意编码格式,防止中文乱码 outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } // 将返回的输入流转换成字符串 InputStream inputStream = httpUrlConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } bufferedReader.close(); inputStreamReader.close(); // 释放资源 inputStream.close(); inputStream = null; httpUrlConn.disconnect(); jsonObject = JSONObject.fromObject(buffer.toString()); } catch (ConnectException ce) { log.error("Weixin server connection timed out."); } catch (Exception e) { log.error("https request error:{}", e); } return jsonObject; } }
四、封装菜单实体类
1:按钮的分类
我们通常会把自定义菜单项看做是按钮,按钮的类型分成click(点击事件)和view(访问网页)。
click类型的按钮有type、name和key3个属性,而view类型的按钮有type、name和url3个属性
2:首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下:
package org.liufeng.weixin.pojo; /** * 微信通用接口凭证 * * @author liufeng * @date 2013-08-08 */ public class AccessToken { // 获取到的凭证 private String token; // 凭证有效时间,单位:秒 private int expiresIn; public String getToken() { return token; } public void setToken(String token) { this.token = token; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } }
接下来是对菜单结构的封装。因为我们是采用面向对象的编程方式,最终提交的json格式菜单数据就应该是由对象直接转换得到,而不是在程序代码中拼一大堆json数据。菜单结构封装的依据是公众平台API文档中给出的那一段json格式的菜单结构,如下所示:
click和view的请求示例
{ "button":[ { "type":"click", "name":"今日歌曲", "key":"V1001_TODAY_MUSIC" }, { "name":"菜单", "sub_button":[ { "type":"view", "name":"搜索", "url":"http://www.soso.com/" }, { "type":"view", "name":"视频", "url":"http://v.qq.com/" }, { "type":"click", "name":"赞一下我们", "key":"V1001_GOOD" }] }] }
3:封装菜单结构
每个按钮对象都要一个共同的name属性,因此需要定义一个按钮对象基类,所有的按钮对象都需要继承该类。基类的代码如下:
package org.liufeng.weixin.pojo; /** * 按钮的基类 * * @author liufeng * @date 2013-08-08 */ public class Button { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下:
package org.liufeng.weixin.pojo; /** * 普通按钮(子按钮) * * @author liufeng * @date 2013-08-08 */ public class CommonButton extends Button { private String type; private String key; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } }
对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下:
package org.liufeng.weixin.pojo; /** * 复杂按钮(父按钮) * * @author liufeng * @date 2013-08-08 */ public class ComplexButton extends Button { private Button[] sub_button; public Button[] getSub_button() { return sub_button; } public void setSub_button(Button[] sub_button) { this.sub_button = sub_button; } }
对整个菜单进行封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项)
package org.liufeng.weixin.pojo; /** * 菜单 * * @author liufeng * @date 2013-08-08 */ public class Menu { private Button[] button; public Button[] getButton() { return button; } public void setButton(Button[] button) { this.button = button; } }
这样我们就完成了菜单实体类的封装。
凭证access_token的获取方法
继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于获取接口访问凭证:
// 获取access_token的接口地址(GET) 限200(次/天) public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; /** * 获取access_token * * @param appid 凭证 * @param appsecret 密钥 * @return */ public static AccessToken getAccessToken(String appid, String appsecret) { AccessToken accessToken = null; String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret); JSONObject jsonObject = httpRequest(requestUrl, "GET", null); // 如果请求成功 if (null != jsonObject) { try { accessToken = new AccessToken(); accessToken.setToken(jsonObject.getString("access_token")); accessToken.setExpiresIn(jsonObject.getInt("expires_in")); } catch (JSONException e) { accessToken = null; // 获取token失败 log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg")); } } return accessToken; }
自定义菜单的创建方法
继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单:
// 菜单创建(POST) 限100(次/天) public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"; /** * 创建菜单 * * @param menu 菜单实例 * @param accessToken 有效的access_token * @return 0表示成功,其他值表示失败 */ public static int createMenu(Menu menu, String accessToken) { int result = 0; // 拼装创建菜单的url String url = menu_create_url.replace("ACCESS_TOKEN", accessToken); // 将菜单对象转换成json字符串 String jsonMenu = JSONObject.fromObject(menu).toString(); // 调用接口创建菜单 JSONObject jsonObject = httpRequest(url, "POST", jsonMenu); if (null != jsonObject) { if (0 != jsonObject.getInt("errcode")) { result = jsonObject.getInt("errcode"); log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg")); } } return result; }
调用封装的方法创建自定义菜单
package org.liufeng.weixin.main; import org.liufeng.weixin.pojo.AccessToken; import org.liufeng.weixin.pojo.Button; import org.liufeng.weixin.pojo.CommonButton; import org.liufeng.weixin.pojo.ComplexButton; import org.liufeng.weixin.pojo.Menu; import org.liufeng.weixin.util.WeixinUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 菜单管理器类 * * @author liufeng * @date 2013-08-08 */ public class MenuManager { private static Logger log = LoggerFactory.getLogger(MenuManager.class); public static void main(String[] args) { // 第三方用户唯一凭证 String appId = "000000000000000000"; // 第三方用户唯一凭证密钥 String appSecret = "00000000000000000000000000000000"; // 调用接口获取access_token AccessToken at = WeixinUtil.getAccessToken(appId, appSecret); if (null != at) { // 调用接口创建菜单 int result = WeixinUtil.createMenu(getMenu(), at.getToken()); // 判断菜单创建结果 if (0 == result) log.info("菜单创建成功!"); else log.info("菜单创建失败,错误码:" + result); } } /** * 组装菜单数据 * * @return */ private static Menu getMenu() { CommonButton btn11 = new CommonButton(); btn11.setName("天气预报"); btn11.setType("click"); btn11.setKey("11"); CommonButton btn12 = new CommonButton(); btn12.setName("公交查询"); btn12.setType("click"); btn12.setKey("12"); CommonButton btn13 = new CommonButton(); btn13.setName("周边搜索"); btn13.setType("click"); btn13.setKey("13"); CommonButton btn14 = new CommonButton(); btn14.setName("历史上的今天"); btn14.setType("click"); btn14.setKey("14"); CommonButton btn21 = new CommonButton(); btn21.setName("歌曲点播"); btn21.setType("click"); btn21.setKey("21"); CommonButton btn22 = new CommonButton(); btn22.setName("经典游戏"); btn22.setType("click"); btn22.setKey("22"); CommonButton btn23 = new CommonButton(); btn23.setName("美女电台"); btn23.setType("click"); btn23.setKey("23"); CommonButton btn24 = new CommonButton(); btn24.setName("人脸识别"); btn24.setType("click"); btn24.setKey("24"); CommonButton btn25 = new CommonButton(); btn25.setName("聊天唠嗑"); btn25.setType("click"); btn25.setKey("25"); CommonButton btn31 = new CommonButton(); btn31.setName("Q友圈"); btn31.setType("click"); btn31.setKey("31"); CommonButton btn32 = new CommonButton(); btn32.setName("电影排行榜"); btn32.setType("click"); btn32.setKey("32"); CommonButton btn33 = new CommonButton(); btn33.setName("幽默笑话"); btn33.setType("click"); btn33.setKey("33"); ComplexButton mainBtn1 = new ComplexButton(); mainBtn1.setName("生活助手"); mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 }); ComplexButton mainBtn2 = new ComplexButton(); mainBtn2.setName("休闲驿站"); mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 }); ComplexButton mainBtn3 = new ComplexButton(); mainBtn3.setName("更多体验"); mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 }); /** * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项 * * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢? * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义: * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 }); */ Menu menu = new Menu(); menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 }); return menu; } }
注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。
响应菜单点击事件
package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-05-20 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // 默认返回的文本消息内容 String respContent = "请求处理异常,请稍候尝试!"; // xml请求解析 MaprequestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 消息类型 String msgType = requestMap.get("MsgType"); // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { respContent = "您发送的是文本消息!"; } // 图片消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { respContent = "您发送的是图片消息!"; } // 地理位置消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { respContent = "您发送的是地理位置消息!"; } // 链接消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { respContent = "您发送的是链接消息!"; } // 音频消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { respContent = "您发送的是音频消息!"; } // 事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { // 事件类型 String eventType = requestMap.get("Event"); // 订阅 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { respContent = "谢谢您的关注!"; } // 取消订阅 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息 } // 自定义菜单点击事件 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { // 事件KEY值,与创建自定义菜单时指定的KEY值对应 String eventKey = requestMap.get("EventKey"); if (eventKey.equals("11")) { respContent = "天气预报菜单项被点击!"; } else if (eventKey.equals("12")) { respContent = "公交查询菜单项被点击!"; } else if (eventKey.equals("13")) { respContent = "周边搜索菜单项被点击!"; } else if (eventKey.equals("14")) { respContent = "历史上的今天菜单项被点击!"; } else if (eventKey.equals("21")) { respContent = "歌曲点播菜单项被点击!"; } else if (eventKey.equals("22")) { respContent = "经典游戏菜单项被点击!"; } else if (eventKey.equals("23")) { respContent = "美女电台菜单项被点击!"; } else if (eventKey.equals("24")) { respContent = "人脸识别菜单项被点击!"; } else if (eventKey.equals("25")) { respContent = "聊天唠嗑菜单项被点击!"; } else if (eventKey.equals("31")) { respContent = "Q友圈菜单项被点击!"; } else if (eventKey.equals("32")) { respContent = "电影排行榜菜单项被点击!"; } else if (eventKey.equals("33")) { respContent = "幽默笑话菜单项被点击!"; } } } textMessage.setContent(respContent); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。