# coding=utf-8 import uuid import requests import json import xmltodict import time from hashlib import md5 from django.conf import settings from util.splitaccount_tool import SplitAccountTool from util.exceptions import CustomError from apps.foundation.models import BizLog # 微信支付sign_type WEIXIN_SIGN_TYPE = 'MD5' # 服务器IP地址 # WEIXIN_SPBILL_CREATE_IP = '81.70.58.181' # 微信支付用途 WEIXIN_BODY = u'小程序支付' # 微信统一下单URL WEIXIN_UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder' # 微信查询订单URL WEIXIN_QUERY_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/orderquery' # 微信支付回调API # WEIXIN_CALLBACK_API = 'https://jpm.zzly.vip/api/wechat_notify/' class SplitAccountFuc(object): '''直连服务商 分账 文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml''' def __init__(self,appid, mchid, cert_serial_no, apiv3_key, proxy=None): self._appid = appid self._mchid = mchid self._core = SplitAccountTool(appid, mchid, cert_serial_no, apiv3_key, proxy=proxy) def splitaccount_order(self, transaction_id, out_order_no, receivers): ''' 请求分账 微信订单支付成功后,服务商代特约商户发起分账请求,将结算后的钱分到分账接收方 注意:对同一笔订单最多能发起50次分账请求,每次请求最多分给50个接收方 此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取 请求分账里边的openid 是需要先调用添加分账接收方接口添加分账关系 ''' path = "/v3/profitsharing/orders" params = { 'appid': self._appid, 'transaction_id': transaction_id, # 微信支付订单号 'out_order_no': out_order_no, # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@ 'receivers': [], 'unfreeze_unsplit': True # 是否解冻剩余未分金额 如果只分一次就填true } for item in receivers: receiver_item = { 'type': 'PERSONAL_OPENID', 'account': item['account'], 'amount': int(round(item['amount'], 0)), # 单位为分 只能为整数 不能超过原订单支付金额及最大分账比例金额 'description': item['description'] } params['receivers'].append(receiver_item) code, message = self._core.request(path, SplitAccountTool.POST, data=params) result = json.loads(message) if code != 200: raise CustomError(u'[{}]分账失败!原因:{}'.format(out_order_no, result)) return result def splitaccount_addreceiver(self, account): '''添加分账接收方''' path = "/v3/profitsharing/receivers/add" params = { 'appid': self._appid, 'type': "PERSONAL_OPENID", 'account': account, # 接收人的openid 'relation_type': "USER", # body子商户与接收方的关系 } code, message = self._core.request(path, SplitAccountTool.POST, data=params) result = json.loads(message) if code != 200: raise CustomError(u'[{}]添加分账接收方失败!原因:{}'.format(account, result)) def splitaccount_deletereceiver(self, account): '''删除分账接收方''' path = "/v3/profitsharing/receivers/delete" params = { 'appid': self._appid, 'type': "PERSONAL_OPENID", 'account': account, # 接收人的openid } code, message = self._core.request(path, SplitAccountTool.POST, data=params) result = json.loads(message) if code != 200: raise CustomError(u'[{}]删除分账接收方失败!原因:{}'.format(account, result)) def splitaccount_orderquery(self, transaction_id, out_order_no): ''' 查询分账结果 transaction_id 微信支付订单号 out_order_no 商户分账单号 ''' if transaction_id and out_order_no: path = '/v3/profitsharing/orders/%s?transaction_id=%s' % (out_order_no, transaction_id) else: raise CustomError(u'[%s]查询分账结果失败!原因:参数错误!' % out_order_no) code, message = self._core.request(path) result = json.loads(message) if code != 200: raise CustomError(u'[{}]查询分账结果失败!原因:{}'.format(out_order_no, result)) return result def splitaccount_return(self): '''请求分账回退''' pass def splitaccount_returnquery(self): '''查询分账回退结果''' pass def splitaccount_unfreeze(self, transaction_id, out_order_no): '''解冻剩余资金''' path = "/v3/profitsharing/orders" params = { 'transaction_id': transaction_id, # 微信支付订单号 'out_order_no': out_order_no, # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@ 'description': "解冻资金" } code, message = self._core.request(path, SplitAccountTool.POST, data=params) result = json.loads(message) if code != 200: raise CustomError(u'[{}]解冻剩余资金失败!原因:{}'.format(out_order_no, result)) return result def splitaccount_amountquery(self): '''查询剩余待分金额''' pass def splitaccount_configquery(self): '''查询最大分账比例''' pass def splitaccount_bill(self): '''申请分账账单''' pass class WeChatResponse(): def __init__(self,appid, agent_num, agent_key): self.params = { "appid": appid, 'mch_id': agent_num, 'nonce_str': '', 'sign_type': WEIXIN_SIGN_TYPE, 'sign': '', 'out_trade_no': '', } self.prepay_id = None self.merchant_key = agent_key # 查询订单 def orderquery(self, out_trade_no): self.params['out_trade_no'] = out_trade_no self.params['nonce_str'] = generate_nonce_str() self.params['sign'] = generate_sign(self.params, self.merchant_key) data = xmltodict.unparse({'xml': self.params}, pretty=True, full_document=False).encode('utf-8') headers = {'Content-Type': 'application/xml'} res = requests.post(WEIXIN_QUERY_ORDER_URL, data=data, headers=headers) if res.status_code != 200: raise CustomError(u'微信请求失败!') result = json.loads(json.dumps(xmltodict.parse(res.content))) if result['xml']['return_code'] != 'SUCCESS': raise CustomError(u'微信通信失败![%s]' % result['xml']['return_msg']) print(u'微信交易状态![%s]' % (result['xml']['trade_state_desc'])) if result['xml']['trade_state'] == 'NOTPAY': return result['xml']['total_fee'] # raise CustomError(u'微信交易状态![%s]' % (result['xml']['trade_state_desc'])) # 其他状态,返回金额0 return 0 # return result['xml']['total_fee'] class WechatPay(): def __init__(self, app): self.params = { 'appid': app.authorizer_appid, 'mch_id': app.agent_num, 'nonce_str': '', 'sign_type': WEIXIN_SIGN_TYPE, 'body': WEIXIN_BODY, 'out_trade_no': '', 'total_fee': '', 'spbill_create_ip': app.create_ip, 'notify_url': app.callback_api + app.authorizer_appid + '/', 'trade_type': 'JSAPI' } self.prepay_id = None self.merchant_key = app.agent_key def getAppString(self): data = { 'appId': self.params['appid'], 'signType': WEIXIN_SIGN_TYPE, 'package': "prepay_id={}".format(self.prepay_id), 'nonceStr': generate_nonce_str(), 'timeStamp': str(int(time.time())) } data['paySign'] = generate_sign(data, self.merchant_key) data.pop('appId') return data def unifiedOrder(self,out_trade_no,total_fee, openid, profit_sharing): self.params['profit_sharing'] = profit_sharing # 是否分账参数 Y 需要分账 N 不分账 字母大写默认不分账 self.params['out_trade_no'] = out_trade_no self.params['total_fee'] = int(round(total_fee, 0)) self.params['openid'] = openid self.params['nonce_str'] = generate_nonce_str() self.params['sign'] = generate_sign(self.params, self.merchant_key) data = xmltodict.unparse({'xml': self.params}, pretty=True, full_document=False).encode('utf-8') headers = {'Content-Type': 'application/xml'} res = requests.post(WEIXIN_UNIFIED_ORDER_URL, data=data, headers=headers) if res.status_code != 200: raise CustomError(u'微信请求失败!') result = json.loads(json.dumps(xmltodict.parse(res.content))) if result['xml']['return_code'] != 'SUCCESS': raise CustomError(u'微信通信失败![%s]' % result['xml']['return_msg']) if result['xml']['result_code'] != 'SUCCESS': raise CustomError(u'微信交易失败![%s:%s]' % (result['xml']['err_code'],result['xml']['err_code_des'])) self.prepay_id = result['xml']['prepay_id'] return result['xml'] class WechatPayNotify(): def __init__(self,params, merchant_key): self.params = params self.merchant_key = merchant_key def handle(self): resp_dict = json.loads(json.dumps(xmltodict.parse(self.params)))['xml'] return_code = resp_dict['return_code'] if return_code != 'SUCCESS': return None if not validate_sign(resp_dict, self.merchant_key): return None return resp_dict @staticmethod def response_ok(): return_info = { 'return_code': 'SUCCESS', 'return_msg': 'OK' } return generate_response_data(return_info) @staticmethod def response_fail(): return_info = { 'return_code': 'FAIL', 'return_msg': 'FAIL' } return generate_response_data(return_info) def generate_nonce_str(): """ 生成随机字符串 """ return str(uuid.uuid4()).replace('-', '') def generate_sign(params, merchant_key): """ 生成md5签名的参数 """ if 'sign' in params: params.pop('sign') src = '&'.join(['%s=%s' % (k, v) for k, v in sorted(params.items())]) + '&key=%s' % merchant_key return md5(src.encode('utf-8')).hexdigest().upper() def validate_sign(resp_dict, merchant_key): """ 验证微信返回的签名 """ if 'sign' not in resp_dict: return False wx_sign = resp_dict['sign'] sign = generate_sign(resp_dict, merchant_key) if sign == wx_sign: return True return False def generate_response_data(resp_dict): """ 字典转xml """ return xmltodict.unparse({'xml': resp_dict}, pretty=True, full_document=False).encode('utf-8')