美文网首页
公众号自定义菜单开发

公众号自定义菜单开发

作者: KEEPINUP | 来源:发表于2020-09-13 17:16 被阅读0次

    写在前面

    因为前边给公众号添加智能对话机器人,启用了公众号后台服务器配置。然后原来的公众号的后台自定义菜单就失效了,所以没办法,我们也只能去自己开发了,也就有了这篇文章。

    这篇文章会用到给你的公众号添加一个智能机器人的一些代码,所以没看过之前文章的同学可以先去看一下。

    虽然自定义菜单的流程和代码都完成了,但是自定义菜单需要认证的公众号才行,目前个人的公众号认证功能正在逐步开放中,应该不久就都可以了,如果你和我一样还没有收到个人认证的通知,那么就耐心等待一段时间吧。

    获取 access_token

    因为在自定义菜单的开发中我们需要用到 access_token,所以我们需要首先获取到 access_token,后边很多其他的业务也需要用到 access_token。

    这是公众号文档里对 access_token 的说明,我们先看一下。

    access_token 是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token 的存储至少要保留 512 个字符空间。access_token 的有效期目前为 2 个小时,需定时刷新,重复获取将导致上次获取的access_token 失效。

    公众平台的API调用所需的access_token的使用及生成方式我们需要遵循以下几个条件和说明:

    • 因为各个接口调用都需要 access_token,我们最好使用中控服务器单独获取和刷新,避免各自刷新生成,造成 access_token 覆盖冲突而影响业务;
    • 在 access_token 中有一个参数 expire_in 来表示 access_token 的有效期,现在是 7200 秒。我们自己可以根据这个时间去提前刷新 access_token,在刷新过程中,老的 access_token,可以继续使用,公众平台后台会保证在5分钟内,新老 access_token 都可用,这保证了第三方业务的平滑过渡;
    • access_token的有效时间可能会在未来有调整,所以我们不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样在调用获知access_token已超时的情况下,可以触发access_token的刷新流程;

    接口调用请求说明

    https请求方式: 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:凭证有效时间,单位:秒

    代码实现

    我们创建一个类来获取和刷新 access_token,basic.py

    import urllib
    import time
    import json
    
    
    class Basic:
        def __init__(self):
            self.__accessToken = ''
            self.__leftTime = 0
    
        def __real_get_access_token(self):
            appId = "你的appId"
            appSecret = "你的appSecret"
            postUrl = ("https://api.weixin.qq.com/cgi-bin/token?grant_type="
                       "client_credential&appid=%s&secret=%s" % (appId, appSecret))
            urlResp = urllib.request.urlopen(postUrl)
            urlResp = json.loads(urlResp.read())
            print(urlResp)
            self.__accessToken = urlResp['access_token']
            self.__leftTime = urlResp['expires_in']
            print(self.__accessToken)
    
        # 外部获取 access_token 接口,同样 leftTime 如果小于十秒我们就刷新 access_token
        def get_access_token(self):
            if self.__leftTime < 10:
                self.__real_get_access_token()
            return self.__accessToken
    
        # 刷新 leftTime,如果小于十秒我们就刷新 access_token
        def run(self):
            while(True):
                if self.__leftTime > 10:
                    time.sleep(2)
                    self.__leftTime -= 2
                else:
                    self.__real_get_access_token()
    
    

    然后我们单独运行一个获取刷新 access_token 的程序。

    accessToken.py

    from basic import Basic
    
    basic = Basic()
    
    
    def getAccessToken():
        return basic.get_access_token()
    
    
    if __name__ == "__main__":
        basic.run()
    

    后面其他的业务需要 access_token,都通过这个 accessToken 的 getAccessToken 方法来获取。后台会自动刷新。

    自定义菜单

    我们需要的 access_token 已经拿到了,那么我们就可以正式开始菜单的开发了。

    自定义菜单要求:

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

    自定义菜单按钮类型:

    • click:点击推事件,用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者;
    • view:跳转URL,用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL。
    • scancode_push:扫码推事件,用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者。
    • scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
    • pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
    • pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
    • pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
    • location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
    • media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。
    • view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。

    接口调用请求说明

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

    • access_token:即我们上面获取的 access_token

    代码实现

    下面我们通过代码来看一实现一个 click、view、media_id 三种类型的按钮。

    menu.py

    import urllib
    import accessToken
    
    class Menu(object):
        postJson = """
        {
            "button":
            [
                {
                    "type": "click",
                    "name": "开发指引",
                    "key":  "mpGuide"
                },
                {
                    "name": "公众平台",
                    "sub_button":
                    [
                        {
                            "type": "view",
                            "name": "更新公告",
                            "url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1418702138&token=&lang=zh_CN"
                        },
                        {
                            "type": "view",
                            "name": "接口权限说明",
                            "url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1418702138&token=&lang=zh_CN"
                        },
                        {
                            "type": "view",
                            "name": "返回码说明",
                            "url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433747234&token=&lang=zh_CN"
                        }
                    ]
                },
                {
                    "type": "media_id",
                    "name": "旅行",
                    "media_id": "z2zOokJvlzCXXNhSjF46gdx6rSghwX2xOD5GUV9nbX4"
                }
              ]
        }
        """.encode('utf-8')
    
        def __init__(self):
            pass
        def create(self, accessToken):
            postUrl = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s" % accessToken
            urlResp = urllib.urlopen(url=postUrl, data=self.postData)
            print urlResp.read()
    
        def query(self, accessToken):
            postUrl = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=%s" % accessToken
            urlResp = urllib.urlopen(url=postUrl)
            print urlResp.read()
    
        def delete(self, accessToken):
            postUrl = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=%s" % accessToken
            urlResp = urllib.urlopen(url=postUrl)
            print urlResp.read()
            
        #获取自定义菜单配置接口
        def get_current_selfmenu_info(self, accessToken):
            postUrl = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=%s" % accessToken
            urlResp = urllib.urlopen(url=postUrl)
            print urlResp.read()
    

    现在自定义的菜单生成了,我们通过 click 类型的 button 为例,来处理当点击菜单时收到的消息。微信后台会推送一个 event 类型的 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值对应

    整个消息的流程图:

    流程图

    我们根据消息格式和流程来写代码。
    修改 main.py

    from flask import Flask
    from flask import request
    import hashlib
    import re
    import tuling
    import receive
    import reply
    from menu import Menu
    import accessToken
    
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return "Hello World!"
    
    # 公众号后台消息路由入口
    @app.route("/wechat", methods=["GET", "POST"])
    def wechat():
        # 验证使用的GET方法
        if request.method == "GET":
            signature = request.args.get('signature')
            timestamp = request.args.get('timestamp')
            nonce = request.args.get('nonce')
            echostr = request.args.get('echostr')
            token = "公众号后台填写的token"
    
            # 进行排序
            dataList = [token, timestamp, nonce]
            dataList.sort()
            result = "".join(dataList)
    
            #哈希加密算法得到hashcode
            sha1 = hashlib.sha1()
            sha1.update(result.encode("utf-8"))
            hashcode = sha1.hexdigest()
    
            if hashcode == signature:
                return echostr
            else:
                return ""
        else:
            recMsg = receive.parse_xml(request.data)
            if isinstance(recMsg, receive.Msg):
                toUser = recMsg.FromUserName
                fromUser = recMsg.ToUserName
                if recMsg.MsgType == 'text':
                    content = recMsg.Content
                    # userId 长度小于等于32位
                    if len(toUser) > 31:
                        userid = str(toUser[0:30])
                    else:
                        userid = str(toUser)
                    userid = re.sub(r'[^A-Za-z0-9]+', '', userid)
                    tulingReplay = tuling.tulingReply(content, userid)
                    replyMsg = reply.TextMsg(toUser, fromUser, tulingReplay)
                    return replyMsg.send()
                elif recMsg.MsgType == 'image':
                    mediaId = recMsg.MediaId
                    replyMsg = reply.ImageMsg(toUser, fromUser, mediaId)
                    return replyMsg.send()
            if isinstance(recMsg, receive.EventMsg):
                if recMsg.Event == 'subscribe':
                    subscribe_reply = "终于等到你了。~\n" \
                                          "在这里,我们可以一起学习知识,\n" \
                                          "一起努力成长。\n" \
                                          "你烦闷时,我还可以陪你聊天解闷哦~"
                    replyMsg = reply.TextMsg(toUser, fromUser, subscribe_reply)
                    return replyMsg.send()
                elif recMsg.Event == 'CLICK':
                    if recMsg.Eventkey == 'mpGuide':
                        content = u"编写中,尚未完成".encode('utf-8')
                        replyMsg = reply.TextMsg(toUser, fromUser, content)
                        return replyMsg.send()
                elif recMsg.Event == 'VIEW':
                    pass
    
            return reply.Msg().send()
    
    if __name__ == "__main__":
        menu = Menu()
        access_token = accessToken.getAccessToken()
        menu.create(access_token)
        app.run(host='0.0.0.0', port=80)    #公众号后台只开放了80端口
    

    修改 receive.py:

    import xml.etree.ElementTree as ET
    
    def parse_xml(receiveData):
        if len(receiveData) == 0:
            return None
        xmlData = ET.fromstring(receiveData)
        msgType = xmlData.find('MsgType').text
        if msgType == 'text':
            return TextMsg(xmlData)
        elif msgType == 'image':
            return ImageMsg(xmlData)
        elif msgType == 'event':
            event_type = xmlData.find('Event').text
            if event_type in ('subscribe', 'unsubscribe'):
                return Subscribe(xmlData)
            elif event_type == 'CLICK':
                return Click(xmlData)
            elif event_type == 'VIEW':
                return View(xmlData)
    
    class Msg(object):
        def __init__(self, xmlData):
            self.ToUserName = xmlData.find('ToUserName').text
            self.FromUserName = xmlData.find('FromUserName').text
            self.CreateTime = xmlData.find('CreateTime').text
            self.MsgType = xmlData.find('MsgType').text
            self.MsgId = xmlData.find('MsgId').text
    
    class TextMsg(Msg):
        def __init__(self, xmlData):
            Msg.__init__(self, xmlData)
            self.Content = xmlData.find('Content').text
    
    class ImageMsg(Msg):
        def __init__(self, xmlData):
            Msg.__init__(self, xmlData)
            self.PicUrl = xmlData.find('PicUrl').text
            self.MediaId = xmlData.find('MediaId').text
    
    class EventMsg(object):
        def __init__(self, xmlData):
            self.ToUserName = xmlData.find('ToUserName').text
            self.FromUserName = xmlData.find('FromUserName').text
            self.CreateTime = xmlData.find('CreateTime').text
            self.MsgType = xmlData.find('MsgType').text
            self.Event = xmlData.find('Event').text
    
    class Subscribe(EventMsg):
        def __init__(self, xmlData):
            EventMsg.__init__(self, xmlData)
    
    class Click(EventMsg):
        def __init__(self, xmlData):
            EventMsg.__init__(self, xmlData)
            self.EventKey = xmlData.find('EventKey').text
    
    class View(EventMsg):
        def __init__(self, xmlData):
            EventMsg.__init__(self, xmlData)
            self.EventKey = xmlData.find('EventKey').text
            self.MenuId = xmlData.find('MenuId').text
    

    然后我们重启后台服务器,就可以测试我们的自定义菜单了,我们上边只对 click 的事件进行了处理,view 类型、media_id 类型的本身就更容易实现,我们这里就不详细展开这两种类型了,其中 media_id 类型的需要一个 media_id 的参数,也就是你公众号后台的素材的 id,我们可以参考微信公众号开发文档中的素材获取来获得。

    好了,我们的自定义菜单到这就完成了,我们可以根据我们自己公众号的不同需求来定义自己的菜单了。

    相关文章

      网友评论

          本文标题:公众号自定义菜单开发

          本文链接:https://www.haomeiwen.com/subject/mnxoektx.html