pay.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import time
  4. import string
  5. import random
  6. import hashlib
  7. import requests
  8. from .base import Map, WeixinError
  9. try:
  10. from flask import request
  11. except Exception:
  12. request = None
  13. try:
  14. from lxml import etree
  15. except ImportError:
  16. from xml.etree import cElementTree as etree
  17. except ImportError:
  18. from xml.etree import ElementTree as etree
  19. __all__ = ("WeixinPayError", "WeixinPay")
  20. FAIL = "FAIL"
  21. SUCCESS = "SUCCESS"
  22. class WeixinPayError(WeixinError):
  23. def __init__(self, msg):
  24. super(WeixinPayError, self).__init__(msg)
  25. class WeixinPay(object):
  26. def __init__(self, app_id, mch_id, mch_key, notify_url, key=None, cert=None):
  27. self.app_id = app_id
  28. self.mch_id = mch_id
  29. self.mch_key = mch_key
  30. self.notify_url = notify_url
  31. self.key = key
  32. self.cert = cert
  33. self.sess = requests.Session()
  34. @property
  35. def remote_addr(self):
  36. if request is not None:
  37. return request.remote_addr
  38. return ""
  39. @property
  40. def nonce_str(self):
  41. char = string.ascii_letters + string.digits
  42. return "".join(random.choice(char) for _ in range(32))
  43. def sign(self, raw):
  44. raw = [(k, str(raw[k]) if isinstance(raw[k], int) else raw[k])
  45. for k in sorted(raw.keys())]
  46. s = "&".join("=".join(kv) for kv in raw if kv[1])
  47. s += "&key={0}".format(self.mch_key)
  48. return hashlib.md5(s.encode("utf-8")).hexdigest().upper()
  49. def check(self, data):
  50. sign = data.pop("sign")
  51. return sign == self.sign(data)
  52. def to_xml(self, raw):
  53. s = ""
  54. for k, v in raw.items():
  55. s += "<{0}>{1}</{0}>".format(k, v)
  56. s = "<xml>{0}</xml>".format(s)
  57. return s.encode("utf-8")
  58. def to_dict(self, content):
  59. raw = {}
  60. root = etree.fromstring(content)
  61. for child in root:
  62. raw[child.tag] = child.text
  63. return raw
  64. def _fetch(self, url, data, use_cert=False):
  65. data.setdefault("appid", self.app_id)
  66. data.setdefault("mch_id", self.mch_id)
  67. data.setdefault("nonce_str", self.nonce_str)
  68. data.setdefault("sign", self.sign(data))
  69. if use_cert:
  70. resp = self.sess.post(url, data=self.to_xml(data), cert=(self.cert, self.key))
  71. else:
  72. resp = self.sess.post(url, data=self.to_xml(data))
  73. content = resp.content.decode("utf-8")
  74. if "return_code" in content:
  75. data = Map(self.to_dict(content))
  76. if data.return_code == FAIL:
  77. raise WeixinPayError(data.return_msg)
  78. if "result_code" in content and data.result_code == FAIL:
  79. raise WeixinPayError(data.err_code_des)
  80. return data
  81. return content
  82. def reply(self, msg, ok=True):
  83. code = SUCCESS if ok else FAIL
  84. return self.to_xml(dict(return_code=code, return_msg=msg))
  85. def unified_order(self, **data):
  86. """
  87. 统一下单
  88. out_trade_no、body、total_fee、trade_type必填
  89. app_id, mchid, nonce_str自动填写
  90. spbill_create_ip 在flask框架下可以自动填写, 非flask框架需要主动传入此参数
  91. """
  92. url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
  93. # 必填参数
  94. if "out_trade_no" not in data:
  95. raise WeixinPayError("缺少统一支付接口必填参数out_trade_no")
  96. if "body" not in data:
  97. raise WeixinPayError("缺少统一支付接口必填参数body")
  98. if "total_fee" not in data:
  99. raise WeixinPayError("缺少统一支付接口必填参数total_fee")
  100. if "trade_type" not in data:
  101. raise WeixinPayError("缺少统一支付接口必填参数trade_type")
  102. # 关联参数
  103. if data["trade_type"] == "JSAPI" and "openid" not in data:
  104. raise WeixinPayError("trade_type为JSAPI时,openid为必填参数")
  105. if data["trade_type"] == "NATIVE" and "product_id" not in data:
  106. raise WeixinPayError("trade_type为NATIVE时,product_id为必填参数")
  107. data.setdefault("notify_url", self.notify_url)
  108. data.setdefault("spbill_create_ip", self.remote_addr)
  109. raw = self._fetch(url, data)
  110. return raw
  111. def jsapi(self, **kwargs):
  112. """
  113. 生成给JavaScript调用的数据
  114. 详细规则参考 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
  115. """
  116. kwargs.setdefault("trade_type", "JSAPI")
  117. raw = self.unified_order(**kwargs)
  118. package = "prepay_id={0}".format(raw["prepay_id"])
  119. timestamp = str(int(time.time()))
  120. nonce_str = self.nonce_str
  121. raw = dict(appId=self.app_id, timeStamp=timestamp,
  122. nonceStr=nonce_str, package=package, signType="MD5")
  123. sign = self.sign(raw)
  124. return dict(package=package, appId=self.app_id,
  125. timeStamp=timestamp, nonceStr=nonce_str, sign=sign)
  126. def order_query(self, **data):
  127. """
  128. 订单查询
  129. out_trade_no, transaction_id至少填一个
  130. appid, mchid, nonce_str不需要填入
  131. """
  132. url = "https://api.mch.weixin.qq.com/pay/orderquery"
  133. if "out_trade_no" not in data and "transaction_id" not in data:
  134. raise WeixinPayError("订单查询接口中,out_trade_no、transaction_id至少填一个")
  135. return self._fetch(url, data)
  136. def close_order(self, out_trade_no, **data):
  137. """
  138. 关闭订单
  139. out_trade_no必填
  140. appid, mchid, nonce_str不需要填入
  141. """
  142. url = "https://api.mch.weixin.qq.com/pay/closeorder"
  143. data.setdefault("out_trace_no", out_trade_no)
  144. return self._fetch(url, data)
  145. def refund(self, **data):
  146. """
  147. 申请退款
  148. out_trade_no、transaction_id至少填一个且
  149. out_refund_no、total_fee、refund_fee、op_user_id为必填参数
  150. appid、mchid、nonce_str不需要填入
  151. """
  152. if not self.key or not self.cert:
  153. raise WeixinError("退款申请接口需要双向证书")
  154. url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
  155. if "out_trade_no" not in data and "transaction_id" not in data:
  156. raise WeixinPayError("退款申请接口中,out_trade_no、transaction_id至少填一个")
  157. if "out_refund_no" not in data:
  158. raise WeixinPayError("退款申请接口中,缺少必填参数out_refund_no");
  159. if "total_fee" not in data:
  160. raise WeixinPayError("退款申请接口中,缺少必填参数total_fee");
  161. if "refund_fee" not in data:
  162. raise WeixinPayError("退款申请接口中,缺少必填参数refund_fee");
  163. if "op_user_id" not in data:
  164. raise WeixinPayError("退款申请接口中,缺少必填参数op_user_id");
  165. return self._fetch(url, data, True)
  166. def refund_query(self, **data):
  167. """
  168. 查询退款
  169. 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,
  170. 用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态。
  171. out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个
  172. appid、mchid、nonce_str不需要填入
  173. """
  174. url = "https://api.mch.weixin.qq.com/pay/refundquery"
  175. if "out_refund_no" not in data and "out_trade_no" not in data \
  176. and "transaction_id" not in data and "refund_id" not in data:
  177. raise WeixinPayError("退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个")
  178. return self._fetch(url, data)
  179. def download_bill(self, bill_date, bill_type="ALL", **data):
  180. """
  181. 下载对账单
  182. bill_date、bill_type为必填参数
  183. appid、mchid、nonce_str不需要填入
  184. """
  185. url = "https://api.mch.weixin.qq.com/pay/downloadbill"
  186. data.setdefault("bill_date", bill_date)
  187. data.setdefault("bill_type", bill_type)
  188. if "bill_date" not in data:
  189. raise WeixinPayError("对账单接口中,缺少必填参数bill_date")
  190. return self._fetch(url, data)