记:微信退款爬坑之旅

最近在做微信退款的功能,首先对照微信退款的文档进行一步一步操作,但是退款接口需要证书,微信文档上只告诉了我们证书如何获取,并没有告知我们证书如何使用。以下是我自己封装的关于支付模块的代码。觉得有用的可以带走
# -*- coding: utf-8 -*-
"""
@Time    : 2020/10/22 2:25 下午
@Author  : LuckyTom
@File    : pay.py
"""
import abc
import hashlib
import logging
import time
import requests
from django.conf import settings
from libs.common import utils

logger = logging.getLogger()

# 微信支付账号信息
APP_ID = settings.APP_ID
MCH_ID = settings.MCH_ID
MCH_KEY = settings.MCH_KEY
PAY_NOTIFY_URL = settings.PAY_NOTIFY_URL
REFUND_NOTIFY_URL = settings.REFUND_NOTIFY_URL


class Payment(metaclass=abc.ABCMeta):
    """支付主类"""

    def __init__(self):
        # 微信支付url
        self.url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
        # 微信退款url
        self.refund_url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
        # 退款查询url
        self.refund_query_url = 'https://api.mch.weixin.qq.com/pay/refundquery'

    @abc.abstractmethod
    def pay(self, *args, **kwargs):
        pass


class WeChatPay(Payment):
    """微信支付"""

    def __init__(self):
        super(WeChatPay, self).__init__()

    def pay(self, money: int, client_ip: str, out_trade_no: str, openid: str) -> dict:
        """
        调取统一下单接口
        :param money: 金额(分)
        :param client_ip: ip地址
        :param out_trade_no: 订单号
        :param openid: 用户openid
        :return:
        """
        # 拿到封装好的xml数据
        sign_obj = GenerateSign()
        body_data = sign_obj.get_body_data(spbill_create_ip=client_ip, out_trade_no=out_trade_no, total_fee=money,
                                           openid=openid)
        # 获取时间戳
        timestamp = str(int(time.time()))
        # 请求微信接口下单
        response = requests.post(self.url, body_data.encode("utf-8"),
                                 headers={'Content-Type': 'text/xml;charset=utf-8'})
        # 返回数据为xml,将其转为字典
        content = utils.xml_to_dict(response.content)
        logger.info(f'微信支付接口返回数据=====> {content}')
        if content["return_code"] == 'SUCCESS':
            # 获取预支付交易会话标识
            prepay_id = content.get("prepay_id")
            # 获取随机字符串
            nonce_str = content.get("nonce_str")
            # 获取paySign签名,这个需要我们根据拿到的prepay_id和nonceStr进行计算签名
            sign = sign_obj.pay_sign_again(prepay_id, timestamp, nonce_str)
            # 封装返回给前端的数据
            data = {
                "order_number": out_trade_no,
                "prepay_id": prepay_id,
                "nonceStr": nonce_str,
                "paySign": sign,
                "timeStamp": timestamp
            }
            return data
        return {}

    def refund(self, out_trade_no: str, out_refund_no: str, total_fee: int, refund_fee: int) -> dict:
        """
        申请退款
        :param out_trade_no: 商户订单号
        :param out_refund_no: 商户退款单号
        :param total_fee: 订单金额(分)
        :param refund_fee: 退款金额(分)
        :return:
        """
        # 拿到封装好的xml数据
        sign_obj = GenerateSign()
        body_data = sign_obj.get_body_data(
            out_trade_no=out_trade_no, out_refund_no=out_refund_no, total_fee=total_fee, refund_fee=refund_fee
        )
        logger.info(body_data)
        headers = {'Content-Type': 'text/xml;charset=utf-8'}
        response = requests.post(self.refund_url, data=body_data.encode("utf-8"), headers=headers,
                                 cert=("/home/cert/wx_cert/apiclient_cert.pem", "/home/cert/wx_cert/apiclient_key.pem"),
                                 verify=True)
        logger.info(response.content)
        try:
            # 返回数据为xml,将其转为字典
            content = utils.xml_to_dict(response.content)
            logger.info(f'微信退款接口返回数据{content}')
            if content["return_code"] == 'SUCCESS':
                logger.info('退款成功')
                return {'refund_id': content['refund_id']}
            logger.info(f"退款失败, 失败原因:{content['return_msg']}")
            return {}
        except Exception as e:
            logger.info(f"退款失败, 失败原因:{e}")
            return {}

    def refund_query(self, refund_id: str) -> dict:
        """
        查询退款状态
        :param refund_id: 微信退款订单号
        :return:
        """
        sign_obj = GenerateSign()
        body_data = sign_obj.get_body_data(refund_id=refund_id)
        response = requests.post(self.refund_query_url, body_data.encode("utf-8"),
                                 headers={'Content-Type': 'text/xml;charset=utf-8'})
        try:
            # 返回数据为xml,将其转为字典
            content = utils.xml_to_dict(response.content)
            logger.info(f'退款查询返回数据=====> {content}')
            if content["return_code"] == 'SUCCESS':
                return content
            else:
                return {}
        except Exception as e:
            logger.info(f"退款查询, 失败原因:{e}")
            return {}


class GenerateSign:
    def pay_sign(self, params: dict, sign_key) -> str:
        """生成签名"""
        return self.encryption(params, sign_key)

    def pay_sign_again(self, prepay_id: str, timestamp: str, nonce_str: str) -> str:
        """根据得到的预支付订单ID再次签名"""
        pay_data = {
            'appId': APP_ID,
            'nonceStr': nonce_str,
            'package': "prepay_id=" + prepay_id,
            'signType': 'MD5',
            'timeStamp': timestamp
        }
        return self.encryption(pay_data)

    @classmethod
    def encryption(cls, params: dict, sign_key=None) -> str:
        # 处理函数,对参数按照key=value的格式,并按照参数名ASCII字典序排序
        str_a = '&'.join(["{0}={1}".format(k, params.get(k)) for k in sorted(params)])
        str_sign_temp = '{0}&key={1}'.format(str_a, sign_key if sign_key else MCH_KEY)
        sign = hashlib.md5(str_sign_temp.encode("utf-8")).hexdigest()
        return sign.upper()

    def get_body_data(self, **kwargs):
        nonce_str = utils.get_nonce_str()  # 随机字符串
        out_refund_no = kwargs.get('out_refund_no')  # 退款单号
        refund_id = kwargs.get('refund_id')  # 微信退款订单号
        params = {
            "appid": APP_ID,
            "mch_id": MCH_ID,
            "nonce_str": nonce_str,
        }
        # 是否退款接口
        if out_refund_no:
            # 退款
            extra_dict = {
                "refund_desc": '自愿退款',
                "notify_url": REFUND_NOTIFY_URL
            }
        elif refund_id:
            # 查询退款
            extra_dict = {}
        else:
            # 支付
            extra_dict = {
                "body": '卡妙思-卡丁会购票',
                "notify_url": PAY_NOTIFY_URL,
                "trade_type": 'JSAPI',
            }
        dict_data = {**kwargs, **params, **extra_dict}
        sign = self.pay_sign(dict_data, None)
        # 沙箱密钥
        # sign_key = self.get_sign_key(nonce_str, sign)
        # sign = self.pay_sign(dict_data, sign_key)
        dict_data['sign'] = sign
        return utils.dict_to_xml(dict_data)

    def get_sign_key(self, nonce_str, sign):
        # 沙箱模式需要获取验签密钥
        url = "https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey"
        data = {
            "mch_id": MCH_ID,
            "nonce_str": nonce_str,
            "sign": sign,
        }
        res = requests.post(url, utils.dict_to_xml(data).encode('utf-8'))
        content = utils.xml_to_dict(res.content)
        if content["return_code"] == 'SUCCESS':
            return content['sandbox_signkey']
        return False

utils 里面的两个方法
def dict_to_xml(dict_data):
    """
    dict to xml
    :param dict_data:
    :return:
    """
    xml = ["<xml>"]
    for k, v in dict_data.items():
        xml.append("<{0}>{1}</{0}>".format(k, v))
    xml.append("</xml>")
    return "".join(xml)


def xml_to_dict(xml_data):
    """
    xml to dict
    :param xml_data:
    :return:
    """
    xml_dict = {}
    root = ET.fromstring(xml_data)
    for child in root:
        xml_dict[child.tag] = child.text
    return xml_dict

重点在于:微信退款方法里面如何携带证书

response = requests.post(self.refund_url, data=body_data.encode(“utf-8”), cert=("/home/xxxx/apiclient_cert.pem", “/home/xxxxx/apiclient_key.pem”), verify=True )

切记:证书下载后放在服务器指定路径。请求中携带证书必须为绝对路径,虽然文档中说使用.p12的证书即可,我这里还是使用了另外两个证书

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值