最近在做微信退款的功能,首先对照微信退款的文档进行一步一步操作,但是退款接口需要证书,微信文档上只告诉了我们证书如何获取,并没有告知我们证书如何使用。以下是我自己封装的关于支付模块的代码。觉得有用的可以带走
# -*- 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的证书即可,我这里还是使用了另外两个证书