欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

公众号自定义菜单开发

程序员文章站 2022-05-30 20:33:41
...

写在前面

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

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

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

获取 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,我们可以参考微信公众号开发文档中的素材获取来获得。

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