wechat.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. """
  4. 微信授权的2种scope区别
  5. 1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。
  6. 用户感知的就是直接进入了回调页(往往是业务页面)
  7. 2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,
  8. 就可在授权后获取该用户的基本信息。
  9. """
  10. import base64
  11. import json
  12. import logging
  13. import datetime
  14. from Crypto.Cipher import PKCS1_OAEP
  15. from Crypto.PublicKey import RSA
  16. from dateutil.relativedelta import relativedelta
  17. from typing import Optional, Union
  18. from typing import TYPE_CHECKING
  19. from werkzeug.utils import cached_property
  20. from apilib.utils_string import cn
  21. from apilib.monetary import RMB
  22. from apps.web.common.models import Banks
  23. from apps.web.core import WechatMixin
  24. from apps.web.core.payment import PaymentGateway, WithdrawGateway
  25. from apps.web.constant import WECHAT_WITHDRAW_STATUS, AppPlatformType
  26. from apps.web.user.conf import PAY_NOTIFY_URL
  27. from apps.web.utils import testcase_point
  28. from library.wechatpy.pay import WeChatPay
  29. from apps.web.core.models import WechatPayApp, BankCard
  30. from library.wechatpayv3 import WechatClientV3
  31. if TYPE_CHECKING:
  32. from apps.web.core.models import WechatMiniApp
  33. logger = logging.getLogger(__name__)
  34. logging.getLogger('dicttoxml').setLevel(logging.WARNING)
  35. class WechatWithdrawQueryResult(dict):
  36. @property
  37. def order_status(self):
  38. return self.get('status')
  39. @property
  40. def is_successful(self):
  41. assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS'
  42. return self.get('status') == WECHAT_WITHDRAW_STATUS.SUCCESS
  43. @property
  44. def is_failed(self):
  45. assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS'
  46. return self.get('status') in frozenset([WECHAT_WITHDRAW_STATUS.FAILED, WECHAT_WITHDRAW_STATUS.BANK_FAIL])
  47. @property
  48. def is_processing(self):
  49. assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS'
  50. return self.get('status') == WECHAT_WITHDRAW_STATUS.PROCESSING
  51. @property
  52. def error_desc(self):
  53. if self.is_successful:
  54. return self.get('status'), u'成功'
  55. if self.is_processing:
  56. return self.get('status'), u'正在处理'
  57. reason = self.get('reason', u'转账失败,请登录微信商户号查询具体订单信息')
  58. return self.get('status'), reason
  59. def __repr__(self):
  60. return '<WechatResultDict successful?(%s), failed?(%s), processing?(%s) \n content=%s' \
  61. % (self.is_successful, self.is_failed, self.is_processing, json.dumps(self, indent = 2),)
  62. class WechatPayMixin(WechatMixin):
  63. @property
  64. def mchid(self):
  65. return self.__app_for_inner__.mchid
  66. @property
  67. def apikey(self):
  68. return self.__app_for_inner__.apikey
  69. @property
  70. def ssl_cert(self):
  71. return str(self.__app_for_inner__.ssl_cert)
  72. @property
  73. def ssl_key(self):
  74. return str(self.__app_for_inner__.ssl_key)
  75. @property
  76. def apikey_v3(self):
  77. return self.__app_for_inner__.apikey_v3
  78. @property
  79. def app_serial_number(self):
  80. return self.__app_for_inner__.app_serial_number
  81. @property
  82. def platform_certificates(self):
  83. return self.__app_for_inner__.platform_certificates
  84. @property
  85. def manual_withdraw(self):
  86. return self.__app_for_inner__.manual_withdraw
  87. def _to_fen(self, rmb):
  88. # type: (RMB)->int
  89. """微信企业付款金额,单位为分"""
  90. return int(rmb.amount * 100)
  91. @cached_property
  92. def public_key(self):
  93. result = self.__client_for_inner__.transfer.get_rsa_public_key()
  94. return RSA.importKey(result['pub_key'])
  95. def encrypt_with_public_key(self, unencrypted_string, public_key = None):
  96. """
  97. `doc`:
  98. 1、 调用获取RSA公钥API获取RSA公钥,落地成本地文件,假设为public.pem
  99. 2、 确定public.pem文件的存放路径,同时修改代码中文件的输入路径,加载RSA公钥
  100. 3、 用标准的RSA加密库对敏感信息进行加密,选择RSA_PKCS1_OAEP_PADDING填充模式
  101. (eg:Java的填充方式要选 " RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING")
  102. 4、 得到进行rsa加密并转base64之后的密文
  103. 5、 将密文传给微信侧相应字段,如付款接口(enc_bank_no/enc_true_name)
  104. 加密签名生成,供与微信通信
  105. :return:
  106. """
  107. if public_key is None:
  108. public_key = self.public_key
  109. cipher = PKCS1_OAEP.new(public_key)
  110. return base64.b64encode(cipher.encrypt(unencrypted_string))
  111. class WechatPaymentGateway(PaymentGateway, WechatPayMixin):
  112. @property
  113. def notifyUrl(self):
  114. return PAY_NOTIFY_URL.WECHAT_PAY_BACK
  115. def __init__(self, app, gateway_type=AppPlatformType.WECHAT):
  116. # type: (Union[WechatPayApp, WechatMiniApp], AppPlatformType)->None
  117. """
  118. :param app: WechatPayApp
  119. """
  120. super(WechatPaymentGateway, self).__init__(app)
  121. self.__gateway_type__ = gateway_type
  122. def __repr__(self):
  123. return \
  124. 'WechatPaymentGateway<agentId=%s, appid=%s, mchid=%s, manual=%s>' \
  125. % (
  126. str(self.occupantId), self.appid, self.mchid, self.manual_withdraw
  127. )
  128. @property
  129. def __client__(self):
  130. # type: ()->WeChatPay
  131. return WeChatPay(appid = self.appid,
  132. api_key = self.apikey,
  133. mch_id = self.mchid)
  134. @property
  135. def ssl_client(self):
  136. return WeChatPay(appid = self.appid,
  137. api_key = self.apikey,
  138. mch_id = self.mchid,
  139. mch_cert = self.ssl_cert,
  140. mch_key = self.ssl_key)
  141. def generate_js_payment_params(self, payOpenId, out_trade_no, notify_url, money, body=cn(u"购买金币"),
  142. spbill_create_ip='127.0.0.1', attach=None):
  143. # type: (basestring, str, basestring, RMB, basestring, str, Optional[dict])->dict
  144. """
  145. 注意: money必须以元为单位
  146. 生成签名,供前端使用,供为用户侧生成微信订单作用
  147. 注意这里有个微信的坑,就是attach参数不能包含空格,否则在某种情况下被转换成+号,造成解析失败
  148. :return: dict
  149. """
  150. raw = self.client.order.create(trade_type='JSAPI',
  151. body=body,
  152. total_fee=str(self._to_fen(money)),
  153. notify_url=notify_url,
  154. client_ip=spbill_create_ip,
  155. user_id=payOpenId,
  156. out_trade_no=out_trade_no,
  157. attach=json.dumps(attach, separators=(',', ':')) if attach else '')
  158. return self.client.jsapi.get_jsapi_params(prepay_id=raw["prepay_id"])
  159. def api_trade_query(self, out_trade_no = None, trade_no = None):
  160. # type:(Optional[str], Optional[str])->dict
  161. """
  162. 查询订单
  163. :param out_trade_no: 商户订单号
  164. :param trade_no: 微信订单号
  165. :return:
  166. """
  167. assert out_trade_no or trade_no, 'must input out_trade_no or trade_no'
  168. return self.client.order.query(transaction_id = trade_no, out_trade_no = out_trade_no)
  169. def download_bill(self, bill_date = None, bill_type = 'ALL', tar_type = 'GZIP'):
  170. """
  171. 下载账单,默认3个月前
  172. 此接口微信处理很粗糙,直接输出csv到http的响应里,远不如支付宝的实现
  173. :param bill_date:
  174. :param bill_type:
  175. :param tar_type:
  176. :return:
  177. """
  178. if bill_date is None:
  179. bill_date = (datetime.date.today() + relativedelta(months = -3)).strftime('%Y%m%d')
  180. return self.client.tools.download_bill(bill_date, bill_type)
  181. @testcase_point()
  182. def refund_to_user(self, out_trade_no, out_refund_no, refund_fee, total_fee, refund_reason, **kwargs):
  183. # type:(str, str, RMB, RMB, str, dict)->dict
  184. return self.ssl_client.refund.apply(
  185. total_fee = self._to_fen(total_fee),
  186. refund_fee = self._to_fen(refund_fee),
  187. out_refund_no = out_refund_no,
  188. out_trade_no = out_trade_no,
  189. refund_desc = refund_reason,
  190. notify_url=kwargs.get('notify_url'))
  191. def api_refund_query(self, out_refund_no=None, refund_no=None):
  192. """
  193. :param out_refund_no: 自己的退款单号
  194. :param refund_no: 微信的退款单号
  195. """
  196. return self.client.refund.query(refund_no, out_refund_no)
  197. class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
  198. def __init__(self, app, gateway_version = "v3", is_ledger = True): # type: (WechatPayApp, str, bool)->None
  199. super(WechatWithdrawGateway, self).__init__(app, is_ledger)
  200. self.version = gateway_version
  201. self.__gateway_type__ = AppPlatformType.WITHDRAW
  202. def __repr__(self):
  203. return \
  204. '<WechatWithdrawGateway(agentId=%s, appid=%s, mchid=%s, apikey=%s, sslcert=%s, sslkey=%s, manual=%s)>' \
  205. % (
  206. str(self.occupantId), self.appid, self.mchid, self.apikey, self.ssl_cert, self.ssl_key,
  207. self.manual_withdraw
  208. )
  209. @property
  210. def __client__(self):
  211. # type: ()->Optional[WeChatPay, WechatClientV3]
  212. if self.version == "v3":
  213. return WechatClientV3(appid = self.appid,
  214. mchid = self.mchid,
  215. private_key = self.ssl_key,
  216. cert_serial_no = self.app_serial_number,
  217. apiv3_key = self.apikey_v3,
  218. certificate_str_list = self.platform_certificates)
  219. else:
  220. return WeChatPay(appid = self.appid,
  221. api_key = self.apikey,
  222. mch_id = self.mchid,
  223. mch_cert = self.ssl_cert,
  224. mch_key = self.ssl_key)
  225. def withdraw_via_changes(self, amount, payOpenId, order_no, real_user_name, subject):
  226. # type:(RMB, str, str, str, basestring)->dict
  227. """
  228. ··参考文档: [0] https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
  229. [1] http://blog.csdn.net/qdseashore/article/details/50517570
  230. 经销商提现通过零钱支付,秒推送至经销商微信钱包
  231. :param amount:
  232. :param payOpenId: 待提现的经销商openId
  233. :param order_no: 本平台商户订单号
  234. :param real_user_name: 用户的实名
  235. :return: result
  236. :rtype: dict
  237. """
  238. return self.client.transfer.transfer(user_id = payOpenId,
  239. amount = self._to_fen(amount),
  240. desc = subject,
  241. check_name = 'FORCE_CHECK',
  242. real_name = real_user_name,
  243. out_trade_no = order_no)
  244. def withdraw_via_bank(self, order_no, total_amount, bank_card, order_title = u'提现到银行卡'):
  245. # type:(str, RMB, BankCard, str)->dict
  246. """
  247. 经销商提现通过银行卡支付
  248. .. 参考文档 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_2
  249. :param total_amount:
  250. :param order_no: 商户企业付款单号(本平台订单号) len(order_no) (- [8-32]
  251. :param bank_card: 开户行
  252. :param order_title: 提现描述
  253. :return:
  254. """
  255. wechat_bank_code = Banks.get_wechat_bank_code(bank_card.bankName)
  256. return self.client.transfer.transfer_bankcard(true_name = bank_card.holderName.encode('utf-8'),
  257. bank_card_no = bank_card.cardNo.encode('utf-8'),
  258. bank_code = wechat_bank_code,
  259. amount = self._to_fen(total_amount),
  260. desc = order_title,
  261. out_trade_no = order_no)
  262. def get_transfer_result_via_bank(self, order_no):
  263. # type:(str)->WechatWithdrawQueryResult
  264. """
  265. 查询银行卡提现的返回结果
  266. :return:
  267. """
  268. return WechatWithdrawQueryResult(self.client.transfer.query_bankcard(out_trade_no = order_no))
  269. def get_transfer_result_via_changes(self, order_no):
  270. """
  271. :param order_no:
  272. :return:
  273. """
  274. if self.version == 'v3':
  275. result = self.client.transfer.query(out_trade_no = order_no)
  276. rv = {
  277. 'return_code': 'SUCCESS',
  278. 'result_code': 'SUCCESS',
  279. 'status': result['detail_status'],
  280. 'reason': result.get('fail_reason', None)
  281. }
  282. return WechatWithdrawQueryResult(rv)
  283. else:
  284. return WechatWithdrawQueryResult(self.client.transfer.query(out_trade_no = order_no))