123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- # -*- 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 '<WechatResultDict successful?(%s), failed?(%s), processing?(%s) \n content=%s' \
- % (self.is_successful, self.is_failed, self.is_processing, json.dumps(self, indent = 2),)
- class WechatPayMixin(WechatMixin):
- @property
- def mchid(self):
- return self.__app_for_inner__.mchid
- @property
- def apikey(self):
- return self.__app_for_inner__.apikey
- @property
- def ssl_cert(self):
- return str(self.__app_for_inner__.ssl_cert)
- @property
- def ssl_key(self):
- return str(self.__app_for_inner__.ssl_key)
- @property
- def apikey_v3(self):
- return self.__app_for_inner__.apikey_v3
- @property
- def app_serial_number(self):
- return self.__app_for_inner__.app_serial_number
- @property
- def platform_certificates(self):
- return self.__app_for_inner__.platform_certificates
- @property
- def manual_withdraw(self):
- return self.__app_for_inner__.manual_withdraw
- def _to_fen(self, rmb):
- # type: (RMB)->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<agentId=%s, appid=%s, mchid=%s, manual=%s>' \
- % (
- 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 \
- '<WechatWithdrawGateway(agentId=%s, appid=%s, mchid=%s, apikey=%s, sslcert=%s, sslkey=%s, manual=%s)>' \
- % (
- 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
|