__init__.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, unicode_literals
  3. import inspect
  4. import logging
  5. import requests
  6. import xmltodict
  7. from xml.parsers.expat import ExpatError
  8. from optionaldict import optionaldict
  9. from wechatpy.utils import random_string
  10. from wechatpy.exceptions import WeChatPayException, InvalidSignatureException
  11. from wechatpy.pay.utils import (
  12. calculate_signature, calculate_signature_hmac, _check_signature, dict_to_xml
  13. )
  14. from wechatpy.pay.base import BaseWeChatPayAPI
  15. from wechatpy.pay import api
  16. logger = logging.getLogger(__name__)
  17. def _is_api_endpoint(obj):
  18. return isinstance(obj, BaseWeChatPayAPI)
  19. class WeChatPay(object):
  20. """
  21. 微信支付接口
  22. :param appid: 微信公众号 appid
  23. :param sub_appid: 当前调起支付的小程序APPID
  24. :param api_key: 商户 key,不要在这里使用小程序的密钥
  25. :param mch_id: 商户号
  26. :param sub_mch_id: 可选,子商户号,受理模式下必填
  27. :param mch_cert: 必填,商户证书路径
  28. :param mch_key: 必填,商户证书私钥路径
  29. :param timeout: 可选,请求超时时间,单位秒,默认无超时设置
  30. :param sandbox: 可选,是否使用测试环境,默认为 False
  31. """
  32. redpack = api.WeChatRedpack()
  33. """红包接口"""
  34. transfer = api.WeChatTransfer()
  35. """企业付款接口"""
  36. coupon = api.WeChatCoupon()
  37. """代金券接口"""
  38. order = api.WeChatOrder()
  39. """订单接口"""
  40. refund = api.WeChatRefund()
  41. """退款接口"""
  42. micropay = api.WeChatMicroPay()
  43. """刷卡支付接口"""
  44. tools = api.WeChatTools()
  45. """工具类接口"""
  46. jsapi = api.WeChatJSAPI()
  47. """公众号网页 JS 支付接口"""
  48. withhold = api.WeChatWithhold()
  49. """代扣接口"""
  50. API_BASE_URL = 'https://api.mch.weixin.qq.com/'
  51. def __new__(cls, *args, **kwargs):
  52. self = super(WeChatPay, cls).__new__(cls)
  53. api_endpoints = inspect.getmembers(self, _is_api_endpoint)
  54. for name, _api in api_endpoints:
  55. api_cls = type(_api)
  56. _api = api_cls(self)
  57. setattr(self, name, _api)
  58. return self
  59. def __init__(self, appid, api_key, mch_id, sub_mch_id=None,
  60. mch_cert=None, mch_key=None, timeout=None, sandbox=False, sub_appid=None):
  61. self.appid = appid
  62. self.sub_appid = sub_appid
  63. self.api_key = api_key
  64. self.mch_id = mch_id
  65. self.sub_mch_id = sub_mch_id
  66. self.mch_cert = mch_cert
  67. self.mch_key = mch_key
  68. self.timeout = timeout
  69. self.sandbox = sandbox
  70. self._sandbox_api_key = None
  71. self._http = requests.Session()
  72. def _fetch_sandbox_api_key(self):
  73. nonce_str = random_string(32)
  74. sign = calculate_signature({'mch_id': self.mch_id, 'nonce_str': nonce_str}, self.api_key)
  75. payload = dict_to_xml({
  76. 'mch_id': self.mch_id,
  77. 'nonce_str': nonce_str,
  78. }, sign=sign)
  79. headers = {'Content-Type': 'text/xml'}
  80. api_url = '{base}sandboxnew/pay/getsignkey'.format(base=self.API_BASE_URL)
  81. response = self._http.post(api_url, data=payload, headers=headers)
  82. return xmltodict.parse(response.text)['xml'].get('sandbox_signkey')
  83. def _request(self, method, url_or_endpoint, **kwargs):
  84. if not url_or_endpoint.startswith(('http://', 'https://')):
  85. api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
  86. if self.sandbox:
  87. api_base_url = '{url}sandboxnew/'.format(url=api_base_url)
  88. url = '{base}{endpoint}'.format(
  89. base=api_base_url,
  90. endpoint=url_or_endpoint
  91. )
  92. else:
  93. url = url_or_endpoint
  94. if isinstance(kwargs.get('data', ''), dict):
  95. data = kwargs['data']
  96. if 'mchid' not in data:
  97. # Fuck Tencent
  98. data.setdefault('mch_id', self.mch_id)
  99. data.setdefault('sub_mch_id', self.sub_mch_id)
  100. data.setdefault('nonce_str', random_string(32))
  101. data = optionaldict(data)
  102. if data.get('sign_type', 'MD5') == 'HMAC-SHA256':
  103. sign = calculate_signature_hmac(data, self.sandbox_api_key if self.sandbox else self.api_key)
  104. else:
  105. sign = calculate_signature(data, self.sandbox_api_key if self.sandbox else self.api_key)
  106. body = dict_to_xml(data, sign)
  107. body = body.encode('utf-8')
  108. kwargs['data'] = body
  109. # 商户证书
  110. if self.mch_cert and self.mch_key:
  111. kwargs['cert'] = (self.mch_cert, self.mch_key)
  112. kwargs['timeout'] = kwargs.get('timeout', self.timeout)
  113. logger.debug('Request to WeChat API: %s %s\n%s', method, url, kwargs)
  114. res = self._http.request(
  115. method=method,
  116. url=url,
  117. **kwargs
  118. )
  119. try:
  120. res.raise_for_status()
  121. except requests.RequestException as reqe:
  122. raise WeChatPayException(
  123. return_code=None,
  124. client=self,
  125. request=reqe.request,
  126. response=reqe.response
  127. )
  128. return self._handle_result(res)
  129. def _handle_result(self, res):
  130. res.encoding = 'utf-8'
  131. xml = res.text
  132. logger.debug('Response from WeChat API \n %s', xml)
  133. try:
  134. data = xmltodict.parse(xml)['xml']
  135. except (xmltodict.ParsingInterrupted, ExpatError):
  136. # 解析 XML 失败
  137. logger.debug('WeChat payment result xml parsing error', exc_info=True)
  138. return xml
  139. return_code = data['return_code']
  140. return_msg = data.get('return_msg')
  141. result_code = data.get('result_code')
  142. errcode = data.get('err_code')
  143. errmsg = data.get('err_code_des')
  144. if return_code != 'SUCCESS' or result_code != 'SUCCESS':
  145. # 返回状态码不为成功
  146. raise WeChatPayException(
  147. return_code,
  148. result_code,
  149. return_msg,
  150. errcode,
  151. errmsg,
  152. client=self,
  153. request=res.request,
  154. response=res
  155. )
  156. return data
  157. def get(self, url, **kwargs):
  158. return self._request(
  159. method='get',
  160. url_or_endpoint=url,
  161. **kwargs
  162. )
  163. def post(self, url, **kwargs):
  164. return self._request(
  165. method='post',
  166. url_or_endpoint=url,
  167. **kwargs
  168. )
  169. def check_signature(self, params):
  170. return _check_signature(params, self.api_key if not self.sandbox else self.sandbox_api_key)
  171. def parse_payment_result(self, xml):
  172. """解析微信支付结果通知"""
  173. try:
  174. data = xmltodict.parse(xml)
  175. except (xmltodict.ParsingInterrupted, ExpatError):
  176. raise InvalidSignatureException()
  177. if not data or 'xml' not in data:
  178. raise InvalidSignatureException()
  179. data = data['xml']
  180. sign = data.pop('sign', None)
  181. real_sign = calculate_signature(data, self.api_key if not self.sandbox else self.sandbox_api_key)
  182. if sign != real_sign:
  183. raise InvalidSignatureException()
  184. for key in ('total_fee', 'settlement_total_fee', 'cash_fee', 'coupon_fee', 'coupon_count'):
  185. if key in data:
  186. data[key] = int(data[key])
  187. data['sign'] = sign
  188. return data
  189. @property
  190. def sandbox_api_key(self):
  191. if self.sandbox and self._sandbox_api_key is None:
  192. self._sandbox_api_key = self._fetch_sandbox_api_key()
  193. return self._sandbox_api_key