ali.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import arrow
  6. import simplejson as json
  7. from django.conf import settings
  8. from typing import TYPE_CHECKING
  9. from apilib.monetary import RMB
  10. from apilib.monetary import quantize
  11. from apilib.utils_string import cn
  12. from apps.web.exceptions import WithdrawOrderNotExist
  13. from apps.web.common.models import WithdrawBankCard
  14. from apps.web.core import AlipayMixin
  15. from apps.web.core.payment.base import PaymentGateway, WithdrawGateway
  16. from apps.web.utils import testcase_point
  17. from library.alipay import AliPayGatewayException, AliErrorCode, AliException
  18. from library.alipay import AliPayServiceException
  19. logger = logging.getLogger(__name__)
  20. if TYPE_CHECKING:
  21. from apps.web.core.models import AliApp
  22. class AlipayWithdrawQueryResult(dict):
  23. @property
  24. def order_status(self):
  25. return self.get('status')
  26. @property
  27. def finished_time(self):
  28. if self.get('pay_date', None):
  29. return arrow.get(self['pay_date'], 'YYYY-MM-DD HH:mm:ss', tzinfo = settings.TIME_ZONE).naive
  30. else:
  31. return datetime.datetime.now()
  32. @property
  33. def extra(self):
  34. return {
  35. 'order_id': self.get('order_id'),
  36. 'pay_fund_order_id': self.get('pay_fund_order_id')
  37. }
  38. @property
  39. def is_successful(self):
  40. return self.get('status') == 'SUCCESS'
  41. @property
  42. def is_failed(self):
  43. return self.get('status') == 'FAIL'
  44. @property
  45. def is_processing(self):
  46. return self.get('status') == 'DEALING'
  47. @property
  48. def is_refund(self):
  49. """
  50. 成功状态可能会转换为退票状态
  51. :return:
  52. """
  53. return self.get('status') == 'REFUND'
  54. @property
  55. def error_desc(self):
  56. if self.is_successful:
  57. return self.get('status'), u'成功'
  58. if self.is_processing:
  59. return self.get('status'), u'正在处理'
  60. if self.is_refund:
  61. return self.get('status'), u'银行退票'
  62. error_code = self.get('error_code', u'1001')
  63. fail_reason = self.get('fail_reason', u'转账失败,请登录支付宝商户号查询具体订单信息')
  64. return self.get('status'), u'{}({})'.format(fail_reason, error_code)
  65. def __repr__(self):
  66. return '<AlipayResultDict successful?(%s), failed?(%s), processing?(%s) \n content=%s' \
  67. % (self.is_successful, self.is_failed, self.is_processing, json.dumps(self, indent = 2),)
  68. class AliPayGateway(PaymentGateway, AlipayMixin):
  69. """
  70. Alipay 支付网关,扩展原库没有的接口 ``alipay.trade.create``
  71. """
  72. def __init__(self, app):
  73. # type: (AliApp)->None
  74. super(AliPayGateway, self).__init__(app)
  75. def __repr__(self):
  76. return '<AliPayPaymentGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)
  77. def api_trade_query(self, out_trade_no = None, trade_no = None):
  78. assert out_trade_no or trade_no, 'must input out_trade_no or trade_no'
  79. result = self.client.api_alipay_trade_query(out_trade_no = out_trade_no, trade_no = trade_no)
  80. if result['code'] == u'10000':
  81. return result
  82. elif result['code'] == u'40004':
  83. raise AliPayServiceException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  84. else:
  85. raise AliPayGatewayException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  86. def api_alipay_trade_create(self, out_trade_no,
  87. money,
  88. subject,
  89. buyer_id,
  90. notify_url,
  91. timeout_express = '2m',
  92. body = '',
  93. **kwargs):
  94. # type:(str, RMB, basestring, str, str, str, str, dict)->dict
  95. """
  96. 手机扫码创建订单
  97. :param body:
  98. :param timeout_express: 订单关闭超时时间
  99. :param out_trade_no: 因需要保存到rechargeRecord里,所以需要在方法外生成
  100. :param money: 交易数额 [0.01,100000000]
  101. :param subject: 标题: 支付宝充值1元
  102. :param buyer_id: 正在扫码准备充值的用户
  103. :param kwargs:
  104. :return:
  105. """
  106. total_amount = quantize(money.amount, places = '0.01')
  107. if total_amount != money.amount:
  108. raise AliException(
  109. errCode = AliErrorCode.MY_INVALID_PARAMETER,
  110. errMsg = u'无效的交易金额',
  111. client = self.client)
  112. extras = {
  113. "buyer_id": buyer_id,
  114. "timeout_express": timeout_express,
  115. "body": body
  116. }
  117. extras.update(**kwargs)
  118. logger.debug('alipay kwargs = {}'.format(extras))
  119. logger.debug('alipay kwargs2,out_trade_no=%s,total_amount=%s,subject=%s,notify_url=%s' % (
  120. out_trade_no, str(total_amount), subject, notify_url))
  121. return self.client.api_alipay_trade_create(out_trade_no = out_trade_no,
  122. total_amount = str(total_amount),
  123. subject = subject,
  124. notify_url = notify_url,
  125. **extras)
  126. def alipay_trade_precreate(self, out_trade_no, money, subject, buyer_id, timeout_express = '2m', body = '',
  127. **kwargs):
  128. """
  129. 当面付的预下单
  130. :param out_trade_no:
  131. :param money:
  132. :param subject:
  133. :param buyer_id:
  134. :param timeout_express:
  135. :param body:
  136. :param kwargs:
  137. :return:
  138. """
  139. total_amount = str(quantize(money.amount, places = '0.01'))
  140. return self.client.api_alipay_trade_precreate(out_trade_no = out_trade_no,
  141. total_amount = total_amount,
  142. subject = subject,
  143. **{"buyer_id": buyer_id,
  144. "timeout_express": timeout_express,
  145. "body": body})
  146. @testcase_point()
  147. def refund_to_user(self, out_trade_no, out_refund_no, refund_fee, total_fee, refund_reason, **kwargs):
  148. """
  149. :param out_trade_no:
  150. :param out_refund_no:
  151. :param refund_fee:
  152. :param total_fee:
  153. :param refund_reason:
  154. :return:
  155. """
  156. return self.client.api_alipay_trade_refund(out_trade_no = out_trade_no,
  157. out_request_no = out_refund_no,
  158. refund_amount = str(refund_fee),
  159. refund_reason = refund_reason)
  160. def download_bill(self, bill_type = 'trade', bill_date = None):
  161. """
  162. 下载支付宝订单用于对账
  163. :param bill_type: (trade|signcustomer)
  164. trade指商户基于支付宝交易收单的业务账单
  165. signcustomer是指基于商户支付宝余额收入及支出等资金变动的帐务账单
  166. :param bill_date: 日账单格式为yyyy-MM-dd,月账单格式为yyyy-MM。
  167. :return:
  168. """
  169. return self.client.api_alipay_data_dataservice_bill_downloadurl_query(bill_type = bill_type,
  170. bill_date = bill_date)
  171. def api_refund_query(self, trade_no, out_trade_no, out_request_no):
  172. return self.client.api_alipay_trade_refund_order_query(trade_no, out_trade_no, out_request_no, ["gmt_refund_pay"])
  173. class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
  174. def __init__(self, app, is_ledger = True):
  175. # type: (AliApp, bool)->None
  176. super(AliPayWithdrawGateway, self).__init__(app, is_ledger)
  177. def __repr__(self):
  178. return '<AliPayWithdrawGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)
  179. def withdraw_via_bank(self, order_no, total_amount, bank_card, order_title = u'服务款项'):
  180. # type:(str, RMB, WithdrawBankCard, str)->dict
  181. """
  182. 经销商提现通过银行卡提现
  183. .. 参考文档 https://opendocs.alipay.com/open/common/transfertocard
  184. :param total_amount:
  185. :param order_no: 商户企业付款单号(本平台订单号) len(order_no) (- [8-32]
  186. :return:
  187. """
  188. payee_info = {
  189. 'identity_type': 'BANKCARD_ACCOUNT',
  190. 'identity': bank_card.accountCode,
  191. 'name': cn(bank_card.accountName)
  192. }
  193. if bank_card.accountType == WithdrawBankCard.AccountType.PUBLIC:
  194. payee_info['bankcard_ext_info'] = {
  195. 'inst_name': cn(bank_card.bankName),
  196. 'account_type': 1
  197. }
  198. if bank_card.cnapsCode:
  199. payee_info['bankcard_ext_info'].update({
  200. 'bank_code': bank_card.cnapsCode
  201. })
  202. else:
  203. payee_info['bankcard_ext_info'].update({
  204. 'inst_province': cn(bank_card.province),
  205. 'inst_city': cn(bank_card.city),
  206. 'inst_branch_name': cn(bank_card.branchBankName)
  207. })
  208. else:
  209. payee_info['bankcard_ext_info'] = {
  210. 'account_type': 2
  211. }
  212. result = self.client.api_alipay_fund_trans_uni_transfer(
  213. out_biz_no = order_no, trans_amount = str(total_amount),
  214. payee_info = payee_info, order_title = order_title)
  215. if result['code'] == u'10000':
  216. return result
  217. elif result['code'] == u'40004':
  218. raise AliPayServiceException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  219. else:
  220. raise AliPayGatewayException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  221. def get_transfer_result_via_bank(self, order_no):
  222. """
  223. 查询银行卡提现的返回结果
  224. :return:
  225. """
  226. result = self.client.api_alipay_fund_trans_common_query(out_biz_no = order_no)
  227. if result['code'] == u'10000':
  228. return AlipayWithdrawQueryResult(result)
  229. elif result['code'] == u'40004':
  230. if result['sub_code'] in ['ORDER_NOT_EXIST']:
  231. raise WithdrawOrderNotExist()
  232. else:
  233. raise AliPayServiceException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  234. else:
  235. raise AliPayGatewayException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  236. def withdraw_via_changes(self, amount, payOpenId, order_no, real_user_name, subject = u'服务款项'):
  237. """
  238. 提现到支付宝账户
  239. :return:
  240. """
  241. payee_info = {
  242. 'identity': payOpenId,
  243. 'identity_type': 'ALIPAY_LOGON_ID',
  244. 'name': cn(real_user_name)
  245. }
  246. result = self.client.api_alipay_fund_trans_uni_transfer(
  247. out_biz_no = order_no, trans_amount = str(amount),
  248. payee_info = payee_info, order_title = subject, product_code = 'TRANS_ACCOUNT_NO_PWD')
  249. if result['code'] == u'10000':
  250. return result
  251. elif result['code'] == u'40004':
  252. raise AliPayServiceException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  253. else:
  254. raise AliPayGatewayException(errCode = result['sub_code'], errMsg = result['sub_msg'], client = self)
  255. def get_transfer_result_via_changes(self, order_no):
  256. """
  257. 查询现金提现的返回结果
  258. :return:
  259. """
  260. return self.get_transfer_result_via_bank(order_no = order_no)