微信小程序+Python Django后端实现小程序线上微信支付,包含完整参数配置、关键代码和注意事项!!

本人也是第一次接触微信支付部分的开发,网上到处翻也没看到一篇能快速帮助梳理的文章,摸索了很久也踩坑无数,发出来一是记录,二是希望能帮到跟我一样对微信支付开发流程不熟悉的人,三是希望大家可以一起交流讨论,看看还有没有值得优化的地方~

目录

参数配置部分

开发部分(Django版本)

1. 前端_将用户openid和支付金额传回后端,用于生成预订单(示例如下)

2. 后端_预订单生成接口,并构造前端所需支付参数返回

(1)settings.py配置

(2)urls.py配置

(3)生成预订单的支付接口(views.py & functions.py)

3. 前端_获取支付参数,调用 wx.requestPayment 发起支付

4. 后端_支付成功后结果回调(views.py & functions.py)

首次开发过程中遇到的部分大小坑,欢迎一起探讨补充~

1. 首先一定一定要提前设置好APIv3密钥

2. 微信支付v3版本接口的参数结构体和v1v2不太一样,注意区分

3. 加载公钥私钥时的读取方式

4. Http头Authorization值格式错误

5. 生成预订单时用到的两次参数字符串,注意一致性

6. 支付时间写入数据库注意修正时区


参数配置部分

官方文档如下:开发必要参数说明,下面我按照过程中使用的先后顺序对参数作简要说明: 

  1. 商户号mchid——不多说,微信支付和小程序后台都可见,唯一注意的是需和appid关联

  2. appid——同样不多说

  3. 商户API证书——

    1. 官方配置指南:什么是商户API证书?如何获取商户API证书和私钥?_通用规则|微信支付商户文档中心;配置好后会提供一个压缩包,下载解压,包含如图内容:2个apiclient_cert是不同格式的商户API证书,apiclient_key.pem是证书生成的商户私钥,记事本是官方说明文档

    2. 主要使用以下两个参数:

      1. 私钥apiclient_key.pem地址:用于签名,发起生成预订单的请求和前端调起支付

      2. 证书序列号serial_no:因为证书中包含商户的商户号、公司名称、公钥信息,微信支付可使用序列号调用证书中的公钥对上述签名进行验签;使用openssl x509 -in apiclient_cert.pem -noout -serial命令获取,会得到类似【serial=1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C】的结果,记录=后边的序列号

  4. 微信支付公钥——

    1. 官方配置指南:微信支付公钥产品简介及使用说明_微信支付公钥|微信支付商户文档中心;配置好后会提供一个pub_key.pem文件,可以跟商户API证书放到一个文件夹里

    2. 作用:支付回调时需要先用微信支付公钥进行验签(不难看出两个公钥都是拿来验签的,只是时机不同:API证书公钥是在生成预订单的时候用,微信支付公钥是在支付完成后回调结果的时候用),再处理加密数据

    3. 注意:官方提供的“开发必要参数说明”文档里还有一个【微信支付平台证书】,这俩的作用是一样的,但官方目前更推荐用【微信支付公钥】,后续代码也是基于公钥完成的

  5. APIv3——

    1. 官方配置指南:配置API key_通用规则|微信支付合作伙伴文档中心

    2. 作用:支付回调数据解密,因为微信在支付成功后向回调地址发送的数据包是加密后的,只有使用APIv3将其解密才能获取真实的订单支付信息;

    3. 注意:这是个自定义的由32个数字/大小写字符构成的密钥,长度不能错,且不要和APIv2混淆,想要调用微信支付v3接口必须设置!必须设置!否则无法回调支付结果!

开发部分(Django版本)

1. 前端_将用户openid和支付金额传回后端,用于生成预订单(示例如下)

export function createOrderAPI(params) {
  return request({
    url: '/pay/',
    method: 'GET',
    data: params
  })
}

let para = {
      openid: wx.getStorageSync('openid'),
      amount: this.data.amount,
    }
let res = await createOrderAPI(para)

2. 后端_预订单生成接口,并构造前端所需支付参数返回

(1)settings.py配置
// settings.py

WECHAT_PAY = {
    'APPID': 'wx90XXXXXXXXXXXXXX',
    'MCHID': 'XXXXXXXXXX',
    'API_V3_KEY': '之前设置的32个字符的密钥',
    'CERT_SERIAL':'之前保存的商户API证书序列号',  # API证书序列号
    'NOTIFY_URL': 'https://yourdomain.com/pay/notify',  # 支付结果通知地址(pay可以替换成前端createOrderAPI里的接口名字,意思是回调地址最好和生成预订单的接口地址在一个目录下;该地址必须外网可访问;如果服务器设置了防火墙,记得把微信支付的IP段加入白名单)
    "CERT_PATH":BASE_DIR / "certs/apiclient_cert.pem",  # API证书pem版本地址,certs文件夹直接放在跟manage.py同级的根目录下
    "KEY_PATH":BASE_DIR / "certs/apiclient_key.pem",  # API证书私钥地址
    "PUB_KEY_PATH":BASE_DIR / "certs/pub_key.pem"  # 微信支付公钥地址
}
(2)urls.py配置
// urls.py

path('pay/', views.create_recharge_order, name='create_recharge_order'),  # 支付接口
path('pay/notify', views.pay_notify, name='pay_notify'),  # 微信支付结果通知
(3)生成预订单的支付接口(views.py & functions.py)
// views.py

from .functions import *
# 支付订单接口
def create_recharge_order(request):
    if request.method == "GET":
        # 获取前端传递的参数
        openid = request.GET.get('openid')
        amount = request.GET.get('amount')  # 单位:元
        # (1)生成商户订单号
        order_id = generate_order_id()
        
        # (2)构建请求参数
        # # 这里使用的是微信支付v3版本接口
        url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"

        wechat_config = settings.WECHAT_PAY
        timestamp = str(int(time.time()))
        nonce_str = generate_nonce_str()  # 生成随机字符串

        # # 这里的参数字段依赖于使用的url接口版本,v1v2v3都是有区别的,官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4012791897
        params = {
            "appid": wechat_config["APPID"],
            "mchid": wechat_config['MCHID'],
            "out_trade_no": order_id,  # 商户订单号
            "description": "商品名称",  # 支付描述(商品名称)
            "notify_url": wechat_config['NOTIFY_URL'],  # 微信支付回调通知地址
            "amount": {
                "total": int(amount) * 100,  # 支付金额,单位:分
                "currency": "CNY"
            },
            "payer": {
                "openid": openid  # 用户 openid
            }
        }

        params_str = json.dumps(params, separators=(',', ':'))
        
        # # 使用私钥生成接口请求签名
        params["sign"] = generate_signature1("POST", "/v3/pay/transactions/jsapi", timestamp, nonce_str, params_str)
        
        # # 生成请求头
        # 生成Authorization——以下包含的参数都不可缺,且timestamp, nonce_str和生成签名使用的必须一致;官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4012365336
        authorization_header = f"WECHATPAY2-SHA256-RSA2048 mchid='{wechat_config['MCHID']}',nonce_str='{nonce_str}',signature='{params['sign']}',serial_no='{wechat_config['CERT_SERIAL']}',timestamp='{timestamp}'" 
        # 以下三项必填
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": authorization_header.replace("'", '"'),  # 注意:Authorization不能出现单引号,如果出现必须替换成双引号,否则会报错 Http头Authorization值格式错误
        }
        
        # (3)调用微信统一下单接口
        response = requests.post(url, data=params_str, headers=headers)
        response_data = response.json()
        if response.status_code == 200 and 'prepay_id' in response_data:
            prepay_id = response_data['prepay_id']
            # 构造前端所需的支付参数
            pay_params = {
                "timeStamp": timestamp,
                "nonceStr": nonce_str,
                "package": f"prepay_id={prepay_id}",
                "signType": "RSA",
            }
            # 获取prepay_id后再次使用私钥生成小程序调起支付签名,用于传给前端调起支付
            pay_params['paySign'] = generate_signature2(wechat_config["APPID"], timestamp, nonce_str, pay_params["package"])
            return HttpResponse(json.dumps({"code": 0, "message": "success", "payment": pay_params}, ensure_ascii=False))
        else:
            return HttpResponse(json.dumps({"code": -1, "message": "支付订单创建失败", "error": response_data}, ensure_ascii=False))
// functions.py 一些接口使用到的函数

from django.conf import settings
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# 生成order_id,是商户系统内唯一的订单标识,通常由 时间戳 + 随机字符串 或 用户ID + 时间戳 构成
def generate_order_id():
    # 时间戳 + 随机字符串,确保唯一性
    timestamp = int(time.time())
    random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
    return f"{timestamp}{random_str}"


# 生成随机字符串
def generate_nonce_str(length=32):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

# 读取商户私钥,生成签名
def load_private_key_sign(sign_string):
    private_key_path = settings.WECHAT_PAY['KEY_PATH']
    ## 读取 PEM 格式的私钥文件
    with open(private_key_path, 'rb') as f:
        private_key_data = f.read()
    # 使用 cryptography 库加载 PKCS#8 格式的私钥
    private_key = serialization.load_pem_private_key(private_key_data, password=None, backend=default_backend())
    # 使用RSA进行签名(SHA256-RSA2048)
    sign_data = sign_string.encode('utf-8')
    signature = private_key.sign(sign_data,
                                 padding.PKCS1v15(),  # 使用PKCS1v15填充
                                 hashes.SHA256())
    # 返回Base64编码的签名
    return base64.b64encode(signature).decode('utf-8')
                                 

# 生成微信支付接口请求签名,用于生成prepay_id;官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4012365336
def generate_signature1(method, url, timestamp, nonce_str, params):
    # 生成待签名字符串
    sign_string = f"{method}\n{url}\n{timestamp}\n{nonce_str}\n{params}\n"  
    
    # 使用私钥进行签名
    return load_private_key_sign(sign_string)


# 生成小程序调起支付签名,签名串中包含prepay_id;官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4012365341
def generate_signature2(appid, timestamp, nonce_str, package):
    # 生成待签名字符串
    sign_string = f"{appid}\n{timestamp}\n{nonce_str}\n{package}\n"  # 使用私钥进行签名
    
    # 使用私钥进行签名
    return load_private_key_sign(sign_string)

3. 前端_获取支付参数,调用 wx.requestPayment 发起支付

// 接1.前端内容

if (res.statusCode === 200 && res.data.message === "success") {
      const payment = res.data.payment;
      // 页面调起支付
      wx.requestPayment({
        timeStamp: payment.timeStamp,
        nonceStr: payment.nonceStr,
        package: payment.package,
        signType: payment.signType,
        paySign: payment.paySign,
        success: function (paymentRes) {
        // 更新页面上相关展示变量等前端逻辑
          console.log('支付成功', paymentRes);
        // 此时支付成功后,微信会将支付数据以JSON格式发送POST请求至之前在后端settings配置好的NOTIFY_URL中
        fail: function (paymentErr) {
          console.log('支付失败', paymentErr);
        }
      });
    } else {
      console.error('获取订单失败');
      wx.showToast(
        {
          title: res.data.message,
          icon: 'error'
        });
    }

4. 后端_支付成功后结果回调(views.py & functions.py)

支付成功后,不需要你手动调用这个接口,微信会自动将支付数据以JSON格式向之前配置的这个支付回调地址NOTIFY_URL发送POST请求(异步通知),你只需要在这个接口里写清楚验签、解密、处理解密数据的三部分代码就好了~

// views.py

from .functions import *
from django.shortcuts import HttpResponse
from django.http import HttpResponseBadRequest

# 支付回调地址
@csrf_exempt
def pay_notify(request):
    if request.method != 'POST':
        return HttpResponseBadRequest("仅支持POST请求")

    # 获取HTTP请求头中的参数
    wechatpay_timestamp = request.headers.get('Wechatpay-Timestamp')
    wechatpay_nonce = request.headers.get('Wechatpay-Nonce')
    wechatpay_signature = request.headers.get('Wechatpay-Signature')
    wechatpay_serial = request.headers.get('Wechatpay-Serial')

    if not all([wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, wechatpay_serial]):
        return HttpResponseBadRequest("缺少必要的HTTP头参数")

    # 获取数据主体
    encrypted_data = request.body.decode('utf-8')
    
    # # (1)使用微信支付公钥验证签名,确保数据的真实性未被篡改;官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4013053249
    # 加载微信支付公钥
    public_key = load_public_key(settings.WECHAT_PAY["PUB_KEY_PATH"])

    # 验证签名
    if not verify_signature(public_key, wechatpay_timestamp, wechatpay_nonce, encrypted_data, wechatpay_signature):
        return HttpResponseBadRequest("签名验证失败")

    # # (2)使用APIv3密钥解密回调信息
    # 获取加密数据
    try:
        resource = json.loads(encrypted_data).get("resource")
        # 假设回调体是 JSON 格式
    except json.JSONDecodeError:
        return JsonResponse({"code": "FAIL", "message": "解析回调数据失败"}, status=400)  
    # 解密
    try:
        associated_data = resource.get("associated_data")
        nonce = resource.get("nonce")
        ciphertext = resource.get("ciphertext")
        decrypted_data = decrypt_callback_data(nonce, ciphertext, associated_data)
    except Exception as e:
        print(f"解密失败: {e}")
        return HttpResponseBadRequest("解密失败")

    # 解析解密后的数据
    try:
        transaction_data = json.loads(decrypted_data)
    except json.JSONDecodeError:
        return HttpResponseBadRequest("请求体格式错误")
    
    # # (3)更新数据库状态等其他逻辑
    try:
        # 更新数据库状态
    except Exception as e:
        print(f"error: {e}")
        return HttpResponseBadRequest("支付数据已写入")
        
    # # (4)返回成功响应
    return HttpResponse("success")
// functions.py 一些接口使用到的函数

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# 加载微信支付公钥
def load_public_key(pem_path):
    with open(pem_path, "rb") as key_file:
        public_key = serialization.load_pem_public_key(
            key_file.read(),
            backend=default_backend()
        )
    return public_key

# 验证签名
def verify_signature(public_key, timestamp, nonce_str, body, signature):
    sign_str = f"{timestamp}\n{nonce_str}\n{body}\n".encode('utf-8')
    signature_bytes = base64.b64decode(signature)
    try:
        public_key.verify(
            signature_bytes,
            sign_str,
            padding.PKCS1v15(),
            hashes.SHA256()
        )
        return True
    except Exception as e:
        print(f"验签失败: {e}")
        return False

# 官方提供的解密回调信息;官方文档看这里https://pay.weixin.qq.com/doc/v3/merchant/4012071382
def decrypt_callback_data(nonce, ciphertext, associated_data):
    key = settings.WECHAT_PAY["API_V3_KEY"]
    key_bytes = str.encode(key)
    nonce_bytes = str.encode(nonce)
    ad_bytes = str.encode(associated_data)
    data = base64.b64decode(ciphertext)
    aesgcm = AESGCM(key_bytes)
    return aesgcm.decrypt(nonce_bytes, data, ad_bytes)

首次开发过程中遇到的部分大小坑,欢迎一起探讨补充~

1. 首先一定一定要提前设置好APIv3密钥

  • 如果你后台的APIv3属于未设置状态,微信支付无法成功将支付结果通知到回调地址;我当时在这里耗了大半天,把其他所有开发部分的NOTIFY_URL相关配置检查了个遍都没有问题,比如使用Postman测试了接口可以收到POST请求;比如接口可以在浏览器中响应,直接通过外网访问到;再比如https换成http......但控制台始终显示未调用支付回调接口。直到最后跟团队同事确认了一下APIv3的设置问题,发现他配成APIv2了,这就是我前面说的一定要注意区分,否则将会白白浪费一天时间。 如果确认配置好了但还是无法收到回调,可按照该文档继续逐一检查无法收到回调全网最全图文指引,看完就能有效的解决你的问题!!!! | 微信开放社区

  • 还有!配置注意是要求的32个字符,同事后面又配成APIv2的长度了,导致我使用AESGCM()时再次报错:“AESGCM key must be 128, 192, or 256 bits.”

2. 微信支付v3版本接口的参数结构体和v1v2不太一样,注意区分

  • 比如之前是mch_id,v3版本是mchid,否则可能报错:"message": "输入源“/body/mchid”映射到字段“直连商户号”必填性规则校验失败,此字段为必填项";

  • 再比如v3版本的请求参数没有nonce_str,放在authorization请求头中即可,否则可能报错:{'code': 'PARAM_ERROR', 'detail': {'location': None, 'value': ['/body/nonce_str']}, 'message': '请求中含有未在API文档中定义的参数'}

3. 加载公钥私钥时的读取方式

  • 注意看pem文件开头是BEGIN RSA PRIVATE KEY,还是BEGIN PRIVATE KEY;前者可以用rsa直接读取,后者用我上边写的加载方式

4. Http头Authorization值格式错误

我遇到过两个不同的问题都报这个错,因为他根本不给你返回具体原因,只是让你知道属于哪个错误类让你自己跟官方文档对照......

  • 签名构造签名串的时候请严格按照官方文档给出的格式,比如“xxx\nxxxx\n”,不要太依赖gpt,我在这个问题上耽误了特别久就是因为gpt一直在用它自己方式构造签名串并生成签名,且最开始没太仔细看官方给定的标准格式,导致报错:"error": {"code": "SIGN_ERROR", "message": "Http头Authorization值格式错误,请参考《微信支付商户REST API签名规则》"

  • Authorization里面各字段的值要手动用双引号括起来,且注意是双引号!不是单引号!比如字符串里面关于商户号的构造应该是【mchid="xxxxxxxxxx"】,而不是【mchid=xxxxxxxxxx】,也不是【mchid='xxxxxxxxxx'】,无论你写成后面哪一种错误形式,都会说你Http头Authorization值不对

5. 生成预订单时用到的两次参数字符串,注意一致性

  • 一次是生成接口请求签名时,另一次是调用微信统一下单接口发送POST请求时

  • 这两个字符串必须一模一样;我之前没太注意,在前者用的是json.dumps(params, separators=(',', ':')),而后者用的是json.dumps(params);

  • 结果报错:{'code': 'SIGN_ERROR', 'detail': {'detail': {'issue': 'sign not match'}, 'field': 'signature', 'location': 'authorization', 'sign_information': {'method': 'POST', 'sign_message_length': 776, 'truncated_sign_message': 'POST\n/v3/pay/transactions/jsapi\nxxxxx\nxxxxxx\n{"appid"\n', 'url': '/v3/pay/transactions/jsapi'}}, 'message': '签名错误,请检查后再试'}

6. 支付时间写入数据库注意修正时区

  • 使用Django将支付成功时间success_time写入数据库时,会自动调整时区,写入时间比实际支付时间早8个小时,所以记得修正

datetime.fromisoformat(transaction_data.get("success_time")).replace(tzinfo=None)

以上就是我靠自己第一次调通小程序线上支付的所有步骤和踩过的坑,已经尽量详细了,希望能帮到大家!也欢迎一起探讨!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值