python3实现微信支付和提现

微信支付--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)
相关链接
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容