wechat.py 14 KB

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