ali.py 12 KB

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