写在前面
因为前边给公众号添加智能对话机器人,启用了公众号后台服务器配置。然后原来的公众号的后台自定义菜单就失效了,所以没办法,我们也只能去自己开发了,也就有了这篇文章。
这篇文章会用到给你的公众号添加一个智能机器人的一些代码,所以没看过之前文章的同学可以先去看一下。
虽然自定义菜单的流程和代码都完成了,但是自定义菜单需要认证的公众号才行,目前个人的公众号认证功能正在逐步开放中,应该不久就都可以了,如果你和我一样还没有收到个人认证的通知,那么就耐心等待一段时间吧。
获取 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,我们可以参考微信公众号开发文档中的素材获取来获得。
好了,我们的自定义菜单到这就完成了,我们可以根据我们自己公众号的不同需求来定义自己的菜单了。