You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
5.2 KiB
199 lines
5.2 KiB
5 years ago
|
#! /usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
# Date: 2018/8/2
|
||
|
|
||
|
import json
|
||
|
import inspect
|
||
|
|
||
|
from urllib.parse import quote_plus
|
||
|
|
||
|
from Crypto.PublicKey import RSA
|
||
|
|
||
|
from django.utils.timezone import now
|
||
|
|
||
|
from .. import BaseAli
|
||
|
from ..tools import calculate_signature
|
||
|
from .api.base import BaseAliPayAPI
|
||
|
from . import api
|
||
|
|
||
|
|
||
|
def _is_api_endpoint(instance):
|
||
|
return issubclass(instance.__class__, BaseAliPayAPI)
|
||
|
|
||
|
|
||
|
class AliPay(BaseAli):
|
||
|
# 在构造函数中初始化
|
||
|
API_BASE_URL = ""
|
||
|
|
||
|
# PC支付
|
||
|
pc = api.AliPage()
|
||
|
# Wap支付
|
||
|
wap = api.AliWap()
|
||
|
# App支付
|
||
|
app = api.AliApp()
|
||
|
# 转账
|
||
|
transfer = api.AliTransfer()
|
||
|
# 统一收单相关
|
||
|
order = api.AliOrder()
|
||
|
|
||
|
def __init__(
|
||
|
self, app_id, app_private_key_path, ali_public_key_path,
|
||
|
notify_url, return_url, debug,
|
||
|
):
|
||
|
|
||
|
# 如果是调试模式,则用调试api
|
||
|
if debug:
|
||
|
setattr(self, "API_BASE_URL", "https://openapi.alipaydev.com/gateway.do")
|
||
|
else:
|
||
|
setattr(self, "API_BASE_URL", "https://openapi.alipay.com/gateway.do")
|
||
|
|
||
|
# 暂时用不到父类方法参数(设置默认值)
|
||
|
super(AliPay, self).__init__(timeout=3, auto_retry=True)
|
||
|
|
||
|
self.app_id = app_id
|
||
|
self.notify_url = notify_url
|
||
|
self.return_url = return_url
|
||
|
|
||
|
# 加载应用的私钥
|
||
|
with open(app_private_key_path) as fp:
|
||
|
self.app_private_key = RSA.importKey(fp.read())
|
||
|
|
||
|
# 加载支付宝公钥
|
||
|
with open(ali_public_key_path) as fp:
|
||
|
self.ali_public_key = RSA.importKey(fp.read())
|
||
|
|
||
|
def __new__(cls, *args, **kwargs):
|
||
|
self = super(AliPay, cls).__new__(cls)
|
||
|
api_endpoints = inspect.getmembers(self, _is_api_endpoint)
|
||
|
for name, api_ins in api_endpoints:
|
||
|
api_cls = type(api_ins)
|
||
|
api_ins = api_cls(self)
|
||
|
setattr(self, name, api_ins)
|
||
|
return self
|
||
|
|
||
|
def request(self, method, url_or_endpoint, **kwargs):
|
||
|
if not url_or_endpoint.startswith(('http://', 'https://')):
|
||
|
api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
|
||
|
url = '{base}{endpoint}'.format(
|
||
|
base=api_base_url,
|
||
|
endpoint=url_or_endpoint
|
||
|
)
|
||
|
else:
|
||
|
url = url_or_endpoint
|
||
|
|
||
|
res = self._http.request(
|
||
|
method=method,
|
||
|
url=url,
|
||
|
**kwargs
|
||
|
)
|
||
|
|
||
|
res.raise_for_status()
|
||
|
|
||
|
return self._handle_result(res)
|
||
|
|
||
|
def _handle_result(self, res):
|
||
|
"""解析请求结果并校验签名
|
||
|
|
||
|
"""
|
||
|
return res.json()
|
||
|
|
||
|
def get_url_params(self, data):
|
||
|
data.pop("sign", None)
|
||
|
# 排序后[(k, v), ...]
|
||
|
ordered_items = sorted(
|
||
|
((k, v if not isinstance(v, dict) else json.dumps(v, separators=(',', ':')))
|
||
|
for k, v in data.items())
|
||
|
)
|
||
|
# 拼接成待签名的字符串
|
||
|
unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in ordered_items)
|
||
|
# 对上一步得到的字符串进行签名
|
||
|
sign = calculate_signature(unsigned_string.encode("utf-8"), self.app_private_key)
|
||
|
# 处理URL
|
||
|
quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in ordered_items)
|
||
|
# 添加签名,获得最终的订单信息字符串
|
||
|
signed_string = quoted_string + "&sign=" + quote_plus(sign)
|
||
|
return signed_string
|
||
|
|
||
|
def build_body(self, method, biz_content, notify_url=None, return_url=None):
|
||
|
"""构建请求体
|
||
|
|
||
|
公有请求体 + 业务请求体
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
method : string
|
||
|
请求API
|
||
|
|
||
|
biz_content: dict
|
||
|
业务参数
|
||
|
|
||
|
notify_url: string, default: None
|
||
|
异步通知地址
|
||
|
|
||
|
return_url: string, default: None
|
||
|
同步地址
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
请求参数: dict
|
||
|
"""
|
||
|
data = {
|
||
|
"app_id": self.app_id,
|
||
|
"method": method,
|
||
|
"charset": "utf-8",
|
||
|
"sign_type": "RSA2",
|
||
|
"timestamp": now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
|
"version": "1.0",
|
||
|
"biz_content": biz_content
|
||
|
}
|
||
|
|
||
|
# 同步回调
|
||
|
if notify_url is not None:
|
||
|
data["notify_url"] = notify_url
|
||
|
|
||
|
# 异步回调
|
||
|
if return_url is not None:
|
||
|
data["return_url"] = return_url
|
||
|
|
||
|
return data
|
||
|
|
||
|
def generate_url(self, method, biz_content, notify_url=None, return_url=None):
|
||
|
"""构建请求体.
|
||
|
公有请求体 + 业务请求体
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
method : string
|
||
|
|
||
|
请求API
|
||
|
|
||
|
biz_content: dict
|
||
|
|
||
|
业务参数
|
||
|
|
||
|
notify_url: string, default: None
|
||
|
|
||
|
异步通知 `url`
|
||
|
|
||
|
return_url: string, default: None
|
||
|
|
||
|
同步通知 `url`
|
||
|
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
string
|
||
|
"""
|
||
|
|
||
|
if notify_url is None:
|
||
|
notify_url = self.notify_url
|
||
|
|
||
|
if return_url is None:
|
||
|
return_url = self.return_url
|
||
|
|
||
|
body = self.build_body(method, biz_content, notify_url, return_url)
|
||
|
|
||
|
params = self.get_url_params(body)
|
||
|
|
||
|
return "{}?{}".format(self.API_BASE_URL, params)
|