介绍微信公众号菜单自定义相关的功能,包括基本的菜单的分类规则和基本的代码实现方式,也系统性的介绍了自定义菜单相关的接口和支持的功能项

1 说明

1.1 菜单分级和命名说明

  • 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
  • 一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“…”代替。
  • 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。
  • 测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

1.2 权限相关

  • 未认证的订阅号不能使用,已认证的订阅号可以使用
  • 服务号不论是否认证均可以使用
  • 可使用测试号进行测试

    1.3 官方开发文档

    自定义菜单的说明以及示例可以参照官方的开发文档,地址:官方的开发文档

    2 基本实现(基于WxJava

    2.1 构建菜单模型

    2.1.1 官方参数:

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

    2.1.2 菜单Bean

    GIT地址

    public class WxMenuButton implements Serializable {
    private static final long serialVersionUID = -1070939403109776555L;
    
    private String type;
    private String name;
    private String key;
    private String url;
    @SerializedName("media_id")
    private String mediaId;
    @SerializedName("appid")
    private String appId;
    @SerializedName("pagepath")
    private String pagePath;
    
    @SerializedName("sub_button")
    private List<WxMenuButton> subButtons = new ArrayList<>();
    
    @Override
    public String toString() {
      return WxGsonBuilder.create().toJson(this);
    }

    2.1.3 组装数据

    即按照官方示例组装成JSON格式的数据

    private WxMenuButton handle(RData rdata) {
      WxMenuButton button = new WxMenuButton();
               if("click".equals(rdata.getString("menuProperty"))) {
                   button.setType(MenuButtonType.CLICK);
                   button.setName(rdata.getString("name"));
                   button.setKey(rdata.getString("key"));
               }else if("view".equals(rdata.getString("menuProperty"))) {
                   button.setType(MenuButtonType.VIEW);
                   button.setName(rdata.getString("name"));
                   button.setUrl(rdata.getString("url"));
               }else if("miniprogram".equals(rdata.getString("menuProperty"))) {
                   button.setType(MenuButtonType.MINIPROGRAM);
                   button.setName(rdata.getString("name"));
                   button.setAppId(rdata.getString("appId"));
                   button.setPagePath(rdata.getString("pagepath"));
                   button.setUrl(rdata.getString("url"));
               }else {
                   button.setName(rdata.getString("name"));
               }         
      return button;
    }

    2.1.4 发送数据

    调用方式

    this.wxService.switchoverTo("weChatId").getMenuService().menuCreate(WxMenu);

    部内实现

    @Override
    public String menuCreate(WxMenu menu) throws WxErrorException {
      String menuJson = menu.toJson();
      String url = API_URL_PREFIX + "/create";
      if (menu.getMatchRule() != null) {
        url = API_URL_PREFIX + "/addconditional";
      }
      String result = this.wxMpService.post(url, menuJson);
      if (menu.getMatchRule() != null) {
        return new JsonParser().parse(result).getAsJsonObject().get("menuid").getAsString();
      }
    
      return null;
    }

    post方法

    public <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
      E dataForLog = DataUtils.handleDataWithSecret(data);
      if (uri.contains("access_token=")) {
        throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
      }
      String accessToken = getAccessToken(false);
      String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
      try {
        T result = executor.execute(uriWithAccessToken, data);
        this.log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
        return result;
      } catch (WxErrorException e) {
        WxError error = e.getError();
        /*
         * 发生以下情况时尝试刷新access_token
         * 40001 获取access_token时AppSecret错误,或者access_token无效
         * 42001 access_token超时
         * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口
         */
        if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001 || error.getErrorCode() == 40014) {
          // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
          this.getWxMpConfigStorage().expireAccessToken();
          if (this.getWxMpConfigStorage().autoRefreshToken()) {
            return this.execute(executor, uri, data);
          }
        }
    
        if (error.getErrorCode() != 0) {
        }
        return null;
      } catch (IOException e) {
      }
    }

    3 自定义菜单说明

    依据官方文档,自定义菜单操作主要包括

  • 菜单创建
  • 菜单查询
  • 菜单删除
  • 个性化菜单
  • 自定义菜单配置

公众号菜单设置分为常规菜单以及个性化菜单。其中个性化菜单会根据访问用户的属性不同显示不同的菜单。

3.1 公众号菜单类型

目前公众号菜单支持以下10种类型

  1. click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
  1. view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
  2. scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
  3. scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
  4. pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
  5. pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
  6. pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
  7. location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
  8. media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
  9. view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
  10. miniprogram跳转到小程序

概括而言,以类型划分,菜单可分为五类,即

  • view:跳转到对应的URL(可集合网页授权接口,获取用户信息)。
  • 点击类型:基于设置的key触发EVENT事件,与后台进行交互
  • 小程序:设置跳转到小程序
  • 图文素材(未认证下使用):在不具备推送条件下使用,进行图文推送
  • 其他:包括扫码,拍照,位置选择

    3.2 菜单创建

    接口请求说明

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

    格式:

     {
       "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"
              }]
         }]
     }

    3.3 菜单查询

    请求说明

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

    返回说明
    使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。另外请注意,在设置了个性化菜单后,使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息。
    注意:通过页面配置的菜单,本接口将无法返回菜单配置数据

示例:
有个性化菜单时:
注:menu为默认菜单,conditionalmenu为个性化菜单列表。字段说明请见个性化菜单接口页的说明。

{
    "menu": {
        "button": [
            {
                "type": "click", 
                "name": "今日歌曲", 
                "key": "V1001_TODAY_MUSIC", 
                "sub_button": [ ]
            }
        ], 
        "menuid": 208396938
    }, 
    "conditionalmenu": [
        {
            "button": [
                {
                    "type": "click", 
                    "name": "今日歌曲", 
                    "key": "V1001_TODAY_MUSIC", 
                    "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": [ ]
                        }
                    ]
                }
            ], 
            "matchrule": {
                "group_id": 2, 
                "sex": 1, 
                "country": "中国", 
                "province": "广东", 
                "city": "广州", 
                "client_platform_type": 2
            }, 
            "menuid": 208396993
        }
    ]

3.5 菜单删除

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

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

3.6 菜单事件推送

用户点击自定义菜单后,微信会将点击事件上报后台(点击一级菜单显示二级菜单将不会上报)。

事件推送事件处理

接收信息

  WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
  WxMpXmlOutMessage outMessage = this.wxService.route(inMessage);

处理信息

 @Override
  public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
                                  Map<String, Object> context, WxMpService weixinService,
                                  WxSessionManager sessionManager) {
    String msg = String.format("type:%s, event:%s, key:%s",
      wxMessage.getMsgType(), wxMessage.getEvent(),
      wxMessage.getEventKey());
    if (WxConsts.MenuButtonType.VIEW.equals(wxMessage.getEvent())) {
      return null;
    }
    return WxMpXmlOutMessage.TEXT().content(msg)
      .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
      .build();
  }

3.6.1 参数说明

参数说明描述
ToUserName通用开发者 微信号
FromUserName通用发送方帐号(一个OpenID)
CreateTime通用消息创建时间 (整型)
MsgType通用消息类型,event
Event通用事件类型,CLICK,view_miniprogram,location_select等,具体参见小程序分类
EventKey通用事件KEY值,与自定义菜单接口中KEY值对应,小程序的路径
SendLocationInfo地理位置选择事件发送的位置信息
Location_X地理位置选择事件X坐标信息
Location_Y地理位置选择事件Y坐标信息
Scale地理位置选择事件精度,可理解为精度或者比例尺、越精细的话 scale越高
Label地理位置选择事件地理位置的字符串信息
Poiname地理位置选择事件朋友圈POI的名字,可能为空
SendPicsInfo拍照发送的图片信息
Count拍照发送的图片数量
PicList拍照图片列表
PicMd5Sum拍照图片的MD5值,开发者若需要,可用于验证接收到图片
MenuID通用菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了
ScanCodeInf扫码相关事件扫描信息
ScanType扫码相关事件扫描类型,一般是qrcode
ScanResult扫码相关事件扫描结果,即二维码对应的字符串信息

3.6.2 微信上报事件数据示例

扫码

<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090502</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[scancode_push]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<ScanCodeInfo><ScanType><![CDATA[qrcode]]></ScanType>
<ScanResult><![CDATA[1]]></ScanResult>
</ScanCodeInfo>
</xml>

跳转

<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[VIEW]]></Event>
<EventKey><![CDATA[www.qq.com]]></EventKey>
<MenuId>MENUID</MenuId>
</xml>

3.7 个性化菜单

为了帮助公众号实现灵活的业务运营,微信公众平台新增了个性化菜单接口,开发者可以通过该接口,让公众号的不同用户群体看到不一样的自定义菜单。该接口开放给已认证订阅号和已认证服务号。

开发者可以通过以下条件来设置用户看到的菜单:

1、用户标签(开发者的业务需求可以借助用户标签来完成)
2、性别
3、手机操作系统
4、地区(用户在微信客户端设置的地区)
5、语言(用户在微信客户端设置的语言)

个性化菜单接口说明:

1、个性化菜单要求用户的微信客户端版本在iPhone6.2.2,Android 6.2.4以上,暂时不支持其他版本微信
2、菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果
3、普通公众号的个性化菜单的新增接口每日限制次数为2000次,删除接口也是2000次,测试个性化菜单匹配结果接口为20000次
4、出于安全考虑,一个公众号的所有个性化菜单,最多只能设置为跳转到3个域名下的链接
5、创建个性化菜单之前必须先创建默认菜单(默认菜单是指使用普通自定义菜单创建接口创建的菜单)。如果删除默认菜单,个性化菜单也会全部删除
6、个性化菜单接口支持用户标签,请开发者注意,当用户身上的标签超过1个时,以最后打上的标签为匹配

个性化菜单匹配规则说明:

个性化菜单的更新是会被覆盖的。
例如公众号先后发布了默认菜单,个性化菜单1,个性化菜单2,个性化菜单3。那么当用户进入公众号页面时,将从个性化菜单3开始匹配,如果个性化菜单3匹配成功,则直接返回个性化菜单3,否则继续尝试匹配个性化菜单2,直到成功匹配到一个菜单。
根据上述匹配规则,为了避免菜单生效时间的混淆,决定不予提供个性化菜单编辑API,开发者需要更新菜单时,需将完整配置重新发布一轮。

3.7.1 参数说明

请求

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

参数:

{
    "button": [
       //与默认参数相同
    ], 
    "matchrule": {
        "tag_id": "2", 
        "sex": "1", 
        "country": "中国", 
        "province": "广东", 
        "city": "广州", 
        "client_platform_type": "2", 
        "language": "zh_CN"
    }
}

参数说明

参数是否必须说明
matchrule菜单匹配规则
tag_id用户标签的id,可通过用户标签管理接口获取
sex性别:男(1)女(2),不填则不做匹配
client_platform_type客户端版本,当前只具体到系统型号:IOS(1), Android(2),Others(3),不填则不做匹配
country国家信息,是用户在微信中设置的地区,具体请参考地区信息表
province省份信息,是用户在微信中设置的地区,具体请参考地区信息表
city城市信息,是用户在微信中设置的地区
  • matchrule共六个字段,均可为空,但不能全部为空,至少要有一个匹配信息是不为空的。
  • country、province、city组成地区信息,将按照country、province、city的顺序进行验证,要符合地区信息表的内容。
  • 地区信息从大到小验证,小的可以不填,即若填写了省份信息,则国家信息也必填并且匹配,城市信息可以不填。 例如 “中国 广东省 广州市”、“中国 广东省”都是合法的地域信息,而“中国 广州市”则不合法,因为填写了城市信息但没有填写省份信息。

    3.7.2 操作说明

  • 创建见上
  • 删除:可基于menUid进行删除
  • 测试匹配结果:传入微信ID,返回菜单匹配结果
  • 删除所有菜单和查询个性化菜单使用默认菜单的接口操作

    3.8 获取自定义菜单配置

    本接口将会提供公众号当前使用的自定义菜单的配置,如果公众号是通过API调用设置的菜单,则返回菜单的开发配置,而如果公众号是在公众平台官网通过网站功能发布菜单,则本接口返回运营者设置的菜单配置。

请注意:

1、第三方平台开发者可以通过本接口,在旗下公众号将业务授权给你后,立即通过本接口检测公众号的自定义菜单配置,并通过接口再次给公众号设置好自动回复规则,以提升公众号运营者的业务体验。
2、本接口与自定义菜单查询接口的不同之处在于,本接口无论公众号的接口是如何设置的,都能查询到接口,而自定义菜单查询接口则仅能查询到使用API设置的菜单配置。
3、认证/未认证的服务号/订阅号,以及接口测试号,均拥有该接口权限。
4、从第三方平台的公众号登录授权机制上来说,该接口从属于消息与菜单权限集。
5、本接口中返回的图片/语音/视频为临时素材(临时素材每次获取都不同,3天内有效,通过素材管理-获取临时素材接口来获取这些素材),本接口返回的图文消息为永久素材素材(通过素材管理-获取永久素材接口来获取这些素材)。

接口调用请求说明

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

返回结果说明
如果公众号是在公众平台官网通过网站功能发布菜单,则本接口返回的自定义菜单配置。

Last modification:June 21, 2019
If you think my article is useful to you, please feel free to appreciate