python3实现微信支付和提现

2021-09-01  本文已影响0人  KS保

微信支付--V3接口

小程序支付

前提:前端获取微信用户的临时code值给到后端,后端根据code值调用微信API获取openid,拿到用户唯一标识openid;

流程:

1、后端首先调用JSAPI下单接口进行预下单,此接口参数中需指定一个通知地址,然后返回一个预支付交易会话标识给到前端

2、前端使用小程序调起支付接口调起支付

3、微信平台获取预下单时指定的通知地址并将支付结果通过该通知地址返回给我们

注:V3所有接口都需要做签名处理,api v3秘钥和api 秘钥不是同一个

小程序调起支付的参数需要按照签名规则进行签名计算:

1、构造签名串

签名串一共有四行,每一行为一个参数。行尾以\n(换行符,ASCII编码值为0x0A)结束,包括最后一行。 如果参数本身以\n结束,也需要附加一个\n

小程序appId
时间戳
随机字符串
订单详情扩展字符串

2、计算签名值

使用商户私钥对*待签名串*进行SHA256 with RSA签名,并对签名结果进行*Base64编码*得到签名值。

命令行演示如何生成签名
$ echo -n -e \
"wx8888888888888888\n1414561699\n5K8264ILTKCH16CQ2502SI8ZNMTM67VS\nprepay_id=wx201410272009395522657a690389285100\n" \
 | openssl dgst -sha256 -sign apiclient_key.pem \
 | openssl base64 -A
 uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
业务流程图
业务流程图.png
api v3证书与秘钥使用
apiv3证书与秘钥使用说明.png
# -*- coding: utf-8 -*-
import json
import time
import requests
import base64
import rsa
import os
import sys
import random
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

WXPAY_APPID="APPID"
WXPAY_APPSECRET="小程序appsecret"
WXPAY_MCHID="商户号"
WXPAY_APIV3_KEY="API v3秘钥"
WXPAY_NOTIFYURL="微信支付结果回调接口"
WXPAY_SERIALNO="商户证书序列号"
WXPAY_CLIENT_PRIKEY="商户私钥"
WXPAY_PAY_DESC="商品描述(统一下单接口用到)"
    
    
def calculate_sign(client_prikey, data):
    """
    签名; 使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值
    :param client_prikey: 商户私钥
    :param data: 待签名数据
    :return :加签后的数据
    """
    with open(client_prikey, "r") as f:
        pri_key = f.read()
    private_key = rsa.PrivateKey.load_pkcs1(pri_key.encode('utf-8'))
    sign_result = rsa.sign(data.encode('utf-8'), private_key, "SHA-256")
    content = base64.b64encode(sign_result)
    return content.decode('utf-8')


def decrypt(apikey, nonce, ciphertext, associated_data):
    """
    证书和回调报文解密
    :param apikey: API V3秘钥
    :param nonce: 加密使用的随机串初始化向量
    :param ciphertext: Base64编码后的密文
    :param associated_data: 附加数据包(可能为空)
    :return :解密后的数据
    """
    key = apikey

    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).decode('utf-8')


def random_str(lengh=32):
    """
    生成随机字符串
    :return :32位随机字符串
    """
    chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
    rand = random.Random()
    return "".join([chars[rand.randint(0, len(chars) - 1)] for i in range(lengh)])


class WeXin(object):
    def __init__(self, appid, mchid, secret, apikey, notify_url, client_prikey, serialno, pay_desc, user_agent=""):
        self.appid = appid                  # APPID
        self.mchid = mchid                  # 商户号
        self.secret = secret                # 小程序appsecret
        self.apikey = apikey                # API v3秘钥
        self.notify_url = notify_url        # 支付结果回调接口
        self.client_prikey = client_prikey  # 商户私钥
        self.serialno = serialno            # 商户证书序列号
        self.pay_desc = pay_desc            # 商品描述
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": user_agent,
            "Authorization": ""
        }

    def getOpenid(self, code):
        """获取用户openid"""
        url = "https://api.weixin.qq.com/sns/jscode2session"
        params = {
            "appid": self.appid,                # 小程序id
            "secret": self.secret,              # 小程序 appSecret
            "js_code": code,                    # 登录时获取的 code
            "grant_type": "authorization_code"  # 授权类型,此处只需填写 authorization_code
        }

        self.headers["Authorization"] = self.create_sign("GET", "/sns/jscode2session", "")

        res = requests.get(url=url, params=params, headers=self.headers)
        try:
            openid = res.json()["openid"]
            msg = "ok"
        except:
            openid = ""
            msg = res.json()

        return openid, msg

    def jsapi(self, openid, amount, orderno, timestamp, randoms, time_expire="", attach="", goods_tag="", detail={}, scene_info={}, settle_info={}):
        """JSAPI下单"""
        jsapi_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
        body = {
            "appid": self.appid,  
            "mchid": self.mchid,
            "description": self.pay_desc,   # 商品描述
            "out_trade_no": orderno,        # 商户订单号
            "notify_url": self.notify_url,  # 通知地址
            "amount": {
                "total": amount,
                "currency": "CNY"
                },    # 订单总金额和货币类型{"total": 100, "currency": "CNY"}
            "payer": {
                "openid": openid
                }     # 支付者信息
            }
        if time_expire:
            body["time_expire"] = time_expire
        if attach:
            body["attach"] = attach
        if goods_tag:
            body["goods_tag"] = goods_tag
        if detail:
            body["detail"] = detail
        if scene_info:
            body["scene_info"] = scene_info
        if settle_info:
            body["settle_info"] = settle_info

        self.headers["Authorization"] = self.create_sign("POST", "/v3/pay/transactions/jsapi", json.dumps(body), timestamp, randoms)

        res = requests.post(jsapi_url, json=body, headers=self.headers)
        prepay_id = res.json().get("prepay_id")

        return prepay_id

    def payapi(self, prepay_id):
        """
        小程序调起支付API
        1、通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付
        """
        timeStamp = str(int(time.time()))
        nonceStr = random_str()
        package = "prepay_id=" + str(prepay_id)
        data = WXPAY_APPID + "\n" + timeStamp + "\n" + nonceStr + "\n" +  package + "\n"
        sign = calculate_sign(self.client_prikey, data)

        params = {
            "timeStamp": timeStamp,
            "nonceStr": nonceStr,
            "package": package,
            "signType": "RSA",
            "paySign": sign
        }

        return params

    def payquery(self, query_param, wx=False):
        """
        查询订单
        1、查询订单状态可通过微信支付订单号或商户订单号两种方式查询
        :param query_param: 微信支付订单号或商户订单号
        """
        if wx:
            # 微信支付订单号查询
            query_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/"
        else:
            # 商户订单号查询
            query_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"

        params = {
            "mchid": self.mchid
        }
        url_path = query_url.split(".com")[1] + query_param + "?mchid=" + self.mchid
        url = query_url + query_param

        self.headers["Authorization"] = self.create_sign("GET", url_path, "")
        res = requests.get(url=url, params=params, headers=self.headers)

        return res.json()

    def payclose(self, out_trade_no):
        """
        关闭订单
        1、商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
        2、系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
        :param out_trade_no: 商户订单号
        """
        url_path = "/v3/pay/transactions/out-trade-no/{}/close".format(out_trade_no)
        url = "https://api.mch.weixin.qq.com" + url_path

        body = {
            "mchid": self.mchid
        }

        self.headers["Authorization"] = self.create_sign("POST", url_path, json.dumps(body))
        res = requests.post(url=url, data=body, headers=self.headers)

        return res.json()  # 正常返回为204

    def getCert(self, base_dir):
        """
        获取微信支付平台证书列表
        :param base_dir :指定生成证书的存放路径
        """
        url = "https://api.mch.weixin.qq.com/v3/certificates"

        self.headers["Authorization"] = self.create_sign("GET", "/v3/certificates", "")

        res = requests.get(url=url, headers=self.headers)

        res_code = res.status_code
        res_body = res.json()
        # print(res_body)

        if res_code != 200:
            print("获取公钥证书失败")
            print(res_body)
            return False

        for i in range(0, len(res_body.get("data"))):
            serial_no = res_body["data"][i]["serial_no"]
            nonce = res_body["data"][i]["encrypt_certificate"]["nonce"]
            ciphertext = res_body["data"][i]["encrypt_certificate"]["ciphertext"]
            associated_data = res_body["data"][i]["encrypt_certificate"]["associated_data"]
            data = decrypt(self.apikey ,nonce, ciphertext, associated_data)

            wxcert_dir = os.path.join(base_dir, "key", serial_no)
            if not os.path.isdir(wxcert_dir):
                os.mkdir(wxcert_dir)
            wxcert_file = os.path.join(wxcert_dir, "wxp_cert.pem")
            with open(wxcert_file, "w") as f:
                f.write(data)
        return True

    def create_sign(self, method, url, body, timestamp="", randoms=""):
        """
        构造签名串
        1、微信支付API v3通过验证签名来保证请求的真实性和数据的完整性。
        2、商户需要使用自身的私钥对API URL、消息体等关键数据的组合进行SHA-256 with RSA签名
        3、请求的签名信息通过HTTP头Authorization 传递
        4、签名生成指南:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
        :param client_prikey: 商户私钥
        :param mchid: 商户号
        :param serialno: 商户证书序列号
        :param method: 请求方式
        :param url: 请求url,去除域名部分得到参与签名的url,如果请求中有查询参数,url末尾应附加有'?'和对应的查询字符串
        :param body: 请求体
        :return : authorization
        """
        if not timestamp:
            timestamp = str(int(time.time()))

        if not randoms:
            randoms = random_str()

        sign_list = [method, url, timestamp, randoms, body]
        sign_str = "\n".join(sign_list) + "\n"

        signature = calculate_sign(self.client_prikey, sign_str)
        authorization = 'WECHATPAY2-SHA256-RSA2048  ' \
                    'mchid="{0}",' \
                    'nonce_str="{1}",' \
                    'signature="{2}",' \
                    'timestamp="{3}",' \
                    'serial_no="{4}"'.\
                    format(self.mchid,
                        randoms,
                        signature,
                        timestamp,
                        self.serialno
                    )
        return authorization


weixinPayV3 = WeXin(WXPAY_APPID, WXPAY_MCHID, WXPAY_APPSECRET, WXPAY_APIV3_KEY, WXPAY_NOTIFYURL, WXPAY_CLIENT_PRIKEY, WXPAY_SERIALNO, WXPAY_PAY_DESC)

微信支付--V3之前接口

付款到零钱

前提:

1、商户号已入驻90日且截止今日回推30天商户号保持连续不间断的交易。

2、登录微信支付商户平台-产品中心,开通付款到零钱。

限制条件:

1、不支持给非实名用户打款

2、一个商户默认同一日付款总额限额10万元,给同一个实名用户付款,单笔单日限额200/200元( 若商户需提升付款额度,可在【商户平台-产品中心-付款到零钱-产品设置-调整额度】页面进入提额申请页面,根据页面指引提交相关资料进行申请)

# -*- coding: utf-8 -*-
import hashlib
import requests
from PIL import Image
import os
import re


WXPAY_APPID="APPID"
WXPAY_APPSECRET="小程序appsecret"
WXPAY_MCHID="商户号"
WXPAY_API_KEY="API 秘钥"
WXPAY_CLIENT_CERT="商户证书"
WXPAY_CLIENT_KEY="商户秘钥(PKCS#8格式化后的密钥格式)"
WXPAY_ADVICE_DESC="付款备注(付款到零钱接口用到)"

def random_str(lengh=32):
    """
    生成随机字符串
    :return :32位随机字符串
    """
    chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
    rand = random.Random()
    return "".join([chars[rand.randint(0, len(chars) - 1)] for i in range(lengh)])


def dict_to_xml(params):
    xml = ["<xml>", ]
    for k, v in params.items():
        xml.append('<%s>%s</%s>' % (k, v, k))
    xml.append('</xml>')
    return ''.join(xml)


def dict_to_xml2(params):
    xml = ["<xml>", ]
    for k, v in params.items():
        xml.append('<%s><![CDATA[%s]]></%s>' % (k, v, k))
    xml.append('</xml>')
    return ''.join(xml)


def xml_to_dict(xml):
    xml = xml.strip()
    if xml[:5].upper() != "<XML>" and xml[-6:].upper() != "</XML>":
        return None, None

    result = {}
    sign = None
    content = ''.join(xml[5:-6].strip().split('\n'))

    pattern = re.compile(r"<(?P<key>.+)>(?P<value>.+)</(?P=key)>")
    m = pattern.match(content)
    while m:
        key = m.group("key").strip()
        value = m.group("value").strip()
        if value != "<![CDATA[]]>":
            pattern_inner = re.compile(r"<!\[CDATA\[(?P<inner_val>.+)\]\]>")
            inner_m = pattern_inner.match(value)
            if inner_m:
                value = inner_m.group("inner_val").strip()
            if key == "sign":
                sign = value
            else:
                result[key] = value

        next_index = m.end("value") + len(key) + 3
        if next_index >= len(content):
            break
        content = content[next_index:]
        m = pattern.match(content)

    return sign, result


class WeiXinPay(object):
    def __init__(self, mch_appid, mchid, api_key):
        self.api_key = api_key
        self.url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"
        self.params = {
            "mch_appid": mch_appid,
            "mchid": mchid
        }

    def update_params(self, kwargs):
        self.params["desc"] = WXPAY_ADVICE_DESC
        self.params["check_name"] = "NO_CHECK"
        self.params.update(kwargs)

    def post_xml(self):
        sign = self.get_sign_content(self.params, self.api_key)
        self.params["sign"] = sign
        xml = dict_to_xml(self.params)
        if self.params["sign"]:
            del self.params["sign"]
        response = requests.post(self.url, data=xml.encode('utf-8'), cert=(WXPAY_CLIENT_CERT, WXPAY_CLIENT_KEY))
        return xml_to_dict(response.text)

    def post_xml2(self):
        sign = self.get_sign_content(self.params, self.api_key)
        self.params["sign"] = sign
        xml = dict_to_xml2(self.params)
        if self.params["sign"]:
            del self.params["sign"]
        response = requests.post(self.url, data=xml.encode('utf-8'), cert=(WXPAY_CLIENT_CERT, WXPAY_CLIENT_KEY))
        return xml_to_dict(response.text)

    def get_sign_content(self, dict, apikey):
        """
        微信付款接口为V2接口,与V3接口规则不同,此函数用于V2接口的签名处理
        1、剔除值为空的参数,并按照参数名ASCII码递增排序(字母升序排序)
        2、将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,得到stringA
        3、在stringA最后拼接上秘钥key,得到stringSignTemp
        4、对stringSignTemp进行MD5运算,然后将结果转大写,得到签名值
        :param dict: 字典数据
        :param apikey: API秘钥
        :return : 
        """
        data = "&".join(['%s=%s' % (key, dict[key]) for key in sorted(dict)])
        if apikey:
            data = '%s&key=%s' % (data, apikey)

        return hashlib.md5(data.encode('utf-8')).hexdigest().upper()

    @staticmethod
    def getAccessToken():
        """
        接口调用凭证
        """
        url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}"

        appid = WXPAY_APPID
        secret = WXPAY_APPSECRET

        url = url.format(appid, secret)
        res = requests.get(url)

        try:
            access_token = res.json().get("access_token")
        except:
            access_token = ""

        return access_token

    @staticmethod
    def createQRCode(path, width):
        """
        获取小程序二维码,适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
        """
        url = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token={}"

        body = {
            "path": path,
            "width": width
        }

        access_token = WeiXinPay.getAccessToken()

        if not access_token:
            return False, "获取access_token失败"

        url = url.format(access_token)

        res = requests.post(url=url, json=body)
        # with open("./test.png", "wb") as f:
        #     f.write(res.content)

        return res.headers, res.content

    @staticmethod
    def imgSecCheck(filepath, imgcheck_dir):
        """
        检验一张图片是否含有违法违规内容
        """
        url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token={}"

        access_token = WeiXinPay.getAccessToken()
        if not access_token:
            return False, "获取access_token失败"

        url = url.format(access_token)

        size = (500, 500)
        img = Image.open(filepath)

        if int(img.height) > 500 or int(img.width) > 500:
            
            img.thumbnail(size, Image.ANTIALIAS)
            file_path, file_name = os.path.split(filepath)
            filepath = os.path.join(imgcheck_dir, file_name)
            img.save(filepath)

        file = {"media": open(filepath, "rb")}

        res = requests.post(url=url, files=file)

        try:
            if int(res.json().get("errcode")) == 0:
                return True, res.json().get("errmsg")
            else:
                return False, res.json().get("errmsg")
        except:
            return False, "检验图片是否违规异常"


class Pay(WeiXinPay):
    """
    付款到零钱
    此处需做添加ip操作
    1、登录到微信支付商户平台
    2、在产品中心找到企业付款到零钱
    3、进入页面之后,找到产品配置按钮,点击进入配置页面。在"发起方式"的页面下方点修改,添加发起支付的服务器外网IP
    """
    def __init__(self, mch_appid, mchid, api_key):
        super(Pay, self).__init__(mch_appid, mchid, api_key)

    def post(self, openid, trade_no, amount, nonce_str, name="", ip=""):
        kwargs = {
            "openid": openid,
            "partner_trade_no": trade_no,
            "amount": amount,  # 付款金额,单位为分
            "nonce_str": nonce_str
        }

        if ip:
            kwargs["spbill_create_ip"] = ip
        
        if name:
            kwargs["re_user_name"] = name

        self.update_params(kwargs)
        return self.post_xml()[1]


class PayQuery(WeiXinPay):
    """
    查询企业付款
    """
    def __init__(self, mch_appid, mchid, api_key):
        super(PayQuery, self).__init__(mch_appid, mchid, api_key)
        self.url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo"

    def post(self, trade_no, nonce_str):
        kwargs = {
            "partner_trade_no": trade_no,
            "nonce_str": nonce_str
        }
        self.update_params(kwargs)
        return self.post_xml2()[1]


weixinPayV2 = Pay(WXPAY_APPID, WXPAY_MCHID, WXPAY_API_KEY)
相关链接
上一篇下一篇

猜你喜欢

热点阅读