第5章菜单和事件的应用开发 本章主要介绍自定义菜单和个性化菜单的要求、接口,以及如何通过这些接口实现自定义菜单。 5.1说明 5.1.1自定义菜单的要求 自定义菜单(使用普通自定义菜单创建接口创建的菜单称为默认菜单)能够丰富界面,让用户更好、更快地理解公众号的功能。自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。一级菜单最多5个汉字,二级菜单最多8个汉字,多出来的部分将会以“...”代替。 如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众号后再次关注,则可以看到创建后的新菜单效果。 5.1.2自定义菜单的按钮类型 自定义菜单接口可实现多种类型按钮。按钮类型和用户单击按钮后微信的反应如表51所示。 表51中第4行(scancode_push)到第9行(location_select)(第1列)的所有事件,仅支持微信iPhone 5.4.1以上版本和Android 5.4以上版本的微信用户。第10行和第11行(第1列)是专门给第三方平台旗下未微信认证的订阅号准备的事件类型,它们是没有事件推送的,能力相对受限。 表51按钮类型和用户单击按钮后微信的反应 类型用户单击按钮后微信的反应 click通过消息接口推送消息类型为event的结构 view打开在按钮中填写的网页URL scancode_push将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL) scancode_waitmsg将调起扫一扫工具,完成扫码操作后,回传扫码的结果,同时收起扫一扫工具,然后弹出“消息接收中”提示框 pic_sysphoto将调起系统相机,完成拍照操作后,回传拍摄的相片,同时收起系统相机 pic_photo_or_album文本消息将弹出选择器供用户选择“拍照”或者“从手机相册选择” pic_weixin 将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,同时收起相册 location_select将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具 media_id将开发者填写的永久素材id下发给用户,草稿接口灰度完成后,用article_id代替它 view_limited将打开在按钮中填写的永久素材id对应的图文消息URL,草稿接口灰度完成后,用article_view_limited代替它 5.1.3自定义菜单的接口 创建自定义菜单的接口URL为https://api.weixin.qq.com/cgibin/menu/create?access_token=ACCESS_TOKEN。 可以使用接口查询自定义菜单的结构。在设置个性化菜单后,使用查询接口可以获取默认菜单和全部个性化菜单信息。查询接口的URL为https://api.weixin.qq.com/cgibin/get_current_selfmenu_info?access_token=ACCESS_TOKEN。 创建自定义菜单后,还可以删除当前使用的自定义菜单。在设置个性化菜单后,调用删除接口会删除默认菜单及全部个性化菜单。删除接口的URL为https://api.weixin.qq.com/cgibin/menu/delete?access_token=ACCESS_TOKEN。 5.1.4个性化菜单接口 为了帮助公众号实现灵活的业务运营,微信公众平台提供了个性化菜单接口,通过该接口,可以让公众号的不同用户群体看到不一样的自定义菜单。可以通过用户标签、用户性别、用户手机操作系统、用户客户端设置的地区和语言等条件来设置用户看到的菜单。 创建个性化菜单之前必须先创建默认菜单。个性化菜单的更新是会被覆盖的。例如公众号先后发布了默认菜单、个性化菜单1、个性化菜单2和个性化菜单3。那么当用户进入公众号页面时,将从个性化菜单3开始匹配,如果个性化菜单3匹配成功,则直接返回个性化菜单3,否则继续尝试匹配个性化菜单2,直到成功匹配到一个菜单。创建个性化菜单的接口URL为https://api.weixin.qq.com/cgibin/menu/addconditional?access_token=ACCESS_TOKEN。 5.2自定义菜单的应用开发 5.2.1创建自定义菜单项类 视频讲解 继续在4.3节的基础上进行开发。在包edu.bookcode中创建exofmenu子包,并在包edu.bookcode.exofmenu中创建menu子包,在包edu.bookcode.exofmenu.menu中创建类Button,代码如例51所示。 【例51】类Button的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class Button { private String name; } 在包edu.bookcode.exofmenu.menu中创建类ClickButton,代码如例52所示。 【例52】类ClickButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class ClickButton extends Button { private String type; private String key; } 在包edu.bookcode.exofmenu.menu中创建类ViewButton,代码如例53所示。 【例53】类ViewButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class ViewButton extends Button { private String type; private String url; } 在包edu.bookcode.exofmenu.menu中创建类ScancodeButton,代码如例54所示。 【例54】类ScancodeButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class ScancodeButton extends Button { private String type; private String key; } 在包edu.bookcode.exofmenu.menu中创建类PicButton,代码如例55所示。 【例55】类PicButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class PicButton extends Button { private String type; private String key; } 在包edu.bookcode.exofmenu.menu中创建类LocationButton,代码如例56所示。 【例56】类LocationButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class LocationButton extends Button { private String type; private String key; } 在包edu.bookcode.exofmenu.menu中创建类ComplexButton,代码如例57所示。 【例57】类ComplexButton的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class ComplexButton extends Button { private Button[] sub_button; } 在包edu.bookcode.exofmenu.menu中创建类Menu,代码如例58所示。 【例58】类Menu的代码示例。 package edu.bookcode.exofmenu.menu; import lombok.Data; @Data public class Menu { private Button[] button; } 5.2.2创建类TextMessageToXML 在包edu.bookcode.exofmenu中创建util子包,在包edu.bookcode.exofmenu.util中创建类TextMessageToXML,代码如例59所示。 【例59】类TextMessageToXML的代码示例。 package edu.bookcode.exofmenu.util; //导入前面的类,也可以将前面的类复制到包edu.bookcode.exofmenu中 import edu.bookcode.exofmessage.message.resp.Article; import edu.bookcode.exofmessage.message.resp.NewsMessage; import edu.bookcode.exofmessage.message.resp.TextMessage; import java.util.Date; import java.util.List; import java.util.Map; public class TextMessageToXML { public static String messageToXML(Map<String, String> message,String content){ TextMessage textMessage=new TextMessage(); textMessage.setToUserName(message.get("ToUserName")); textMessage.setFromUserName(message.get("FromUserName")); textMessage.setCreateTime(new Date().getTime()); textMessage.setContent(content); textMessage.setMsgType("text"); String xml= textMessageToXML(textMessage); return xml; } private static String textMessageToXML(TextMessage textMessage) { String xml= "<xml>" + "<ToUserName>"+textMessage.getFromUserName()+"</ToUserName>" + "<FromUserName>"+textMessage.getToUserName()+ "</FromUserName>" + "<CreateTime>"+textMessage.getCreateTime()+"</CreateTime>" + "<MsgType>text</MsgType>" + "<Content>"+textMessage.getContent()+"</Content>"+ "</xml>"; return xml; } public static String newsToXML(Map<String, String> message, List<Article> articleList ) { NewsMessage newsMessage = new NewsMessage(); newsMessage.setToUserName(message.get("ToUserName")); newsMessage.setFromUserName(message.get("FromUserName")); newsMessage.setCreateTime(new Date().getTime()); newsMessage.setMsgType("news"); newsMessage.setArticleCount(articleList.size()); newsMessage.setArticles(articleList); String xml= newsMessageToXML(newsMessage); return xml; } private static String newsMessageToXML(NewsMessage newsMessage) { String xml= "<xml>" + "<ToUserName>"+newsMessage.getFromUserName()+"</ToUserName>" + "<FromUserName>"+newsMessage.getToUserName()+ "</FromUserName>" + "<CreateTime>"+newsMessage.getCreateTime()+"</CreateTime>" + "<MsgType>news</MsgType>" + "<ArticleCount>1</ArticleCount>" + "<Articles>" + "<item>" + "<Title>"+newsMessage.getArticles().get(0).getTitle()+"</Title>"+ "<Description>"+newsMessage.getArticles().get(0).getDescription()+ "</Description>"+ "<PicUrl>"+newsMessage.getArticles().get(0).getPicUrl()+ "</PicUrl>" + "<Url>"+newsMessage.getArticles().get(0).getUrl()+"</Url>" + "</item>" + "</Articles>" + "</xml>"; return xml; } public static String processScanPush(String fromUserName,String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>scancode_push</Event>"+ "<EventKey>rselfmenu22</EventKey>"+ "<ScanCodeInfo>" + "<ScanType>qrcode</ScanType>" + "<ScanResult>1</ScanResult>" + "</ScanCodeInfo>"+ "</xml>"; return xml; } public static String processScanWaitMsg(String fromUserName, String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>scancode_waitmsg</Event>"+ "<EventKey>rselfmenu21</EventKey>"+ "<ScanCodeInfo>" + "<ScanType>qrcode</ScanType>" + "<ScanResult>1</ScanResult>" + "</ScanCodeInfo>"+ "</xml>"; return xml; } public static String processSysphoto(String fromUserName, String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>pic_sysphoto</Event>"+ "<EventKey>6</EventKey>"+ "<SendPicsInfo><Count>1</Count>"+ "<PicList><item><PicMd5Sum>1b5f7c23b5bf75682a53e7b6d163e185" + "</PicMd5Sum>\n" + "</item>\n" + "</PicList>\n" + "</SendPicsInfo>"+ "</xml>"; return xml; } public static String processPhotoOrAlbum(String fromUserName, String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>pic_photo_or_album</Event>"+ "<EventKey>rselfmenu24</EventKey>"+ "<SendPicsInfo><Count>1</Count>"+ "<PicList><item><PicMd5Sum>5a75aaca956d97be686719218f275c6b" + "</PicMd5Sum>\n" + "</item>\n" + "</PicList>\n" + "</SendPicsInfo>"+ "</xml>"; return xml; } public static String processPicWeiXin(String fromUserName, String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>pic_weixin</Event>"+ "<EventKey>rselfmenu25</EventKey>"+ "<SendPicsInfo><Count>1</Count>"+ "<PicList><item><PicMd5Sum>5a75aaca956d97be686719218f275c6b" + "</PicMd5Sum>\n" + "</item>\n" + "</PicList>\n" + "</SendPicsInfo>"+ "</xml>"; return xml; } public static String processLocation(String fromUserName, String toUserName) { String xml="<xml>"+ "<ToUserName>"+fromUserName+"</ToUserName>" + "<FromUserName>"+toUserName+ "</FromUserName>" + "<CreateTime>"+new Date().getTime()+"</CreateTime>" + "<MsgType>event</MsgType>" + "<Event>location_select</Event>"+ "<EventKey>rselfmenu26</EventKey>"+ "<SendLocationInfo><Location_X>23></Location_X>\n" + "<Location_Y>113</Location_Y>\n" + "<Scale>15</Scale>\n" + "<Label>广州市海珠区客村艺苑路 106号</Label>\n" + "<Poiname></Poiname>\n" + "</SendLocationInfo>"+ "</xml>"; return xml; } } 5.2.3创建类MenuUtil 在包edu.bookcode.exofmenu.util中创建类MenuUtil,代码如例510所示。 【例510】类MenuUtil的代码示例。 package edu.bookcode.exofmenu.util; import edu.bookcode.exofmenu.menu.Menu; import edu.bookcode.service.CommonUtil;//导入 import net.sf.json.JSONObject; public class MenuUtil { //创建、查询、删除相关API的URL public final static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"; public final static String menu_get_url = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN"; public final static String menu_delete_url = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN"; //创建自定义菜单 public static boolean createMenu(Menu menu, String accessToken) { boolean result = false; String url = menu_create_url.replace("ACCESS_TOKEN", accessToken); String jsonMenu = JSONObject.fromObject(menu).toString(); JSONObject jsonObject = CommonUtil.httpsRequest(url, "POST", jsonMenu); if (null != jsonObject) { int errorCode = jsonObject.getInt("errcode"); if (0 == errorCode) { result = true; } else { result = false; System.out.println("errcode:{"+jsonObject.getInt("errcode")+"},errmsg:{"+jsonObject.getString("errmsg")+"}"); } } return result; } //查询菜单信息 public static String getMenu(String accessToken) { String result = null; String requestUrl = menu_get_url.replace("ACCESS_TOKEN", accessToken); JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null); if (null != jsonObject) { result = jsonObject.toString(); } return result; } //删除菜单 public static boolean deleteMenu(String accessToken) { boolean result = false; String requestUrl = menu_delete_url.replace("ACCESS_TOKEN", accessToken); JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null); if (null != jsonObject) { int errorCode = jsonObject.getInt("errcode"); if (0 == errorCode) { result = true; } else { result = false; System.out.println("errcode:{"+jsonObject.getInt("errcode")+"},errmsg:{"+jsonObject.getString("errmsg")+"}"); } } return result; } } 5.2.4创建类ButtonMenuService 在包edu.bookcode.exofmenu中创建service子包,在包edu.bookcode.exofmenu.service中创建类ButtonMenuService,代码如例511所示。 【例511】类ButtonMenuService的代码示例。 package edu.bookcode.exofmenu.service; import java.util.*; import javax.servlet.http.HttpServletRequest; import edu.bookcode.exofmenu.util.TextMessageToXML; //导入前面的类,也可以将前面的类复制到包edu.bookcode.exofmenu中 import edu.bookcode.exofmessage.message.resp.Article; import edu.bookcode.exofmessage.util.MessageUtil; public class ButtonMenuService { public static String processRequest(HttpServletRequest request) { String xml; try { Map<String, String> requestMap = MessageUtil.parseXml(request); String msgType = requestMap.get("MsgType"); String fromUserName = requestMap.get("FromUserName"); String toUserName = requestMap.get("ToUserName"); String content=""; String eventKey; if (msgType.equals("event")) { String eventType = requestMap.get("Event").toLowerCase(); switch (eventType) { case "subscribe": content="谢谢您的关注!"; break; case "unsubscribe": //取消关注后,用户不会再收到公众号发送的消息,因此不需要回复 break; //扫码后会开始接收推送消息(官方文档中将其简称为扫码堆事件)的 //事件推送 case "scancode_push": xml = TextMessageToXML.processScanPush(fromUserName,toUserName); return xml; //扫码带提示的事件推送 case "scancode_waitmsg": xml = TextMessageToXML.processScanWaitMsg(fromUserName,toUserName); return xml; case "pic_sysphoto": xml = TextMessageToXML.processSysphoto(fromUserName,toUserName); return xml; case "pic_photo_or_album": xml = TextMessageToXML.processPhotoOrAlbum(fromUserName,toUserName); return xml; case "pic_weixin": xml = TextMessageToXML.processPicWeiXin(fromUserName,toUserName); return xml; //此例省略了location_select等情形,读者可以参考完成 case "click": eventKey = requestMap.get("EventKey"); //根据key值判断用户单击的按钮 if (eventKey.equals("QQ")) { Article article = new Article(); article.setTitle("QQ的联系方式"); article.setDescription("QQ不一定能及时回复。"); article.setPicUrl(""); article.setUrl("http://qq.com"); List<Article> articleList = new ArrayList<Article>(); articleList.add(article); //创建图文消息 xml=TextMessageToXML.newsToXML(requestMap,articleList); return xml; } else if (eventKey.equals("WeiXin")) { content="微信号:jsnuws"; } else if (eventKey.equals("Phone")) { content="手机号:12345678901"; } else if (eventKey.equals("Email")) { content="邮箱:6780912345@qq.com"; } break; default: break; } } xml = TextMessageToXML.messageToXML(requestMap,content); return xml; } catch (Exception e) { e.printStackTrace(); } return "error"; } } 5.2.5创建类MenuInit 在包edu.bookcode.exofmenu中创建类MenuInit,该类主要定义了菜单信息,代码如例512所示。 【例512】类MenuInit的代码示例。 package edu.bookcode.exofmenu; import edu.bookcode.exofmenu.menu.*; import edu.bookcode.exofmenu.util.MenuUtil; import edu.bookcode.service.TemptTokenUtil; public class MenuInit { //注意菜单项层级、子项、命名等规定的限制 publicstatic Menu getMenu() { //第1列子菜单中第1项子菜单项 ViewButton btn11 = new ViewButton(); btn11.setName("微信小程序开发基础"); btn11.setType("view"); btn11.setUrl("https://item.jd.com/10026528815782.html"); //第1列子菜单中第2项子菜单项 ViewButton btn12= new ViewButton(); btn12.setName("微信小程序云开发"); btn12.setType("view"); btn12.setUrl("https://item.jd.com/12958844.html"); ViewButton btn13 = new ViewButton(); btn13.setName("Spring Boot区块链应用开发入门"); btn13.setType("view"); btn13.setUrl("https://item.jd.com/12735489.html"); ViewButton btn14 = new ViewButton(); btn14.setName("Spring Boot开发实战"); btn14.setType("view"); btn14.setUrl("https://item.jd.com/10026542588356.html"); ViewButton btn15 = new ViewButton(); btn15.setName("Spring Cloud 微服务开发实战"); btn15.setType("view"); btn15.setUrl("https://item.jd.com/10026550550811.html"); ScancodeButton btn21 = new ScancodeButton(); btn21.setName("扫码带提示"); btn21.setType("scancode_waitmsg"); btn21.setKey("rselfmenu21"); ScancodeButton btn22 = new ScancodeButton(); btn22.setName("扫码推事件"); btn22.setType("scancode_push"); btn22.setKey("rselfmenu22"); PicButton btn23 = new PicButton(); btn23.setName("系统拍照发图"); btn23.setType("pic_sysphoto"); btn23.setKey("rselfmenu23"); PicButton btn24 = new PicButton(); btn24.setName("拍照或者相册发图"); btn24.setType("pic_photo_or_album"); btn24.setKey("rselfmenu24"); PicButton btn25 = new PicButton(); btn25.setName("微信相册发图"); btn25.setType("pic_weixin"); btn25.setKey("rselfmenu25"); ClickButton btn31 = new ClickButton(); btn31.setName("QQ"); btn31.setType("click"); btn31.setKey("QQ"); ClickButton btn32 = new ClickButton(); btn32.setName("WeiXin"); btn32.setType("click"); btn32.setKey("WeiXin"); ClickButton btn33 = new ClickButton(); btn33.setName("Phone"); btn33.setType("click"); btn33.setKey("Phone"); ClickButton btn34 = new ClickButton(); btn34.setName("Email"); btn34.setType("click"); btn34.setKey("Email"); //第1列子菜单 ComplexButton mainBtn1 = new ComplexButton(); mainBtn1.setName("图书"); mainBtn1.setSub_button(new Button[] { btn11, btn12, btn13, btn14, btn15}); ComplexButton mainBtn2 = new ComplexButton(); mainBtn2.setName("扫码和发图"); mainBtn2.setSub_button(new Button[] { btn21, btn22, btn23, btn24, btn25 }); ComplexButton mainBtn3 = new ComplexButton(); mainBtn3.setName("联系方式"); mainBtn3.setSub_button(new Button[] { btn31, btn32 , btn33, btn34 }); Menu menu = new Menu(); menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 }); return menu; } //使用main方法是为了简化测试的需要,实际开发时可以自动创建菜单 public static void main(String[] args) { boolean result = MenuUtil.createMenu(getMenu(),new TemptTokenUtil().getTokenInfo()); if (result) System.out.println("菜单创建成功!"); else System.out.println("菜单创建失败!"); } } 5.2.6创建类ExOfMenuController 在包edu.bookcode.exofmenu中创建controller子包,在包edu.bookcode.exofmenu.controller中创建类ExOfMenuController,代码如例513所示。 【例513】类ExOfMenuController的代码示例。 package edu.bookcode.exofmenu.controller; import edu.bookcode.exofmenu.MenuInit; import edu.bookcode.exofmenu.service.ButtonMenuService; import edu.bookcode.exofmenu.util.MenuUtil; //导入前面的类 import edu.bookcode.service.TemptTokenUtil; import edu.bookcode.util.OutAndSendUtil; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController public class ExOfMenuController { //下面一行是运行本类时的相对地址 @RequestMapping("/") //为了测试方便,在运行其他类时,必须注释掉上一行代码,即修改相对地址 //并可以去掉下一行代码的注释,修改本类的相对地址 //@RequestMapping("/testMenu") public void testMenu(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); String respXml = ButtonMenuService.processRequest(request); String xml= ""; if(respXml.contains("<Content></Content>")) { xml=respXml.replace("<Content></Content>","<Content>您好,有什么可以帮到您?</Content>"); System.out.println(xml); } else { xml =respXml; System.out.println(xml); } OutAndSendUtil.sendMessageToWXAppClient(xml,response); //首次注释下一行,运行程序 //menuProcess(); //第2次、第3次取消注释,再运行程序 //第4次取消下面两行注释 //System.out.println("重新增加菜单后:"+new MyMenuExDemo().getSpecialMenuJson("2","Java")); } //对菜单的创建、查询和删除 private void menuProcess() { String accessToken= new TemptTokenUtil().getTokenInfo(); //显示菜单信息 System.out.println("菜单信息:"+MenuUtil.getMenu(accessToken)); //显示菜单删除,菜单为空 System.out.println("删除菜单后:"+MenuUtil.deleteMenu(accessToken)); //第2次运行时,注释掉下一行的语句,可以观察到删除菜单后菜单为空 System.out.println("重新增加菜单后:"+MenuUtil.createMenu(MenuInit.getMenu(),accessToken)); //第3次运行时,启用上一行的语句,可以观察到菜单再次出现 } } 5.2.7运行程序 启动内网穿透工具后,按照例446中注释给出的提示修改ExOfMessageController的相对地址,并在IDEA中先运行类MenuInit再运行项目入口类WxgzptkfbookApplication。 手机微信公众号中第1级菜单如图51所示,第2级菜单第1列如图52所示,第2级菜单第2列如图53所示,第2级菜单第3列如图54所示。单击图52中“Spring Cloud 微服务开发实战”菜单项,跳转到对应网址的图书页面,如图55所示。单击图53中“拍照或者相册发图”菜单项,结果如图56所示。依次单击图54中QQ、WeiXin菜单项,结果如图57所示。在图56中选择“拍照”或“从相册选择”后发送图片,结果如图58所示。 图51第1级菜单在手机微信公众号中的输出(底部) 图52第2级菜单第1列(图书)在手机微信公众号中的输出 图53第2级菜单第2列(扫码和发图)在手机微信公众号中的输出 图54第2级菜单第3列(联系方式)在手机微信公众号中的输出 图55单击图52中“Spring Cloud 微服务开发实战”菜单项后跳转到对应网址的图书页面 图56单击图53中“拍照或者相册发图”菜单项的结果 图57依次单击图54中QQ、WeiXin菜单项的结果 图58在图56中选择“拍照”或“从相册选择”后发送图片的结果(部分) 按照例513所示修改类ExOfMenuController的代码(修改注释,详细操作可以参考视频讲解),可以得到菜单信息在控制台的输出结果如图59所示,删除菜单之后控制台的输出结果如图510所示,增加菜单之后控制台的输出结果如图511所示。单击图54中WeiXin菜单项后控制台的输出结果如图512所示。 图59菜单信息在控制台的输出结果(部分) 图510删除菜单之后控制台的输出结果(部分) 图511增加菜单之后控制台的输出结果(部分) 图512单击图54中WeiXin菜单项后控制台的输出结果 习题5 简答题 1. 简述对自定义菜单要求的理解。 2. 简述对自定义菜单按钮类型的理解。 实验题 1. 实现示例: 自定义菜单的应用开发。 2. 用自定义菜单独立完成一个实例。