开发过程
- 项目背景:
- 功能描述:
- 参考文档以及调试链接:
- 技术架构:
- 准备工作
- 公众号的注册以及设置
- 域名的准备
- 服务器的租赁
- 内网穿透
- 微信支付的注册
- 功能开发细节
- 微信公众号自定义菜单
- 获取access_token
- 创建菜单
- 查询菜单
- 删除菜单
- 个性化菜单
- 创建个性化菜单:
- 删除个性化菜单:
- 自定义菜单事件接收到的消息
- 接收xml形式转为json格式
- 主动向用户发送消息
- 扫码功能
- 网页调取扫一扫
- 后端加密接口
- 获取用户信息
- snsapi_base授权获取openid
- snsapi_userinfo获取个人信息
- 推荐码的生成
- 获取分享二维码
- 添加素材资源到微信平台
- 发送图片消息
- 微信支付
- 前端调用支付
- 后端代码
- 获取订单状态
项目背景:
开发一款商用性的微信公众号后端服务,需要对接微信公众号服务接口,实现自动回复功能,以下介绍一下整个项目的开发流程以及探索思路。
功能描述:
- 自动回复
- 菜单栏生成
- 扫码功能
- 微信支付功能
- 获取微信个人信息
- 推荐码的生成
参考文档以及调试链接:
微信公众平台: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为主。
准备工作
公众号的注册以及设置
- 微信公众号需要进行服务号认证,认证费是300块,必须认证,不然很多功能没有权限实现。
- 获取appid以及AppSecret
- 进行服务器配置
这一步就需要在后端开启一个端口接收解密并处理
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)
- 白名单的设置
- 设置js授权域名
域名的准备
域名的租赁我用的是阿里云,需要提前进行备案处理。
服务器的租赁
服务器也是租的阿里云,配置是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种类型:
- click:当用户点击该类子菜单时,微信系统将向响应系统推送类型为event的消息并附带该菜单项的key值,开发者可以据此对该事件做出响应;
- view:当用户点击该类子菜单时,微信客户端将打开开发者在该菜单项中设置的网页URL,在该URL内开发者可以配合网页授权接口获得用户的基本信息以开展业务服务;
- scancode_push:当用户点击此类型子菜单时,微信客户端将调起扫一扫工具,并将结果展示给用户,如果识别结果为URL,将进入该URL,同时响应系统将接收到该消息;
- scancode_waitmsg:当用户点击该类子菜单时,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
pic_sysphoto:当用户点击该类子菜单时,微信客户端将调起系统相机,完成拍照操作后,将拍摄的图片发送给响应系统,同时收起系统相机,等待开发者下发消息; - pic_photo_or_album:基本功能同上,但是会给用户两种选择:拍照或者系统相册上传;
- pic_weixin:基本功能同上,但是会使用微信相册;
- location_select:弹出地理位置选择器,完成操作后,将选择地理信息发送给响应系统,收起地理位置选择器后,等待开发者下发消息;
- media_id:当用户点击该类子菜单项后,微信系统将对应永久素材id的素材下发给用户;
- view_limited:当用户点击该类型子菜单时,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL;
获取access_token
#获取 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类型的消息推送:
<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>
view类型的消息推送:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[VIEW]]></Event>
<EventKey><![CDATA[www.qq.com]]></EventKey>
<MenuId>MENUID</MenuId>//指菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了。
</xml>
scancode_push类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090502</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[scancode_push]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<ScanCodeInfo><ScanType><![CDATA[qrcode]]></ScanType>
<ScanResult><![CDATA[1]]></ScanResult>
</ScanCodeInfo>
</xml>
scancode_waitmsg类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090606</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[scancode_waitmsg]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<ScanCodeInfo><ScanType><![CDATA[qrcode]]></ScanType>
<ScanResult><![CDATA[2]]></ScanResult>
</ScanCodeInfo>
</xml>
pic_sysphoto类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090651</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[pic_sysphoto]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<SendPicsInfo><Count>1</Count>
<PicList><item><PicMd5Sum><![CDATA[1b5f7c23b5bf75682a53e7b6d163e185]]></PicMd5Sum>
</item>
</PicList>
</SendPicsInfo>
</xml>
pic_photo_or_album类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090816</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[pic_photo_or_album]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<SendPicsInfo><Count>1</Count>
<PicList><item><PicMd5Sum><![CDATA[5a75aaca956d97be686719218f275c6b]]></PicMd5Sum>
</item>
</PicList>
</SendPicsInfo>
</xml>
pic_weixin类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408090816</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[pic_weixin]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<SendPicsInfo><Count>1</Count>
<PicList><item><PicMd5Sum><![CDATA[5a75aaca956d97be686719218f275c6b]]></PicMd5Sum>
</item>
</PicList>
</SendPicsInfo>
</xml>
location_select类型的消息推送:
<xml><ToUserName><![CDATA[gh_e136c6e50636]]></ToUserName>
<FromUserName><![CDATA[oMgHVjngRipVsoxg6TuX3vz6glDg]]></FromUserName>
<CreateTime>1408091189</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[location_select]]></Event>
<EventKey><![CDATA[6]]></EventKey>
<SendLocationInfo><Location_X><![CDATA[23]]></Location_X>
<Location_Y><![CDATA[113]]></Location_Y>
<Scale><![CDATA[15]]></Scale>
<Label><![CDATA[ 广州市海珠区客村艺苑路 106号]]></Label>
<Poiname><![CDATA[]]></Poiname>
</SendLocationInfo>
</xml>
接收xml形式转为json格式
#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"
}
}
发送文本消息时,文本内容中可以携带跳转小程序的文字链:<a href="http://www.qq.com" data-miniprogram-appid="appid" data-miniprogram-path="pages/index/index">点击跳小程序</a>
参数说明:
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)
扫码功能
网页调取扫一扫
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=Yes">
<title>扫码查询</title>
</head>
<body>
</body>
</html>
<script src="../static/js/jweixin-1.6.0.js"></script>
<script src="../static/js/jquery-3.4.1.min.js"></script>
<script>
$(document).ready(function () {
$.get("/signature", function (obj) {
wx.config({
beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: obj.appId, // 必填,公众号的唯一标识
timestamp: obj.timestamp, // 必填,生成签名的时间戳 <%= Html.Encode(ViewData["timestamp" ]) %>
nonceStr: obj.nonceStr, // 必填,生成签名的随机串
signature: obj.signature, // 必填,签名
jsApiList: ['checkJsApi', 'scanQRCode'] // 必填,需要使用的JS接口列表, 这里只需要调用扫一扫
});
});
wx.ready(function () {
wx.checkJsApi({ //判断当前客户端版本是否支持指定JS接口
jsApiList: ['scanQRCode'],
success: function (res) {// 以键值对的形式返回,可用true,不可用false。如:{"checkResult":{"scanQRCode":true},"errMsg":"checkJsApi:ok"}
if (res.checkResult.scanQRCode != true) {
alert('抱歉,当前客户端版本不支持扫一扫');
}
},
fail: function (res) { //检测getNetworkType该功能失败时处理
alert('checkJsApi error');
}
}); //wx.checkJsApi结束
// 调起企业微信扫一扫接口
wx.scanQRCode({
desc: 'scanQRCode desc',
needResult: 1, // 默认为0,扫描结果由企业微信处理,1则直接返回扫描结果,
scanType: ["qrCode", "barCode"], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
// console.log("调用扫描成功", res);
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
// $("#codeValue").val(res.resultStr);//显示结果
// alert("扫码结果为:" + res);
},
error: function (res) {
console.log(res)
if (res.errMsg.indexOf('function_not_exist') > 0) {
alert('版本过低请升级')
}
}
}); //wx.scanQRcode结束
}); //wx.ready结束
wx.error(function (res) {
alert("错误信息:" + JSON.stringify(res));
});
});
</script>
后端加密接口
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
主要有两种授权方式:
- 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
- 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
snsapi_base授权获取openid
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
snsapi_userinfo获取个人信息
前端跳转授权:
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)
微信支付
前端调用支付
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=Yes">
<title>确认支付</title>
</head>
<body>
</body>
</html>
<script src="../static/js/jweixin-1.6.0.js"></script>
<script src="../static/js/jquery-3.4.1.min.js"></script>
<script>
$(document).ready(function () {
$.get("/signature", function (obj) {
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: obj.appId, // 必填,公众号的唯一标识
timestamp: obj.timestamp, // 必填,生成签名的时间戳
nonceStr: obj.nonceStr, // 必填,生成签名的随机串
signature: obj.signature,// 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
});
});
wx.ready(function () {
wx.checkJsApi({ //判断当前客户端版本是否支持指定JS接口
jsApiList: ['chooseWXPay'],
success: function (res) {// 以键值对的形式返回,可用true,不可用false。如:{"checkResult":{"scanQRCode":true},"errMsg":"checkJsApi:ok"}
if (res.checkResult.chooseWXPay != true) {
alert('抱歉,当前客户端版本不支持充值');
}
},
fail: function (res) { //检测getNetworkType该功能失败时处理
alert('checkJsApi error');
}
}); //wx.checkJsApi结束
wx.chooseWXPay({
beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
debug: false,
timestamp: "{{obj.data.timeStamp}}", // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: "{{obj.data.nonceStr}}", // 支付签名随机串,不长于 32 位
package: "{{obj.data.package}}", // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: "{{obj.data.signType}}", // 微信支付V3的传入RSA,微信支付V2的传入格式与V2统一下单的签名格式保持一致
paySign: "{{obj.data.paySign}}", // 支付签名
success: function (res) {
window.location.href = "/exit"
// 支付成功后的回调函数,比如支付成功后提示,或返回某个页面
}, cancel: function (res) {
history.go(-1)
}, fail: function (res) {
window.location.href = "/exit"
// 支付成功后的回调函数,比如支付成功后提示,或返回某个页面
}
});
}); //wx.ready结束
wx.error(function (res) {
alert("错误信息:" + JSON.stringify(res));
});
});
</script>
后端代码
# 请求统一支付接口,多一个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']