threeypay-sdk 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- threeypay/__init__.py +37 -0
- threeypay/account/__init__.py +5 -0
- threeypay/account/account.py +77 -0
- threeypay/balanceshare/__init__.py +5 -0
- threeypay/balanceshare/balance_share.py +86 -0
- threeypay/channel/__init__.py +5 -0
- threeypay/channel/channel.py +65 -0
- threeypay/client.py +146 -0
- threeypay/config.py +68 -0
- threeypay/exceptions/__init__.py +27 -0
- threeypay/merchant/__init__.py +5 -0
- threeypay/merchant/merchant.py +128 -0
- threeypay/nonmch/__init__.py +5 -0
- threeypay/nonmch/non_mch.py +61 -0
- threeypay/payment/__init__.py +5 -0
- threeypay/payment/payment.py +258 -0
- threeypay/payout/__init__.py +5 -0
- threeypay/payout/payout.py +67 -0
- threeypay/profitsharing/__init__.py +5 -0
- threeypay/profitsharing/profit_sharing.py +119 -0
- threeypay/sdk.py +126 -0
- threeypay/transfer/__init__.py +5 -0
- threeypay/transfer/transfer.py +176 -0
- threeypay/utils/signer.py +179 -0
- threeypay_sdk-1.1.0.dist-info/METADATA +104 -0
- threeypay_sdk-1.1.0.dist-info/RECORD +29 -0
- threeypay_sdk-1.1.0.dist-info/WHEEL +5 -0
- threeypay_sdk-1.1.0.dist-info/licenses/LICENSE +21 -0
- threeypay_sdk-1.1.0.dist-info/top_level.txt +1 -0
threeypay/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
3YPay Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "1.1.0"
|
|
6
|
+
__author__ = "3YPay Team"
|
|
7
|
+
|
|
8
|
+
from threeypay.sdk import ThreeYPay
|
|
9
|
+
from threeypay.config import Config
|
|
10
|
+
from threeypay.client import Client
|
|
11
|
+
from threeypay.exceptions import HttpException, InvalidSignException
|
|
12
|
+
from threeypay.payment import Payment
|
|
13
|
+
from threeypay.transfer import Transfer
|
|
14
|
+
from threeypay.profitsharing import ProfitSharing
|
|
15
|
+
from threeypay.account import Account
|
|
16
|
+
from threeypay.merchant import Merchant
|
|
17
|
+
from threeypay.payout import Payout
|
|
18
|
+
from threeypay.balanceshare import BalanceShare
|
|
19
|
+
from threeypay.nonmch import NonMch
|
|
20
|
+
from threeypay.channel import Channel
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ThreeYPay",
|
|
24
|
+
"Config",
|
|
25
|
+
"Client",
|
|
26
|
+
"HttpException",
|
|
27
|
+
"InvalidSignException",
|
|
28
|
+
"Payment",
|
|
29
|
+
"Transfer",
|
|
30
|
+
"ProfitSharing",
|
|
31
|
+
"Account",
|
|
32
|
+
"Merchant",
|
|
33
|
+
"Payout",
|
|
34
|
+
"BalanceShare",
|
|
35
|
+
"NonMch",
|
|
36
|
+
"Channel",
|
|
37
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
分账账户与提现类
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Account:
|
|
9
|
+
"""账户管理类"""
|
|
10
|
+
|
|
11
|
+
API_BALANCE_QUERY = "/mchShare/withdrawal/account"
|
|
12
|
+
API_WITHDRAW = "/mchShare/withdrawal/commit"
|
|
13
|
+
API_WITHDRAW_QUERY = "/mchShare/withdrawal/query"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client):
|
|
16
|
+
self.client = client
|
|
17
|
+
|
|
18
|
+
def balance_query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
|
+
"""账户余额查询(accountNo 必填)"""
|
|
20
|
+
self._validate_required(params, ["accountNo"])
|
|
21
|
+
response = self.client.post(self.API_BALANCE_QUERY, params)
|
|
22
|
+
return self.client.parse_response(response)
|
|
23
|
+
|
|
24
|
+
def query_balance(self, account_no: str) -> Dict[str, Any]:
|
|
25
|
+
return self.balance_query({"accountNo": account_no})
|
|
26
|
+
|
|
27
|
+
def withdraw(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
"""提现申请(mchSerialNo、accountNo、amount、remark 必填,notifyUrl 可选)"""
|
|
29
|
+
payload = dict(params)
|
|
30
|
+
self._validate_required(payload, ["mchSerialNo", "accountNo", "remark"])
|
|
31
|
+
payload["amount"] = self._amount(payload.get("amount"), "amount")
|
|
32
|
+
|
|
33
|
+
response = self.client.post(self.API_WITHDRAW, payload)
|
|
34
|
+
return self.client.parse_response(response)
|
|
35
|
+
|
|
36
|
+
def submit_withdraw(
|
|
37
|
+
self, mch_serial_no: str, account_no: str, amount: str, remark: str,
|
|
38
|
+
notify_url: str = "",
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
params: Dict[str, Any] = {
|
|
41
|
+
"mchSerialNo": mch_serial_no,
|
|
42
|
+
"accountNo": account_no,
|
|
43
|
+
"amount": amount,
|
|
44
|
+
"remark": remark,
|
|
45
|
+
}
|
|
46
|
+
if notify_url:
|
|
47
|
+
params["notifyUrl"] = notify_url
|
|
48
|
+
return self.withdraw(params)
|
|
49
|
+
|
|
50
|
+
def withdraw_query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
51
|
+
"""提现查询(mchSerialNo 与 serialNo 二选一)"""
|
|
52
|
+
self._validate_one_of(params, ["mchSerialNo", "serialNo"])
|
|
53
|
+
response = self.client.post(self.API_WITHDRAW_QUERY, params)
|
|
54
|
+
return self.client.parse_response(response)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _amount(value: Any, field: str) -> float:
|
|
58
|
+
try:
|
|
59
|
+
amount = float(value)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
raise ValueError(f"金额参数无效:{field}")
|
|
62
|
+
if amount <= 0:
|
|
63
|
+
raise ValueError(f"金额参数无效:{field}")
|
|
64
|
+
return amount
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _validate_required(params: Dict[str, Any], fields: List[str]) -> None:
|
|
68
|
+
for field in fields:
|
|
69
|
+
if field not in params or params[field] is None or params[field] == "":
|
|
70
|
+
raise ValueError(f"必填参数不能为空:{field}")
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _validate_one_of(params: Dict[str, Any], fields: List[str]) -> None:
|
|
74
|
+
for field in fields:
|
|
75
|
+
if field in params and params[field] is not None and params[field] != "":
|
|
76
|
+
return
|
|
77
|
+
raise ValueError(f"以下参数至少填一个:{', '.join(fields)}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
余额分账类
|
|
3
|
+
|
|
4
|
+
金额单位:元(number)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BalanceShare:
|
|
11
|
+
"""余额分账类"""
|
|
12
|
+
|
|
13
|
+
API_BALANCE_SHARE = "/mchShare/balance/share"
|
|
14
|
+
API_BALANCE_SHARE_QUERY = "/mchShare/balance/shareQuery"
|
|
15
|
+
API_BALANCE_REFUND = "/mchShare/balance/refund"
|
|
16
|
+
API_BALANCE_REFUND_QUERY = "/mchShare/balance/refundQuery"
|
|
17
|
+
|
|
18
|
+
def __init__(self, client):
|
|
19
|
+
self.client = client
|
|
20
|
+
|
|
21
|
+
def apply(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
|
+
"""余额分账申请(mchBatchNo、accountNo、appChannelSn 必填,acctInfos 子项需 accountNo/amount/description)"""
|
|
23
|
+
payload = dict(params)
|
|
24
|
+
self._validate_required(payload, ["mchBatchNo", "accountNo", "appChannelSn"])
|
|
25
|
+
self._validate_acct_infos(payload, ["accountNo", "amount", "description"])
|
|
26
|
+
|
|
27
|
+
response = self.client.post(self.API_BALANCE_SHARE, payload)
|
|
28
|
+
return self.client.parse_response(response)
|
|
29
|
+
|
|
30
|
+
def query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
"""余额分账查询(batchNo 与 mchBatchNo 二选一)"""
|
|
32
|
+
self._validate_one_of(params, ["batchNo", "mchBatchNo"])
|
|
33
|
+
response = self.client.post(self.API_BALANCE_SHARE_QUERY, params)
|
|
34
|
+
return self.client.parse_response(response)
|
|
35
|
+
|
|
36
|
+
def refund(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
37
|
+
"""余额分账退款(mchRefundOrderNo 必填,批次二选一,acctInfos 子项需 accOrderNo/amount/description)"""
|
|
38
|
+
payload = dict(params)
|
|
39
|
+
self._validate_required(payload, ["mchRefundOrderNo"])
|
|
40
|
+
self._validate_one_of(payload, ["batchNo", "mchBatchNo"])
|
|
41
|
+
self._validate_acct_infos(payload, ["accOrderNo", "amount", "description"])
|
|
42
|
+
|
|
43
|
+
response = self.client.post(self.API_BALANCE_REFUND, payload)
|
|
44
|
+
return self.client.parse_response(response)
|
|
45
|
+
|
|
46
|
+
def refund_query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
47
|
+
"""余额分账退款查询(batchNo 与 mchBatchNo 二选一)"""
|
|
48
|
+
self._validate_one_of(params, ["batchNo", "mchBatchNo"])
|
|
49
|
+
response = self.client.post(self.API_BALANCE_REFUND_QUERY, params)
|
|
50
|
+
return self.client.parse_response(response)
|
|
51
|
+
|
|
52
|
+
def _validate_acct_infos(self, params: Dict[str, Any], required_fields: List[str]) -> None:
|
|
53
|
+
acct_infos = params.get("acctInfos")
|
|
54
|
+
if not isinstance(acct_infos, list) or not acct_infos:
|
|
55
|
+
raise ValueError("acctInfos参数格式错误")
|
|
56
|
+
|
|
57
|
+
for index, item in enumerate(acct_infos):
|
|
58
|
+
if not isinstance(item, dict):
|
|
59
|
+
raise ValueError(f"acctInfos[{index}]格式错误")
|
|
60
|
+
for field in required_fields:
|
|
61
|
+
if field not in item or item[field] is None or item[field] == "":
|
|
62
|
+
raise ValueError(f"acctInfos[{index}].{field}不能为空")
|
|
63
|
+
item["amount"] = self._amount(item["amount"], f"acctInfos[{index}].amount")
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _amount(value: Any, field: str) -> float:
|
|
67
|
+
try:
|
|
68
|
+
amount = float(value)
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
raise ValueError(f"金额参数无效:{field}")
|
|
71
|
+
if amount <= 0:
|
|
72
|
+
raise ValueError(f"金额参数无效:{field}")
|
|
73
|
+
return amount
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _validate_required(params: Dict[str, Any], fields: List[str]) -> None:
|
|
77
|
+
for field in fields:
|
|
78
|
+
if field not in params or params[field] is None or params[field] == "":
|
|
79
|
+
raise ValueError(f"必填参数不能为空:{field}")
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _validate_one_of(params: Dict[str, Any], fields: List[str]) -> None:
|
|
83
|
+
for field in fields:
|
|
84
|
+
if field in params and params[field] is not None and params[field] != "":
|
|
85
|
+
return
|
|
86
|
+
raise ValueError(f"以下参数至少填一个:{', '.join(fields)}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
分账关系管理类
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Channel:
|
|
9
|
+
"""分账关系类"""
|
|
10
|
+
|
|
11
|
+
API_BINDING = "/mchShare/channel/binding"
|
|
12
|
+
API_UNBINDING = "/mchShare/channel/unBinding"
|
|
13
|
+
API_UPDATE = "/mchShare/channel/update"
|
|
14
|
+
API_BINDING_QUERY = "/mchShare/channel/bindingQuery"
|
|
15
|
+
|
|
16
|
+
def __init__(self, client):
|
|
17
|
+
self.client = client
|
|
18
|
+
|
|
19
|
+
def binding(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
20
|
+
"""分账关系绑定(accountNo、appChannelSn、fundName、defaultRate 必填)"""
|
|
21
|
+
self._validate_required(params, ["accountNo", "appChannelSn", "fundName", "defaultRate"])
|
|
22
|
+
self._validate_rate(params, "defaultRate")
|
|
23
|
+
self._validate_rate(params, "defaultFee", required=False)
|
|
24
|
+
response = self.client.post(self.API_BINDING, params)
|
|
25
|
+
return self.client.parse_response(response)
|
|
26
|
+
|
|
27
|
+
def unbind(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
"""分账关系解绑"""
|
|
29
|
+
self._validate_required(params, ["accountNo", "appChannelSn"])
|
|
30
|
+
response = self.client.post(self.API_UNBINDING, params)
|
|
31
|
+
return self.client.parse_response(response)
|
|
32
|
+
|
|
33
|
+
def update(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
34
|
+
"""分账关系更新(accountNo、appChannelSn、defaultRate 必填)"""
|
|
35
|
+
self._validate_required(params, ["accountNo", "appChannelSn", "defaultRate"])
|
|
36
|
+
self._validate_rate(params, "defaultRate")
|
|
37
|
+
self._validate_rate(params, "defaultFee", required=False)
|
|
38
|
+
response = self.client.post(self.API_UPDATE, params)
|
|
39
|
+
return self.client.parse_response(response)
|
|
40
|
+
|
|
41
|
+
def query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
"""分账关系查询(appChannelSn 必填)"""
|
|
43
|
+
self._validate_required(params, ["appChannelSn"])
|
|
44
|
+
response = self.client.post(self.API_BINDING_QUERY, params)
|
|
45
|
+
return self.client.parse_response(response)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _validate_required(params: Dict[str, Any], fields: List[str]) -> None:
|
|
49
|
+
for field in fields:
|
|
50
|
+
if field not in params or params[field] is None or params[field] == "":
|
|
51
|
+
raise ValueError(f"必填参数不能为空:{field}")
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _validate_rate(params: Dict[str, Any], field: str, required: bool = True) -> None:
|
|
55
|
+
if field not in params or params[field] is None or params[field] == "":
|
|
56
|
+
if required:
|
|
57
|
+
raise ValueError(f"必填参数不能为空:{field}")
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
rate = float(params[field])
|
|
61
|
+
except (TypeError, ValueError):
|
|
62
|
+
raise ValueError(f"比例参数无效:{field}")
|
|
63
|
+
if rate < 0 or rate > 100:
|
|
64
|
+
raise ValueError(f"比例参数无效:{field}")
|
|
65
|
+
params[field] = rate
|
threeypay/client.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP 客户端
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from threeypay.config import Config
|
|
12
|
+
from threeypay.exceptions import HttpException
|
|
13
|
+
from threeypay.utils.signer import Signer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Client:
|
|
17
|
+
"""3YPay HTTP 客户端类"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Config, session: Optional[requests.Session] = None):
|
|
20
|
+
self.config = config
|
|
21
|
+
self.session = session or requests.Session()
|
|
22
|
+
self.session.headers.update({
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Accept": "application/json",
|
|
25
|
+
})
|
|
26
|
+
self.sub_sn = ""
|
|
27
|
+
|
|
28
|
+
def set_sub_sn(self, sub_sn: str) -> "Client":
|
|
29
|
+
self.sub_sn = sub_sn
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def set_timeout(self, timeout: int) -> "Client":
|
|
33
|
+
self.config.set_timeout(timeout)
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def post(self, endpoint: str, biz_content: Dict[str, Any]) -> Dict[str, Any]:
|
|
37
|
+
"""发送 POST 请求"""
|
|
38
|
+
public_params = self._build_public_params(biz_content)
|
|
39
|
+
public_params["sign"] = Signer.sign(public_params, self.config.get_private_key())
|
|
40
|
+
|
|
41
|
+
url = self.config.get_gateway_url().rstrip("/") + endpoint
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
response = self.session.post(
|
|
45
|
+
url,
|
|
46
|
+
json=public_params,
|
|
47
|
+
timeout=self.config.get_timeout(),
|
|
48
|
+
)
|
|
49
|
+
except requests.RequestException as exc:
|
|
50
|
+
raise HttpException(f"网络请求异常:{exc}") from exc
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
response_data = response.json()
|
|
54
|
+
except ValueError as exc:
|
|
55
|
+
raise HttpException(
|
|
56
|
+
"无效的响应格式",
|
|
57
|
+
response_body=response.text,
|
|
58
|
+
status_code=response.status_code,
|
|
59
|
+
) from exc
|
|
60
|
+
|
|
61
|
+
if not isinstance(response_data, dict):
|
|
62
|
+
raise HttpException(
|
|
63
|
+
"无效的响应格式",
|
|
64
|
+
response_body=response_data,
|
|
65
|
+
status_code=response.status_code,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
69
|
+
code = response_data.get("code", response.status_code)
|
|
70
|
+
msg = response_data.get("msg") or f"HTTP {response.status_code}"
|
|
71
|
+
raise HttpException(
|
|
72
|
+
f"API 请求失败:{msg}",
|
|
73
|
+
self._to_int(code, response.status_code),
|
|
74
|
+
response_data,
|
|
75
|
+
response.status_code,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return response_data
|
|
79
|
+
|
|
80
|
+
def _build_public_params(self, biz_content: Dict[str, Any]) -> Dict[str, Any]:
|
|
81
|
+
"""构建公共请求参数"""
|
|
82
|
+
params = {
|
|
83
|
+
"appId": self.config.get_app_id(),
|
|
84
|
+
"requestId": Signer.generate_request_id(),
|
|
85
|
+
"signType": self.config.get_sign_type(),
|
|
86
|
+
"timestamp": int(time.time() * 1000),
|
|
87
|
+
"version": self.config.get_version(),
|
|
88
|
+
"charset": self.config.get_charset(),
|
|
89
|
+
"bizContent": json.dumps(biz_content, ensure_ascii=False, separators=(",", ":")),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if self.sub_sn:
|
|
93
|
+
params["subSn"] = self.sub_sn
|
|
94
|
+
|
|
95
|
+
return params
|
|
96
|
+
|
|
97
|
+
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
|
98
|
+
"""验证回调通知签名"""
|
|
99
|
+
return Signer.verify(data, self.config.get_platform_public_key())
|
|
100
|
+
|
|
101
|
+
def verify_response(self, response: Dict[str, Any]) -> bool:
|
|
102
|
+
"""验证响应签名"""
|
|
103
|
+
if not isinstance(response, dict) or not response.get("sign"):
|
|
104
|
+
return False
|
|
105
|
+
return Signer.verify(response, self.config.get_platform_public_key())
|
|
106
|
+
|
|
107
|
+
def is_success(self, response: Dict[str, Any]) -> bool:
|
|
108
|
+
"""提现类接口文档以 0 表示成功,其余接口为 200。"""
|
|
109
|
+
if not isinstance(response, dict) or "code" not in response:
|
|
110
|
+
return False
|
|
111
|
+
try:
|
|
112
|
+
code = int(response["code"])
|
|
113
|
+
except (TypeError, ValueError):
|
|
114
|
+
return False
|
|
115
|
+
return code in (200, 0)
|
|
116
|
+
|
|
117
|
+
def parse_response(self, response: Dict[str, Any], verify_sign: bool = True) -> Dict[str, Any]:
|
|
118
|
+
"""解析业务数据(先判断业务码,成功后再验签)"""
|
|
119
|
+
if not isinstance(response, dict) or "code" not in response:
|
|
120
|
+
raise HttpException("无效的响应格式", response_body=response)
|
|
121
|
+
|
|
122
|
+
if not self.is_success(response):
|
|
123
|
+
msg = response.get("msg", "未知错误")
|
|
124
|
+
raise HttpException(
|
|
125
|
+
f"API 请求失败:code={response['code']}, msg={msg}",
|
|
126
|
+
self._to_int(response["code"], 0),
|
|
127
|
+
response,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if verify_sign and not self.verify_response(response):
|
|
131
|
+
raise HttpException("签名验证失败", response_body=response)
|
|
132
|
+
|
|
133
|
+
if isinstance(response.get("data"), str) and response["data"] != "":
|
|
134
|
+
try:
|
|
135
|
+
response["data"] = json.loads(response["data"])
|
|
136
|
+
except ValueError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _to_int(value: Any, default: int) -> int:
|
|
143
|
+
try:
|
|
144
|
+
return int(value)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return default
|
threeypay/config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置类
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
"""3YPay SDK 配置类"""
|
|
8
|
+
|
|
9
|
+
GATEWAY_PROD = "https://openapi.3ypay.com"
|
|
10
|
+
GATEWAY_TEST = "https://test-openapi.3ypay.com"
|
|
11
|
+
|
|
12
|
+
def __init__(self, app_id: str, private_key: str, platform_public_key: str, sandbox: bool = False):
|
|
13
|
+
self.app_id = app_id
|
|
14
|
+
self.private_key = private_key
|
|
15
|
+
self.platform_public_key = platform_public_key
|
|
16
|
+
self.sandbox = sandbox
|
|
17
|
+
self.gateway_url = self.GATEWAY_TEST if sandbox else self.GATEWAY_PROD
|
|
18
|
+
self.sign_type = "RSA2"
|
|
19
|
+
self.version = "1.0"
|
|
20
|
+
self.charset = "UTF-8"
|
|
21
|
+
self.timeout = 30
|
|
22
|
+
|
|
23
|
+
def get_app_id(self) -> str:
|
|
24
|
+
return self.app_id
|
|
25
|
+
|
|
26
|
+
def get_private_key(self) -> str:
|
|
27
|
+
return self.private_key
|
|
28
|
+
|
|
29
|
+
def get_platform_public_key(self) -> str:
|
|
30
|
+
return self.platform_public_key
|
|
31
|
+
|
|
32
|
+
def get_gateway_url(self) -> str:
|
|
33
|
+
return self.gateway_url
|
|
34
|
+
|
|
35
|
+
def get_sign_type(self) -> str:
|
|
36
|
+
return self.sign_type
|
|
37
|
+
|
|
38
|
+
def get_version(self) -> str:
|
|
39
|
+
return self.version
|
|
40
|
+
|
|
41
|
+
def get_charset(self) -> str:
|
|
42
|
+
return self.charset
|
|
43
|
+
|
|
44
|
+
def get_timeout(self) -> int:
|
|
45
|
+
return self.timeout
|
|
46
|
+
|
|
47
|
+
def set_timeout(self, timeout: int) -> "Config":
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def set_sign_type(self, sign_type: str) -> "Config":
|
|
52
|
+
self.sign_type = sign_type
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def set_version(self, version: str) -> "Config":
|
|
56
|
+
self.version = version
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def set_charset(self, charset: str) -> "Config":
|
|
60
|
+
self.charset = charset
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def set_gateway_url(self, gateway_url: str) -> "Config":
|
|
64
|
+
self.gateway_url = gateway_url.rstrip("/")
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def is_sandbox(self) -> bool:
|
|
68
|
+
return self.sandbox
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP 客户端异常
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpException(Exception):
|
|
9
|
+
"""HTTP / 业务异常类"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
code: int = 0,
|
|
15
|
+
response_body: Optional[Any] = None,
|
|
16
|
+
status_code: Optional[int] = None,
|
|
17
|
+
):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.code = code
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.response_body = response_body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidSignException(Exception):
|
|
25
|
+
"""无效签名异常类"""
|
|
26
|
+
|
|
27
|
+
pass
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
商户进件与 AUT 类
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import mimetypes
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from threeypay.utils.signer import Signer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Merchant:
|
|
14
|
+
"""商户管理类"""
|
|
15
|
+
|
|
16
|
+
API_FILE_UPLOAD = "/mchAccess/mch/fileUp"
|
|
17
|
+
API_MERCHANT_ACCESS = "/mchAccess/mch/access"
|
|
18
|
+
API_ACCESS_QUERY = "/mchAccess/mch/query"
|
|
19
|
+
API_MODIFY_MCH_CARD = "/mchAccess/mch/modify/mchCard"
|
|
20
|
+
API_MODIFY_PRODUCT = "/mchAccess/mch/modify/product"
|
|
21
|
+
API_MODIFY_QUERY = "/mchAccess/mch/modify/query"
|
|
22
|
+
API_AUT_QUERY = "/mchAccess/mch/aut/query"
|
|
23
|
+
API_AUT_CONFIG = "/mchAccess/mch/aut/config"
|
|
24
|
+
API_AUT_CONFIG_INFO = "/mchAccess/mch/aut/config/info"
|
|
25
|
+
API_AUT_SPLIT = "/mchAccess/mch/aut/split"
|
|
26
|
+
|
|
27
|
+
def __init__(self, client):
|
|
28
|
+
self.client = client
|
|
29
|
+
|
|
30
|
+
def file_upload(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
"""进件文件上传(fileContent 必填,传 Base64;fileName/contentType 可选)"""
|
|
32
|
+
self._validate_required(params, ["fileContent"])
|
|
33
|
+
payload = dict(params)
|
|
34
|
+
content = str(payload["fileContent"])
|
|
35
|
+
if not Signer.is_base64(content):
|
|
36
|
+
payload["fileContent"] = base64.b64encode(content.encode("utf-8")).decode("utf-8")
|
|
37
|
+
|
|
38
|
+
if not payload.get("contentType") and payload.get("fileName"):
|
|
39
|
+
payload["contentType"] = self._detect_content_type(str(payload["fileName"]))
|
|
40
|
+
|
|
41
|
+
response = self.client.post(self.API_FILE_UPLOAD, payload)
|
|
42
|
+
return self.client.parse_response(response)
|
|
43
|
+
|
|
44
|
+
def file_upload_from_path(self, file_path: str) -> Dict[str, Any]:
|
|
45
|
+
path = Path(file_path)
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
raise ValueError(f"文件不存在或不可读:{file_path}")
|
|
48
|
+
return self.file_upload({
|
|
49
|
+
"fileName": path.name,
|
|
50
|
+
"contentType": self._detect_content_type(str(path)),
|
|
51
|
+
"fileContent": base64.b64encode(path.read_bytes()).decode("utf-8"),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _detect_content_type(file_name: str) -> str:
|
|
56
|
+
return mimetypes.guess_type(file_name)[0] or "application/octet-stream"
|
|
57
|
+
|
|
58
|
+
def access(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
59
|
+
"""商户入网申请(mchAccessNo、mchInfo、mchBusiness、mchCard、products 必填)"""
|
|
60
|
+
self._validate_required(params, ["mchAccessNo", "mchInfo", "mchBusiness", "mchCard", "products"])
|
|
61
|
+
response = self.client.post(self.API_MERCHANT_ACCESS, params)
|
|
62
|
+
return self.client.parse_response(response)
|
|
63
|
+
|
|
64
|
+
def query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
"""商户入网信息查询(accessSn、mchAccessNo、requestId 三选一)"""
|
|
66
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo", "requestId"])
|
|
67
|
+
response = self.client.post(self.API_ACCESS_QUERY, params)
|
|
68
|
+
return self.client.parse_response(response)
|
|
69
|
+
|
|
70
|
+
def modify_mch_card(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""商户结算信息变更(mchModifyNo、mchCard 必填,accessSn/mchAccessNo 二选一)"""
|
|
72
|
+
self._validate_required(params, ["mchModifyNo", "mchCard"])
|
|
73
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
74
|
+
response = self.client.post(self.API_MODIFY_MCH_CARD, params)
|
|
75
|
+
return self.client.parse_response(response)
|
|
76
|
+
|
|
77
|
+
def modify_product(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
78
|
+
"""商户产品信息变更(mchModifyNo、products 必填,accessSn/mchAccessNo 二选一)"""
|
|
79
|
+
self._validate_required(params, ["mchModifyNo", "products"])
|
|
80
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
81
|
+
response = self.client.post(self.API_MODIFY_PRODUCT, params)
|
|
82
|
+
return self.client.parse_response(response)
|
|
83
|
+
|
|
84
|
+
def modify_query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
85
|
+
"""商户信息变更查询(accessSn/mchAccessNo、modifyId/mchModifyNo 分别二选一)"""
|
|
86
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
87
|
+
self._validate_one_of(params, ["modifyId", "mchModifyNo"])
|
|
88
|
+
response = self.client.post(self.API_MODIFY_QUERY, params)
|
|
89
|
+
return self.client.parse_response(response)
|
|
90
|
+
|
|
91
|
+
def aut_query(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
"""商户 AUT 报备查询(accessSn/mchAccessNo 二选一)"""
|
|
93
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
94
|
+
response = self.client.post(self.API_AUT_QUERY, params)
|
|
95
|
+
return self.client.parse_response(response)
|
|
96
|
+
|
|
97
|
+
def aut_config(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
98
|
+
"""微信支付配置(accessSn/mchAccessNo 二选一,code 必填)"""
|
|
99
|
+
self._validate_required(params, ["code"])
|
|
100
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
101
|
+
response = self.client.post(self.API_AUT_CONFIG, params)
|
|
102
|
+
return self.client.parse_response(response)
|
|
103
|
+
|
|
104
|
+
def aut_config_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
105
|
+
"""微信支付配置查询(accessSn/mchAccessNo 二选一,code 必填)"""
|
|
106
|
+
self._validate_required(params, ["code"])
|
|
107
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
108
|
+
response = self.client.post(self.API_AUT_CONFIG_INFO, params)
|
|
109
|
+
return self.client.parse_response(response)
|
|
110
|
+
|
|
111
|
+
def aut_split(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
112
|
+
"""商户 AUT 拆分(accessSn/mchAccessNo 二选一)"""
|
|
113
|
+
self._validate_one_of(params, ["accessSn", "mchAccessNo"])
|
|
114
|
+
response = self.client.post(self.API_AUT_SPLIT, params)
|
|
115
|
+
return self.client.parse_response(response)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _validate_required(params: Dict[str, Any], fields: List[str]) -> None:
|
|
119
|
+
for field in fields:
|
|
120
|
+
if field not in params or params[field] is None or params[field] == "":
|
|
121
|
+
raise ValueError(f"必填参数不能为空:{field}")
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _validate_one_of(params: Dict[str, Any], fields: List[str]) -> None:
|
|
125
|
+
for field in fields:
|
|
126
|
+
if field in params and params[field] is not None and params[field] != "":
|
|
127
|
+
return
|
|
128
|
+
raise ValueError(f"以下参数至少填一个:{', '.join(fields)}")
|