__init__.py 9.3 KB

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