微信公众帐号开发-自定义菜单的创建及菜单事件响应的实例
微信开发公众平台自定义菜单需要花钱认证才能实现,不想花钱只能玩测试账号了,不过这并不影响开发。我的开发都是基于柳峰老师的微信公众平台应用开发做的。
只要我们使用公众平台测试账号就可以开发自定义菜单了,比较方便,测试账号开放了很多接口,很方便。
在开发自定义菜单的时候可以参考微信公众平台开发者文档的自定义菜单创建。
一、自定义菜单
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接口,代码如下:
packageorg.liufeng.weixin.util; importjava.security.cert.CertificateException; importjava.security.cert.X509Certificate; importjavax.net.ssl.X509TrustManager; /** *证书信任管理器(用于https请求) * *@authorliufeng *@date2013-08-08 */ publicclassMyX509TrustManagerimplementsX509TrustManager{ publicvoidcheckClientTrusted(X509Certificate[]chain,StringauthType)throwsCertificateException{ } publicvoidcheckServerTrusted(X509Certificate[]chain,StringauthType)throwsCertificateException{ } publicX509Certificate[]getAcceptedIssuers(){ returnnull; } }
证书管理器的作用就是信任指定所有证书。
2:创建通用https请求
通用https请求应该
1)支持HTTPS请求;
2)支持GET、POST两种方式;
3)支持参数提交,也支持无参数的情况;
packageorg.liufeng.weixin.util; importjava.io.BufferedReader; importjava.io.InputStream; importjava.io.InputStreamReader; importjava.io.OutputStream; importjava.net.ConnectException; importjava.net.URL; importjavax.net.ssl.HttpsURLConnection; importjavax.net.ssl.SSLContext; importjavax.net.ssl.SSLSocketFactory; importjavax.net.ssl.TrustManager; importnet.sf.json.JSONObject; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; /** *公众平台通用接口工具类 * *@authorliuyq *@date2013-08-09 */ publicclassWeixinUtil{ privatestaticLoggerlog=LoggerFactory.getLogger(WeixinUtil.class); /** *发起https请求并获取结果 * *@paramrequestUrl请求地址 *@paramrequestMethod请求方式(GET、POST) *@paramoutputStr提交的数据 *@returnJSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */ publicstaticJSONObjecthttpRequest(StringrequestUrl,StringrequestMethod,StringoutputStr){ JSONObjectjsonObject=null; StringBufferbuffer=newStringBuffer(); try{ //创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[]tm={newMyX509TrustManager()}; SSLContextsslContext=SSLContext.getInstance("SSL","SunJSSE"); sslContext.init(null,tm,newjava.security.SecureRandom()); //从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactoryssf=sslContext.getSocketFactory(); URLurl=newURL(requestUrl); HttpsURLConnectionhttpUrlConn=(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){ OutputStreamoutputStream=httpUrlConn.getOutputStream(); //注意编码格式,防止中文乱码 outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } //将返回的输入流转换成字符串 InputStreaminputStream=httpUrlConn.getInputStream(); InputStreamReaderinputStreamReader=newInputStreamReader(inputStream,"utf-8"); BufferedReaderbufferedReader=newBufferedReader(inputStreamReader); Stringstr=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(ConnectExceptionce){ log.error("Weixinserverconnectiontimedout."); }catch(Exceptione){ log.error("httpsrequesterror:{}",e); } returnjsonObject; } }
四、封装菜单实体类
1:按钮的分类
我们通常会把自定义菜单项看做是按钮,按钮的类型分成click(点击事件)和view(访问网页)。
click类型的按钮有type、name和key3个属性,而view类型的按钮有type、name和url3个属性
2:首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下:
packageorg.liufeng.weixin.pojo; /** *微信通用接口凭证 * *@authorliufeng *@date2013-08-08 */ publicclassAccessToken{ //获取到的凭证 privateStringtoken; //凭证有效时间,单位:秒 privateintexpiresIn; publicStringgetToken(){ returntoken; } publicvoidsetToken(Stringtoken){ this.token=token; } publicintgetExpiresIn(){ returnexpiresIn; } publicvoidsetExpiresIn(intexpiresIn){ 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属性,因此需要定义一个按钮对象基类,所有的按钮对象都需要继承该类。基类的代码如下:
packageorg.liufeng.weixin.pojo; /** *按钮的基类 * *@authorliufeng *@date2013-08-08 */ publicclassButton{ privateStringname; publicStringgetName(){ returnname; } publicvoidsetName(Stringname){ this.name=name; } }
接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下:
packageorg.liufeng.weixin.pojo; /** *普通按钮(子按钮) * *@authorliufeng *@date2013-08-08 */ publicclassCommonButtonextendsButton{ privateStringtype; privateStringkey; publicStringgetType(){ returntype; } publicvoidsetType(Stringtype){ this.type=type; } publicStringgetKey(){ returnkey; } publicvoidsetKey(Stringkey){ this.key=key; } }
对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下:
packageorg.liufeng.weixin.pojo; /** *复杂按钮(父按钮) * *@authorliufeng *@date2013-08-08 */ publicclassComplexButtonextendsButton{ privateButton[]sub_button; publicButton[]getSub_button(){ returnsub_button; } publicvoidsetSub_button(Button[]sub_button){ this.sub_button=sub_button; } }
对整个菜单进行封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项)
packageorg.liufeng.weixin.pojo; /** *菜单 * *@authorliufeng *@date2013-08-08 */ publicclassMenu{ privateButton[]button; publicButton[]getButton(){ returnbutton; } publicvoidsetButton(Button[]button){ this.button=button; } }
这样我们就完成了菜单实体类的封装。
凭证access_token的获取方法
继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于获取接口访问凭证:
//获取access_token的接口地址(GET)限200(次/天) publicfinalstaticStringaccess_token_url="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; /** *获取access_token * *@paramappid凭证 *@paramappsecret密钥 *@return */ publicstaticAccessTokengetAccessToken(Stringappid,Stringappsecret){ AccessTokenaccessToken=null; StringrequestUrl=access_token_url.replace("APPID",appid).replace("APPSECRET",appsecret); JSONObjectjsonObject=httpRequest(requestUrl,"GET",null); //如果请求成功 if(null!=jsonObject){ try{ accessToken=newAccessToken(); accessToken.setToken(jsonObject.getString("access_token")); accessToken.setExpiresIn(jsonObject.getInt("expires_in")); }catch(JSONExceptione){ accessToken=null; //获取token失败 log.error("获取token失败errcode:{}errmsg:{}",jsonObject.getInt("errcode"),jsonObject.getString("errmsg")); } } returnaccessToken; }
自定义菜单的创建方法
继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单:
//菜单创建(POST)限100(次/天) publicstaticStringmenu_create_url="https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"; /** *创建菜单 * *@parammenu菜单实例 *@paramaccessToken有效的access_token *@return0表示成功,其他值表示失败 */ publicstaticintcreateMenu(Menumenu,StringaccessToken){ intresult=0; //拼装创建菜单的url Stringurl=menu_create_url.replace("ACCESS_TOKEN",accessToken); //将菜单对象转换成json字符串 StringjsonMenu=JSONObject.fromObject(menu).toString(); //调用接口创建菜单 JSONObjectjsonObject=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")); } } returnresult; }
调用封装的方法创建自定义菜单
packageorg.liufeng.weixin.main; importorg.liufeng.weixin.pojo.AccessToken; importorg.liufeng.weixin.pojo.Button; importorg.liufeng.weixin.pojo.CommonButton; importorg.liufeng.weixin.pojo.ComplexButton; importorg.liufeng.weixin.pojo.Menu; importorg.liufeng.weixin.util.WeixinUtil; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; /** *菜单管理器类 * *@authorliufeng *@date2013-08-08 */ publicclassMenuManager{ privatestaticLoggerlog=LoggerFactory.getLogger(MenuManager.class); publicstaticvoidmain(String[]args){ //第三方用户唯一凭证 StringappId="000000000000000000"; //第三方用户唯一凭证密钥 StringappSecret="00000000000000000000000000000000"; //调用接口获取access_token AccessTokenat=WeixinUtil.getAccessToken(appId,appSecret); if(null!=at){ //调用接口创建菜单 intresult=WeixinUtil.createMenu(getMenu(),at.getToken()); //判断菜单创建结果 if(0==result) log.info("菜单创建成功!"); else log.info("菜单创建失败,错误码:"+result); } } /** *组装菜单数据 * *@return */ privatestaticMenugetMenu(){ CommonButtonbtn11=newCommonButton(); btn11.setName("天气预报"); btn11.setType("click"); btn11.setKey("11"); CommonButtonbtn12=newCommonButton(); btn12.setName("公交查询"); btn12.setType("click"); btn12.setKey("12"); CommonButtonbtn13=newCommonButton(); btn13.setName("周边搜索"); btn13.setType("click"); btn13.setKey("13"); CommonButtonbtn14=newCommonButton(); btn14.setName("历史上的今天"); btn14.setType("click"); btn14.setKey("14"); CommonButtonbtn21=newCommonButton(); btn21.setName("歌曲点播"); btn21.setType("click"); btn21.setKey("21"); CommonButtonbtn22=newCommonButton(); btn22.setName("经典游戏"); btn22.setType("click"); btn22.setKey("22"); CommonButtonbtn23=newCommonButton(); btn23.setName("美女电台"); btn23.setType("click"); btn23.setKey("23"); CommonButtonbtn24=newCommonButton(); btn24.setName("人脸识别"); btn24.setType("click"); btn24.setKey("24"); CommonButtonbtn25=newCommonButton(); btn25.setName("聊天唠嗑"); btn25.setType("click"); btn25.setKey("25"); CommonButtonbtn31=newCommonButton(); btn31.setName("Q友圈"); btn31.setType("click"); btn31.setKey("31"); CommonButtonbtn32=newCommonButton(); btn32.setName("电影排行榜"); btn32.setType("click"); btn32.setKey("32"); CommonButtonbtn33=newCommonButton(); btn33.setName("幽默笑话"); btn33.setType("click"); btn33.setKey("33"); ComplexButtonmainBtn1=newComplexButton(); mainBtn1.setName("生活助手"); mainBtn1.setSub_button(newCommonButton[]{btn11,btn12,btn13,btn14}); ComplexButtonmainBtn2=newComplexButton(); mainBtn2.setName("休闲驿站"); mainBtn2.setSub_button(newCommonButton[]{btn21,btn22,btn23,btn24,btn25}); ComplexButtonmainBtn3=newComplexButton(); mainBtn3.setName("更多体验"); mainBtn3.setSub_button(newCommonButton[]{btn31,btn32,btn33}); /** *这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项 * *在某个一级菜单下没有二级菜单的情况,menu该如何定义呢? *比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义: *menu.setButton(newButton[]{mainBtn1,mainBtn2,btn33}); */ Menumenu=newMenu(); menu.setButton(newButton[]{mainBtn1,mainBtn2,mainBtn3}); returnmenu; } }
注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。
响应菜单点击事件
packageorg.liufeng.course.service; importjava.util.Date; importjava.util.Map; importjavax.servlet.http.HttpServletRequest; importorg.liufeng.course.message.resp.TextMessage; importorg.liufeng.course.util.MessageUtil; /** *核心服务类 * *@authorliufeng *@date2013-05-20 */ publicclassCoreService{ /** *处理微信发来的请求 * *@paramrequest *@return */ publicstaticStringprocessRequest(HttpServletRequestrequest){ StringrespMessage=null; try{ //默认返回的文本消息内容 StringrespContent="请求处理异常,请稍候尝试!"; //xml请求解析 Map<String,String>requestMap=MessageUtil.parseXml(request); //发送方帐号(open_id) StringfromUserName=requestMap.get("FromUserName"); //公众帐号 StringtoUserName=requestMap.get("ToUserName"); //消息类型 StringmsgType=requestMap.get("MsgType"); //回复文本消息 TextMessagetextMessage=newTextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(newDate().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); //文本消息 if(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)){ respContent="您发送的是文本消息!"; } //图片消息 elseif(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)){ respContent="您发送的是图片消息!"; } //地理位置消息 elseif(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)){ respContent="您发送的是地理位置消息!"; } //链接消息 elseif(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)){ respContent="您发送的是链接消息!"; } //音频消息 elseif(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)){ respContent="您发送的是音频消息!"; } //事件推送 elseif(msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)){ //事件类型 StringeventType=requestMap.get("Event"); //订阅 if(eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)){ respContent="谢谢您的关注!"; } //取消订阅 elseif(eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)){ //TODO取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息 } //自定义菜单点击事件 elseif(eventType.equals(MessageUtil.EVENT_TYPE_CLICK)){ //事件KEY值,与创建自定义菜单时指定的KEY值对应 StringeventKey=requestMap.get("EventKey"); if(eventKey.equals("11")){ respContent="天气预报菜单项被点击!"; }elseif(eventKey.equals("12")){ respContent="公交查询菜单项被点击!"; }elseif(eventKey.equals("13")){ respContent="周边搜索菜单项被点击!"; }elseif(eventKey.equals("14")){ respContent="历史上的今天菜单项被点击!"; }elseif(eventKey.equals("21")){ respContent="歌曲点播菜单项被点击!"; }elseif(eventKey.equals("22")){ respContent="经典游戏菜单项被点击!"; }elseif(eventKey.equals("23")){ respContent="美女电台菜单项被点击!"; }elseif(eventKey.equals("24")){ respContent="人脸识别菜单项被点击!"; }elseif(eventKey.equals("25")){ respContent="聊天唠嗑菜单项被点击!"; }elseif(eventKey.equals("31")){ respContent="Q友圈菜单项被点击!"; }elseif(eventKey.equals("32")){ respContent="电影排行榜菜单项被点击!"; }elseif(eventKey.equals("33")){ respContent="幽默笑话菜单项被点击!"; } } } textMessage.setContent(respContent); respMessage=MessageUtil.textMessageToXml(textMessage); }catch(Exceptione){ e.printStackTrace(); } returnrespMessage; } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。