wechatpay.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. # coding=utf-8
  2. import uuid
  3. import requests
  4. import json
  5. import xmltodict
  6. import time
  7. from hashlib import md5
  8. from django.conf import settings
  9. from util.splitaccount_tool import SplitAccountTool
  10. from util.exceptions import CustomError
  11. from apps.foundation.models import BizLog
  12. # 微信支付sign_type
  13. WEIXIN_SIGN_TYPE = 'MD5'
  14. # 服务器IP地址
  15. WEIXIN_SPBILL_CREATE_IP = '139.9.148.181'
  16. # 微信支付用途
  17. WEIXIN_BODY = u'小程序支付'
  18. # 微信统一下单URL
  19. WEIXIN_UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
  20. # 微信查询订单URL
  21. WEIXIN_QUERY_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
  22. # 微信支付回调API
  23. WEIXIN_CALLBACK_API = 'https://lsr.zzly.vip/api/wechat_notify/'
  24. class SplitAccountFuc(object):
  25. '''直连服务商 分账 文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml'''
  26. def __init__(self,appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=None, proxy=None):
  27. self._appid = appid
  28. self._mchid = mchid
  29. self._core = SplitAccountTool(appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=cert_dir, proxy=proxy)
  30. def splitaccount_order(self, transaction_id, out_order_no, receivers):
  31. '''
  32. 请求分账 微信订单支付成功后,服务商代特约商户发起分账请求,将结算后的钱分到分账接收方
  33. 注意:对同一笔订单最多能发起50次分账请求,每次请求最多分给50个接收方
  34. 此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取
  35. 请求分账里边的openid 是需要先调用添加分账接收方接口添加分账关系
  36. '''
  37. path = "/v3/profitsharing/orders"
  38. params = {
  39. 'appid': self._appid,
  40. 'transaction_id': transaction_id, # 微信支付订单号
  41. 'out_order_no': out_order_no, # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@
  42. 'receivers': [],
  43. 'unfreeze_unsplit': True # 是否解冻剩余未分金额 如果只分一次就填true
  44. }
  45. for item in receivers:
  46. receiver_item = {
  47. 'type': 'PERSONAL_OPENID',
  48. 'account': item['account'],
  49. 'amount': int(round(item['amount'], 0)), # 单位为分 只能为整数 不能超过原订单支付金额及最大分账比例金额
  50. 'description': item['description']
  51. }
  52. params['receivers'].append(receiver_item)
  53. success = True
  54. data = {}
  55. try:
  56. code, message = self._core.request(path, SplitAccountTool.POST, data=params)
  57. result = json.loads(message)
  58. if code == 200:
  59. data['order_id'] = result.get('order_id')
  60. data['state'] = result.get('state')
  61. data['receivers'] = result.get('receivers')
  62. else:
  63. success = False
  64. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + result.get('code'))
  65. except CustomError as e:
  66. success = False
  67. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + e.get_error_msg())
  68. except Exception as e:
  69. success = False
  70. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + str(e))
  71. return success, data
  72. def splitaccount_addreceiver(self, account):
  73. '''添加分账接收方'''
  74. path = "/v3/profitsharing/receivers/add"
  75. params = {
  76. 'appid': self._appid,
  77. 'type': "PERSONAL_OPENID",
  78. 'account': account, # 接收人的openid
  79. 'relation_type': "USER", # body子商户与接收方的关系
  80. }
  81. success = True
  82. try:
  83. code, message = self._core.request(path, SplitAccountTool.POST, data=params)
  84. result = json.loads(message)
  85. if code != 200:
  86. success = False
  87. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + result.get('code'))
  88. except CustomError as e:
  89. success = False
  90. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + e.get_error_msg())
  91. except Exception as e:
  92. success = False
  93. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + str(e))
  94. return success
  95. def splitaccount_deletereceiver(self, account):
  96. '''删除分账接收方'''
  97. path = "/v3/profitsharing/receivers/delete"
  98. params = {
  99. 'appid': self._appid,
  100. 'type': "PERSONAL_OPENID",
  101. 'account': account, # 接收人的openid
  102. }
  103. success = True
  104. try:
  105. code, message = self._core.request(path, SplitAccountTool.POST, data=params)
  106. result = json.loads(message)
  107. if code != 200:
  108. success = False
  109. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + result.get('code'))
  110. except CustomError as e:
  111. success = False
  112. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + e.get_error_msg())
  113. except Exception as e:
  114. success = False
  115. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + str(e))
  116. return success
  117. def splitaccount_orderquery(self, transaction_id, out_order_no):
  118. '''
  119. 查询分账结果
  120. transaction_id 微信支付订单号
  121. out_order_no 商户分账单号
  122. '''
  123. success = True
  124. data = {}
  125. if transaction_id and out_order_no:
  126. path = '/v3/profitsharing/orders/%s?transaction_id=%s' % (out_order_no, transaction_id)
  127. else:
  128. success = False
  129. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:参数错误!' % out_order_no)
  130. return success, data
  131. try:
  132. code, message = self._core.request(path)
  133. result = json.loads(message)
  134. if code == 200:
  135. data['order_id'] = result.get('order_id')
  136. data['state'] = result.get('state')
  137. data['receivers'] = result.get('receivers')
  138. else:
  139. success = False
  140. # data = result.get('code')
  141. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + result.get('code'))
  142. except CustomError as e:
  143. success = False
  144. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + e.get_error_msg())
  145. except Exception as e:
  146. success = False
  147. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + str(e))
  148. return success, data
  149. def splitaccount_return(self):
  150. '''请求分账回退'''
  151. pass
  152. def splitaccount_returnquery(self):
  153. '''查询分账回退结果'''
  154. pass
  155. def splitaccount_unfreeze(self, transaction_id, out_order_no):
  156. '''解冻剩余资金'''
  157. path = "/v3/profitsharing/orders"
  158. params = {
  159. 'transaction_id': transaction_id, # 微信支付订单号
  160. 'out_order_no': out_order_no, # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@
  161. 'description': "解冻资金"
  162. }
  163. success = True
  164. data = {}
  165. try:
  166. code, message = self._core.request(path, SplitAccountTool.POST, data=params)
  167. result = json.loads(message)
  168. if code == 200:
  169. data['order_id'] = result.get('order_id')
  170. data['state'] = result.get('state')
  171. data['receivers'] = result.get('receivers')
  172. else:
  173. success = False
  174. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + result.get('code'))
  175. except CustomError as e:
  176. success = False
  177. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + e.get_error_msg())
  178. except Exception as e:
  179. success = False
  180. BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + str(e))
  181. return success, data
  182. def splitaccount_amountquery(self):
  183. '''查询剩余待分金额'''
  184. pass
  185. def splitaccount_configquery(self):
  186. '''查询最大分账比例'''
  187. pass
  188. def splitaccount_bill(self):
  189. '''申请分账账单'''
  190. pass
  191. class WeChatResponse():
  192. def __init__(self,appid, agent_num, agent_key):
  193. self.params = {
  194. "appid": appid,
  195. 'mch_id': agent_num,
  196. 'nonce_str': '',
  197. 'sign_type': WEIXIN_SIGN_TYPE,
  198. 'sign': '',
  199. 'out_trade_no': '',
  200. }
  201. self.prepay_id = None
  202. self.merchant_key = agent_key
  203. # 查询订单
  204. def orderquery(self, out_trade_no):
  205. self.params['out_trade_no'] = out_trade_no
  206. self.params['nonce_str'] = generate_nonce_str()
  207. self.params['sign'] = generate_sign(self.params, self.merchant_key)
  208. data = xmltodict.unparse({'xml': self.params}, pretty=True, full_document=False).encode('utf-8')
  209. headers = {'Content-Type': 'application/xml'}
  210. res = requests.post(WEIXIN_QUERY_ORDER_URL, data=data, headers=headers)
  211. if res.status_code != 200:
  212. raise CustomError(u'微信请求失败!')
  213. result = json.loads(json.dumps(xmltodict.parse(res.content)))
  214. if result['xml']['return_code'] != 'SUCCESS':
  215. raise CustomError(u'微信通信失败![%s]' % result['xml']['return_msg'])
  216. print(u'微信交易状态![%s]' % (result['xml']['trade_state_desc']))
  217. if result['xml']['trade_state'] == 'NOTPAY':
  218. return result['xml']['total_fee']
  219. # raise CustomError(u'微信交易状态![%s]' % (result['xml']['trade_state_desc']))
  220. # 其他状态,返回金额0
  221. return 0
  222. # return result['xml']['total_fee']
  223. class WechatPay():
  224. def __init__(self, appid, mch_id, merchant_key):
  225. self.params = {
  226. 'appid': appid,
  227. 'mch_id': mch_id,
  228. 'nonce_str': '',
  229. 'sign_type': WEIXIN_SIGN_TYPE,
  230. 'body': WEIXIN_BODY,
  231. 'out_trade_no': '',
  232. 'total_fee': '',
  233. 'spbill_create_ip': WEIXIN_SPBILL_CREATE_IP,
  234. 'notify_url': WEIXIN_CALLBACK_API + appid + '/',
  235. 'trade_type': 'JSAPI'
  236. }
  237. self.prepay_id = None
  238. self.merchant_key = merchant_key
  239. def getAppString(self):
  240. data = {
  241. 'appId': self.params['appid'],
  242. 'signType': WEIXIN_SIGN_TYPE,
  243. 'package': "prepay_id={}".format(self.prepay_id),
  244. 'nonceStr': generate_nonce_str(),
  245. 'timeStamp': str(int(time.time()))
  246. }
  247. data['paySign'] = generate_sign(data, self.merchant_key)
  248. data.pop('appId')
  249. return data
  250. def unifiedOrder(self,out_trade_no,total_fee, openid, profit_sharing):
  251. self.params['profit_sharing'] = profit_sharing # 是否分账参数 Y 需要分账 N 不分账 字母大写默认不分账
  252. self.params['out_trade_no'] = out_trade_no
  253. self.params['total_fee'] = int(round(total_fee, 0))
  254. self.params['openid'] = openid
  255. self.params['nonce_str'] = generate_nonce_str()
  256. self.params['sign'] = generate_sign(self.params, self.merchant_key)
  257. data = xmltodict.unparse({'xml': self.params}, pretty=True, full_document=False).encode('utf-8')
  258. headers = {'Content-Type': 'application/xml'}
  259. res = requests.post(WEIXIN_UNIFIED_ORDER_URL, data=data, headers=headers)
  260. if res.status_code != 200:
  261. raise CustomError(u'微信请求失败!')
  262. result = json.loads(json.dumps(xmltodict.parse(res.content)))
  263. if result['xml']['return_code'] != 'SUCCESS':
  264. raise CustomError(u'微信通信失败![%s]' % result['xml']['return_msg'])
  265. if result['xml']['result_code'] != 'SUCCESS':
  266. raise CustomError(u'微信交易失败![%s:%s]' % (result['xml']['err_code'],result['xml']['err_code_des']))
  267. self.prepay_id = result['xml']['prepay_id']
  268. return result['xml']
  269. class WechatPayNotify():
  270. def __init__(self,params, merchant_key):
  271. self.params = params
  272. self.merchant_key = merchant_key
  273. def handle(self):
  274. resp_dict = json.loads(json.dumps(xmltodict.parse(self.params)))['xml']
  275. return_code = resp_dict['return_code']
  276. if return_code != 'SUCCESS':
  277. return None
  278. if not validate_sign(resp_dict, self.merchant_key):
  279. return None
  280. return resp_dict
  281. @staticmethod
  282. def response_ok():
  283. return_info = {
  284. 'return_code': 'SUCCESS',
  285. 'return_msg': 'OK'
  286. }
  287. return generate_response_data(return_info)
  288. @staticmethod
  289. def response_fail():
  290. return_info = {
  291. 'return_code': 'FAIL',
  292. 'return_msg': 'FAIL'
  293. }
  294. return generate_response_data(return_info)
  295. def generate_nonce_str():
  296. """
  297. 生成随机字符串
  298. """
  299. return str(uuid.uuid4()).replace('-', '')
  300. def generate_sign(params, merchant_key):
  301. """
  302. 生成md5签名的参数
  303. """
  304. if 'sign' in params:
  305. params.pop('sign')
  306. src = '&'.join(['%s=%s' % (k, v) for k, v in sorted(params.items())]) + '&key=%s' % merchant_key
  307. return md5(src.encode('utf-8')).hexdigest().upper()
  308. def validate_sign(resp_dict, merchant_key):
  309. """
  310. 验证微信返回的签名
  311. """
  312. if 'sign' not in resp_dict:
  313. return False
  314. wx_sign = resp_dict['sign']
  315. sign = generate_sign(resp_dict, merchant_key)
  316. if sign == wx_sign:
  317. return True
  318. return False
  319. def generate_response_data(resp_dict):
  320. """
  321. 字典转xml
  322. """
  323. return xmltodict.unparse({'xml': resp_dict}, pretty=True, full_document=False).encode('utf-8')