注:本文仅提供 pc 端微信扫码支付(模式一)的示例代码。
关于对接过程中遇到的问题总结在本文最下方。
参考: 官方文档,
https://blog.csdn.net/lm_is_dc/article/details/83312706
一。wxpay_settings.py (配置基本参数和创建订单时必要的方法,如 随机生成字符串,加密签名,生成支付二维码等)
# encoding: utf-8
import random
import os
import time
import requests
import hashlib
from random import Random
import qrcode
from bs4 import BeautifulSoup
from appname import settings
APP_ID = "" # 公众账号appid
MCH_ID = "" # 商户号
API_KEY = "" # 微信商户平台(pay.weixin.qq.com) -->账户设置 -->API安全 -->密钥设置,设置完成后把密钥复制到这里
UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder" # url是微信下单api
NOTIFY_URL = "http://xxx/get_pay/callback/" # 微信支付结果回调接口,需要你自定义
CREATE_IP = '111.111.11.11' # 你服务器上的ip
def random_str(randomlength=8):
"""
生成随机字符串
:param randomlength: 字符串长度
:return:
"""
str = ''
chars = 'AaBbCcDdEeFfGgHhIiJjKbkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
length = len(chars) - 1
random = Random()
print "randomlength : ", randomlength
for i in range(randomlength):
str += chars[random.randint(0, length)]
print "str : ", str
return str
def order_num(phone):
"""
生成扫码付款订单号
:param phone: 手机号
:return:
"""
local_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
result = phone + 'T' + local_time + random_str(5)
return result
def get_sign(data_dict, key):
# 签名函数,参数为签名的数据和密钥
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() # 完成加密并转为大写
return sign
def trans_dict_to_xml(data_dict): # 定义字典转XML的函数
data_xml = []
for k in sorted(data_dict.keys()): # 遍历字典排序后的key
v = data_dict.get(k) # 取出字典中key对应的value
if k == 'detail' and not v.startswith('<![CDATA['): # 添加XML标记
v = '<![CDATA[{}]]>'.format(v)
data_xml.append('<{key}>{value}</{key}>'.format(key=k, value=v))
return '<xml>{}</xml>'.format(''.join(data_xml)).encode('utf-8') # 返回XML,并转成utf-8,解决中文的问题
def trans_xml_to_dict(data_xml):
soup = BeautifulSoup(data_xml, features='xml')
xml = soup.find('xml') # 解析XML
if not xml:
return {}
data_dict = dict([(item.name, item.text) for item in xml.find_all()])
return data_dict
def wx_pay_unifiedorde(detail):
"""
访问微信支付统一下单接口
:param detail:
:return:
"""
detail['sign'] = get_sign(detail, API_KEY)
xml = trans_dict_to_xml(detail) # 转换字典为XML
response = requests.request('post', UFDODER_URL, data=xml) # 以POST方式向微信公众平台服务器发起请求
return response.content
def pay_fail(err_msg):
"""
微信支付失败
:param err_msg: 失败原因
:return:
"""
data_dict = {'return_msg': err_msg, 'return_code': 'FAIL'}
return trans_dict_to_xml(data_dict)
def create_qrcode(username, url):
"""
生成扫码支付二维码
:param username: 用户名
:param url: 支付路由
:return:
"""
get_path = "%s/%s" % (settings.MEDIA_ROOT, "QRcode") # 创建文件夹路径
img_path = "%s/%s" % (settings.MEDIA_ROOT, "QRcode/") # 创建文件路径
img = qrcode.make(url) # 创建支付二维码片
# 你存放二维码的地址 uploads/media
img_url = img_path + username + '.png' # 创建文件
if not os.path.exists(get_path):
os.makedirs(get_path)
img.save(img_url)
ret_url = settings.MEDIA_URL + 'QRcode/' + username + '.png' # 前端展示路径
return ret_url
二。URL
url(r'^create_order/$', CreateOrderViews.as_view()), # 创建订单和生成二维码
url(r'^get_pay/$', Wxpay_QRccode.as_view()), # 微信支付二维码展示页 wx
url(r'^get_pay/callback/$', Wxpay_ModelOne_pay.as_view()), # 支付回调接口 wx
三。Views
class CreateOrderViews(APIView):
"""
创建订单和支付二维码
"""
# 生成订单(省略部分操作..)
order_obj = models.Order.objects.create()
# 调用微信支付 (下面部分参数的详细说明请看第一步骤)
paydict = {
'appid': APP_ID,
'mch_id': MCH_ID,
'nonce_str': random_str(),
'product_id': order_obj.id, # 商品id,可自定义
'time_stamp': int(time.time()),
}
paydict['sign'] = get_sign(paydict, API_KEY)
url = "weixin://wxpay/bizpayurl?appid=%s&mch_id=%s&nonce_str=%s&product_id=%s&time_stamp=%s&sign=%s" \
% (paydict['appid'], paydict['mch_id'], paydict['nonce_str'], paydict['product_id'],
paydict['time_stamp'], paydict['sign'])
# 可以直接在微信中点击该url,如果有错误,微信会弹出提示框,如果是扫码,如果失败,什么提示都没有,不利于调试
# 创建二维码
img_url = create_qrcode(user_obj.username, url) # 调用生成二维码的方法(具体看第一步 wxpay_settings.py 的create_qrcode方法)
order_obj.wx_pay_path = img_url # 将支付二维码路径存储在订单表中
order_obj.save() # 保存订单
pay_url = "/order/get_pay/?order_id=%s" % (order_obj.id)
return JSONResponse({"pay_url": pay_url})
class Wxpay_QRccode(APIView):
"""
返回二维码图片路径接口
"""
def get(self, request, *args, **kwargs):
username = request.session.get('username')
if username:
order_id = request.GET.get("order_id")
order_obj = Order.objects.filter(id=order_id, create_user__username=username,
status=Order.STATUS_DEFAULT).first()
return JSONResponse({"pay_img": order_obj.wx_pay_path}) # 从数据库中获取图片路径
return JSONResponse({"error": "xxx"})
@method_decorator(csrf_exempt, name='dispatch')
class Wxpay_ModelOne_pay(APIView):
"""
使用微信扫一扫扫描二维码,微信系统会自动回调此路由,Post请求
"""
def post(self, request, *args, **kwargs):
"""
扫描二维码后,微信系统回调的地址处理
微信传来的参数格式经trans_xml_to_dict()转成字典
{'openid': 'xxx',
'is_subscribe': 'Y',
'mch_id': 'xxx',
'nonce_str': 'xxx',
'sign': 'xxx',
'product_id': 'xxx',
'appid': 'xxx'}
:param request:
:param args:
:param kwargs:
:return:
"""
try:
data_dict = trans_xml_to_dict(request.body) # 回调数据转字典
sign = data_dict.pop('sign') # 取出签名
key = API_KEY # 商户交易密钥
back_sign = get_sign(data_dict, key) # 计算签名
if data_dict.get('product_id'): # 第一次微信扫码请求,确认是否有该订单(并确认订单状态等是否正确)
order_obj = Order.objects.filter(id=data_dict.get('product_id'), status=Order.STATUS_DEFAULT).first()
if not order_obj:
return HttpResponse(pay_fail('交易信息有误,未找到该订单'))
elif data_dict.get('return_code') == "SUCCESS": # 此处是支付成功后的回调,第二次请求(用于修改订单状态,保存openid等)
order_obj = Order.objects.filter(vx_out_trade_no=data_dict.get('out_trade_no'),
status=Order.STATUS_DEFAULT).first()
with transaction.atomic(): # 开启事务操作
order_obj.status = Order.STATUS_PAYED
order_obj.pay_mode = Order.WEIXIN
order_obj.save()
return HttpResponse("SUCCESS") # 最终记得返回 SUCCESS,否则微信会重复访问该接口,并返回支付结果。
else:
return HttpResponse(pay_fail('支付状态返回错误!'))
if sign == back_sign: # 验证签名是否与回调签名相同(第一次请求)
params = {
'appid': APP_ID, # APPID
'mch_id': MCH_ID, # 商户号
'nonce_str': random_str(16), # 随机字符串
'out_trade_no': order_num(data_dict.get('product_id')), # 订单编号
'total_fee': int(Decimal(order_obj.total_fee) * 100), # 付款金额,单位是分,必须是整数
'spbill_create_ip': CREATE_IP, # 发送请求服务器的IP地址
'body': 'xxx', # 商品描述
'detail': 'xxx', # 商品详情
'notify_url': NOTIFY_URL, # 微信支付结果回调接口
'trade_type': 'NATIVE', # 扫码支付类型
}
# 调用微信统一下单支付接口url
notify_result = wx_pay_unifiedorde(params)
if data_dict.get('product_id'):
# 保存微信订单(用于后续修改订单状态)
order_obj = Order.objects.filter(id=data_dict.get('product_id'),
status=Order.STATUS_DEFAULT).first()
order_obj.vx_out_trade_no = params.get('out_trade_no')
order_obj.save()
return HttpResponse(notify_result)
return HttpResponse(pay_fail('交易信息有误,请重新扫码'))
except Exception as e:
return HttpResponse(pay_fail('程序异常'))
四。错误总结
1. 开发中第一个错误就是扫码时的服务器繁忙(好像是叫这个错误来着。),解决方案:去微信公众号后台设置服务器地址。
2. 参数错误的话就请检查下是否和微信公众号里面的匹配。
3. 参数回调问题:NOTIFY_URL 这个参数填异步回调地址(必须是服务器地址,微信公众号后台设置),
data_dict = trans_xml_to_dict(request.body) 这个方式是可以拿到微信返回的所有回调参数,
其实这里会被请求两次,第一次是扫码请求,这里主要是给让用户看到支付前微信生成商品信息(商品价格等),第二次是扫码后的支付成功/失败的回调请求,这里主要就是对数据库的操作了(订单状态,商品属性等),data_dict.get('return_code') == "SUCCESS" ,return_code参数是只有回调才会传,所以根据这个判断需要做什么操作。
五。 本文可能有些错误的地方,欢迎指出,同时有什么问题也欢迎提出。