django——公众号服务开发

开发过程

  • 项目背景:
  • 功能描述:
  • 参考文档以及调试链接:
  • 技术架构:
    • 准备工作
    • 公众号的注册以及设置
    • 域名的准备
    • 服务器的租赁
    • 内网穿透
    • 微信支付的注册
  • 功能开发细节
    • 微信公众号自定义菜单
      • 获取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']

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/153408.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx反向代理与负载均衡与504错误

Nginx反向代理与负载均衡概念简介 关于代理 什么是代理 类似中介 在没有代理模式的情况下&#xff0c;客户端和Nginx服务端&#xff0c;都是客户端直接请求服务端&#xff0c;服务端直接响应客户端。 那么在互联网请求里面&#xff0c;客户端往往无法直接向服务端发起请求…

【LeetCode刷题-滑动窗口】--76.最小覆盖子串

76.最小覆盖子串 class Solution {//建立两个hashMap&#xff0c;ori用于存储目标字符串t中每个字符的出现次数//cnt用于存储当前窗口中每个字符的出现次数Map<Character,Integer> ori new HashMap<Character,Integer>();Map<Character,Integer> cnt new H…

PyTorch:计算图

在深度学习和神经网络领域&#xff0c;计算图是一种重要的概念&#xff0c;它在理解和实现神经网络模型的训练过程中起着至关重要的作用。PyTorch作为一款优秀的深度学习框架&#xff0c;自然也包含了计算图的概念和实现。本文将深入探讨PyTorch中计算图的原理、应用以及对深度…

mp4封装格式各box类型讲解及IBP帧计算

作者 —— 靑い空゛ 出处&#xff1a;http://www.cnblogs.com/ailumiyana/ 音视频流媒体高级开发教程 MP4文件封装格式&#xff0c;对应的标准为ISO/IEC 14496-12&#xff0c;即信息技术 视听对象编码的第12部分 ISO 基本媒体文件格式&#xff08;Information technology Codi…

最新版仿东郊到家小程序源码 上门服务小程序源码

最新版仿东郊到家小程序源码 上门服务小程序源码 1、数据概况&#xff08;新增业务城市用户投票功能&#xff0c;更加直观的查看业务城市的关注度、人气和影响力,促进业务开展&#xff09; 2、数据概况 &#xff08;增加可视化数据大盘&#xff0c;代理商端可查看自己下面的技…

【java学习—十五】线程的同步与死锁(5)

文章目录 1. 多线程产生的问题2. Synchronized 的使用方法3. 线程的死锁问题 1. 多线程产生的问题 问题&#xff1a; 同一个账户&#xff0c;支付宝转账&#xff0c;微信转账。两个手机&#xff0c;一个手机开支付宝&#xff0c;另一个手机开微信。假设账户上有3000元&#xff…

OCC教学:拓扑

拓扑&#xff1a;1.介绍 几何限制 OCCT 曲面支持矩形修剪。布尔运算后可能会出现非矩形域。 如何存储剪切操作的结果&#xff1f; 拓扑的目的 一般来说&#xff0c;拓扑是描述对象局限性的一种手段。 OCC拓扑被用于用于描述&#xff1a; 物体的边界&#xff1b;对象之…

Mars3d-vue最简项目模板集成使用Mars3d的UI控件样板

备注说明&#xff1a; 1.小白可看步骤一二&#xff0c;进阶小白可直接看步骤三 步骤一&#xff1a;新建文件夹<uitest>&#xff0c;在mars3d仓库拉一份最简项目模板&#xff1a; git clone mars3d-vue-template: Vue3.x 技术栈下的Mars3D项目模板 步骤二&#xff1a;运…

01_SHELL编程之变量定义(一)

SHELL编程 该课程主要包括以下内容&#xff1a; ① Shell的基本语法结构 如&#xff1a;变量定义、条件判断、循环语句(for、until、while)、分支语句、函数和数组等&#xff1b; ② 基本正则表达式的运用&#xff1b; ③ 文件处理三剑客&#xff1a;grep、sed、awk工具的使用&…

最好用的Python库推荐总结,每一个都用处很大!

文章目录 分词 - jieba词云库 - wordcloud可视化进度条 - tpdm优美的表格 - PrettyTable多进程 - multiprocessing多线程 - threading谷歌翻译 - googletrans重复回调 - retrying游戏开发 - pygame绘图教程 - turtle数据分析 - pandas算法加密 - pycryto操作 win 电脑 - pywin3…

2—10岁女童羽绒服,黑色长款也太好看了吧

冬天怎么能没有一件暖呼呼的羽绒服呢&#xff1f; 黑色长款羽绒服也赞了吧 大长款连帽&#xff0c;防风保暖设计 时尚与美观度都兼具呢&#xff01;好穿又耐穿&#xff01;

qt定时器的使用

在QWidget中进行声明

大数据毕业设计之前端01:我的前端之路

初遇前端 初次接触前端还是2016年&#xff0c;那一年暑假心血来潮&#xff0c;在网易云课堂上学着前端三剑客&#xff08;html、js、css&#xff09;。18年毕业&#xff0c;把用各色水笔手写的花花绿绿笔记寄回家里&#xff0c;投身奔赴后端与大数据开发的征程。 遥记18年的毕…

关于hadoop报错ERROR: Cannot set priority of namenode process与jps仅有自身的某类解决办法

运行start-sh.all发现了如图的问题 也是搞了很久搜了很多教程&#xff0c;发现很多人并不是大毛病而是很多小细节出了错误。 首先检查如下hadoop-env.sh &#xff0c;core-site.xml &#xff0c;hdfs-site.xml &#xff0c;mapred-site.xml &#xff0c;yarn-site.xml 内容是…

flutter 绘制右上角圆角三角形标签

绘制&#xff1a; import package:jade/utils/JadeColors.dart; import package:flutter/material.dart; import dart:math as math;class LabelTopRightYellow extends StatefulWidget {final String labelTitle; // 只能两个字的&#xff08;文字偏移量没有根据文字长度改变…

要在伦敦银技术分析史上留名 这可能吗?

在学习伦敦银投资的时候&#xff0c;我们都很羡慕那些以人的名字命名的交易工具或者策略&#xff0c;例如布林带、帝纳波利点位、加特利形态、艾略特波浪理论等等。投资者也有一个希望&#xff0c;就是开发属于自己的交易策略或者工具&#xff0c;这并不是不可能的&#xff0c;…

C 语言指针和数组

C 语言指针和数组 在本教程中&#xff0c;您将了解C语言编程中数组与指针之间的关系。您还将学习使用指针访问数组元素。 在了解数组与指针之间的关系之前&#xff0c;请确保检查以下两个主体&#xff1a; [C 数组](C 语言数组-CSDN博客)[C 指针](C 语言指针-CSDN博客) 数组…

springboot327基于Java的医院急诊系统

交流学习&#xff1a; 更多项目&#xff1a; 全网最全的Java成品项目列表 https://docs.qq.com/doc/DUXdsVlhIdVlsemdX 演示 项目功能演示&#xff1a; ————————————————

解决编译时提示“没有那个文件或目录 #include <pcap.h>”的问题

解决编译时提示“没有那个文件或目录 #include 当你在编译代码时遇到“没有那个文件或目录 #include <pcap.h>”的错误提示&#xff0c;这通常意味着编译器在你的系统路径中找不到 pcap.h 头文件。pcap.h 是网络流量捕获库 pcap 的头文件&#xff0c;用于在 C/C 程序中捕…

高效能人士的七个习惯

今天小编给大家推荐最近读的一本书&#xff0c;史蒂芬柯维的《高效能人士的七个习惯》&#xff0c;分别是积极主动、以始为终、要事第一、双赢思维、知己解彼、综合高效及不断更新。 一、个人领域&#xff1a;从依赖到独立 习惯一&#xff1a;积极主动——个人愿景的原则付诸行…