开发一款商用性的微信公众号后端服务,需要对接微信公众号服务接口,实现自动回复功能,以下介绍一下整个项目的开发流程以及探索思路。
微信公众平台:https://mp.weixin.qq.com/
微信支付平台:https://pay.weixin.qq.com/
微信官方文档地址:https://mp.weixin.qq.com/wiki
微信支付-JSAPI开发文档:https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/development.html
微信 JS 接口签名校验工具:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
本次采用技术框架主要以django为主。
def verifys(request):
if request.method == 'GET':
signature = request.GET.get('signature', '')
timestamp = request.GET.get('timestamp', '')
nonce = request.GET.get('nonce', '')
echostr = request.GET.get('echostr', '')
token = '1231231' # 设置的Token
s = [timestamp, nonce, token]
s.sort()
s = ''.join(s)
if hashlib.sha1(s.encode('utf-8')).hexdigest() == signature:
return HttpResponse(echostr)
域名的租赁我用的是阿里云,需要提前进行备案处理。
服务器也是租的阿里云,配置是4g+4core的。
在开发过程还是建议配置一下内网穿透,这样子方便调试,不然每更新一下代码就要上传一次服务器,开发效率很低,尝试了很多内网穿透产品,觉得比较好用的还是这个,本次使用的网云穿:https://xiaomy.net/pay/?type=1
进行微信支付注册,需要给认证费用300块,需要拿到以下信息
NOTIFY_URL = URL+"/recharge/call_pay/" # 回调函数,完整路由,服务器要带上域名,对应的视图是下面4中的回调函数
TRADE_TYPE = 'JSAPI' # 交易方式
MCH_ID = "" # 商户号
API_KEY = "" # 微信商户平台(pay.weixin.qq.com) -->账户设置 -->API安全 -->密钥设置,设置完成后把密钥复制到这里
UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder" # 该url是微信下单api
# UFDODER_URL = "https://api.mch.weixin.qq.com/xdc/apiv2sandbox/pay/unifiedorder" # 该url是微信下单api
CREATE_IP = '' # 服务器IP
开启服务模式后,将无法在微信公众号后台内实现对菜单的自定义,但是微信公众号后台内可以自定义的菜单点击类型十分有限(2/10),可以通过使用微信提供的菜单管理接口来对菜单进行管理。
基本介绍
微信公众号内允许3个一级菜单,每个一级菜单允许5个二级子菜单;一级菜单最多4个汉字,二级菜单最多7个汉字;
菜单项共有10种类型:
#获取 access_token
def get_token():
AppID=settings.APPID
AppSecret=settings.APPSECRET
access_token=''
if access_token ==None:
url=f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={AppID}&secret={AppSecret}"
headers = {
'content-type': 'application/json'
}
res = requests.get(url, headers=headers)
print(res.text)
access_token= json.loads(res.text)['access_token']
redis_get.set("access_token",access_token,1800)
return access_token
接口说明:
接口地址:https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
访问方式:POST
参数说明:
ACCESS_TOKEN即为获取的access_token;
POST的数据为JSON字符串,其中button定义了菜单,为一个JSON数组;数组中每一个元素都是一个一级菜单,其中sub_button属性为该一级菜单的二级菜单,同样也是JSON数组;每一个菜单项包含type(上面提到的10种)、name、key等信息。
{
"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"
}]
}]
}
代码:
# 修改菜单
def menu_start():
access_token=get_token()
url=f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={access_token}"
headers={
'content-type':'application/json'
}
with open("./config_file/menu.json","r+",encoding="utf-8") as f:
data=json.loads(f.read())
res=requests.post(url,headers=headers,data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
if json.loads(res.text)['errmsg']=="ok":
return "菜单初始化成功"
print(res.text)
return "菜单初始化失败"
正确返回消息:
{"errcode":0,"errmsg":"ok"}
出错时返回消息:
{"errcode":40018,"errmsg":"invalid button name size"}
这里需要注意的是,POST的内容类型(content-type)需要设置为application/json;
创建自定义菜单后,可使用该接口查询自定义菜单的结构。如果使用了个性化菜单,那么该接口将返回默认菜单和全部个性化菜单的信息;
请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN
返回数据(无个性化菜单时):
{
"menu": {
"button": [
{
"type": "click",
"name": "今日歌曲",
"key": "V1001_TODAY_MUSIC",
"sub_button": [ ]
},
{
"type": "click",
"name": "歌手简介",
"key": "V1001_TODAY_SINGER",
"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": [ ]
}
]
}
]
}
}
返回结果(有个性化菜单时):
{
"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
}
]
}
接口说明:
http请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN
正确返回消息:
{"errcode":0,"errmsg":"ok"}
错误返回消息同上
为了帮助公众号实现灵活的业务运营,微信公众平台新增了个性化菜单接口,开发者可以通过该接口,让公众号的不同用户群体看到不一样的自定义菜单。该接口开放给已认证订阅号和已认证服务号。
开发者可以使用如下方式标志用户:
用户标签(开发者的业务需求可以借助用户标签来完成)
性别
手机操作系统地区(用户在微信客户端设置的地区)
语言(用户在微信客户端设置的语言)
使用个性化菜单需要有以下几点注意:
个性化菜单要求用户的微信客户端版本在iPhone6.2.2,Android 6.2.4以上,暂时不支持其他版本微信;
菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果;
普通公众号的个性化菜单的新增接口每日限制次数为2000次,删除接口也是2000次,测试个性化菜单匹配结果接口为20000次;
出于安全考虑,一个公众号的所有个性化菜单,最多只能设置为跳转到3个域名下的链接;
创建个性化菜单之前必须先创建默认菜单(默认菜单是指使用普通自定义菜单创建接口创建的菜单)。如果删除默认菜单,个性化菜单也会全部删除;
个性化菜单接口支持用户标签,请开发者注意,当用户身上的标签超过1个时,以最后打上的标签为匹配;
请求方式:POST(请使用https协议)
https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=ACCESS_TOKEN
POST数据为JSON对象;
{
"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"
}]
}],
"matchrule":{
"tag_id":"2",
"sex":"1",
"country":"中国",
"province":"广东",
"city":"广州",
"client_platform_type":"2",
"language":"zh_CN"
}
}
正确返回消息:
{"menuid":"208379533"}——menuid即为该菜单的标记;可用于以后删除使用;
请求方式:POST(请使用https协议)
https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=ACCESS_TOKEN
参数说明:
POST数据为JSON字符串
{"menuid":"208379533"}
正确返回:
{"errcode":0,"errmsg":"ok"}
错误返回:
通用
注意,第3个到第8个的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。
click类型的消息推送:
123456789
view类型的消息推送:
123456789
MENUID //指菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了。
scancode_push类型的消息推送:
1408090502
scancode_waitmsg类型的消息推送:
1408090606
pic_sysphoto类型的消息推送:
1408090651
1
pic_photo_or_album类型的消息推送:
1408090816
1
pic_weixin类型的消息推送:
1408090816
1
location_select类型的消息推送:
1408091189
#xml格式转为字典格式
def trans_xml_to_dict(data_xml):
"""
定义XML转字典的函数
:param data_xml:
:return:
"""
data_dict = {}
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
root = ET.fromstring(data_xml)
for child in root:
data_dict[child.tag] = child.text
return data_dict
请求方式: POST
请求地址:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
各消息类型所需的JSON数据包如下:
文本消息:
{
"touser":"OPENID",
"msgtype":"text",
"text":
{
"content":"Hello World"
}
}
发送文本消息时,文本内容中可以携带跳转小程序的文字链:点击跳小程序
参数说明:
1.data-miniprogram-appid 项,填写小程序appid,则表示该链接跳小程序;
2.data-miniprogram-path项,填写小程序路径,路径与app.json中保持一致,可带参数;
3.对于不支持data-miniprogram-appid 项的客户端版本,如果有herf项,则仍然保持跳href中的网页链接;
注意,data-miniprogram-appid对应的小程序必须与公众号有绑定关系。
发送图片消息:
{
"touser":"OPENID",
"msgtype":"image",
"image":
{
"media_id":"MEDIA_ID"
}
}
发送语音消息:
{
"touser":"OPENID",
"msgtype":"voice",
"voice":
{
"media_id":"MEDIA_ID"
}
}
发送视频消息:
{
"touser":"OPENID",
"msgtype":"video",
"video":
{
"media_id":"MEDIA_ID",
"thumb_media_id":"MEDIA_ID",
"title":"TITLE",
"description":"DESCRIPTION"
}
}
发送音乐消息:
{
"touser":"OPENID",
"msgtype":"music",
"music":
{
"title":"MUSIC_TITLE",
"description":"MUSIC_DESCRIPTION",
"musicurl":"MUSIC_URL",
"hqmusicurl":"HQ_MUSIC_URL",
"thumb_media_id":"THUMB_MEDIA_ID"
}
}
发送图文消息(点击跳转到外链) 图文消息条数限制在8条以内,注意,如果图文数超过8,则将会无响应。
{
"touser":"OPENID",
"msgtype":"news",
"news":{
"articles": [
{
"title":"Happy Day",
"description":"Is Really A Happy Day",
"url":"URL",
"picurl":"PIC_URL"
},
{
"title":"Happy Day",
"description":"Is Really A Happy Day",
"url":"URL",
"picurl":"PIC_URL"
}
]
}
}
发送图文消息:
{
"touser":"OPENID",
"msgtype":"mpnews",
"mpnews":
{
"media_id":"MEDIA_ID"
}
}
发送卡券:
{
"touser":"OPENID",
"msgtype":"wxcard",
"wxcard":{
"card_id":"123dsdajkasd231jhksad"
},
}
发送小程序卡片:
{
"touser":"OPENID",
"msgtype":"miniprogrampage",
"miniprogrampage":
{
"title":"title",
"appid":"appid",
"pagepath":"pagepath",
"thumb_media_id":"thumb_media_id"
}
}
代码
# 发送文本消息
def send_text(self,openid,content):
access_token=wx_interaction.get_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={access_token}"
headers = {
'content-type': 'application/json'
}
data = {
"touser": openid,
"msgtype": "text",
"text":
{
"content": content
}
}
res = requests.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
print(res.text)
return json.loads(res.text)
# 发送图片消息
def send_image(self,openid,media_id):
access_token=wx_interaction.get_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={access_token}"
headers = {
'content-type': 'application/json'
}
data = {
"touser":openid,
"msgtype":"image",
"image":
{
"media_id":media_id
}
}
res = requests.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
print(res.text)
return json.loads(res.text)
扫码查询
def signature(self, current_url):
# 获取access token
access_token = wx_interaction.get_token()
# 获取jsapi ticket
jsapi_ticket = self.get_jsapi_ticket(access_token)
# 在0-9 A-Z a-z里获取随机字符串
noncestr = self.get_nonce_str()
# 获取整型的时间戳
timestamp = int(time.time())
# 当前访问的页面的完整URL
parameters = {
"noncestr": noncestr,
"jsapi_ticket": jsapi_ticket,
"timestamp": timestamp,
"url": current_url
}
# 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串
unsinged_str = '&'.join(['{}={}'.format(key.lower(), parameters[key]) for key in sorted(parameters)])
# 进行sha1签名,得到signature
signedstr = hashlib.sha1(unsinged_str.encode("utf-8")).hexdigest()
# return signedstr, noncestr, timestamp, self.APP_ID
return {
"appId":self.APP_ID,
"timestamp":timestamp,
"nonceStr":noncestr,
"signature":signedstr,
}
参考链接:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#3
主要有两种授权方式:
https://open.weixin.qq.com/connect/oauth2/authorize?appid={appid}&redirect_uri={跳转链接}&response_type=code&scope=snsapi_base&state=“发送消息内容”#wechat_redirect
def get_openid(code):
AppID=settings.APPID
AppSecret=settings.APPSECRET
url=f"https://api.weixin.qq.com/sns/oauth2/access_token?appid={AppID}&secret={AppSecret}&code={code}&grant_type=authorization_code"
headers = {
'content-type': 'application/json'
}
res = requests.get(url, headers=headers)
print(res.text)
openid= json.loads(res.text)['openid']
return openid
前端跳转授权:
https://open.weixin.qq.com/connect/oauth2/authorize?appid={appid}&redirect_uri={跳转链接}&response_type=code&scope=snsapi_userinfo&state=login#wechat_redirect
获取access_token:
关于网页授权access_token和普通access_token的区别
微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息;
其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用
https://api.weixin.qq.com/sns/oauth2/access_token?appid={appid}&secret={secret}&code={前端跳转获取的code}&grant_type=authorization_code
获取个人信息:
https://api.weixin.qq.com/sns/userinfo?access_token={access_token}&openid=11&lang=zh_CN
代码:
def get_user_info(code):
url = f"https://api.weixin.qq.com/sns/oauth2/access_token?appid={settings.APPID}&secret={settings.APPSECRET}&code={code}&grant_type=authorization_code"
res = requests.get(url)
if 'access_token' in json.loads(res.text).keys():
access_token = json.loads(res.text)['access_token']
redis_get.set(code,access_token,3600)
else :
access_token=redis_get.get(code)
url1 = f"https://api.weixin.qq.com/sns/userinfo?access_token={access_token}&openid=11&lang=zh_CN"
res1 = requests.get(url1)
res1.encoding = 'utf-8'
return json.loads(res1.text.encode("utf-8"))
def get_ticket_share(openid):
access_token=get_token()
url=f"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={access_token}"
headers = {
'content-type': 'application/json'
}
print(openid)
data={"expire_seconds": settings.SHARE_TIME, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": openid}}}
res = requests.post(url, headers=headers,data=json.dumps(data))
# print(res.text)
ticket= json.loads(res.text)['ticket']
images=requests.get("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+ticket).content
# print(os.listdir("./"))
with open(f"./static/media/{openid}.png","wb+") as f:
f.write(images)
f.close()
images_cl(f"{openid}.png")
redis_get.set(openid+"_image",add_material(openid),settings.SHARE_TIME)
return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+ticket
def add_material(openid):
acs_token = get_token()
url = f"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={acs_token}&&type=image"
files = {'media': open(rf'./static/media/{openid}.png', 'rb')}
# files = {'media': images}
res = requests.post(url, files=files)
print(res.text)
media_id=json.loads(res.text)['media_id']
return media_id
def send_image(self,openid,media_id):
access_token=wx_interaction.get_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={access_token}"
headers = {
'content-type': 'application/json'
}
data = {
"touser":openid,
"msgtype":"image",
"image":
{
"media_id":media_id
}
}
res = requests.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
print(res.text)
return json.loads(res.text)
确认支付
# 请求统一支付接口,多一个openid,JSAPI方式请求时必须带上这个openid参数
def wxpay_js(order_id, order_name, order_price_detail, order_total_price, openid=''):
nonce_str = random_str() # 拼接出随机的字符串即可,我这里是用 时间+随机数字+5个随机字母
total_fee = int(float(order_total_price) * 100) # 付款金额,单位是分,必须是整数
print(total_fee)
params = {
'appid': settings.APPID, # APPID
'mch_id': settings.MCH_ID, # 商户号
'nonce_str': nonce_str, # 随机字符串
'out_trade_no': order_id, # 订单编号,可自定义
'total_fee': total_fee, # 订单总金额
'spbill_create_ip': settings.CREATE_IP, # 自己服务器的IP地址
'notify_url': settings.NOTIFY_URL, # 回调地址,微信支付成功后会回调这个url,告知商户支付结果
'body': order_name, # 商品描述
'detail': order_price_detail, # 商品描述
'trade_type': settings.TRADE_TYPE, # 扫码支付类型
'openid': openid
}
sign = get_sign(params, settings.API_KEY) # 获取签名
params['sign'] = sign # 添加签名到参数字典
xml = trans_dict_to_xml(params) # 转换字典为XML
response = requests.request('post', settings.UFDODER_URL, data=xml.encode()) # 以POST方式向微信公众平台服务器发起请求
data_dict = trans_xml_to_dict(response.content) # 将请求返回的数据转为字典
# print(response.text)
# print("-----------")
return data_dict
# 获取加密sign
def get_sign(data_dict, key):
"""
签名函数
:param data_dict: 需要签名的参数,格式为字典
:param key: 密钥 ,即上面的API_KEY
:return: 字符串
"""
params_list = sorted(data_dict.items(), key=lambda e: e[0], reverse=False) # 参数字典倒排序为列表
params_str = "&".join(u"{}={}".format(k, v) for k, v in params_list) + '&key=' + key
# 组织参数字符串并在末尾添加商户交易密钥
md5 = hashlib.md5() # 使用MD5加密模式
md5.update(params_str.encode('utf-8')) # 将参数字符串传入
sign = md5.hexdigest().upper() # 完成加密并转为大写
print(sign)
return sign
def wx_pay_js1(order_id, order_name, order_detail, total_price, openid):
# data = json.loads(request.body)
# print(request.body)
total_price = 0.01 # 订单总价
# order_name = 'asdsd' # 订单名字
# order_detail = 'ewe' # 订单描述
# order_id = 20200411234567 # 自定义的订单号
# openid = "" # 用户的openid,在这种类型中支付必传
data_dict = wxpay_js(order_id, order_name, order_detail, total_price, openid)
# 如果请求成功
if data_dict.get('return_code') == 'SUCCESS':
prepay_id = data_dict.get('prepay_id', "")
nonce_str = data_dict.get('nonce_str', "")
data = {} # 前端需要这些参数才能调用微信支付页面
data['appId'] = settings.APPID
data['timeStamp'] = str(time.time()) # 必填,生成签名的时间戳
data['nonceStr'] = nonce_str
data['package'] = "prepay_id=" + prepay_id
data['signType'] = "MD5" # 添加签名加密类型
# data['signType'] = "HMACSHA256" # 添加签名加密类型
sign = get_sign(data, settings.API_KEY) # 获取签名
data['paySign'] = sign # 添加签名到参数字典
if prepay_id:
s = {
"code": 1000,
"msg": "获取成功",
"data": data
}
# s = json.dumps(s, ensure_ascii=False)
print(s)
print("-----------")
return s
s = {
"code": 1001,
"msg": "获取失败",
"data": ""
}
print(s)
s = json.dumps(s, ensure_ascii=False)
return HttpResponse(s)
def order_status(out_trade_no):
url = "https://api.mch.weixin.qq.com/pay/orderquery"
key = settings.API_KEY # 商户api密钥
params = {
'appid': settings.APPID, # APPID
'mch_id': settings.MCH_ID, # 商户号
'out_trade_no': out_trade_no, # 订单编号
'nonce_str': "21deqew" # 随机字符串
}
sign = get_sign(params, key) # 获取签名
params.setdefault('sign', sign) # 添加签名到参数字典
xml = trans_dict_to_xml(params) # 转换字典为XML
response = requests.request('post', url, data=xml) # 以POST方式向微信公众平台服务器发起请求
data_dict = trans_xml_to_dict(response.content) # 将请求返回的数据转为字典
return data_dict['trade_state_desc']