微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。菜单分为默认菜单与个性化菜单。个性化菜单接口是为了帮助公众号实现灵活的业务运营,开发者可以通过该接口,让公众号的不同用户群体看到不一样的自定义菜单。该接口开放给已认证订阅号和已认证服务号。本文主要介绍微信自定义菜单的创建、查询、删除、各菜单类型事件的响应方法,以及菜单应用的界面整合参考。由于篇幅有限不可能面面俱到,只能抛砖迎玉,大家就可以据此扩展做深入的应用。

  1. 自定义菜单分为一级菜单和二级菜单。
  2. 一级菜单数量为1-3个,即打开公众账号直接可以看到排列在最下方的最多3个按钮。一级菜单的文字最多不能4个汉字,多出来的部分将会以“...”代替。
  3. 二级菜单从属于一级菜单,数量为1-5个。二级菜单的文字不最多不能超过8个汉字,多出来的部分将会以“...”代替。
  4. 无论一级菜单还是二级菜单,都有两个触发事件可以选择,分别是:点击(click,值不能超过128字节)和打开网址(view,url不能超过256个字节)。
  5. 当一个一级菜单下有二级菜单存在的时候,这个一级菜单按钮被点击不会有任何事件发生。
  6. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号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。

请注意,3到8的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。9和10,是专门给第三方平台旗下未微信认证(具体而言,是资质认证未通过)的订阅号准备的事件类型,它们是没有事件推送的,能力相对受限,其他类型的公众号不必使用。

微信提供了接口API可以创建自定义菜单,接口相关说明如下。

http请求方式:POST(请使用https协议)

https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

click和view的请求示例

{
     "button":[
     {    
          "type":"click",
          "name":"今日歌曲",
          "key":"V1001_TODAY_MUSIC"
      },
      {
           "name":"菜单",
           "sub_button":[
           {    
               "type":"view",
               "name":"搜索",
               "url":"http://www.soso.com/"
            },
            {
                 "type":"miniprogram",
                 "name":"wxa",
                 "url":"http://mp.weixin.qq.com",
                 "appid":"wx286b93c14bbf93aa",
                 "pagepath":"pages/lunar/index"
             },
            {
               "type":"click",
               "name":"赞一下我们",
               "key":"V1001_GOOD"
            }]
       }]
 }

其他新增按钮类型的请求示例

{
    "button": [
        {
            "name": "扫码", 
            "sub_button": [
                {
                    "type": "scancode_waitmsg", 
                    "name": "扫码带提示", 
                    "key": "rselfmenu_0_0", 
                    "sub_button": [ ]
                }, 
                {
                    "type": "scancode_push", 
                    "name": "扫码推事件", 
                    "key": "rselfmenu_0_1", 
                    "sub_button": [ ]
                }
            ]
        }, 
        {
            "name": "发图", 
            "sub_button": [
                {
                    "type": "pic_sysphoto", 
                    "name": "系统拍照发图", 
                    "key": "rselfmenu_1_0", 
                   "sub_button": [ ]
                 }, 
                {
                    "type": "pic_photo_or_album", 
                    "name": "拍照或者相册发图", 
                    "key": "rselfmenu_1_1", 
                    "sub_button": [ ]
                }, 
                {
                    "type": "pic_weixin", 
                    "name": "微信相册发图", 
                    "key": "rselfmenu_1_2", 
                    "sub_button": [ ]
                }
            ]
        }, 
        {
            "name": "发送位置", 
            "type": "location_select", 
            "key": "rselfmenu_2_0"
        },
        {
           "type": "media_id", 
           "name": "图片", 
           "media_id": "MEDIA_ID1"
        }, 
        {
           "type": "view_limited", 
           "name": "图文消息", 
           "media_id": "MEDIA_ID2"
        }
    ]
}

参数说明

参数             是否必须                    说明
button           是                        一级菜单数组,个数应为1~3个
sub_button    否                        二级菜单数组,个数应为1~5个
type            是                        菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型
name        是        菜单标题,        不超过16个字节,子菜单不超过60个字节
key            click等点击类型必须        菜单KEY值,用于消息接口推送,不超过128字节
url            view、miniprogram类型必须    网页 链接,用户点击菜单可打开链接,不超过1024字节。 type为miniprogram时,不支持小程序的老版本客户端将打开本url。
media_id    media_id类型和view_limited类型必须    调用新增永久素材接口返回的合法media_id
appid        miniprogram类型必须        小程序的appid(仅认证公众号可配置)
pagepath    miniprogram类型必须        小程序的页面路径

使用微信提供的接口创建菜单编码量还是非常大,在此我们借助Senparc.Weixin.MP SDK来快速的创建自定义菜单。非常简单,只需三步:

第一步:获取AccessToken

var accessToken = AccessTokenContainer.TryGetToken(appId, appSecret).access_token;

PS:如果第三步中使用AppId取代AccessToken,则这一步可以省略。

第二步:组织菜单内容

ButtonGroup bg = new ButtonGroup();

//菜单1
var subButton = new SubButton()
{
    name = "菜单1"
};

bg.button.Add(subButton);
subButton.sub_button.Add(new SingleClickButton()
{
    key = "SubClickRoot_Text",
    name = "返回文本"
});
subButton.sub_button.Add(new SingleClickButton()
{
    key = "SubClickRoot_News",
    name = "返回图文"
});
subButton.sub_button.Add(new SingleClickButton()
{
    key = "SubClickRoot_Music",
    name = "返回音乐"
});
subButton.sub_button.Add(new SingleViewButton()
{
    url = "http://www.rdiframework.net/",
    name = "Url跳转"
});

//菜单2
var subButton2 = new SubButton()
{
    name = "菜单2"
};

bg.button.Add(subButton2);
subButton2.sub_button.Add(new SingleClickButton()
{
    key = "SubClickRoot_Text",
    name = "返回文本"
});
subButton2.sub_button.Add(new SingleClickButton()
{
    key = "SubClickRoot_News",
    name = "返回图文"
});

第三步:提交到微信服务器

var result = CommonApi.CreateMenu(accessToken, bg);

上面SingleClickButton和SingleViewButton分别对应了微信API中的click和view两种菜单响应方式。

通过执行上述代码我们可以看到创建的菜单如下所示,可以看到上面我们通过代码创建的自定义菜单。

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。另外请注意,在设置了个性化菜单后,使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息。
请求说明

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

需要注意的时返回的Json数据分两种类型,默认菜单与个性化的菜单。
menu为默认菜单,conditionalmenu为个性化菜单列表。字段说明请见个性化菜单接口页的说明
使用Senparc.Weixin.MP SDK查询已创建的菜单接口只需一行代码,如下:

var result = CommonApi.GetMenu(accessToken);

使用接口创建自定义菜单后,开发者还可使用接口删除当前使用的自定义菜单。另请注意,在个性化菜单时,调用此接口会删除默认菜单及全部个性化菜单。

请求说明

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

使用Senparc.Weixin.MP SDK删除已创建的菜单接口只需一行代码,如下:

var result = CommonApi.DeleteMenu(accessToken);

无论是click还是view,服务器都会收到不同的事件响应。不同的是,click之后客户端可以得到返回信息,而view在收到请求后,无论返回什么信息,客户端都无法收到(直接打开URL了)。用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。另外第3个到第8个类型的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。

要对菜单响应事件的处理,我们只需要重写Senparc.Weixin.MP SDK中的对应事件,具体详情如下。下面给出了各类型事件处理的参考代码,具体业务应用可以就此扩展开来做深入的应用即可。

public override IResponseMessageBase OnTextOrEventRequest(RequestMessageText requestMessage)
{
    // 预处理文字或事件类型请求。
    // 这个请求是一个比较特殊的请求,通常用于统一处理来自文字或菜单按钮的同一个执行逻辑,
    // 会在执行OnTextRequest或OnEventRequest之前触发,具有以下一些特征:
    // 1、如果返回null,则继续执行OnTextRequest或OnEventRequest
    // 2、如果返回不为null,则终止执行OnTextRequest或OnEventRequest,返回最终ResponseMessage
    // 3、如果是事件,则会将RequestMessageEvent自动转为RequestMessageText类型,其中RequestMessageText.Content就是RequestMessageEvent.EventKey

    if (requestMessage.Content == "OneClick")
    {
        var strongResponseMessage = CreateResponseMessage<ResponseMessageText>();
        strongResponseMessage.Content = "您点击了底部按钮。\r\n为了测试微信软件换行bug的应对措施,这里做了一个——\r\n换行";
        return strongResponseMessage;
    }
    return null;//返回null,则继续执行OnTextRequest或OnEventRequest
}


public override IResponseMessageBase OnEvent_ClickRequest(RequestMessageEvent_Click requestMessage)
{
    //获得当前公众号
    WeixinOfficialAccountEntity account = RDIFrameworkService.Instance.WeixinBasicService.GetOfficialAccountEntity(Id);

    IResponseMessageBase reponseMessage = null;
    //菜单点击,需要跟创建菜单时的Key匹配
    switch (requestMessage.EventKey)
    {
        case "OneClick":
            {
                //这个过程实际已经在OnTextOrEventRequest中完成,这里不会执行到。
                var strongResponseMessage = CreateResponseMessage<ResponseMessageText>();
                reponseMessage = strongResponseMessage;
                strongResponseMessage.Content = "您点击了底部按钮。\r\n为了测试微信软件换行bug的应对措施,这里做了一个——\r\n换行";
            }
            break;
        case "SubClickRoot_Text":
            {
                var strongResponseMessage = CreateResponseMessage<ResponseMessageText>();
                reponseMessage = strongResponseMessage;
                strongResponseMessage.Content = "您点击了子菜单按钮。";
            }
            break;
        case "SubClickRoot_News":
            {
                var strongResponseMessage = CreateResponseMessage<ResponseMessageNews>();
                reponseMessage = strongResponseMessage;
                strongResponseMessage.Articles.Add(new Article()
                {
                    Title = "您点击了子菜单图文按钮",
                    Description = "您点击了子菜单图文按钮,这是一条图文信息。",
                    PicUrl = "http://www.rdiframework.net/img/weixing-ma.png",
                    Url = "http://www.rdiframework.net/"
                });
            }
            break;
        case "SubClickRoot_Music":
            {
                //上传缩略图
                var uploadResult = MediaApi.UploadTemporaryMedia(account.AccessToken, UploadMediaFileType.image,Server.GetMapPath("~/Content/Images/weixing-ma.png"));
                //设置音乐信息
                var strongResponseMessage = CreateResponseMessage<ResponseMessageMusic>();
                reponseMessage = strongResponseMessage;
                strongResponseMessage.Music.Title = "天籁之音";
                strongResponseMessage.Music.Description = "真的是天籁之音";
                strongResponseMessage.Music.MusicUrl = "http://www.rdiframework.net/resource/music/music1.mp3";
                strongResponseMessage.Music.HQMusicUrl = "http://www.rdiframework.net/resource/music/music1.mp3";
                strongResponseMessage.Music.ThumbMediaId = uploadResult.media_id;
            }
            break;
        case "SubClickRoot_Image":
            {
                //上传图片
                var uploadResult = MediaApi.UploadTemporaryMedia(account.AccessToken, UploadMediaFileType.image, Server.GetMapPath("~/Content/Images/weixing-ma.png"));
                //设置图片信息
                var strongResponseMessage = CreateResponseMessage<ResponseMessageImage>();
                reponseMessage = strongResponseMessage;
                strongResponseMessage.Image.MediaId = uploadResult.media_id;
            }
            break;           
        default:
            {
                var strongResponseMessage = CreateResponseMessage<ResponseMessageText>();
                strongResponseMessage.Content = "您点击了按钮,EventKey:" + requestMessage.EventKey;
                reponseMessage = strongResponseMessage;
            }
            break;
    }

    return reponseMessage;
}

在4.2小节我们创建的菜单中,我们单击“返回图文”二级菜单项,由于我们为这个菜单项定义的KEY为“SubClickRoot_News”,如下图的调试状态所示。

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

我们返回到微信查看一下运行效果,可以看到我们自己的服务器根据定义已经正确的返回了图文信息给我们,其他的菜单事件操作类似。

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

又如我们返回一首音乐,还可以直接播放返回的音乐,如下图所示。

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

public override IResponseMessageBase OnEvent_ViewRequest(RequestMessageEvent_View requestMessage)
{
   //说明:这条消息只作为接收,下面的responseMessage到达不了客户端,类似OnEvent_UnsubscribeRequest
   var responseMessage = CreateResponseMessage<ResponseMessageText>();
   responseMessage.Content = "您点击了view按钮,将打开网页:" + requestMessage.EventKey;
   return responseMessage;
}
/// <summary>
/// 事件之扫码推事件(scancode_push)
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_ScancodePushRequest(RequestMessageEvent_Scancode_Push requestMessage)
{
    var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
    responseMessage.Content = "事件之扫码推事件";
    return responseMessage;
}
/// <summary>
/// 事件之扫码推事件且弹出“消息接收中”提示框(scancode_waitmsg)
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_ScancodeWaitmsgRequest(RequestMessageEvent_Scancode_Waitmsg requestMessage)
{
    var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
    responseMessage.Content = "事件之扫码推事件且弹出“消息接收中”提示框";
    return responseMessage;
}
/// <summary>
/// 事件之弹出系统拍照发图(pic_sysphoto)
/// 实际测试时发现微信并没有推送RequestMessageEvent_Pic_Sysphoto消息,只能接收到用户在微信中发送的图片消息。
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_PicSysphotoRequest(RequestMessageEvent_Pic_Sysphoto requestMessage)
{
    var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
    responseMessage.Content = "事件之弹出系统拍照发图";
    return responseMessage;
}
/// <summary>
/// 事件之弹出拍照或者相册发图(pic_photo_or_album)
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_PicPhotoOrAlbumRequest(RequestMessageEvent_Pic_Photo_Or_Album requestMessage)
{
   var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
   responseMessage.Content = "事件之弹出拍照或者相册发图";
   return responseMessage;
}
/// <summary>
/// 事件之弹出微信相册发图器(pic_weixin)
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_PicWeixinRequest(RequestMessageEvent_Pic_Weixin requestMessage)
{
    var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
    responseMessage.Content = "事件之弹出微信相册发图器";
    return responseMessage;
}
/// <summary>
/// 事件之弹出地理位置选择器(location_select)
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_LocationSelectRequest(RequestMessageEvent_Location_Select requestMessage)
{
    var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
    responseMessage.Content = "事件之弹出地理位置选择器";
    return responseMessage;
}

实际应用中菜单随时都可能在变化,我们的最终使用者是不可能用编码的方式去对菜单进行处理的。这时就需要一个集中的界面可对菜单的集中配置。我们开发了菜单的应用参考界面,如下图所示。

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

在上图中,我们通过“获取菜单”按钮可以得到当前已经定义好的菜单及菜单的事件响应的处理,我们对微信常规菜单以及个性化菜单的定义都进行了处理。各菜单的定义完全按照微信的要求标准进行了处理。对于界面右侧的“按钮其他参数”的的设置,针对不同的类型分别进行了处理。对于菜单的修改,直接单击“更新到服务器”即可完成同步,非常的方便。

微信公众平台技术文档-官方

Senparc.Weixin SDK + 官网示例源代码

RDIFramework.NET — 基于.NET的快速信息化系统开发框架 — 系列目录

RDIFramework.NET ━ .NET快速信息化系统开发框架 ━ 工作流程组件介绍

RDIFramework.NET框架SOA解决方案(集Windows服务、WinForm形式与IIS形式发布)-分布式应用

RDIFramework.NET代码生成器全新V3.5版本发布-重大升级


一路走来数个年头,感谢RDIFramework.NET框架的支持者与使用者,大家可以通过下面的地址了解详情。

RDIFramework.NET官方网站:http://www.rdiframework.net/

RDIFramework.NET官方博客:http://blog.rdiframework.net/

同时需要说明的,以后的所有技术文章以官方网站为准,欢迎大家收藏!

RDIFramework.NET框架由专业团队长期打造、一直在更新、一直在升级,请放心使用!

欢迎关注RDIFramework.net框架官方公众微信(微信号:guosisoft),及时了解最新动态。

扫描二维码立即关注

微信公众号开发C#系列-8、自定义菜单及菜单响应事件的处理

相关推荐