__init__.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. from django.http import HttpResponse
  6. from django.utils.module_loading import import_string
  7. from typing import TYPE_CHECKING, Optional
  8. from apilib.systypes import Singleton
  9. from apilib.utils_sys import memcache_lock
  10. from apps.web.core import PayAppType
  11. from library.alipay import AliPayNetworkException, AliException
  12. from library.wechatbase.exceptions import WechatNetworkException, WeChatException
  13. if TYPE_CHECKING:
  14. from django.core.handlers.wsgi import WSGIRequest
  15. from apps.web.common.transaction.pay import RefundRecordT
  16. from apilib.monetary import RMB
  17. from apps.web.user.models import RechargeRecord
  18. from apps.web.dealer.models import DealerRechargeRecord
  19. from apps.web.common.models import RefundOrderBase
  20. logger = logging.getLogger(__name__)
  21. class RefundNotifier(object):
  22. """
  23. 退款通知基类
  24. 退款通知主要负责处理三件事情
  25. 1. 接收数据、解密数据并校验数据的有效性(时效性以及正确性)
  26. 2. 对于第三方机构进行回复 (外层函数根据是否抛出异常以及异常状态 执行相应的回复策略)
  27. 3. 相关联订单的状态 信息进行修改、补充、完成 (do 函数执行)
  28. """
  29. def __init__(self, request, refund_order_getter):
  30. self.payload = self.parse_request(request)
  31. self.refund_order_getter = refund_order_getter
  32. def parse_request(self, request): # type:(WSGIRequest) -> dict
  33. """ 从wsgiRequest 中解析出加密数据 """
  34. raise NotImplementedError(u"需要实现")
  35. def verify_payload(self, payload): # type:(dict) -> bool
  36. """ 验证解密后的数据是否有效 """
  37. raise NotImplementedError(u"需要实现")
  38. def handle_refund_order(self, refundOrder, refund_post_callable): # type:(RefundRecordT, callable) -> None
  39. """ 具体的处理订单的逻辑 根据回调中的成功与否决定下一步的走向"""
  40. raise NotImplementedError(u"需要实现")
  41. def do(self, post_refund):
  42. logger.debug(u"refundNotifier {} do work, payload = {}".format(self.__class__.__name__, self.payload))
  43. if not self.verify_payload(self.payload):
  44. logger.info(
  45. u"refundNotifier {} verify payload error, payload = {}".format(
  46. self.__class__.__name__, self.payload))
  47. return HttpResponse(self.successResponse)
  48. refundOrder = self.refund_order_getter(self.refund_order_filter) # type: RefundRecordT
  49. if not refundOrder:
  50. logger.info(
  51. u"refundNotifier {} record not refund, payload = {}".format(self.__class__.__name__, self.payload))
  52. return HttpResponse(self.successResponse)
  53. with memcache_lock(key = str(refundOrder.id), value = 1, expire = 60) as acquired:
  54. if not acquired:
  55. logger.warning(u"refundNotifier {}, record = {}, not get lock key".format(
  56. self.__class__.__name__, refundOrder.id))
  57. return
  58. self.handle_refund_order(refundOrder, post_refund)
  59. return HttpResponse(self.successResponse)
  60. @property
  61. def successResponse(self): # type:() -> str
  62. """ 回复异步回调 告知成功处理 不需要再次重发 """
  63. raise NotImplementedError(u"需要实现")
  64. @property
  65. def errorResponse(self): # type:() -> str
  66. """ 回复异步回调 告知未成功处理 需要重发 """
  67. raise NotImplementedError(u"需要实现")
  68. @property
  69. def refund_order_filter(self): # type: ()->dict
  70. raise NotImplementedError(u"需要实现")
  71. class RefundPuller(object):
  72. """ 拉取退款单的详情 """
  73. def __init__(self, refundOrder): # type:(RefundRecordT) -> None
  74. self._refundOrder = refundOrder
  75. def pull(self, refund_post_callable, **kwargs): # type:(callable, dict) -> bool
  76. raise NotImplementedError(u"must implement pull.")
  77. def parse_error(self, errorCode, errorDesc, refund_post_callable):
  78. raise NotImplementedError(u"must implement parse_error.")
  79. class RefundCashMixin(object):
  80. def __init__(self, rechargeOrder, refundFee):
  81. # type:(Optional[RechargeRecord, DealerRechargeRecord], RMB) -> None
  82. self.paySubOrder = rechargeOrder
  83. self.payOrder = self.paySubOrder.payOrder
  84. self.refundFee = refundFee
  85. # self._nextSeq = 1
  86. @property
  87. def outTradeNo(self):
  88. """
  89. 交易单号
  90. :return:
  91. """
  92. return self.payOrder.orderNo
  93. @property
  94. def totalFee(self):
  95. return self.payOrder.money
  96. @property
  97. def totalCoins(self):
  98. return self.payOrder.coins
  99. @property
  100. def subTotalFee(self):
  101. return self.paySubOrder.money
  102. @property
  103. def subTotalCoins(self):
  104. return self.paySubOrder.coins
  105. @property
  106. def refund_paras(self):
  107. return 'mixOrderNo = {} totalFee = {} orderNo = {} subTotalFee = {} refundFee = {}'.format(
  108. self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee, self.refundFee)
  109. def submit_refund(self, refundOrder, partition_map, reason, notify_url, post_refund):
  110. # type:(RefundOrderBase, Optional[dict], basestring, basestring, callable)->None
  111. payGateway = refundOrder.my_payment_gateway
  112. if payGateway.pay_app_type == PayAppType.ALIPAY:
  113. # 支付宝的退款方式
  114. # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
  115. # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
  116. try:
  117. result = payGateway.refund_to_user(
  118. out_trade_no = self.outTradeNo,
  119. out_refund_no = refundOrder.orderNo,
  120. refund_fee = refundOrder.money,
  121. total_fee = self.totalFee,
  122. refund_reason = reason)
  123. logger.debug('AliPay Refund request successfully! return = {}'.format(result))
  124. if result['code'] == '10000': # 接口调用成功
  125. if result['fund_change'] == 'Y':
  126. if "gmt_refund_pay" in result:
  127. finishedTime = datetime.datetime.strptime(result["gmt_refund_pay"], "%Y-%m-%d %H:%M:%S")
  128. else:
  129. finishedTime = datetime.datetime.now()
  130. matched = refundOrder.succeed(tradeRefundNo = result['trade_no'], finishedTime = finishedTime)
  131. if matched:
  132. post_refund(refundOrder, True)
  133. elif result['code'] == '20000': # 20000 服务不可用 稍后重试
  134. refundOrder.fail(errorCode = result.get("code"), errorDesc = result.get("msg"))
  135. else: # 其他都代表失败
  136. if result['code'] == '40004': # 业务错误
  137. if result['sub_code'] in [
  138. 'ACQ.REFUND_AMT_NOT_EQUAL_TOTAL',
  139. 'ACQ.REASON_TRADE_REFUND_FEE_ERR',
  140. 'ACQ.TRADE_NOT_ALLOW_REFUND',
  141. 'ACQ.REFUND_FEE_ERROR',
  142. 'ACQ.BUYER_NOT_EXIST',
  143. 'ACQ.ONLINE_TRADE_VOUCHER_NOT_ALLOW_REFUND',
  144. 'ACQ.TRADE_HAS_FINISHED'
  145. ]:
  146. matched = refundOrder.closed(
  147. errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
  148. if matched:
  149. post_refund(refundOrder, False)
  150. elif result['sub_code'] in ['ACQ.SELLER_BALANCE_NOT_ENOUGH']:
  151. # 40004-ACQ.SELLER_BALANCE_NOT_ENOUGH: 卖家余额不足
  152. refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
  153. else:
  154. refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
  155. else:
  156. refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
  157. except AliPayNetworkException as e:
  158. # 网络调用失败, 无法判定是否成功, 不改变状态, 等拉取订单状态后在更改订单状态
  159. logger.warning(
  160. 'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
  161. except AliException as e:
  162. # 目前只有签名失败
  163. logger.warning(
  164. 'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
  165. refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
  166. elif payGateway.pay_app_type in [PayAppType.WECHAT]:
  167. try:
  168. result = payGateway.refund_to_user(
  169. out_trade_no = self.outTradeNo,
  170. out_refund_no = refundOrder.orderNo,
  171. refund_fee = refundOrder.money,
  172. total_fee = self.totalFee,
  173. refund_reason = reason,
  174. notify_url = notify_url)
  175. logger.debug('WeChat Refund request successfully! return = {}'.format(result))
  176. except WechatNetworkException as e:
  177. logger.warning(
  178. 'WeChat Refund request exception! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
  179. except WeChatException as e:
  180. logger.warning(
  181. 'WeChat Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
  182. puller = RefundManager().get_poller(refundOrder.pay_app_type)
  183. puller(refundOrder).parse_error(e.errCode, e.errMsg, post_refund)
  184. else:
  185. matched = refundOrder.closed(errorCode = u'NOT_SUPPORT_REFUND', errorDesc = u"不支持的退款模式")
  186. if matched:
  187. post_refund(refundOrder, False)
  188. def check_wallet(self, proxy, order):
  189. pass
  190. def pre_check(self):
  191. """
  192. 退款的预检查
  193. :return:
  194. """
  195. raise NotImplementedError('must implement pre_check')
  196. class RefundManager(Singleton):
  197. def __init__(self):
  198. super(RefundManager, self).__init__()
  199. self.map_dict = {
  200. PayAppType.WECHAT:
  201. {
  202. 'poller': import_string('apps.web.common.transaction.refund.wechat.WechatRefundPuller'),
  203. 'notifier': import_string('apps.web.common.transaction.refund.wechat.WechatRefundNotifier'),
  204. },
  205. PayAppType.ALIPAY: {
  206. 'poller': import_string('apps.web.common.transaction.refund.alipay.AliRefundPuller'),
  207. 'notifier': import_string('apps.web.common.transaction.refund.alipay.AliRefundNotifier'),
  208. },
  209. }
  210. def get_poller(self, pay_app_type):
  211. assert pay_app_type in self.map_dict, 'not register pay app type'
  212. return self.map_dict[pay_app_type]['poller']
  213. def get_notifier(self, pay_app_type):
  214. assert pay_app_type in self.map_dict, 'not register pay app type'
  215. return self.map_dict[pay_app_type]['notifier']