微信APP支付V3版本签名详解与优化指南
问题背景
最近接入微信支付,微信官方并没有提供Python版的服务端SDK,因而只能根据文档手动实现一版,这里记录一下微信支付的整体流程、踩坑过程与最终具体实现。
微信支付APP下单流程
根据微信官方文档: 开发指引-APP支付 | 微信支付商户平台文档中心
开发指引-APP支付 | 微信支付商户平台文档中心
下单流程:
和支付宝不同,微信多了一个预付单的概念,这里把APP下单实际分为四大部分,其中包含请求微信后端需要的首次签名和需要返回给APP的二次支付信息签名–这里踩一个小坑,流程图中并没把第二次签名支付信息需要返回给APP的步骤画出来(即下面的步骤6.5),因而一开始误以为只需要返回prepay_id给客户端,导致校验失败。
一. 对应步骤1~4,APP 请求业务后端,业务后台进行V3签名后,请求微信后端生成预付单prepay_id
二. 对应步骤5~6.5,业务后端收到微信后端返回prepay_id,将支付相关参数打包进行二次签名后返回给APP,这里相比流程图多了一个6.5–即业务后端返回签名支付信息到APP
三. 对应步骤7~18,APP收到业务后端返回签名支付信息后调起SDK发起支付请求,收到同步消息结果通知
实现方法:
这是我实际的商品购买的业务代码,用户需要登录授权之后,在下单是把用户的openid和商品价格商品描述发送给后端。
需要安装
wechatpayv3
pip install wechatpayv3
我封装好的支付方法: Pay.py
import json
from wechatpayv3 import WeChatPay, WeChatPayType
from utils.pay.config import *
class Pay():
def __init__(self):
"""
:param wechatpay_type: 微信支付类型,示例值:WeChatPayType.MINIPROG
:param mchid: 直连商户号,示例值:'1230000109'
:param private_key: 商户证书私钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...'
:param cert_serial_no: 商户证书序列号,示例值:'444F4864EA9B34415...'
:param appid: 应用ID,示例值:'wxd678efh567hg6787'
:param apiv3_key: 商户APIv3密钥,示例值:'a12d3924fd499edac8a5efc...'
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php'
:param cert_dir: 平台证书存放目录,示例值:'/server/cert'
:param partner_mode: 接入模式,默认False为直连商户模式,True为服务商模式
:param proxy: 代理设置,示例值:{"https": "http://10.10.1.10:1080"},没有就是None
:param timeout: 超时时间,示例值:(10, 30), 10为建立连接的最大超时时间,30为读取响应的最大超时实践
"""
self.wxpay = WeChatPay(
wechatpay_type=WeChatPayType.MINIPROG,
mchid=MCHID,
private_key=PRIVATE_KEY,
cert_serial_no=CERT_SERIAL_NO,
apiv3_key=APIV3_KEY,
appid=APPID,
notify_url=NOTIFY_URL,
cert_dir=CERT_DIR,
partner_mode=PARTNER_MODE,
proxy=PROXY,
timeout=TIMEOUT,
)
def pay(self,openid,price,nonce_str,description):
"""
:param openid: 微信用户的唯一标识
:param price: 该订单的价格
:param nonce_str: 请求随机串nonce_str,与签名使用的随机字符串值仙童
:param description: 商户证书序列号,示例值:'444F4864EA9B34415...'
return prepay_id : 【预支付交易会话标识】 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
"""
code, message = self.wxpay.pay(
description=description,
out_trade_no=nonce_str,
amount={'total': price},
payer={'openid': openid}
)
# print('code: %s, message: %s' % (code, message))
message = json.loads(message)
print(message)
return message["prepay_id"]
def sign(self,prepay_id, timeStamp, nonce_str):
"""
:param prepay_id: 预支付交易会话标识
:param price: 该订单的价格
:param nonce_str: 请求随机串nonce_str,与签名使用的随机字符串值仙童
:param description: 商户证书序列号,示例值:'444F4864EA9B34415...'
return prepay_id : 【预支付交易会话标识】 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
"""
# :微信支付订单采用RSAwithSHA256算法时,示例值: ['wx888', '1414561699', '5K8264ILTKCH16CQ2502S....','prepay_id=wx201410272009395522657....']
sign = [APPID, timeStamp, nonce_str, f'prepay_id={prepay_id}']
# print(self.wxpay.sign(sign))
paySign = self.wxpay.sign(sign)
return {
"timeStamp": timeStamp,
"nonceStr": nonce_str,
"package": f"prepay_id={prepay_id}",
"signType": "RSA",
"paySign": paySign
}
def decrypt_callback(self,headers, body):
"""
:param headers:
:param body:
return prepay_id
"""
return self.wxpay.decrypt_callback(headers, body)
在pay方法中需要构造对微信官方的请求:
JSAPI下单(可以看里面官方需要的必须数据:)
sign方法中是对应答prepay_id做签名处理:
如何生成请求签名(官方)
并返回前端需要的对接官方接口:wx.requestPayment(Object object)的请求数据:
decrypt_callback方法是对回调的通知接口,用于接收微信官方的响应,判断支付是否成功
django框架下实现代码:
config.py配置文件代码:
# 微信支付商户号(直连模式)或服务商商户号(服务商模式,即sp_mchid)
MCHID = '***************'
# 商户证书私钥
with open('utils/zhi_cert/apiclient_key.pem') as f:
PRIVATE_KEY = f.read()
# 商户证书序列号
CERT_SERIAL_NO = '***************'
# API v3密钥, https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_2.shtml
APIV3_KEY = '***************'
# APPID,应用ID或服务商模式下的sp_appid
APPID = '***************'
# 写自己小程序的密钥
AppSecret = "***************"
# 回调地址,也可以在调用接口的时候覆盖
NOTIFY_URL = 'http://63m4gj.natappfree.cc/front-lp/wechatnotify'
# 微信支付平台证书缓存目录,初始调试的时候可以设为None,首次使用确保此目录为空目录。
CERT_DIR = './zhijiao/cert'
# 接入模式:False=直连商户模式,True=服务商模式。
PARTNER_MODE = False
# 代理设置,None或者{"https": "http://10.10.1.10:1080"},详细格式参见[https://requests.readthedocs.io/en/latest/user/advanced/#proxies](https://requests.readthedocs.io/en/latest/user/advanced/#proxies)
PROXY = None
# 请求超时时间配置
TIMEOUT = (10, 30) # 建立连接最大超时时间是10s,读取响应的最大超时时间是30s
然后接口代码实现与前端请求响应代码:url.py
from django.urls import path
from appFrontLP.views import *
from django.conf.urls.static import static
urlpatterns=[
path("wechatnotify", WeChatNotifyView.as_view()),#微信支付回调通知
path('wechatpay', WeChatPayView.as_view()),#支付
]
视图函数view.py方法
class WeChatPayView(APIView):
def post(self, request):
openid = request.data.get('openid')
price = int(request.data.get('price'))
description = request.data.get('description')
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
current_time = datetime.now()
current_date = int(datetime.timestamp(current_time))
pay = Pay()
#下单
prepay_id = pay.pay(openid,price,nonce_str,description)
#吊起支付
order = pay.sign(prepay_id,f"{current_date}",nonce_str)
return Response(order)
class WeChatNotifyView(APIView,Cursor):
def post(self, request):
headers = {
'Wechatpay-Signature': request.META.get('HTTP_WECHATPAY_SIGNATURE'),
'Wechatpay-Timestamp': request.META.get('HTTP_WECHATPAY_TIMESTAMP'),
'Wechatpay-Nonce': request.META.get('HTTP_WECHATPAY_NONCE'),
'Wechatpay-Serial': request.META.get('HTTP_WECHATPAY_SERIAL')
}
result = Pay().decrypt_callback(headers=headers, body=request.body)
.................
return Response({"status": 200})
下面是前端uniapp的代码:
// 表单提交
const formSub = async () => {
uni.showLoading({
title:'支付中',
icon:'none'
})
// 实际支付等于 总价(all_cost)- 折扣金额(deduction) = 实际支付(actual_pay)
orderData.value.actual_pay = orderData.value.all_cost - orderData.value.deduction
console.log('订单表单',orderData.value);
const res = await postApi.api('/order', orderData.value);
id.value = res.data.order_id
if (res.data.status == 200) {
// 表单
const Form = {
openid: orderData.value.openid,
price: orderData.value.actual_pay,
description: '测试',
recharge_type: '1'
}
console.log('支付表单',Form);
const payRes = await postApi.api('/wechatpay', Form)
console.log('支付参数', payRes);
uni.hideLoading()
if(payRes.statusCode == 200){
uni.requestPayment({
provider: 'wxpay',
// orderInfo: '1',
timeStamp: String(payRes.data.timeStamp), // 时间戳
nonceStr: payRes.data.nonceStr, // 随机字符串
package: payRes.data.package,
signType: payRes.data.signType, // 签名算法
paySign: payRes.data.paySign, // 签名
success: async(res) => {
uni.showLoading({
title:'订单提交中',
icon:'none'
})
console.log(res);
const wechatnotifyForm = {
order_id:id.value,
count:orderData.value.count
}
console.log('回调表单',wechatnotifyForm);
const payRes = await postApi.api('/wechatnotify',wechatnotifyForm )
console.log('回调成功',payRes);
// 成功带参跳转ok
uni.hideLoading()
uni.navigateTo({
url:'/pages/myself/sonMoreGoods?orderId=' + id.value
})
},
fail: (err) => {
console.log(err);
// 失败带参跳转fail cancel
uni.showToast({
title:'支付失败',
icon:'none'
})
uni.navigateTo({
url:'/pages/myself/sonMoreGoods?orderId=' + id.value
})
},
})
}else{
uni.showToast({
title: '订单异常',
icon: 'none'
})
}
} else {
uni.showToast({
title: '订单异常',
icon: 'none'
})
}
};
作者:赖鹏-spiderman