微信服务商+付款码支付+MAC+python

2021-04-28  本文已影响0人  叶叶阿姨

我基本上找了很多的代码来借鉴学习,因为官网没有SDK需要自己一点点搭建,期间还是踩了特别的的坑,所以打算写个详细点的教程(当我完成的时候觉得挺简单,所以我好多坑都是自己不够细心导致的,就没有写出来了,反正有什么不懂的可以问我,我尽力解决)

首先我们需要了解付款码支付的流程

支付

一、支付流程
1、获取到商品相关的基础信息
2、会去商户相关信息,比如商户号、商户密钥key等(服务商需要子商户号)注:这里的商户密钥key是服务商的密钥可以是需要在商户平台设置的
3、将所有的参数进行签名计算
4、将计算好的签名添加到所有的请求参数中
5、发起请求,微信会返回一些信息,但是支付是否成功是需要自己写查询接口查询的,稳妥起见支付完成后需要在调一次查询接口查看是否支付成功
二、支付代码
1、我将随机字符串、计算签名、查询订单、字典转XML等多个方法都封装成了方法,方法如下

import hashlib
import json
import os
from collections import defaultdict
from random import Random
import xmltodict

import requests
import OpenSSL

APP_ID = "****"  # 公众账号上的appid
MCH_ID = "****"  # 商户号
API_KEY = "*****"  # 微信商户平台(pay.weixin.qq.com) -->账户设置 -->API安全 -->密钥设置,设置完成后把密钥复制到这里
SUB_MCH_ID = "******"  # 子商户号


def random_str(randomlength=16):
    """
    生成随机字符串
    :param randomlength: 字符串长度
    :return:
    """
    strs = ''
    chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
    length = len(chars) - 1
    random = Random()
    for i in range(randomlength):
        strs += chars[random.randint(0, length)]
    return strs


def get_sign(data_dict, key):
    """
    签名函数
    :param data_dict: 需要签名的参数,格式为字典
    :param key: 密钥 ,即上面的API_KEY
    :return: 字符串
    注意:签名是需要将所有的请求的参赛参与计算,空值不参与计算
    """
    data = {}
    for k in sorted(data_dict.keys()):  # 遍历字典参数名ASCII字典序排序后的key
        v = data_dict.get(k)  # 取出字典中key对应的value
        if type(v) == list:  # 添加XML标记
            v = '![CDATA[{}]]'.format(v)
        data[k] = v
    params_list = sorted(data.items(), key=lambda e: e[0], reverse=False)  # 参数字典参数名ASCII字典序排序为列表
    params_str = "&".join(u"{}={}".format(k, v) for k, v in params_list) + '&key=' + key  # 组织参数字符串并在末尾添加商户交易密钥
    md5 = hashlib.md5()  # 使用MD5加密模式
    md5.update(params_str.encode('utf-8'))  # 将参数字符串传入
    sign = md5.hexdigest().upper()  # 完成加密并转为大写
    return sign


def trans_dict_to_xml(data_dict):
    """
    定义字典转XML的函数
    :param data_dict:
    :return:
    """
    data_xml = []
    for k in data_dict.keys():  # 遍历字典的key
        v = data_dict.get(k)  # 取出字典中key对应的value
        if type(v) == list:  # 添加XML标记
            v = '![CDATA[{}]]'.format(v)
        data_xml.append('<{key}>{value}</{key}>'.format(key=k, value=v))
    return '<xml>{}</xml>'.format(''.join(data_xml))  # 返回XML


def p12_to_pem(certname, pwd):
    """
    p12证书转pem
    """
    if (certname != ""):
        pem_name = certname + ".pem"
        f_pem = open(pem_name, 'wb')
        p12file = certname + ".p12"
        path = os.path.abspath(p12file)  # 证书路径
        a = os.path.normpath(path)
        p12 = OpenSSL.crypto.load_pkcs12(open(a, 'rb').read(), pwd)
        f_pem.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, p12.get_privatekey()))
        f_pem.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, p12.get_certificate()))
        ca = p12.get_ca_certificates()
        if ca is not None:
            for cert in ca:
                f_pem.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
        f_pem.close()
    else:
        pem_name = None

    return pem_name


def query_tool(json_data):
    """
    查询
    """
    try:
        # 必带参数
        data = {
            "appid": APP_ID,  # 服务商的APPID
            "mch_id": MCH_ID,  # 商户号
            "sub_mch_id": SUB_MCH_ID,  # 子商户号
            "out_trade_no": json_data["out_trade_no"],  # 商户订单号
            "nonce_str": random_str(),  # 随机字符串
            "sign_type": "MD5"  # 签名类型
        }
        sign = get_sign(data, API_KEY)  # 生成签名
        data['sign'] = sign  # 必带参数中添加签名
        xml_str = trans_dict_to_xml(data)  # 字典转换xml
        res = requests.request('post', "https://api.mch.weixin.qq.com/pay/refundquery", data=xml_str.encode())  # 以POST方式向微信公众平台服务器发起请求
        if res.status_code != 200:
            return defaultdict(str)  # key不存在就会返回默认值
        result = json.loads(json.dumps(xmltodict.parse(res.content)))  # 将xml数据转为python中的dict字典数据
        if result['xml']['return_code'] == 'SUCCESS':
            return result
        else:
            return result

    except Exception as e:
        raise e


def query_refund(json_data):
    """
    查询退款
    """
    url = "https://api.mch.weixin.qq.com/pay/refundquery"  # 退款请求接口
    try:
        # 必带参数
        data = {
            "appid": APP_ID,  # 服务商的APPID
            "mch_id": MCH_ID,  # 商户号
            "sub_mch_id": SUB_MCH_ID,  # 子商户号
            "nonce_str": random_str(),  # 随机字符串
            "sign_type": "MD5",  # 签名类型
            "out_trade_no": json_data["out_trade_no"]  # 商户订单号
            }
        sign = get_sign(data, API_KEY)  # 生成签名
        data['sign'] = sign  # 必带参数中添加签名
        xml_str = trans_dict_to_xml(data)  # 字典转换xml
        res = requests.request('post', url, data=xml_str.encode())  # 以POST方式向微信公众平台服务器发起请求
        if res.status_code != 200:
            return defaultdict(str)  # key不存在就会返回默认值
        result = json.loads(json.dumps(xmltodict.parse(res.content)))  # 将xml数据转为python中的dict字典数据
        if result['xml']['return_code'] == 'SUCCESS':
            return result['xml']
        else:
            return result['xml']

    except Exception as e:
        raise e


def revoke(json_data):
    """
    撤销订单
    """
    url = "https://api.mch.weixin.qq.com/secapi/pay/reverse"  # 撤销请求接口
    try:
        # 必带参数
        data = {
            "appid": json_data['appid'],  # 服务商的APPID
            "mch_id": json_data['mch_id'],  # 商户号
            "sub_mch_id": json_data['sub_mch_id'],  # 子商户号
            "nonce_str": random_str(),  # 随机字符串
            "sign_type": "MD5",  # 签名类型
            "out_trade_no": json_data["out_trade_no"]  # 商户订单号
            }
        sign = get_sign(data, API_KEY)  # 生成签名
        data['sign'] = sign  # 必带参数中添加签名
        xml_str = trans_dict_to_xml(data)  # 字典转换xml
        res = requests.request('post', url, data=xml_str.encode())  # 以POST方式向微信公众平台服务器发起请求
        if res.status_code != 200:
            return defaultdict(str)  # key不存在就会返回默认值
        result = json.loads(json.dumps(xmltodict.parse(res.content)))  # 将xml数据转为python中的dict字典数据
        if result['xml']['return_code'] == 'SUCCESS':
            return result['xml']
        else:
            return result['xml']

    except Exception as e:
        raise e

2、接下来就是支付的代码了

import time
import flask
import app.wxpay

# --- 微信支付

from .tool import *

# try:
#     from .tool import *
# except Exception:
#     raise ImportError("tool")

UFDODER_URL = "https://api.mch.weixin.qq.com/pay/micropay"  # 该url是微信下单api


@app.wxpay.wsgi.route("/Pay", methods=['GET', 'POST'])
def pay_v2():
    """
    1、得到支付信息后,向商户收银后台发起支付请求
    """
    try:
        form_data = flask.request.get_data()  # 获取未经处理过的原始数据而不管内容类型
        json_data = json.loads(form_data.decode('utf-8'))  # 字符串转化为字典
        for i in ('body', 'detail', 'detail', 'out_trade_no', 'total_fee', 'fee_type',
                  'goods_tag', 'limit_pay', 'time_start', 'receipt', 'auth_code', 'profit_sharing'):
            if i not in json_data.keys():
                # 将没有指定参添加上
                json_data[i] = None
        # 获取请求参数,并增加必带参数
        data = {
            "appid": APP_ID,  # 服务商的APPID
            "mch_id": MCH_ID,  # 商户号
            "device_info": json_data["device_info"],  # 设备号
            "body": json_data["body"].encode(),  # 商品描述
            "nonce_str": random_str(),  # 随机字符串
            "sign_type": "MD5",  # 签名类型
            "sub_mch_id": SUB_MCH_ID,  # 子商户号
            "detail": json_data["detail"],  # 商品详情
            "out_trade_no": json_data["out_trade_no"],  # 商户订单号
            "total_fee": json_data["total_fee"],  # 订单金额
            "fee_type": json_data["fee_type"],  # 货币类型
            "goods_tag": json_data["goods_tag"],  # 订单优惠标记
            "limit_pay": json_data["limit_pay"],  # 指定支付方式
            "time_start": json_data["time_start"],  # 交易起始时间 datetime.datetime.now().strftime('%Y-%m-%d')
            "receipt": json_data["receipt"],  # 电子发票入口开放标识
            "auth_code": json_data["auth_code"],  # 付款码
            "profit_sharing": json_data["profit_sharing"],  # 是否需要分账
            "spbill_create_ip": "**.***.***"  # 调用微信支付的本机ip
        }
        sign = get_sign(data, API_KEY)  # 生成签名
        data["sign"] = sign  # 请求的参数中添加签名
        xml_str = trans_dict_to_xml(data)  # 字典转换xml
        res = requests.request('post', UFDODER_URL, data=xml_str.encode())  # 以POST方式向微信公众平台服务器发起请求
        if res.status_code != 200:
            return defaultdict(str)  # key不存在就会返回默认值
        result = json.loads(json.dumps(xmltodict.parse(res.content)))  # 将xml数据转为python中的dict字典数据
        if result['xml']['err_code'] == 'USERPAYING':  # 需要输入密码的情况
            time.sleep(8)  # 等待8秒,然后调用查询API
            rest = query_tool(data)  # 查看结果然后决定调查询还是撤销订单
            return rest
        # 其他情况
        else:
            return result
    except Exception as e:
        print(e)
        raise e

查询

1、查询代码

import flask
import app.wxpay
from .tool import *

# try:
#     from .tool import *
# except Exception:
#     raise ImportError("tool")


@app.wxpay.wsgi.route("/Query", methods=['GET', 'POST'])
def query():
    """
    查询
    """
    try:
        form_data = flask.request.get_data()  # 获取到请求带的信息
        json_data = json.loads(form_data.decode('utf-8'))  # 转为json格式
        result = query_tool(json_data)
        return result

    except Exception as e:
        raise e
退款

1、退款代码如下,坑点:退款中需要证书,因为requests不支持p12,但是支持pem所以我将证书放在项目中的某个地方 然后将p12转化为pem格式然后带入请求的。


import flask
import app.wxpay
from .tool import *

# try:
#     from .tool import *
# except Exception:
#     raise ImportError("tool")

url = "https://api.mch.weixin.qq.com/secapi/pay/refund"


@app.wxpay.wsgi.route("/Refund", methods=['GET', 'POST'])
def refund():
    """
    退款
    """
    try:
        form_data = flask.request.get_data()  # 获取到请求带的信息
        json_data = json.loads(form_data.decode('utf-8'))  # 转为json格式
        data = {
            "appid": APP_ID,  # 服务商的APPID
            "mch_id": MCH_ID,  # 商户号
            "sub_mch_id": SUB_MCH_ID,  # 子商户号
            "nonce_str": random_str(),  # 随机字符串
            "sign_type": "MD5",  # 签名类型
            "out_trade_no": json_data["out_trade_no"],  # 商户订单号
            "out_refund_no": json_data["out_refund_no"],  # 商户退款单号
            "total_fee": json_data["total_fee"],  # 订单金额
            "refund_fee": json_data["refund_fee"],  # 退款金额
            "refund_fee_type": "CNY",  # 退款货币种类
            "refund_desc": json_data["refund_desc"]  # 退款原因
        }  # 必带参数
        sign = get_sign(data, API_KEY)   # 生成签名
        data['sign'] = sign  # 必带参数中添加签名
        xml_str = trans_dict_to_xml(data)  # 字典转换xml
        headers = {'Content-Type': 'application/xml'}  # 指定浏览器将以什么形式、什么编码读取这个文件
        # 将p12转化为pem格式的证书及私钥
        cert = p12_to_pem("apiclient_cert", MCH_ID)
        res = requests.post(url, data=xml_str.encode(), headers=headers, cert=cert)
        # 然后发起退款请求(带上pem证书和私钥)
        if res.status_code != 200:
            return defaultdict(str)  # key不存在就会返回默认值
        result = json.loads(json.dumps(xmltodict.parse(res.content)))  # 将xml数据转为python中的dict字典数据
        if result['xml']['return_code'] == 'SUCCESS':
            res = query_refund(data)
            return res
        else:
            return result['xml']

    except Exception as e:
        raise e

查询退款

1、退款和支付原理一样都是要在结束请求后发起查询才可以查看准确结果(支付有支付查询,退款有退款查询二者不一样的哟)

import flask
import app.wxpay
from .tool import *

# try:
#     from .tool import *
# except Exception:
#     raise ImportError("tool")


@app.wxpay.wsgi.route("/QueryRefund", methods=['GET', 'POST'])
def query_refund():
    """
    查询退款
    """
    try:
        form_data = flask.request.get_data()  # 获取到请求带的信息
        json_data = json.loads(form_data.decode('utf-8'))  # 转为json格式
        result = query_refund(json_data)
        return result
    except Exception as e:
        raise e
撤销

1、撤销其实我没有用到但是还是写了(没有测试但是应该没啥问题)

import flask
import app.wxpay

from .tool import *

# try:
#     from .tool import *
# except Exception:
#     raise ImportError("tool")


@app.wxpay.wsgi.route("/Revoke", methods=['GET', 'POST'])
def revoke(json_data):
    """
    撤销订单
    """
    try:
        form_data = flask.request.get_data()  # 获取到请求带的信息
        json_data = json.loads(form_data.decode('utf-8'))  # 转为json格式
        result = revoke(json_data)
        return result
    except Exception as e:
        raise e

那到这里就没有啦~其他接口也是大同小异
注:支付宝的支付比较简单带上密钥就可以的了所以暂时没有写教程,有需要加告诉我我在写

上一篇下一篇

猜你喜欢

热点阅读