withdraw.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import traceback
  6. from django.conf import settings
  7. from typing import TYPE_CHECKING, Callable, Union, Optional, cast
  8. from apilib.monetary import RMB
  9. from apilib.utils_sys import memcache_lock
  10. from apps.web.agent.models import Agent
  11. from apps.web.common.models import Banks
  12. from apps.web.common.transaction import WithdrawStatus, WITHDRAW_PAY_TYPE, WithdrawResult
  13. from apps.web.core import ROLE
  14. from apps.web.core.exceptions import ServiceException, WithdrawError
  15. from apps.web.core.models import BankCard
  16. from apps.web.core.payment import WithdrawGateway
  17. from apps.web.core.payment.ali import AliPayWithdrawGateway
  18. from apps.web.core.payment.wechat import WechatWithdrawGateway
  19. from library.alipay import AliPayGatewayException
  20. from library.alipay import AliPayServiceException
  21. from library.wechatbase.exceptions import WechatNetworkException, WeChatPayException, WeChatException
  22. if TYPE_CHECKING:
  23. from contextlib import GeneratorContextManager
  24. from apps.web.common.models import CapitalUser
  25. from apps.web.common.models import WithdrawRecord
  26. from apps.web.core.payment.wechat import WechatWithdrawQueryResult
  27. logger = logging.getLogger(__name__)
  28. def withdraw_lock_key_fn(role_type, role_id):
  29. # type: (str, str)->str
  30. return '{role_type}-{role_id}-withdraw-lock'.format(role_type = role_type, role_id = role_id)
  31. def withdraw_lock(type_, id_, key_fn = withdraw_lock_key_fn, value = 1, timeout = 100):
  32. # type: (str, str, Callable, int, int)->GeneratorContextManager
  33. return memcache_lock(key_fn(type_, id_), value, timeout)
  34. def withdraw_via_bank_in_ali(gateway, record, bankcard):
  35. # type: (AliPayWithdrawGateway, WithdrawRecord, BankCard)->WithdrawResult
  36. def error_handler(err_code, err_code_des):
  37. # type:(str, str)->WithdrawResult
  38. remarks = u'{err_code_des}({err_code})'.format(err_code = err_code, err_code_des = err_code_des)
  39. if err_code == 'SYSTEM_ERROR' or err_code == 'PROCESS_FAIL`':
  40. return WithdrawResult(
  41. False, 0, False, remarks,
  42. u"商户平台系统错误,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1001)")
  43. elif err_code in ['PAYMENT_MONEY_NOT_ENOUGH', 'PAYER_BALANCE_NOT_ENOUGH', 'BALANCE_IS_NOT_ENOUGH']:
  44. from taskmanager.mediator import task_caller
  45. task_caller('withdraw_error_alert', err_type = 'NOTENOUGH')
  46. return WithdrawResult(False, 0, True, remarks,
  47. u"商户平台系统繁忙,请过一小时后再试。验证码每天次数有限,频繁提交提现可能消耗完当天验证码次数(1001)")
  48. elif err_code in ['INVALID_PARAMETER', 'EXCEED_LIMIT_SM_AMOUNT', 'EXCEED_LIMIT_MM_AMOUNT',
  49. 'PAYCARD_UNABLE_PAYMENT', 'PAYER_STATUS_ERROR', 'PAYER_CERTIFY_CHECK_FAIL',
  50. 'PAYER_STATUS_ERROR', 'PAYER_USER_INFO_ERROR', 'PAYMENT_INFO_INCONSISTENCY',
  51. 'CARD_BIN_ERROR', 'PAYEE_CARD_INFO_ERROR', 'INST_PAY_UNABLE', 'REQUEST_PROCESSING',
  52. 'MEMO_REQUIRED_IN_TRANSFER_ERROR', 'PERMIT_CHECK_PERM_IDENTITY_THEFT',
  53. 'REMARK_HAS_SENSITIVE_WORD', 'EXCEED_LIMIT_DM_AMOUNT', 'NO_ACCOUNT_RECEIVE_PERMISSION',
  54. 'NO_ACCOUNT_PAYMENT_PERMISSION', 'INVALID_PARAMETER', 'PAYER_NOT_EXIST',
  55. 'PRODUCT_NOT_SIGN', 'PAYMENT_TIME_EXPIRE', 'PAYEE_NOT_EXIST', 'PAYEE_ACCOUNT_STATUS_ERROR',
  56. 'PERMIT_NON_BANK_LIMIT_PAYEE', 'PERMIT_NON_BANK_LIMIT_PAYEE',
  57. 'PAYEE_TRUSTEESHIP_ACC_OVER_LIMIT',
  58. 'NO_PERMISSION_ACCOUNT', 'TRUSTEESHIP_ACCOUNT_NOT_EXIST', 'PAYEE_ACCOUNT_NOT_EXSIT',
  59. 'ORDER_NOT_EXIST', 'PAYEE_USERINFO_STATUS_ERROR', 'TRUSTEESHIP_RECIEVE_QUOTA_LIMIT',
  60. 'SECURITY_CHECK_FAILED', 'NO_ORDER_PERMISSION', 'ORDER_STATUS_INVALID',
  61. 'PERM_AML_NOT_REALNAME_REV', 'PERM_AML_NOT_REALNAME_REV', 'USER_AGREEMENT_VERIFY_FAIL',
  62. 'PAYER_NOT_EQUAL_PAYEE_ERROR', 'EXCEED_LIMIT_DC_RECEIVED', 'PAYER_PERMLIMIT_CHECK_FAILURE',
  63. 'PAYEE_ACC_OCUPIED', 'PAYER_PAYEE_CANNOT_SAME', 'PERMIT_CHECK_PERM_LIMITED',
  64. 'PERMIT_CHECK_PERM_LIMITED', 'RESOURCE_LIMIT_EXCEED', 'INVALID_PAYER_ACCOUNT',
  65. 'EXCEED_LIMIT_DM_MAX_AMOUNT', 'EXCEED_LIMIT_PERSONAL_SM_AMOUNT',
  66. 'EXCEED_LIMIT_UNRN_DM_AMOUNT', 'INVALID_CARDNO', 'RELEASE_USER_FORBBIDEN_RECIEVE',
  67. 'PAYEE_USER_TYPE_ERROR', 'EXCEED_LIMIT_SM_MIN_AMOUNT', 'PERMIT_CHECK_RECEIVE_LIMIT',
  68. 'NOT_IN_WHITE_LIST', 'MONEY_PAY_CLOSED', 'NO_AVAILABLE_PAYMENT_TOOLS',
  69. 'PAYEE_NOT_RELNAME_CERTIFY', 'OVERSEA_TRANSFER_CLOSE', 'PAYMENT_FAIL',
  70. 'ALREADY_WITHDRAW_STD_RED_PACKET', 'BLOCK_USER_FORBBIDEN_RECIEVE', 'REQUEST_PROCESSING',
  71. 'USER_NOT_EXIST', 'PARAM_ILLEGAL', 'PROCESS_FAIL', 'CURRENCY_NOT_SUPPORT',
  72. 'PAYER_REQUESTER_RELATION_INVALID']:
  73. return WithdrawResult(False, 0, True, remarks, err_code_des)
  74. else:
  75. return WithdrawResult(False, 0, False, remarks,
  76. u"系统异常,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1001)")
  77. # 调用付款到银行接口
  78. try:
  79. result = gateway.withdraw_via_bank(total_amount = record.actualPay,
  80. order_no = record.order,
  81. bank_card = bankcard)
  82. if result['status'] == 'FAIL':
  83. return WithdrawResult(False, 0, True, u'支付失败', u'支付失败')
  84. elif result['status'] == 'REFUND':
  85. return WithdrawResult(False, 0, True, u'银行退单', u'银行退单')
  86. else:
  87. return WithdrawResult(True, 1, False, u'提现申请已经受理', u'提现申请已经受理')
  88. except AliPayGatewayException as e:
  89. logger.error(repr(e))
  90. return error_handler(e.errCode, e.errMsg)
  91. except AliPayServiceException as e:
  92. logger.error(repr(e))
  93. return error_handler(e.errCode, e.errMsg)
  94. except Exception as e:
  95. logger.exception(e)
  96. return WithdrawResult(False, 0, False, traceback.format_exc(), u'系统异常,无法确定转账是否成功,请联系客服确认结果(1001)')
  97. def withdraw_via_bank_in_wechat(gateway, record, bankcard):
  98. # type: (WechatWithdrawGateway, WithdrawRecord, BankCard)->WithdrawResult
  99. def error_handler(err_code, err_code_des):
  100. # type:(str, str)->WithdrawResult
  101. remarks = u'{err_code_des}({err_code})'.format(err_code = err_code, err_code_des = err_code_des)
  102. if err_code in ['SYSTEMERROR', 'SEND_FAILED']:
  103. return WithdrawResult(
  104. False, 0, False, remarks,
  105. u"商户平台系统错误,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1002)")
  106. elif err_code == 'NOTENOUGH':
  107. from taskmanager.mediator import task_caller
  108. task_caller('withdraw_error_alert', err_type = 'NOTENOUGH')
  109. return WithdrawResult(False, 0, True, remarks,
  110. u"商户平台系统繁忙,请过一小时后再试。验证码每天次数有限,频繁提交提现可能消耗完当天验证码次数(1002)")
  111. elif err_code in ['AMOUNT_LIMIT', 'SENDNUM_LIMIT', 'INVALID_REQUEST', 'PARAM_ERROR', 'SIGNERROR', 'ORDERPAID',
  112. 'FATAL_ERROR', 'FREQUENCY_LIMITED', 'RECV_ACCOUNT_NOT_ALLOWED', 'PAY_CHANNEL_NOT_ALLOWED',
  113. 'NO_AUTH', 'FREQ_LIMIT', 'OPENID_ERROR', 'NOTENOUGH']:
  114. return WithdrawResult(False, 0, True, remarks, err_code_des)
  115. else:
  116. return WithdrawResult(
  117. False, 0, False, remarks,
  118. u"系统异常,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1002)")
  119. # 调用付款到银行接口
  120. try:
  121. gateway.withdraw_via_bank(order_no = record.order,
  122. total_amount = record.actualPay,
  123. bank_card = bankcard)
  124. return WithdrawResult(True, 1, False, u'提现申请微信已经受理', u'提现申请微信已经受理')
  125. except WechatNetworkException as e:
  126. logger.error(repr(e))
  127. if e.errMsg:
  128. return WithdrawResult(False, 0, False, e.errMsg, e.errMsg)
  129. else:
  130. return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
  131. except WeChatPayException as e:
  132. logger.error(repr(e))
  133. return error_handler(e.errCode, e.errMsg)
  134. except Exception as e:
  135. logger.exception(e)
  136. return WithdrawResult(False, 0, False, traceback.format_exc(), u'系统异常,无法确定转账是否成功,请联系客服确认结果(1002)')
  137. def withdraw_via_bank(gateway, record, bankcard):
  138. # type: (WithdrawGateway, WithdrawRecord, BankCard)->(basestring, WithdrawResult)
  139. if isinstance(gateway, AliPayWithdrawGateway):
  140. return 'alipay', withdraw_via_bank_in_ali(gateway, record, bankcard)
  141. if isinstance(gateway, WechatWithdrawGateway):
  142. return 'wechat', withdraw_via_bank_in_wechat(gateway, record, bankcard)
  143. raise Exception(u'不支持的提现网关类型')
  144. def withdraw_via_wechat(gateway, record, payOpenId, real_user_name):
  145. # type:(cast[WithdrawGateway], WithdrawRecord, str, str)->WithdrawResult
  146. """
  147. 微信支付到个人微信
  148. :param gateway:
  149. :param record:
  150. :param payOpenId:
  151. :param real_user_name:
  152. :return:
  153. """
  154. def withdraw_via_v1(gateway, record, payOpenId, real_user_name):
  155. def error_handler(err_code, err_code_des):
  156. # type:(str, str)->(bool, bool, str, str)
  157. """
  158. SYSTEMERROR, SEND_FAILED: 不确定是否成功,这个必须必须设置为错误,联系微信确认订单是否成功
  159. 目前已经在微信中明确的错误码可以自动退款,没有明确的其他错误码必须联系微信确认
  160. """
  161. remarks = u'{err_code_des}({err_code})'.format(err_code = err_code, err_code_des = err_code_des)
  162. if err_code in ['SYSTEMERROR', 'SEND_FAILED']:
  163. # 该情况不能确认是否成功,需要平台侧查找订单确认
  164. return WithdrawResult(
  165. False, 0, False, remarks,
  166. u"商户平台系统错误,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1003)")
  167. elif err_code == 'NOTENOUGH':
  168. from taskmanager.mediator import task_caller
  169. task_caller('withdraw_error_alert', err_type = 'NOTENOUGH')
  170. return WithdrawResult(False, 0, True, remarks,
  171. u"商户平台系统繁忙,请过一小时后再试。验证码每天次数有限,频繁提交提现可能消耗完当天验证码次数(1003)")
  172. elif err_code == 'NAME_MISMATCH':
  173. return WithdrawResult(False, 2, True, remarks, u"实名校验失败,请确保经销商名字与账户登录微信一致后重试。")
  174. elif err_code == 'V2_ACCOUNT_SIMPLE_BAN':
  175. return WithdrawResult(False, 4, True, remarks, u'您的微信尚未实名认证,请去微信绑定银行卡或身份证完成实名认证。')
  176. elif err_code in ['AMOUNT_LIMIT', 'MONEY_LIMIT', 'SENDNUM_LIMIT', 'NO_AUTH', 'PARAM_ERROR', 'OPENID_ERROR',
  177. 'SIGN_ERROR', 'XML_ERROR', 'FATAL_ERROR', 'FREQUENCY_LIMITED',
  178. 'FREQ_LIMIT', 'CA_ERROR', 'PARAM_IS_NOT_UTF8', 'RECV_ACCOUNT_NOT_ALLOWED',
  179. 'PAY_CHANNEL_NOT_ALLOWED']:
  180. return WithdrawResult(False, 0, True, remarks, err_code_des)
  181. else:
  182. return WithdrawResult(False, 0, False, remarks,
  183. u"系统异常,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1010)")
  184. try:
  185. gateway.withdraw_via_changes(amount = record.actualPay,
  186. payOpenId = payOpenId,
  187. order_no = record.order,
  188. real_user_name = real_user_name,
  189. subject = u'服务款项')
  190. return WithdrawResult(True, 1, False, u'提现成功', u'提现成功')
  191. except WechatNetworkException as e:
  192. logger.error(repr(e))
  193. if e.errMsg:
  194. return WithdrawResult(False, 0, False, e.errMsg, e.errMsg)
  195. else:
  196. return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
  197. except WeChatPayException as e:
  198. logger.error(repr(e))
  199. return error_handler(e.errCode, e.errMsg)
  200. except Exception as e:
  201. logger.exception(e)
  202. return WithdrawResult(False, 0, False, traceback.format_exc(), u'系统异常,无法确定转账是否成功,请联系客服确认结果(1003)')
  203. def withdraw_via_v3(gateway, record, payOpenId, real_user_name):
  204. def error_handler(err_code, err_code_des):
  205. # type:(str, str)->(bool, bool, str, str)
  206. """
  207. SYSTEM_ERROR, SEND_FAILED: 不确定是否成功,这个必须必须设置为错误,联系微信确认订单是否成功
  208. 目前已经在微信中明确的错误码可以自动退款,没有明确的其他错误码必须联系微信确认
  209. """
  210. remarks = u'{err_code_des}({err_code})'.format(err_code = err_code, err_code_des = err_code_des)
  211. if err_code in ['SYSTEM_ERROR', 'SEND_FAILED']:
  212. # 该情况不能确认是否成功,需要平台侧查找订单确认
  213. return WithdrawResult(
  214. False, 0, False, remarks,
  215. u"商户平台系统错误,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1003)")
  216. elif err_code == 'NOT_ENOUGH':
  217. from taskmanager.mediator import task_caller
  218. task_caller('withdraw_error_alert', err_type = 'NOTENOUGH')
  219. return WithdrawResult(False, 0, True, remarks,
  220. u"商户平台系统繁忙,请过一小时后再试。验证码每天次数有限,频繁提交提现可能消耗完当天验证码次数(1003)")
  221. elif err_code == 'NAME_MISMATCH':
  222. return WithdrawResult(False, 2, True, remarks, u"实名校验失败,请确保经销商名字与账户登录微信一致后重试。")
  223. elif err_code == 'V2_ACCOUNT_SIMPLE_BAN':
  224. return WithdrawResult(False, 4, True, remarks, u'您的微信尚未实名认证,请去微信绑定银行卡或身份证完成实名认证。')
  225. elif err_code in ['AMOUNT_LIMIT', 'MONEY_LIMIT', 'SENDNUM_LIMIT', 'NO_AUTH', 'PARAM_ERROR', 'OPENID_ERROR',
  226. 'SIGN_ERROR', 'XML_ERROR', 'FATAL_ERROR', 'FREQUENCY_LIMITED',
  227. 'FREQ_LIMIT', 'CA_ERROR', 'PARAM_IS_NOT_UTF8', 'RECV_ACCOUNT_NOT_ALLOWED',
  228. 'PAY_CHANNEL_NOT_ALLOWED']:
  229. return WithdrawResult(False, 0, True, remarks, err_code_des)
  230. else:
  231. return WithdrawResult(False, 0, False, remarks,
  232. u"系统异常,无法获取转账状态。后台会每日定时重试,如果3个工作日仍然是失败状态,请联系客服确认结果(1010)")
  233. try:
  234. gateway.withdraw_via_changes(amount = record.actualPay,
  235. payOpenId = payOpenId,
  236. order_no = record.order,
  237. real_user_name = real_user_name,
  238. subject = u'服务款项')
  239. return WithdrawResult(True, 1, False, u'提现成功', u'提现成功')
  240. except WechatNetworkException as e:
  241. logger.error(repr(e))
  242. if e.errMsg:
  243. return WithdrawResult(False, 0, False, e.errMsg, e.errMsg)
  244. else:
  245. return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
  246. except WeChatPayException as e:
  247. logger.error(repr(e))
  248. return error_handler(e.errCode, e.errMsg)
  249. except Exception as e:
  250. logger.exception(e)
  251. return WithdrawResult(False, 0, False, traceback.format_exc(), u'系统异常,无法确定转账是否成功,请联系客服确认结果(1003)')
  252. if gateway.version == 'v3':
  253. return withdraw_via_v3(gateway, record, payOpenId, real_user_name)
  254. else:
  255. return withdraw_via_v1(gateway, record, payOpenId, real_user_name)
  256. class WithdrawService(object):
  257. def __init__(self, payee, income_type, amount, pay_type, bank_card_no = None):
  258. # type: (CapitalUser, str, RMB, str, Union[str, Optional[str]]) -> None
  259. self.payee = payee
  260. self.income_type = income_type
  261. self.amount = amount
  262. self.pay_type = pay_type
  263. self.bank_card_no = bank_card_no
  264. self.record = None
  265. def execute(self, source_key, recurrent = False):
  266. try:
  267. logger.debug('now to withdraw for {}'.format(source_key))
  268. if not settings.CAN_WITHDRAW_FUND:
  269. logger.info('test environment do not support withdraw.')
  270. raise ServiceException({'result': 0, 'description': u'测试环境不允许提现', 'payload': {}})
  271. if self.amount < RMB(settings.WITHDRAW_MINIMUM):
  272. raise ServiceException(
  273. {'result': 0, 'description': u"提现金额不能少于%s元" % (settings.WITHDRAW_MINIMUM,), 'payload': {}})
  274. if self.amount > RMB(settings.WITHDRAW_MAXIMUM):
  275. raise ServiceException(
  276. {'result': 0, 'description': u"单次提现金额不得大于%s元" % (settings.WITHDRAW_MAXIMUM,), 'payload': {}})
  277. #: 大额提现单预警
  278. if self.amount >= RMB(settings.WHALE_WITHDRAWAL_ORDER_AMOUNT):
  279. from taskmanager.mediator import task_caller
  280. task_caller('whale_withdraw_order_alert')
  281. if self.payee.no_withdraw:
  282. raise ServiceException(
  283. {'result': 0, 'description': u"您的账号权限不足或者异常,暂时不能提现。", 'payload': {}})
  284. if not self.payee.can_withdraw_today:
  285. raise ServiceException(
  286. {'result': 0, 'description': u"超过每日最大提现次数,请明天再试。", 'payload': {}})
  287. with withdraw_lock(self.payee.role, str(self.payee.id)) as acquired:
  288. if acquired:
  289. if self.payee.sub_balance(self.income_type, source_key) < self.amount:
  290. raise ServiceException({'result': 0, 'description': u'余额不足', 'payload': {}})
  291. withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
  292. default_withdraw_gateway = withdraw_gateway_list['wechat'] # type: WechatWithdrawGateway
  293. if not default_withdraw_gateway.ledger:
  294. raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1001)', 'payload': {}})
  295. if self.pay_type == WITHDRAW_PAY_TYPE.WECHAT:
  296. if default_withdraw_gateway.manual_withdraw:
  297. logger.debug('gateway<{}> is manual withdraw.'.format(repr(default_withdraw_gateway)))
  298. raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1002)', 'payload': {}})
  299. pay_entity = BankCard(
  300. cardNo = self.payee.withdraw_open_id,
  301. holderName = self.payee.nickname,
  302. bankName = u'微信',
  303. branchName = u'微信企业付款'
  304. )
  305. self.record = self.payee.new_withdraw_record(
  306. withdraw_gateway = default_withdraw_gateway,
  307. pay_entity = pay_entity,
  308. source_key = source_key,
  309. income_type = self.income_type,
  310. amount = self.amount,
  311. pay_type = self.pay_type,
  312. manual = default_withdraw_gateway.manual_withdraw,
  313. recurrent = recurrent)
  314. if not self.record:
  315. raise ServiceException({'result': 0, 'description': u'系统繁忙,请稍后再试', 'payload': {}})
  316. if self.payee.abnormal:
  317. raise ServiceException({'result': 0, 'description': u'该帐号资金异常,请联系客服处理', 'payload': {}})
  318. handler = self.payee.new_withdraw_handler(self.record)
  319. updated = self.payee.freeze_balance(self.record.incomeType,
  320. self.record.amount,
  321. self.record.source_key,
  322. self.record.order)
  323. if not updated:
  324. handler.revoke(remarks = u'扣款失败', description = u'扣款失败')
  325. raise ServiceException({'result': 0, 'description': u'扣款失败', 'payload': {}})
  326. withdraw_result = withdraw_via_wechat(
  327. gateway = default_withdraw_gateway,
  328. record = self.record,
  329. payOpenId = self.payee.withdraw_open_id,
  330. real_user_name = self.payee.nickname) # type: WithdrawResult
  331. logger.debug(
  332. 'withdraw via wechat. record = %s; result = %s' % (
  333. self.record.order, repr(withdraw_result)))
  334. if withdraw_result.result is True:
  335. handler.approve(finishedTime=datetime.datetime.now())
  336. return {'result': 1, 'description': withdraw_result.show_message,
  337. 'payload': {'paymentId': str(self.record.id)}}
  338. else:
  339. if withdraw_result.refund:
  340. handler.revoke(remarks = withdraw_result.message,
  341. description = withdraw_result.show_message)
  342. else:
  343. handler.fail(remarks = withdraw_result.message,
  344. description = withdraw_result.show_message)
  345. raise ServiceException(
  346. {'result': withdraw_result.err_code, 'description': withdraw_result.show_message,
  347. 'payload': {}})
  348. if self.pay_type == WITHDRAW_PAY_TYPE.BANK:
  349. bank_card = self.payee.withdraw_bank_card(self.bank_card_no)
  350. if not bank_card:
  351. raise ServiceException({'result': 0, 'description': u'银行卡不存在', 'payload': {}})
  352. manual = False
  353. withdraw_gateway = default_withdraw_gateway
  354. if bank_card.manual:
  355. manual = True
  356. elif bank_card.accountType == BankCard.AccountType.PUBLIC:
  357. if withdraw_gateway_list['alipay']:
  358. withdraw_gateway = withdraw_gateway_list['alipay'] # type: AliPayWithdrawGateway
  359. if not Banks.support_public(bank_card.bankName):
  360. raise ServiceException({
  361. 'result': 0,
  362. 'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1001)',
  363. 'payload': {}})
  364. else:
  365. manual = True
  366. else:
  367. if default_withdraw_gateway.manual_withdraw:
  368. manual = True
  369. else:
  370. wechat_bank_code = Banks.get_wechat_bank_code(bank_card.bankName)
  371. if not wechat_bank_code:
  372. raise ServiceException({
  373. 'result': 0,
  374. 'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1002)',
  375. 'payload': {}})
  376. self.record = self.payee.new_withdraw_record(
  377. withdraw_gateway = withdraw_gateway,
  378. pay_entity = bank_card,
  379. source_key = source_key,
  380. income_type = self.income_type,
  381. amount = self.amount,
  382. pay_type = self.pay_type,
  383. manual = manual,
  384. recurrent = recurrent) # type: WithdrawRecord
  385. if not self.record:
  386. raise ServiceException({'result': 0, 'description': u'系统繁忙,请稍后再试(1002)', 'payload': {}})
  387. handler = self.payee.new_withdraw_handler(self.record)
  388. updated = self.payee.freeze_balance(self.record.incomeType,
  389. self.record.amount,
  390. self.record.source_key,
  391. self.record.order)
  392. if not updated:
  393. handler.revoke(remarks = u'扣款失败', description = u'扣款失败')
  394. raise ServiceException({'result': 0, 'description': u'扣款失败', 'payload': {}})
  395. # 对公的提现, 或者不支持的银行卡(使用manual标记的BankCard 或者 Merchant)
  396. if manual:
  397. withdraw_result = WithdrawResult(True, 1, False, u'提现申请已经受理', u'提现申请已经受理')
  398. logger.debug(
  399. 'withdraw via bank(manual). record = %s; result = %s' % (
  400. self.record.order, repr(withdraw_result)))
  401. handler.processing(remarks = u'提现申请已经受理', description = u'提现申请已经受理')
  402. return {
  403. 'result': withdraw_result.err_code,
  404. 'description': withdraw_result.show_message,
  405. 'payload': {'paymentId': str(self.record.id)}}
  406. else:
  407. gateway_type, withdraw_result = withdraw_via_bank(withdraw_gateway, self.record, bank_card)
  408. logger.debug(
  409. 'withdraw via bank({}). record = {}; result = {}'.format(
  410. gateway_type, self.record.order, repr(withdraw_result)))
  411. if withdraw_result.result is True:
  412. handler.processing(remarks = u'提现申请已经受理', description = u'提现申请已经受理')
  413. return {
  414. 'result': 1,
  415. 'description': u'提现申请已经受理',
  416. 'payload': {
  417. 'paymentId': str(self.record.id)
  418. }
  419. }
  420. else:
  421. if withdraw_result.refund:
  422. handler.revoke(remarks = withdraw_result.message,
  423. description = withdraw_result.show_message)
  424. else:
  425. handler.fail(remarks = withdraw_result.message,
  426. description = withdraw_result.show_message)
  427. raise ServiceException(
  428. {'result': withdraw_result.err_code, 'description': withdraw_result.show_message,
  429. 'payload': {}})
  430. raise ServiceException({'result': 0, 'description': u'系统错误,请稍后再试', 'payload': {}})
  431. else:
  432. raise ServiceException({'result': 0, 'description': u'操作频繁,请稍后再试', 'payload': {}})
  433. except ServiceException as e:
  434. logger.exception(e)
  435. if self.record:
  436. if 'payload' in e.result:
  437. e.result['payload'].update({'paymentId': str(self.record.id)})
  438. else:
  439. e.result['payload'] = {'paymentId': str(self.record.id)}
  440. return e.result
  441. except WithdrawError as e:
  442. logger.exception(e)
  443. return {
  444. 'result': 0,
  445. 'description': e.message,
  446. 'payload': {'paymentId': str(self.record.id)} if str(self.record.id) else {}
  447. }
  448. class WithdrawRetryService(object):
  449. def __init__(self, record, only_fail = True):
  450. # type: (WithdrawRecord, bool)->None
  451. self.record = record # type : WithdrawRecord
  452. self.only_fail = only_fail
  453. def get_payee(self):
  454. return ROLE.from_role_id(self.record.role, self.record.ownerId)
  455. def check_retry_over(self, handler):
  456. end = self.record.postTime + datetime.timedelta(days = 3) # type: datetime
  457. now = datetime.datetime.now()
  458. if end.year > now.year or end.month > now.month or end.day > now.day:
  459. handler.revoke(remarks = u'平台退单', description = u'提现失败')
  460. raise ServiceException(
  461. {
  462. 'result': 0,
  463. 'description': u'重试次数超限,平台退单',
  464. 'payload': {}
  465. })
  466. def execute(self):
  467. try:
  468. if self.record.refunded:
  469. raise ServiceException(
  470. {
  471. 'result': 0,
  472. 'description': 'record<id={}> has refunded.'.format(str(self.record.id)),
  473. 'payload': {}
  474. })
  475. if self.record.manual:
  476. raise ServiceException(
  477. {
  478. 'result': 0,
  479. 'description': 'record<id={}> can not be manual.'.format(str(self.record.id)),
  480. 'payload': {}
  481. })
  482. if self.only_fail:
  483. if self.record.status != WithdrawStatus.FAILED:
  484. raise ServiceException(
  485. {
  486. 'result': 0,
  487. 'description': 'record<id={}> must be fail.'.format(str(self.record.id)),
  488. 'payload': {}
  489. })
  490. else:
  491. if self.record.status not in [WithdrawStatus.SUBMITTED, WithdrawStatus.FAILED]:
  492. raise ServiceException(
  493. {
  494. 'result': 0,
  495. 'description': 'record<id={}> must be fail.'.format(str(self.record.id)),
  496. 'payload': {}
  497. })
  498. payee = self.get_payee() # type: CapitalUser
  499. if payee.role != self.record.role:
  500. raise ServiceException(
  501. {
  502. 'result': 0,
  503. 'description': 'role is not same',
  504. 'payload': {}
  505. })
  506. withdraw_gateway = WithdrawGateway.from_withdraw_gateway_key(
  507. self.record.withdrawGatewayKey,
  508. self.record.extras.get('gateway_version', 'v1')) # type: WithdrawGateway
  509. if self.record.payType == WITHDRAW_PAY_TYPE.WECHAT:
  510. handler = payee.new_withdraw_handler(self.record)
  511. try:
  512. query_result = withdraw_gateway.get_transfer_result_via_changes(
  513. self.record.order) # type: WechatWithdrawQueryResult
  514. errcode, errmsg = query_result.error_desc
  515. if query_result.is_failed:
  516. handler.revoke(remarks = errcode,
  517. description = errmsg)
  518. return {
  519. 'result': 1,
  520. 'description': errmsg,
  521. 'payload': {
  522. 'paymentId': str(self.record.id)
  523. }
  524. }
  525. if query_result.is_successful:
  526. handler.approve()
  527. return {
  528. 'result': 1,
  529. 'description': 'SUCCESS',
  530. 'payload': {
  531. 'paymentId': str(self.record.id)
  532. }
  533. }
  534. if query_result.is_processing:
  535. pass
  536. else:
  537. raise ServiceException(
  538. {
  539. 'result': 0,
  540. 'description': u'未知订单状态{}'.format(query_result.order_status),
  541. 'payload': {}
  542. })
  543. except WeChatPayException as e:
  544. # 如果业务状态是查不到订单, 需要继续处理, 其他抛出异常
  545. if e.errCode not in ['ORDERNOTEXIST', 'NOT_FOUND']:
  546. raise e
  547. self.check_retry_over(handler)
  548. payee.freeze_balance(self.record.incomeType,
  549. self.record.amount,
  550. self.record.source_key,
  551. self.record.order)
  552. withdraw_result = withdraw_via_wechat(withdraw_gateway,
  553. self.record,
  554. self.record.accountCode,
  555. self.record.cardUserName) # type: WithdrawResult
  556. logger.debug(
  557. 'withdraw via wechat. record = %s; result = %s' % (
  558. self.record.order, repr(withdraw_result)))
  559. if withdraw_result.result is True:
  560. handler.approve()
  561. return {
  562. 'result': 1,
  563. 'description': withdraw_result.show_message,
  564. 'payload': {
  565. 'paymentId': str(self.record.id)
  566. }
  567. }
  568. else:
  569. raise ServiceException(
  570. {
  571. 'result': withdraw_result.err_code,
  572. 'description': withdraw_result.show_message,
  573. 'payload': {}
  574. })
  575. if self.record.payType == WITHDRAW_PAY_TYPE.BANK:
  576. handler = payee.new_withdraw_handler(self.record)
  577. try:
  578. query_result = withdraw_gateway.get_transfer_result_via_bank(
  579. self.record.order) # type: WechatWithdrawQueryResult
  580. errcode, errmsg = query_result.error_desc
  581. if query_result.is_failed:
  582. handler.revoke(remarks = errcode,
  583. description = errmsg)
  584. return {
  585. 'result': 1,
  586. 'description': errmsg,
  587. 'payload': {
  588. 'paymentId': str(self.record.id)
  589. }
  590. }
  591. if query_result.is_successful:
  592. handler.approve()
  593. return {
  594. 'result': 1,
  595. 'description': 'SUCCESS',
  596. 'payload': {
  597. 'paymentId': str(self.record.id)
  598. }
  599. }
  600. if query_result.is_processing:
  601. pass
  602. else:
  603. raise ServiceException(
  604. {
  605. 'result': 0,
  606. 'description': u'未知订单状态{}'.format(query_result.order_status),
  607. 'payload': {}
  608. })
  609. except (WeChatPayException, AliPayServiceException) as e:
  610. # 如果业务状态是查不到订单, 需要继续处理, 否则继续抛出异常
  611. if e.errCode not in ['ORDERNOTEXIST', 'NOT_FOUND', 'ORDER_NOT_EXIST']:
  612. raise e
  613. self.check_retry_over(handler)
  614. bank_card = payee.withdraw_bank_card(self.record.accountCode)
  615. if not bank_card:
  616. raise ServiceException(
  617. {
  618. 'result': 0,
  619. 'description': '银行卡不存在',
  620. 'payload': {}
  621. })
  622. payee.freeze_balance(self.record.incomeType,
  623. self.record.amount,
  624. self.record.source_key,
  625. self.record.order)
  626. gateway_type, withdraw_result = withdraw_via_bank(withdraw_gateway, self.record, bank_card)
  627. logger.debug(
  628. 'withdraw via bank({}). record = {}; result = {}'.format(
  629. gateway_type, self.record.order, repr(withdraw_result)))
  630. if withdraw_result.result is True:
  631. handler.processing(remarks=withdraw_result.message, description=withdraw_result.show_message)
  632. return {
  633. 'result': withdraw_result.err_code,
  634. 'description': withdraw_result.show_message,
  635. 'payload': {
  636. 'paymentId': str(self.record.id)
  637. }
  638. }
  639. else:
  640. raise ServiceException(
  641. {
  642. 'result': withdraw_result.err_code,
  643. 'description': withdraw_result.show_message,
  644. 'payload': {}
  645. })
  646. except WechatNetworkException as e:
  647. logger.exception(e)
  648. return {'result': 0, 'description': e.errMsg, 'payload': {}}
  649. except WeChatPayException as e:
  650. logger.exception(e)
  651. return {'result': 0, 'description': e.errMsg, 'payload': {}}
  652. except ServiceException as e:
  653. return e.result
  654. except WithdrawError as e:
  655. logger.exception(e)
  656. return {'result': 0, 'description': e.message, 'payload': {}}
  657. except Exception as e:
  658. logger.exception(e)
  659. return {'result': 0, 'description': e.message, 'payload': {}}