微信公众平台开发教程(五)详解自定义菜单


Posted in Javascript onDecember 02, 2016

一、概述:

如果只有输入框,可能太简单,感觉像命令行。自定义菜单,给我们提供了很大的灵活性,更符合用户的操作习惯。在一个小小的微信对话页面,可以实现更多的功能。菜单直观明了,不仅能提供事件响应,还支持URL跳转,如果需要的功能比较复杂,我们大可以使用URL跳转,跳转至我们的网页即可。

注意:自定义菜单,只有服务号才有此功能

接着我们详细介绍,如何实现自定义菜单?

二、详细步骤:

1、首先获取access_token

access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。正常情况下access_token有效期为7200秒,重复获取将导致上次获取的access_token失效。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在开发模式中获得(需要已经成为开发者,且帐号没有异常状态)。注意调用所有微信接口时均需使用https协议。

接口调用请求说明

http请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,既appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}
参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

{"errcode":40013,"errmsg":"invalid appid"}

2、创建自定义菜单

自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。开启自定义菜单后,公众号界面如图所示:

微信公众平台开发教程(五)详解自定义菜单

目前自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。请注意,创建自定义菜单后,由于微信客户端缓存,需要24小时微信客户端才会展现出来。建议测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

目前自定义菜单接口可实现两种类型按钮,如下:

click:

用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;

view:

用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的url值 (即网页链接),达到打开网页的目的,建议与网页授权获取用户基本信息接口结合,获得用户的登入个人信息。

接口调用请求说明

http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

请求示例

{
  "button":[
  { 
   "type":"click",
   "name":"今日歌曲",
   "key":"V1001_TODAY_MUSIC"
  },
  {
   "type":"click",
   "name":"歌手简介",
   "key":"V1001_TODAY_SINGER"
  },
  {
   "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"
   }]
  }]
 }

参数说明

参数 是否必须 说明
button 一级菜单数组,个数应为1~3个
sub_button 二级菜单数组,个数应为1~5个
type 菜单的响应动作类型,目前有click、view两种类型
name 菜单标题,不超过16个字节,子菜单不超过40个字节
key click类型必须 菜单KEY值,用于消息接口推送,不超过128字节
url view类型必须 网页链接,用户点击菜单可打开链接,不超过256字节

 返回结果

正确时的返回JSON数据包如下:

{"errcode":0,"errmsg":"ok"}

错误时的返回JSON数据包如下(示例为无效菜单名长度):

{"errcode":40018,"errmsg":"invalid button name size"}

3、查询菜单

使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。

请求说明

http请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN

返回说明

对应创建接口,正确的Json返回结果:

{"menu":{"button":[{"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]},{"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]},{"name":"菜单","sub_button":[{"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]},{"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]},{"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}]}]}}

4、删除菜单

使用接口创建自定义菜单后,开发者还可使用接口删除当前使用的自定义菜单。

请求说明

http请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN

返回说明
对应创建接口,正确的Json返回结果:

{"errcode":0,"errmsg":"ok"}

5、事件处理

用户点击自定义菜单后,如果菜单按钮设置为click类型,则微信会把此次点击事件推送给开发者,注意view类型(跳转到URL)的菜单点击不会上报。

推送XML数据包示例:

<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[CLICK]]></Event>
<EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>

参数说明:

参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,CLICK
EventKey 事件KEY值,与自定义菜单接口中KEY值对应

三、实例讲解

还接着上一篇文章讲。微信公众账号开发教程(三) 实例入门:机器人(附源码)

我们将在上一篇文章基础上,添加自定义菜单功能。

1、获取access_token

首先需要得到AppId和AppSecret

当你成为开发者后,自然能够在,开发者模式,便可看到这两个值,可以重置。

然后调用按照二.1中所示,进行操作。

注意:access_token有过期时间,如果过期,需要重新获取。

代码如下:

private static DateTime GetAccessToken_Time;
  /// <summary>
  /// 过期时间为7200秒
  /// </summary>
  private static int Expires_Period = 7200;
  /// <summary>
  /// 
  /// </summary>
  private static string mAccessToken;
  /// <summary>
  /// 
  /// </summary>
  public static string AccessToken
  {
   get
   {
    //如果为空,或者过期,需要重新获取
    if (string.IsNullOrEmpty(mAccessToken) || HasExpired())
    {
     //获取
     mAccessToken = GetAccessToken(AppID, AppSecret);
    }

    return mAccessToken;
   }
  }
  /// <summary>
  /// 
  /// </summary>
  /// <param name="appId"></param>
  /// <param name="appSecret"></param>
  /// <returns></returns>
  private static string GetAccessToken(string appId, string appSecret)
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", appId, appSecret);
   string result = HttpUtility.GetData(url);

   XDocument doc = XmlUtility.ParseJson(result, "root");
   XElement root = doc.Root;
   if (root != null)
   {
    XElement access_token = root.Element("access_token");
    if (access_token != null)
    {
     GetAccessToken_Time = DateTime.Now;
     if (root.Element("expires_in")!=null)
     {
      Expires_Period = int.Parse(root.Element("expires_in").Value);
     }
     return access_token.Value;
    }
    else
    {
     GetAccessToken_Time = DateTime.MinValue;
    }
   }

   return null;
  }
  /// <summary>
  /// 判断Access_token是否过期
  /// </summary>
  /// <returns>bool</returns>
  private static bool HasExpired()
  {
   if (GetAccessToken_Time != null)
   {
    //过期时间,允许有一定的误差,一分钟。获取时间消耗
    if (DateTime.Now > GetAccessToken_Time.AddSeconds(Expires_Period).AddSeconds(-60))
    {
     return true;
    }
   }
   return false;
  }

2、设置菜单

菜单需根据需要,按照实际要求进行设定。

这里我们提供天气查询功能,将常用的城市列出来,点击即可查询。

然后还提供了友情链接,这里提供了view类型的菜单,直接可以跳转至URL页面,为跳转做个好的演示。

具体菜单如下:

{
 "button": [
  {
   "name": "链接", 
   "sub_button": [
    {
     "type": "view", 
     "name": "搜索", 
     "url": "http://www.baidu.com/"
    }, 
    {
     "type": "view", 
     "name": "视频", 
     "url": "http://v.qq.com/"
    }, 
    {
     "type": "click", 
     "name": "赞一下我们", 
     "key": "BTN_GOOD"
    }
   ]
  }, 
  {
   "name": "查询天气", 
   "sub_button": [
    {
     "type": "click", 
     "name": "武汉", 
     "key": "BTN_TQ_WUHAN"
    }, 
    {
     "type": "click", 
     "name": "上海", 
     "key": "BTN_TQ_SHANGHAI"
    }, 
    {
     "type": "click", 
     "name": "北京", 
     "key": "BTN_TQ_BEIJING"
    }
   ]
  }, 
  {
   "type": "click", 
   "name": "帮助", 
   "key": "BTN_HELP"
  }
 ]
}

3、管理菜单

因为菜单的变更没有那么频繁,因此通过txt文件来设置菜单,并通过管理界面来管理菜单。

主要的管理功能:

1)从文件加载菜单

2)创建菜单。即将菜单通知微信服务端,并更新至微信客户端

3)查询菜单。获取当前系统的菜单。

4)删除菜单。从微信服务器删除菜单,也可以删除后再创建。

实现代码如下:

public class MenuManager
 {
  /// <summary>
  /// 菜单文件路径
  /// </summary>
  private static readonly string Menu_Data_Path = System.AppDomain.CurrentDomain.BaseDirectory + "/Data/menu.txt";
  /// <summary>
  /// 获取菜单
  /// </summary>
  public static string GetMenu()
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}", Context.AccessToken);

   return HttpUtility.GetData(url);
  }
  /// <summary>
  /// 创建菜单
  /// </summary>
  public static void CreateMenu(string menu)
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}", Context.AccessToken);
   //string menu = FileUtility.Read(Menu_Data_Path);
   HttpUtility.SendHttpRequest(url, menu);
  }
  /// <summary>
  /// 删除菜单
  /// </summary>
  public static void DeleteMenu()
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}", Context.AccessToken);
   HttpUtility.GetData(url);
  }
  /// <summary>
  /// 加载菜单
  /// </summary>
  /// <returns></returns>
  public static string LoadMenu()
  {
   return FileUtility.Read(Menu_Data_Path);
  }
 }

 4、基本方法

上面的代码,其实我们对一些公共功能做了封装。如进行get请求、POST提交等操作,读取文件等。

这里我们提供进行get、Post提交的方法案例代码,如果使用,建议优化。

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace Yank.WeiXin.Robot.Utility
{
 /// <summary>
 /// Http帮助类
 /// </summary>
 class HttpUtility
 {
  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="data">数据</param>
  public static string SendHttpRequest(string url, string data)
  {
   return SendPostHttpRequest(url,"application/x-www-form-urlencoded",data); 
  }
  /// <summary>
  /// 
  /// </summary>
  /// <param name="url"></param>
  /// <returns></returns>
  public static string GetData(string url)
  {
   return SendGetHttpRequest(url,"application/x-www-form-urlencoded"); 
  }

  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="method">方法(post或get)</param>
  /// <param name="method">数据类型</param>
  /// <param name="requestData">数据</param>
  public static string SendPostHttpRequest(string url,string contentType,string requestData)
  {
   WebRequest request = (WebRequest)HttpWebRequest.Create(url);
   request.Method = "POST";
   byte[] postBytes = null;
   request.ContentType = contentType;
   postBytes = Encoding.UTF8.GetBytes(requestData);
   request.ContentLength = postBytes.Length;
   using (Stream outstream = request.GetRequestStream())
   {
    outstream.Write(postBytes, 0, postBytes.Length);
   }
   string result = string.Empty;
   using (WebResponse response = request.GetResponse())
   {
    if (response != null)
    {     
     using (Stream stream = response.GetResponseStream())
     {
      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
      {
       result = reader.ReadToEnd();
      }
     }
     
    }
   }
   return result;
  }

  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="method">方法(post或get)</param>
  /// <param name="method">数据类型</param>
  /// <param name="requestData">数据</param>
  public static string SendGetHttpRequest(string url, string contentType)
  {
   WebRequest request = (WebRequest)HttpWebRequest.Create(url);
   request.Method = "GET";
   request.ContentType = contentType;
   string result = string.Empty;
   using (WebResponse response = request.GetResponse())
   {
    if (response != null)
    {
     using (Stream stream = response.GetResponseStream())
     {
      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
      {
       result = reader.ReadToEnd();
      }
     }
    }
   }
   return result;
  }
 }
}
using System;
using System.Xml.Linq;
using Newtonsoft.Json;

namespace Yank.WeiXin.Robot.Utility
{
 class XmlUtility
 {
  /// <summary>
  /// 
  /// </summary>
  /// <param name="json"></param>
  /// <param name="rootName"></param>
  /// <returns></returns>
  public static XDocument ParseJson(string json,string rootName)
  {
   return JsonConvert.DeserializeXNode(json, rootName);
  }
 }
}

5、事件处理

设置了菜单,这下需要处理事件了。跟我们之前设计ASPX或者WinForm一样,都要绑定按钮的事件。这里只是通过XML消息将请求传递过来。

通过“2、设置菜单"中具体的菜单内容,我们便已经知道需要进行哪些事件处理了。对于按钮类型为view的,无须处理,它会自动跳转至指定url.

需要处理的点击事件:

1)赞一下

2)查询某城市的天气,北京、上海、武汉

3)帮助

这个还要沿用上章中的事件处理器EventHandler来扩展处理。

具体的实现代码吧:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Yank.WeiXin.Robot.Messages;

namespace Yank.WeiXin.Robot.Handlers
{
 class EventHandler : IHandler
 {
  /// <summary>
  /// 请求的xml
  /// </summary>
  private string RequestXml { get; set; }
  /// <summary>
  /// 构造函数
  /// </summary>
  /// <param name="requestXml"></param>
  public EventHandler(string requestXml)
  {
   this.RequestXml = requestXml;
  }
  /// <summary>
  /// 处理请求
  /// </summary>
  /// <returns></returns>
  public string HandleRequest()
  {
   string response = string.Empty;
   EventMessage em = EventMessage.LoadFromXml(RequestXml);
   if (em != null)
   {
    switch (em.Event.ToLower())
    {
     case ("subscribe"):
      response = SubscribeEventHandler(em);
      break;
     case "click":
      response = ClickEventHandler(em);
      break;
    }
   }

   return response;
  }
  /// <summary>
  /// 关注
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string SubscribeEventHandler(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = "欢迎您关注***,我是大哥大,有事就问我,呵呵!\n\n";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 处理点击事件
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string ClickEventHandler(EventMessage em)
  {
   string result = string.Empty;
   if (em != null && em.EventKey != null)
   {
    switch (em.EventKey.ToUpper())
    {
     case "BTN_GOOD":
      result = btnGoodClick(em);
      break;
     case "BTN_TQ_BEIJING":
      result = searchWeather("beijing", em);
      break;
     case "BTN_TQ_SHANGHAI":
      result = searchWeather("shanghai", em);
      break;
     case "BTN_TQ_WUHAN":
      result = searchWeather("wuhai", em);
      break;
     case "BTN_HELP":
      result = btnHelpClick(em);
      break;
    }
   }

   return result;
  }
  /// <summary>
  /// 赞一下
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string btnGoodClick(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = @"谢谢您的支持!";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 帮助
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string btnHelpClick(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = @"查询天气,输入tq 城市名称\拼音\首字母";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 查询天气
  /// </summary>
  /// <param name="cityName"></param>
  /// <param name="em"></param>
  /// <returns></returns>
  private string searchWeather(string cityName, EventMessage em)
  {
   TextMessage tm = new TextMessage();
   tm.Content = WeatherHelper.GetWeather(cityName);
   //进行发送者、接收者转换
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   return tm.GenerateContent();
  }
 }
}

6、效果图

终于大工告成,最后来看下效果图吧

微信公众平台开发教程(五)详解自定义菜单

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
用js计算页面执行时间的函数
Dec 07 Javascript
做网页的一些技巧
Feb 01 Javascript
编写高性能的JavaScript 脚本的加载与执行
Apr 19 Javascript
jQuery select操作控制方法小结
May 26 Javascript
微信企业号开发之微信考勤Cookies的使用
Sep 11 Javascript
JavaScript生成验证码并实现验证功能
Sep 24 Javascript
详解ES6中的let命令
Apr 05 Javascript
JavaScript实现格式化字符串函数String.format
Dec 16 Javascript
Angular实现图片裁剪工具ngImgCrop实践
Aug 17 Javascript
微信小程序实现动态设置placeholder提示文字及按钮选中/取消状态的方法
Dec 14 Javascript
详解JavaScript栈内存与堆内存
Apr 04 Javascript
谈谈node.js中的模块系统
Sep 01 Javascript
基于jQuery实现滚动切换效果
Dec 02 #Javascript
遍历json 对象的属性并且动态添加属性的实现
Dec 02 #Javascript
Vue.js组件tree实现无限级树形菜单
Dec 02 #Javascript
js中的eval()函数把含有转义字符的字符串转换成Object对象的方法
Dec 02 #Javascript
Vue.js第四天学习笔记
Dec 02 #Javascript
微信公众平台开发教程(四) 实例入门:机器人回复(附源码)
Dec 02 #Javascript
基于javascript实现按圆形排列DIV元素(三)
Dec 02 #Javascript
You might like
打造计数器DIY三步曲(上)
2006/10/09 PHP
一个可以找出源代码中所有中文的工具
2006/10/25 PHP
php实例分享之mysql数据备份
2014/05/19 PHP
php实现读取手机客户端浏览器的类
2015/01/09 PHP
php生成固定长度纯数字编码的方法
2015/07/09 PHP
PHP实现蛇形矩阵,回环矩阵及数字螺旋矩阵的方法分析
2017/05/29 PHP
PHP检查文件是否存在,不存在自动创建及读取文件内容操作示例
2020/01/23 PHP
jQuery TextBox自动完成条
2009/07/22 Javascript
JavaScript中的运算符种类及其规则介绍
2013/09/26 Javascript
js中小数转换整数的方法
2014/01/26 Javascript
jquery学习总结(超级详细)
2014/09/04 Javascript
AJAX实现瀑布流触发分页与分页触发瀑布流的方法
2016/05/23 Javascript
微信小程序链接传参并跳转新页面
2016/11/29 Javascript
Angular.JS中指令ng-if的注意事项小结
2017/06/21 Javascript
通过JS运行机制的角度说说作用域
2019/03/12 Javascript
解决layui 三级联动下拉框更新时回显的问题
2019/09/03 Javascript
使用flow来规范javascript的变量类型
2019/09/12 Javascript
JavaScript实现捕获鼠标坐标
2020/04/12 Javascript
[01:29:17]RNG vs Liquid 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.23
2019/09/05 DOTA
Python循环语句之break与continue的用法
2015/10/14 Python
用 Python 爬了爬自己的微信朋友(实例讲解)
2017/08/25 Python
numpy自动生成数组详解
2017/12/15 Python
Python中 传递值 和 传递引用 的区别解析
2018/02/22 Python
python pyheatmap包绘制热力图
2018/11/09 Python
Python Pandas 转换unix时间戳方式
2019/12/07 Python
python如何使用Redis构建分布式锁
2020/01/16 Python
详解python metaclass(元类)
2020/08/13 Python
Python wordcloud库安装方法总结
2020/12/31 Python
解决Pycharm 运行后没有输出的问题
2021/02/05 Python
纯CSS实现设置半个字符的样式
2014/07/03 HTML / CSS
保安队长职务说明书
2014/02/23 职场文书
数学检讨书1000字
2014/02/24 职场文书
党的群众路线教育实践活动个人整改落实情况汇报
2014/10/28 职场文书
四风专项整治工作情况汇报
2014/10/28 职场文书
区域销售大会开幕词
2016/03/04 职场文书
SpringCloud之@FeignClient()注解的使用方式
2021/09/25 Java/Android