2021-05-27

最近,公司要写一个app,支付方式为微信支付和支付宝支付,后台是用python写的,支付宝支付网上有很多的教程,但是微信支付,网上的教程却微乎其微,并且微信支付的文档写的确实垃圾的很,想要询问客服,却要排很长时间的队,但是功夫不负有心人,经过半个多月的折磨,终究是把它搞出来了。现在记录一下当时的心酸史。

首先要熟悉一下微信支付的整个流程让大家有一个宏观的认识。下面贴图

2021-05-27


整个流程我做一个总结:

首先确定我们是用什么方式来进行微信支付的,二维码(native)?app?小程序?收款码?,然后去查找对应的文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml,前期的准本工作,我这里就不做叙述了,官方的文档写的很清楚,要生成商户号,生成证书、生成apiV3_key 等等。我这里就讲一下实现微信支付的流程。我用的是二维码(native)支付的,我就用这个做为例子来讲,app支付的话,需要前端引入微信的sdk,安卓有安卓的,ios有ios的,自己去找。

第一步:要调用微信的api接口,发起统一下单,请求成功后,微信支付会返回一个链接为 { "code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz" } 大概是这个样式的。
第二步:我们要将这个链接生成二维码,生成二维码的操作,是需要自己的代码实现的。网上一大堆的教程,我这里就不叙述了。
第三步:就是前端展示二维码,然后让用户来扫,扫完以后,就进入了手机微信页面,然后进行支付(这一步都是微信来做的,我们自己的程序不需要处理什么)

第四步:用户支付成功后,微信会回调我们第一步传给微信的一个回调地址,这个地址必须是https的,而且是公网可以访问,后面我会叙述。

第五部:如果没有收到回调,也不要着急,微信提供了订单查询的接口,可以查询订单的支付状态,但是需要我们主动去调用。

自此,整个微信支付的过程,就算完成了,后面对于回调成功后该如何处理,就是自己的事情了。下面到了小伙伴最爱的贴代码环节。

这里我要强调一下,微信支付文档中的很多细节方面的东西,确实需要完善,不然即使是单纯的接口调用,也会让你死去活来。

# -*- coding: UTF-8 -*-

import requests
import json
import time
import uuid
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import b64encode
import urllib3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
import rsa
import qrcode
import config
import os

# APIV3_key = "D65555FE59D5408BA4722A211E843B6E"

class wepay():

    def __init__(self):
        self.appid = config.appid  # appid
        self.mchid = config.mchid  # 商户号
        self.APIV3_key = config.APIV3_key  # apiv3 key
        self.serial_no = config.serial_no  # 商户平台证书序列号
        self.sh_certificate_url = "/Users/xxx/cert/apiclient_key.pem"  # 商户证书在服务器上的位置
        self.sh_certificate_url_cert = "/Users/xxxx/cert/apiclient_cert.pem"  # 商户证书在服务器上的位置
    # http get 方法
    def http_get(self, url, data, header=''):
        try:
            rs = requests.get(url, json=json.dumps(data), headers=header)
            return rs.text, rs.headers
        except:
            pass

    # http post
    def http_post(self, url, data, header):
        try:
            response = requests.request("POST", url, headers=header, json=data)
            return response.text
        except:
            pass

    # 获取当前时间戳
    def getTime(self):
        return str(int(time.time()))

    # 获取32位不带- 的uuid
    def getUUID(self):
        return "".join(str(uuid.uuid4()).split("-")).upper()

    # 生成签名
    def sign_str(self, method, url_path, timestamp, nonce_str, request_body):
        if request_body:
            # POST
            sign_list = [
                method,
                url_path,
                timestamp,
                nonce_str,
                request_body
            ]
            return '\n'.join(sign_list) + '\n'
        else:
            # GET
            sign_list = [
                method,
                url_path,
                timestamp,
                nonce_str
            ]
            return '\n'.join(sign_list) + '\n\n'

    # 调起支付签名
    def pay_sign_str(self, appid, timestamp, nonce_str, prepay_id):
        sign_list = [
            appid,
            timestamp,
            nonce_str,
            prepay_id
        ]
        return '\n'.join(sign_list) + '\n'

    # 签名加密
    def sign(self, sign_str):
        with open(self.sh_certificate_url, "r") as f:
            private_key = f.read()
            # 这里要注意的秘钥只能有三行
            # -----BEGIN PRIVATE KEY-----
            # ******************秘钥只能在一行,不能换行*****************
            # -----END PRIVATE KEY-----
            f.close()
            pkey = RSA.importKey(private_key)
            # h = SHA256.new(sign_str.encode('utf-8'))
            # signature = PKCS1_v1_5.new(pkey).sign(h)
            # sign = base64.b64encode(signature).decode()
            signer = PKCS1_v1_5.new(pkey)
            digest = SHA256.new(sign_str.encode('utf8'))
            sign = b64encode(signer.sign(digest)).decode('utf-8')
            return sign, private_key

    # 生成获取平台证书请求头authorization 参数值
    def authorization(self, method, url_path, nonce_str, timestamp, body=None, mchid=config.mchid):
            # 加密子串
            signstr = self.sign_str(method=method, url_path=url_path, timestamp=timestamp, nonce_str=nonce_str, request_body=body)
            print(signstr)
            # 加密后子串
            sign1, key = self.sign(signstr)
            authorization = 'WECHATPAY2-SHA256-RSA2048 ' \
                            'mchid="{mchid}",' \
                            'nonce_str="{nonce_str}",' \
                            'signature="{sign}",' \
                            'timestamp="{timestamp}",' \
                            'serial_no="{serial_no}"'.\
                            format(mchid=mchid,
                                   nonce_str=nonce_str,
                                   sign=sign1,
                                   timestamp=timestamp,
                                   serial_no=self.serial_no)

            return authorization

# 记录一下这里的辛酸历史
    # 解密证书需要四个参数,分别是随机数, APIv3key, 密文,和附加数据包,其中apiv3key是在微信商户平台的api安全里能找见,其余的三个参数都是在平台证书返回的报文中获取的,
    # 2021年4月21号,我被整整卡了一天来解析ciphertext参数, 官方文档中,被没有提到ciphertext参数为平台公钥,我是排了5个小时的队,才咨询到了客服,客服才告诉我ciphertext参数为平台公钥,
    # 然后我就开始解密,每次解密都会报错cryptography.exceptions.InvalidTag 网上给的答案是apiv3key有问题,经过我一天反复排查,发现是随机数出现了问题,随机数需要使用平台证书报文中返回的哪个随机数,微信文档中只字不提,简直是坑
    # 特此记录,谨让以后开发此处的小伙伴不要踩坑。
    # 报文解密
    def decrypt(self, key, nonce, associated_data, ciphertext):

        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)

    # 生成验签名串和signature
    def verify_signature(self, body, header):
        timeStamp = header['Wechatpay-Timestamp']
        nonce = header['Wechatpay-Nonce']
        # print({timeStamp}, {nonce})
        str_list=[
            timeStamp,
            nonce,
            body,
        ]
        # 验签名串
        verify_signature_str = "\n".join(str_list)
        wechatpay_signature = header['Wechatpay-Signature']
        return verify_signature_str, wechatpay_signature

    # native统一下单
    def native_order(self, description= "", total=0):
        # 订单号
        # 这里有一个小坑,total一定要是int格式,否则即使生成了支付二维码,支付的时候,依然会报支付系统繁忙。
        out_trade_no = self.getUUID()
        order = {
            'appid': self.appid, # appid
            'mchid': self.mchid, # 商户号
            'description': description,  # 产品描述
            'out_trade_no': out_trade_no,  # 订单号,建议是32位的uuid
            'notify_url': 'https://xxxx/wechat',  # 通知地址,是微信支付成功后通知的地址,为自身服务的地址,也就是上文提到的微信回调地址
            'amount': {
                'total': total,  # 支付金额(单位:分)
                'currency': 'CNY'  #  支持币种,CNY:人民币,境内商户号仅支持人民币。
            }
        }
        data = json.dumps(order)
        # 微信支付无论请求什么接口,都是需要附带请求头的,请求头的加密方式,也是写到了上面
        authorization = self.authorization('POST', '/v3/pay/transactions/native', self.getUUID(), self.getTime(), data)
        # 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
        headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
        res1 = requests.post("https://api.mch.weixin.qq.com/v3/pay/transactions/native", data=data, headers=headers)
        status_code = res1.status_code
        if status_code == 200:
            data_json = res1.json()
            url = data_json['code_url']
            return url, out_trade_no
        else:
            return None

    # 生成支付二维码
    def native_code(self, code_url, out_trade_no):
        img = qrcode.make(code_url)

        # 二维码存放在服务器上的路径
        code_dir = os.path.join(config.report_public_root, 'image', 'code', 'native')
        if not os.path.exists(code_dir):
            os.makedirs(code_dir)
        # 二维码存放在服务器上的名字,以订单号命名
        code_name = "{}.{}".format(out_trade_no, "png")
        patched_file_path = os.path.join(code_dir, code_name)
        with open(patched_file_path, 'wb') as f:
            img.save(f)

        return patched_file_path.replace(config.report_public_root+'/', "")

    # 支付状态查询
    def select_order_status(self, out_trade_no):
        authorization = self.authorization('GET',  '/v3/pay/transactions/out-trade-no/{}?mchid={}'.format(out_trade_no, config.mchid), self.getUUID(), self.getTime())
        # 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
        headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
        res, header = self.http_get("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{}?mchid={}".format(out_trade_no, config.mchid),  data=''.format(), header=headers)
        return res

    # 关闭订单
    def close_order(self, out_trade_no):
        data = {
            "mchid": self.mchid
        }
        authorization = self.authorization('POST', '/v3/pay/transactions/out-trade-no/{}/close'.format(out_trade_no), self.getUUID(), self.getTime(), json.dumps(data))
        # 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
        headers = {'Authorization': authorization, 'Content-Type': 'application/json'}

        res = requests.post("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{}/close".format(out_trade_no), data=json.dumps(data), headers=headers)

        status_code = res.status_code
        if status_code == 204:
            return '订单关闭成功!'
        else:
            return None

 

上一篇:基于Pacemkaer Resource Agent的LVS负载均衡


下一篇:【Redis】特殊数据类型 - bitmap (位图)