# -*- coding: utf-8 -*- # !/usr/bin/env python """ 微信授权的2种scope区别 1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。 用户感知的就是直接进入了回调页(往往是业务页面) 2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注, 就可在授权后获取该用户的基本信息。 """ import base64 import datetime import json import logging from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from dateutil.relativedelta import relativedelta from typing import Optional, Union from typing import TYPE_CHECKING from werkzeug.utils import cached_property from apilib.monetary import RMB from apilib.utils_string import cn from apps.web.exceptions import WithdrawOrderNotExist from apps.web.common.models import WithdrawBankCard, WithdrawBanks from apps.web.constant import WECHAT_WITHDRAW_STATUS, AppPlatformType from apps.web.core import WechatMixin from apps.web.core.models import WechatPayApp from apps.web.core.payment import PaymentGateway, WithdrawGateway from apps.web.utils import testcase_point from library.wechatpayv3 import WechatClientV3 from library.wechatpy.pay import WeChatPay from library.wechatbase.exceptions import WeChatException if TYPE_CHECKING: pass logger = logging.getLogger(__name__) logging.getLogger('dicttoxml').setLevel(logging.WARNING) class WechatWithdrawQueryResult(dict): @property def order_status(self): return self.get('status') @property def finished_time(self): return datetime.datetime.now() @property def extra(self): return {} @property def is_successful(self): assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS' return self.get('status') == WECHAT_WITHDRAW_STATUS.SUCCESS @property def is_failed(self): assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS' return self.get('status') in frozenset([ WECHAT_WITHDRAW_STATUS.FAILED, WECHAT_WITHDRAW_STATUS.BANK_FAIL, WECHAT_WITHDRAW_STATUS.FAIL]) @property def is_processing(self): assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS' return self.get('status') == WECHAT_WITHDRAW_STATUS.PROCESSING @property def is_refund(self): return False @property def error_desc(self): if self.is_successful: return self.get('status'), u'成功' if self.is_processing: return self.get('status'), u'正在处理' reason = self.get('reason', u'转账失败,请登录微信商户号查询具体订单信息') return self.get('status'), reason def __repr__(self): return 'int """微信企业付款金额,单位为分""" return int(rmb.amount * 100) @cached_property def public_key(self): result = self.__client_for_inner__.transfer.get_rsa_public_key() return RSA.importKey(result['pub_key']) def encrypt_with_public_key(self, unencrypted_string, public_key = None): """ `doc`: 1、 调用获取RSA公钥API获取RSA公钥,落地成本地文件,假设为public.pem 2、 确定public.pem文件的存放路径,同时修改代码中文件的输入路径,加载RSA公钥 3、 用标准的RSA加密库对敏感信息进行加密,选择RSA_PKCS1_OAEP_PADDING填充模式 (eg:Java的填充方式要选 " RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING") 4、 得到进行rsa加密并转base64之后的密文 5、 将密文传给微信侧相应字段,如付款接口(enc_bank_no/enc_true_name) 加密签名生成,供与微信通信 :return: """ if public_key is None: public_key = self.public_key cipher = PKCS1_OAEP.new(public_key) return base64.b64encode(cipher.encrypt(unencrypted_string)) class WechatPaymentGateway(PaymentGateway, WechatPayMixin): def __init__(self, app, gateway_type = AppPlatformType.WECHAT): # type: (Union[WechatPayApp], AppPlatformType)->None """ :param app: WechatPayApp """ super(WechatPaymentGateway, self).__init__(app) self.__gateway_type__ = gateway_type def __repr__(self): return \ 'WechatPaymentGateway' \ % ( str(self.occupantId), self.appid, self.mchid, self.manual_withdraw ) @property def __client__(self): # type: ()->WeChatPay return WeChatPay(appid = self.appid, api_key = self.apikey, mch_id = self.mchid) @property def ssl_client(self): return WeChatPay(appid = self.appid, api_key = self.apikey, mch_id = self.mchid, mch_cert = self.ssl_cert, mch_key = self.ssl_key) def generate_js_payment_params(self, payOpenId, out_trade_no, notify_url, money, body = cn(u"购买金币"), spbill_create_ip = '127.0.0.1', attach = None): # type: (basestring, str, basestring, RMB, basestring, str, Optional[dict])->dict """ 注意: money必须以元为单位 生成签名,供前端使用,供为用户侧生成微信订单作用 注意这里有个微信的坑,就是attach参数不能包含空格,否则在某种情况下被转换成+号,造成解析失败 :return: dict """ raw = self.client.order.create(trade_type = 'JSAPI', body = body, total_fee = str(self._to_fen(money)), notify_url = notify_url, client_ip = spbill_create_ip, user_id = payOpenId, out_trade_no = out_trade_no, attach = json.dumps(attach, separators = (',', ':')) if attach else '') return self.client.jsapi.get_jsapi_params(prepay_id = raw["prepay_id"]) def api_trade_query(self, out_trade_no = None, trade_no = None): # type:(Optional[str], Optional[str])->dict """ 查询订单 :param out_trade_no: 商户订单号 :param trade_no: 微信订单号 :return: """ assert out_trade_no or trade_no, 'must input out_trade_no or trade_no' return self.client.order.query(transaction_id = trade_no, out_trade_no = out_trade_no) def download_bill(self, bill_date = None, bill_type = 'ALL', tar_type = 'GZIP'): """ 下载账单,默认3个月前 此接口微信处理很粗糙,直接输出csv到http的响应里,远不如支付宝的实现 :param bill_date: :param bill_type: :param tar_type: :return: """ if bill_date is None: bill_date = (datetime.date.today() + relativedelta(months = -3)).strftime('%Y%m%d') return self.client.tools.download_bill(bill_date, bill_type) @testcase_point() def refund_to_user(self, out_trade_no, out_refund_no, refund_fee, total_fee, refund_reason, **kwargs): # type:(str, str, RMB, RMB, str, dict)->dict return self.ssl_client.refund.apply( total_fee = self._to_fen(total_fee), refund_fee = self._to_fen(refund_fee), out_refund_no = out_refund_no, out_trade_no = out_trade_no, refund_desc = refund_reason, notify_url=kwargs.get('notify_url')) def api_refund_query(self, out_refund_no, out_trade_no=None): """ :param out_trade_no: :param out_refund_no: 自己的退款单号 """ return self.client.refund.query(out_refund_no = out_refund_no) class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin): def __init__(self, app, gateway_version = "v3"): # type: (WechatPayApp, str)->None super(WechatWithdrawGateway, self).__init__(app) self.version = gateway_version self.__gateway_type__ = AppPlatformType.WITHDRAW def __repr__(self): return \ '' \ % ( str(self.occupantId), self.appid, self.mchid, self.apikey, self.ssl_cert, self.ssl_key, self.manual_withdraw ) @property def __client__(self): # type: ()->Optional[WeChatPay, WechatClientV3] if self.version == "v3": return WechatClientV3(appid = self.appid, mchid = self.mchid, private_key = self.ssl_key, cert_serial_no = self.app_serial_number, apiv3_key = self.apikey_v3, certificate_str_list = self.platform_certificates) else: return WeChatPay(appid = self.appid, api_key = self.apikey, mch_id = self.mchid, mch_cert = self.ssl_cert, mch_key = self.ssl_key) def withdraw_via_changes(self, amount, payOpenId, order_no, real_user_name, subject): # type:(RMB, str, str, str, basestring)->dict """ ··参考文档: [0] https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 [1] http://blog.csdn.net/qdseashore/article/details/50517570 经销商提现通过零钱支付,秒推送至经销商微信钱包 :param amount: :param payOpenId: 待提现的经销商openId :param order_no: 本平台商户订单号 :param real_user_name: 用户的实名 :return: result :rtype: dict """ return self.client.transfer.transfer(user_id = payOpenId, amount = self._to_fen(amount), desc = subject, check_name = 'FORCE_CHECK', real_name = real_user_name, out_trade_no = order_no) def withdraw_via_bank(self, order_no, total_amount, bank_card, order_title = u'服务款项'): # type:(str, RMB, WithdrawBankCard, str)->dict """ 经销商提现通过银行卡支付 .. 参考文档 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_2 :param total_amount: :param order_no: 商户企业付款单号(本平台订单号) len(order_no) (- [8-32] :param bank_card: 开户行 :param order_title: 提现描述 :return: """ wechat_bank_code = WithdrawBanks.get_wechat_bank_code(bank_card.bankName) return self.client.transfer.transfer_bankcard(true_name = bank_card.accountName.encode('utf-8'), bank_card_no = bank_card.accountCode.encode('utf-8'), bank_code = wechat_bank_code, amount = self._to_fen(total_amount), desc = order_title, out_trade_no = order_no) def get_transfer_result_via_bank(self, order_no): # type:(str)->WechatWithdrawQueryResult """ 查询银行卡提现的返回结果 :return: """ try: return WechatWithdrawQueryResult(self.client.transfer.query_bankcard(out_trade_no = order_no)) except WeChatException as e: if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']: raise WithdrawOrderNotExist() else: raise e def get_transfer_result_via_changes(self, order_no): """ :param order_no: :return: """ try: if self.version == 'v3': result = self.client.transfer.query(out_trade_no = order_no) rv = { 'return_code': 'SUCCESS', 'result_code': 'SUCCESS', 'status': result['detail_status'], 'reason': result.get('fail_reason', None) } return WechatWithdrawQueryResult(rv) else: return WechatWithdrawQueryResult(self.client.transfer.query(out_trade_no = order_no)) except WeChatException as e: if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']: raise WithdrawOrderNotExist() else: raise e