目录
由于上面那个原因,在做微信开发的时候若是启用了服务器配置,原本在微信后台设置的自动回复和自定义菜单将会失效。为解决这个问题,这里将会讲django实现微信自定义菜单功能
实现的效果图
近来在用python写微信的自定义菜单,先看效果截图如下:
# 图1:django的后台管理系统之自定义菜单
(相关代码实现见下文)
# 图2:django的后台管理系统之新增自定义菜单
(相关代码实现见下文)
# 图3:在公众号上的实现效果
OK,大概效果是长上面那亚子。接下来讲讲代码实现部分。
一、先看看我们需要做什么
1、看微信的文档
我们需要做微信自定义菜单,效果图是上面那些截图的样子。
先查看微信的开发文档,把菜单转换为微信所需要的格式长这样子:
2、用到的工具 wechatpy
用的wechatpy:一个微信 (WeChat) 的第三方 Python SDK, 实现了微信公众号、企业微信和微信支付等 API。
安装按照文档的要求来吧,安装在这里不说。直接拉到自定义菜单那边看看:wechatpy的自定义菜单
要的其实主要是create()和删除delete()
wechatpy/client/api/menu.py 工具类长这个亚子:
(不用自己写的!是wechatpy提供的!就看看它长啥样就好了,看看能怎么用它)
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.exceptions import WeChatClientException
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatMenu(BaseWeChatAPI):
def get(self):
"""
查询自定义菜单。
详情请参考
http://mp.weixin.qq.com/wiki/16/ff9b7b85220e1396ffa16794a9d95adc.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
menu = client.menu.get()
"""
try:
return self._get('menu/get')
except WeChatClientException as e:
if e.errcode == 46003:
# menu not exist
return None
else:
raise e
def create(self, menu_data):
"""
创建自定义菜单 ::
from wechatpy import WeChatClient
client = WeChatClient("appid", "secret")
client.menu.create({
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"view",
"name":"视频",
"url":"http://v.qq.com/"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}
]
}
]
})
详情请参考
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013
:param menu_data: Python 字典
:return: 返回的 JSON 数据包
"""
return self._post(
'menu/create',
data=menu_data
)
def update(self, menu_data):
"""
更新自定义菜单 ::
from wechatpy import WeChatClient
client = WeChatClient("appid", "secret")
client.menu.update({
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"view",
"name":"视频",
"url":"http://v.qq.com/"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}
]
}
]
})
详情请参考
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013
:param menu_data: Python 字典
:return: 返回的 JSON 数据包
"""
return self.create(menu_data)
def delete(self):
"""
删除自定义菜单。
详情请参考
http://mp.weixin.qq.com/wiki/16/8ed41ba931e4845844ad6d1eeb8060c8.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.delete()
"""
return self._get('menu/delete')
def get_menu_info(self):
"""
获取自定义菜单配置
详情请参考
http://mp.weixin.qq.com/wiki/17/4dc4b0514fdad7a5fbbd477aa9aab5ed.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
menu_info = client.menu.get_menu_info()
"""
return self._get('get_current_selfmenu_info')
def add_conditional(self, menu_data):
"""
创建个性化菜单 ::
from wechatpy import WeChatClient
client = WeChatClient("appid", "secret")
client.menu.add_conditional({
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"view",
"name":"视频",
"url":"http://v.qq.com/"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}
]
}
],
"matchrule":{
"group_id":"2",
"sex":"1",
"country":"中国",
"province":"广东",
"city":"广州",
"client_platform_type":"2"
}
})
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param menu_data: Python 字典
:return: 返回的 JSON 数据包
"""
return self._post(
'menu/addconditional',
data=menu_data
)
def del_conditional(self, menu_id):
"""
删除个性化菜单
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param menu_id: 菜单ID
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.del_conditional('menu_id')
"""
return self._post(
'menu/delconditional',
data={'menuid': menu_id}
)
def try_match(self, user_id):
"""
测试个性化菜单匹配结果
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param user_id: 可以是粉丝的OpenID,也可以是粉丝的微信号。
:return: 该接口将返回菜单配置
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.try_match('openid')
"""
return self._post(
'menu/trymatch',
data={'user_id': user_id}
)
只讲讲create那边。看到create需要传个menu_data参数进去,menu_data是微信所需要的菜单,要求格式已经列出了,是个python字典。
3、顺一下思路
在我django管理后台那边保存所设置的菜单的时候,将保存到数据库的菜单数据读取出来。然后按照微信所需要的格式进行输出个menu_data,将这个menu_data扔在wechatpy提供的create()中。
二、代码实现
其实也查过好些博客,但是很多都是直接将菜单的设置直接写在了代码中。而我这次需要可以在django的管理后台动态设置菜单,而不是说跑去代码那边改。
这是项目目录,只改动到wechat这个app(专门用来放微信开发功能的):
动到的文件有:admin.py、models.py、utils.py
wechat/models.py:
from account.models import MerchantModel
# 自定义菜单
# 一级菜单数组,个数应为1~3个,必须
# 二级菜单数组,个数应为1~5个,非必须
# type,name,key或者有子菜单的:name,sub_btn[]
# type,name,key/url
class WechatMenuModel(TimeStampedModel):
# 菜单类型
BUTTON_TYPE=(
('view','跳转网页'),
('click','发送消息'),
('miniprogram','跳转小程序')
)
# 当BUTTON_TYPE=="click"时的回复消息类型
REPLY_MESSAGE_TYPE = (
('text', '文字'),
('media_id', '图片'),
('view', '图文消息'),
)
button = models.CharField(verbose_name='一级菜单',max_length=16, help_text='一级菜单可设置1~3个')
sub_button = models.CharField(verbose_name='二级菜单',max_length=60, null=True, blank=True, help_text='二级菜单可设置1~5个')
type = models.CharField(verbose_name='菜单类型',max_length=100,choices=BUTTON_TYPE)
reply_type = models.CharField(verbose_name='回复的消息类型',max_length=100,choices=REPLY_MESSAGE_TYPE, null=True, blank=True)
# 当type=="click"
## 文字回复
key = models.CharField(verbose_name='文字回复', max_length=128, null=True, blank=True, help_text='菜单KEY值,用于消息接口推送')
## 图片回复
media_id = models.CharField(verbose_name='图片回复', null=True, blank=True, max_length=1000, help_text='通过公众号上传多媒体文件,得到的id')
## 图文消息回复. view、miniprogram类型必须
url = models.URLField(verbose_name='图文回复(点击图文消息跳转链接)',null=True,blank=True, max_length=1024, help_text='菜单类型为"跳转网页"或"跳转小程序"时必填')
# 当type=="miniprogram"时必填
appid = models.CharField(verbose_name='小程序appid', null=True, blank=True, max_length=1000, help_text='小程序的appid(仅认证公众号可配置). 菜单类型为"跳转小程序"时必填')
pagepath = models.CharField(verbose_name='页面路径', null=True, blank=True, max_length=1000, help_text='小程序的页面路径. 菜单类型为"跳转小程序"时必填')
merchant = models.ForeignKey(MerchantModel, verbose_name='商户')
def __str__(self):
return str(self.id)
class Meta:
ordering=('-created',)
managed = True
db_table = 't_wechat_custom_menu'
app_label = 'wechat'
verbose_name = u'自定义菜单'
verbose_name_plural = u'自定义菜单'
此处说明一下,merchant是存放merchant_id的。merchant大概是这样子:
account/models.py:
class MerchantModel(TimeStampedModel):
name = models.CharField(verbose_name='商户名称', max_length=100)
appid = models.CharField(verbose_name='AppId', max_length=50, unique=True)
app_secret = models.CharField(verbose_name='AppSecret', max_length=100)
app_token = models.CharField(verbose_name='APPToken', max_length=128)
encoding_aes_key = models.CharField(verbose_name='消息加解密密钥', max_length=128, null=True, blank=True)
user = models.OneToOneField(User, verbose_name='商户账号')
def __str__(self):
return self.name
class Meta:
ordering = ('-created',)
managed = True
db_table = 't_account_merchant'
app_label = 'account'
verbose_name = u'商户信息'
verbose_name_plural = u'商户信息'
wechat/utils.py:
专门放工具函数
from wechat.models import WechatMenuModel
from wechatpy import WeChatClient
def del_dict_null(dic):
""""
删除字典中的空值
:param dic 要处理的字典
"""
for i in list(dic.keys()):
if not dic[i]:
del dic[i]
def del_repeat_dict(ls):
""""
删除列表中重复的字典
:param ls 要处理的列表
"""
ls2 = list()
ls2.append(ls[0])
for dic in ls:
k = 0
for item in ls2:
# print 'item'
if dic['name'] != item['name']:
k = k + 1
# continue
else:
break
if k == len(ls2):
ls2.append(dic)
return ls2
def menu_create(merchant):
""""
获取菜单数据,转换为字典,并创建菜单
:param merchant 商户id
"""
button = []
ls = WechatMenuModel.objects.filter(merchant=merchant).order_by('id')
for i in range(len(ls)): # 循环一级菜单
if ls[i].sub_button is None:
btn_dict = dict(name=ls[i].button, type=ls[i].type, key=ls[i].key, url=ls[i].url)
del_dict_null(btn_dict) # 去字典空值
button.append(btn_dict)
else:
sub_ls = WechatMenuModel.objects.filter(merchant=merchant, button=ls[i].button).order_by('id')
sub_button = []
if sub_ls[0].button == ls[i].button:
for k in range(len(sub_ls)): # 循环二级菜单
sub_btn_dict = dict(name=sub_ls[k].sub_button, type=sub_ls[k].type, key=sub_ls[k].key, url=sub_ls[k].url)
del_dict_null(sub_btn_dict)
sub_button.append(sub_btn_dict)
btn_dict = dict(name=ls[i].button, sub_button=sub_button)
button.append(btn_dict)
new_button = del_repeat_dict(button)
menu_data = {'button': new_button}
client = WeChatClient(merchant.appid, merchant.app_secret)
client.menu.create(menu_data)
最主要是menu_create(),它上面那些都是辅助函数。这里的两个for循环想了其实有点久。
循环获取所设置的菜单,字段名是按照微信的规则来的。button为一级菜单,sub_button为二级菜单。实际上有两种list,一种是父菜单button[{},{},{}],另一种是某个父菜单下的子菜单sub_button[{},{},{},{},{}]
数据库:
wechat/admin.py:
这里是关联到django的后台管理系统
from django.contrib import admin
from django.contrib import messages
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.http import Http404
from django.http import HttpResponseRedirect
from wechatpy import WeChatClient
from wechatpy.client.api import WeChatCustomService
from wechat.utils import menu_create
from wechat.models import WechatMenuModel
class WechatMenuModelAdmin(admin.ModelAdmin):
# 在后台管理系统的界面显示一级菜单名、二级菜单名、菜单类型、回复类型
list_display = ('button', 'sub_button', 'type', 'reply_type', )
# 可对二级菜单名进行搜索
search_fields = ('sub_button',)
list_fields = ('sub_button', 'reply_type')
merchant_fields = (
'button', 'sub_button', 'type', 'reply_type', 'key', 'media_id', 'url', 'appid', 'pagepath', )
super_fields = (
'button', 'sub_button', 'type', 'reply_type', 'key', 'media_id', 'url', 'appid', 'pagepath', 'merchant', )
merchant_fieldsets = (
(None, {'fields': ('button', 'sub_button', 'type', 'reply_type',)}),
(u'文字回复', {'fields': (
'key',
)}),
(u'图片回复', {'fields': (
'media_id',
)}),
(u'图文回复', {'fields': (
'url',
)}),
(u'小程序信息', {'fields': (
'appid', 'pagepath',
)}),
),
super_fieldsets = (
(None, {'fields': ('button', 'sub_button', 'type', 'reply_type', 'merchant')}),
(u'文字回复', {'fields': (
'key',
)}),
(u'图片回复', {'fields': (
'media_id',
)}),
(u'图文回复', {'fields': (
'url',
)}),
(u'小程序信息', {'fields': (
'appid', 'pagepath',
)}),
)
def get_fieldsets(self, request, obj=None):
if request.user.is_superuser or request.user.groups.filter(name='Admin'):
return self.super_fieldsets
if hasattr(request.user, 'merchantmodel') and request.user.groups.filter(name='MerchantGroup'):
return self.merchant_fieldsets
else:
return None
def get_fields(self, request, obj=None):
if request.user.is_superuser or request.user.groups.filter(name='Admin'):
return self.super_fields
if hasattr(request.user, 'merchantmodel') and request.user.groups.filter(name='MerchantGroup'):
return self.merchant_fields
else:
return None
def save_model(self, request, obj, form, change):
if hasattr(request.user, 'merchantmodel') and request.user.groups.filter(name='MerchantGroup'):
obj.merchant = request.user.merchantmodel
if obj.type == 'click':
if obj.reply_type is None or len(obj.reply_type.replace(' ', '')) == 0:
messages.error(request, '保存失败:回复的消息内容不能为空')
else:
if obj.reply_type == 'text':
if obj.key is None or len(obj.key.replace(' ', '')) == 0:
messages.error(request, '保存失败:"文本回复正文"不能为空')
else:
obj.save()
menu_create(obj.merchant)
elif obj.reply_type == 'media_id':
if obj.media_id is None or len(obj.media_id.replace(' ', '')) == 0:
messages.error(request, '保存失败:"图片媒体ID"不能为空')
else:
obj.save()
menu_create(obj.merchant)
elif obj.type == 'view':
if obj.url is None or len(obj.reply_type.replace(' ', '')) == 0:
messages.error(request, '保存失败:图文回复的跳转链接不能为空')
else:
obj.save()
menu_create(obj.merchant)
elif obj.type == 'miniprogram':
if obj.appid is None or obj.pagepath is None or obj.url is None:
messages.error(request, '保存失败:小程序appid, 小程序的页面路径,图文回复的跳转链接不能为空')
else:
obj.save()
menu_create(obj.merchant)
def get_queryset(self, request):
qs = super(WechatMenuModelAdmin, self).get_queryset(request)
if request.user.is_superuser or request.user.groups.filter(name='Admin'):
return qs
if hasattr(request.user, 'merchantmodel') and request.user.groups.filter(name='MerchantGroup'):
return qs.filter(merchant=request.user.merchantmodel)
else:
raise Http404(u'您没有该权限,请联系管理员!')
def response_add(self, request, obj, post_url_continue=None):
opts = obj._meta
preserved_filters = self.get_preserved_filters(request)
if hasattr(request, 'err') and request.err:
self.message_user(request, request.err_msg, messages.ERROR)
redirect_url = request.path
redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
return HttpResponseRedirect(redirect_url)
else:
return super(WechatMenuModelAdmin, self).response_add(request, obj, post_url_continue)
def response_change(self, request, obj):
opts = self.model._meta
preserved_filters = self.get_preserved_filters(request)
if hasattr(request, 'err') and request.err:
self.message_user(request, request.err_msg, messages.ERROR)
redirect_url = request.path
redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
return HttpResponseRedirect(redirect_url)
else:
return super(WechatMenuModelAdmin, self).response_change(request, obj)
# 将WechatMenuModel与WechatMenuModelAdmin
admin.site.register(WechatMenuModel, WechatMenuModelAdmin)
上面主要是save_model()下的
obj.save()
menu_create(obj.merchant)
用以完成对微信自定义菜单功能的实现。
服务器上跑一下,是前面几张图的效果。基本达到所需
感觉是写得不够优雅的,有更好的想法的话欢迎留言!