# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging from django.http import HttpResponse from django.utils.module_loading import import_string from typing import TYPE_CHECKING, Optional from apilib.systypes import Singleton from apilib.utils_sys import memcache_lock from apps.web.core import PayAppType from library.alipay import AliPayNetworkException, AliException from library.wechatbase.exceptions import WechatNetworkException, WeChatException if TYPE_CHECKING: from django.core.handlers.wsgi import WSGIRequest from apps.web.common.transaction.pay import RefundRecordT from apilib.monetary import RMB from apps.web.user.models import RechargeRecord from apps.web.dealer.models import DealerRechargeRecord from apps.web.common.models import RefundOrderBase logger = logging.getLogger(__name__) class RefundNotifier(object): """ 退款通知基类 退款通知主要负责处理三件事情 1. 接收数据、解密数据并校验数据的有效性(时效性以及正确性) 2. 对于第三方机构进行回复 (外层函数根据是否抛出异常以及异常状态 执行相应的回复策略) 3. 相关联订单的状态 信息进行修改、补充、完成 (do 函数执行) """ def __init__(self, request, refund_order_getter): self.payload = self.parse_request(request) self.refund_order_getter = refund_order_getter def parse_request(self, request): # type:(WSGIRequest) -> dict """ 从wsgiRequest 中解析出加密数据 """ raise NotImplementedError(u"需要实现") def verify_payload(self, payload): # type:(dict) -> bool """ 验证解密后的数据是否有效 """ raise NotImplementedError(u"需要实现") def handle_refund_order(self, refundOrder, refund_post_callable): # type:(RefundRecordT, callable) -> None """ 具体的处理订单的逻辑 根据回调中的成功与否决定下一步的走向""" raise NotImplementedError(u"需要实现") def do(self, post_refund): logger.debug(u"refundNotifier {} do work, payload = {}".format(self.__class__.__name__, self.payload)) if not self.verify_payload(self.payload): logger.info( u"refundNotifier {} verify payload error, payload = {}".format( self.__class__.__name__, self.payload)) return HttpResponse(self.successResponse) refundOrder = self.refund_order_getter(self.refund_order_filter) # type: RefundRecordT if not refundOrder: logger.info( u"refundNotifier {} record not refund, payload = {}".format(self.__class__.__name__, self.payload)) return HttpResponse(self.successResponse) with memcache_lock(key = str(refundOrder.id), value = 1, expire = 60) as acquired: if not acquired: logger.warning(u"refundNotifier {}, record = {}, not get lock key".format( self.__class__.__name__, refundOrder.id)) return self.handle_refund_order(refundOrder, post_refund) return HttpResponse(self.successResponse) @property def successResponse(self): # type:() -> str """ 回复异步回调 告知成功处理 不需要再次重发 """ raise NotImplementedError(u"需要实现") @property def errorResponse(self): # type:() -> str """ 回复异步回调 告知未成功处理 需要重发 """ raise NotImplementedError(u"需要实现") @property def refund_order_filter(self): # type: ()->dict raise NotImplementedError(u"需要实现") class RefundPuller(object): """ 拉取退款单的详情 """ def __init__(self, refundOrder): # type:(RefundRecordT) -> None self._refundOrder = refundOrder def pull(self, refund_post_callable, **kwargs): # type:(callable, dict) -> bool raise NotImplementedError(u"must implement pull.") def parse_error(self, errorCode, errorDesc, refund_post_callable): raise NotImplementedError(u"must implement parse_error.") class RefundCashMixin(object): def __init__(self, rechargeOrder, refundFee): # type:(Optional[RechargeRecord, DealerRechargeRecord], RMB) -> None self.paySubOrder = rechargeOrder self.payOrder = self.paySubOrder.payOrder self.refundFee = refundFee # self._nextSeq = 1 @property def outTradeNo(self): """ 交易单号 :return: """ return self.payOrder.orderNo @property def totalFee(self): return self.payOrder.money @property def totalCoins(self): return self.payOrder.coins @property def subTotalFee(self): return self.paySubOrder.money @property def subTotalCoins(self): return self.paySubOrder.coins @property def refund_paras(self): return 'mixOrderNo = {} totalFee = {} orderNo = {} subTotalFee = {} refundFee = {}'.format( self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee, self.refundFee) def submit_refund(self, refundOrder, partition_map, reason, notify_url, post_refund): # type:(RefundOrderBase, Optional[dict], basestring, basestring, callable)->None payGateway = refundOrder.my_payment_gateway if payGateway.pay_app_type == PayAppType.ALIPAY: # 支付宝的退款方式 # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功 # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态 try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo, refund_fee = refundOrder.money, total_fee = self.totalFee, refund_reason = reason) logger.debug('AliPay Refund request successfully! return = {}'.format(result)) if result['code'] == '10000': # 接口调用成功 if result['fund_change'] == 'Y': if "gmt_refund_pay" in result: finishedTime = datetime.datetime.strptime(result["gmt_refund_pay"], "%Y-%m-%d %H:%M:%S") else: finishedTime = datetime.datetime.now() matched = refundOrder.succeed(tradeRefundNo = result['trade_no'], finishedTime = finishedTime) if matched: post_refund(refundOrder, True) elif result['code'] == '20000': # 20000 服务不可用 稍后重试 refundOrder.fail(errorCode = result.get("code"), errorDesc = result.get("msg")) else: # 其他都代表失败 if result['code'] == '40004': # 业务错误 if result['sub_code'] in [ 'ACQ.REFUND_AMT_NOT_EQUAL_TOTAL', 'ACQ.REASON_TRADE_REFUND_FEE_ERR', 'ACQ.TRADE_NOT_ALLOW_REFUND', 'ACQ.REFUND_FEE_ERROR', 'ACQ.BUYER_NOT_EXIST', 'ACQ.ONLINE_TRADE_VOUCHER_NOT_ALLOW_REFUND', 'ACQ.TRADE_HAS_FINISHED' ]: matched = refundOrder.closed( errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg")) if matched: post_refund(refundOrder, False) elif result['sub_code'] in ['ACQ.SELLER_BALANCE_NOT_ENOUGH']: # 40004-ACQ.SELLER_BALANCE_NOT_ENOUGH: 卖家余额不足 refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg")) else: refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg")) else: refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg")) except AliPayNetworkException as e: # 网络调用失败, 无法判定是否成功, 不改变状态, 等拉取订单状态后在更改订单状态 logger.warning( 'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e))) except AliException as e: # 目前只有签名失败 logger.warning( 'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e))) refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg) elif payGateway.pay_app_type in [PayAppType.WECHAT]: try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo, refund_fee = refundOrder.money, total_fee = self.totalFee, refund_reason = reason, notify_url = notify_url) logger.debug('WeChat Refund request successfully! return = {}'.format(result)) except WechatNetworkException as e: logger.warning( 'WeChat Refund request exception! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e))) except WeChatException as e: logger.warning( 'WeChat Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e))) puller = RefundManager().get_poller(refundOrder.pay_app_type) puller(refundOrder).parse_error(e.errCode, e.errMsg, post_refund) else: matched = refundOrder.closed(errorCode = u'NOT_SUPPORT_REFUND', errorDesc = u"不支持的退款模式") if matched: post_refund(refundOrder, False) def check_wallet(self, proxy, order): pass def pre_check(self): """ 退款的预检查 :return: """ raise NotImplementedError('must implement pre_check') class RefundManager(Singleton): def __init__(self): super(RefundManager, self).__init__() self.map_dict = { PayAppType.WECHAT: { 'poller': import_string('apps.web.common.transaction.refund.wechat.WechatRefundPuller'), 'notifier': import_string('apps.web.common.transaction.refund.wechat.WechatRefundNotifier'), }, PayAppType.ALIPAY: { 'poller': import_string('apps.web.common.transaction.refund.alipay.AliRefundPuller'), 'notifier': import_string('apps.web.common.transaction.refund.alipay.AliRefundNotifier'), }, } def get_poller(self, pay_app_type): assert pay_app_type in self.map_dict, 'not register pay app type' return self.map_dict[pay_app_type]['poller'] def get_notifier(self, pay_app_type): assert pay_app_type in self.map_dict, 'not register pay app type' return self.map_dict[pay_app_type]['notifier']