__init__.py 9.3 KB

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