124 lines
3.7 KiB
Python
124 lines
3.7 KiB
Python
"""支付宝当面付扫码(precreate)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from base64 import b64decode, b64encode
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from urllib.parse import quote_plus, urlencode
|
|
|
|
import httpx
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
|
from auth.system_settings import SystemSettingsData
|
|
|
|
logger = logging.getLogger("payments.alipay")
|
|
|
|
ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do"
|
|
ALIPAY_SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
|
|
|
|
|
def _normalize_pem(text: str, label: str) -> str:
|
|
body = text.strip()
|
|
if "BEGIN" in body:
|
|
return body
|
|
return f"-----BEGIN {label}-----\n{body}\n-----END {label}-----"
|
|
|
|
|
|
def _load_private_key(pem: str):
|
|
return serialization.load_pem_private_key(
|
|
_normalize_pem(pem, "RSA PRIVATE KEY").encode("utf-8"),
|
|
password=None,
|
|
)
|
|
|
|
|
|
def _load_public_key(pem: str):
|
|
return serialization.load_pem_public_key(
|
|
_normalize_pem(pem, "PUBLIC KEY").encode("utf-8"),
|
|
)
|
|
|
|
|
|
def _sign_content(content: str, private_key) -> str:
|
|
signature = private_key.sign(
|
|
content.encode("utf-8"),
|
|
padding.PKCS1v15(),
|
|
hashes.SHA256(),
|
|
)
|
|
return b64encode(signature).decode("utf-8")
|
|
|
|
|
|
def _gateway(settings: SystemSettingsData) -> str:
|
|
return ALIPAY_SANDBOX_GATEWAY if settings.alipay_sandbox else ALIPAY_GATEWAY
|
|
|
|
|
|
def create_precreate_order(
|
|
settings: SystemSettingsData,
|
|
order_no: str,
|
|
subject: str,
|
|
amount_yuan: str,
|
|
notify_url: str,
|
|
) -> str:
|
|
"""创建当面付预下单,返回 qr_code 字符串。"""
|
|
private_key = _load_private_key(settings.alipay_private_key)
|
|
biz_content = {
|
|
"out_trade_no": order_no,
|
|
"total_amount": amount_yuan,
|
|
"subject": subject[:256],
|
|
}
|
|
params = {
|
|
"app_id": settings.alipay_app_id.strip(),
|
|
"method": "alipay.trade.precreate",
|
|
"format": "JSON",
|
|
"charset": "utf-8",
|
|
"sign_type": "RSA2",
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
"version": "1.0",
|
|
"notify_url": notify_url,
|
|
"biz_content": json.dumps(biz_content, ensure_ascii=False, separators=(",", ":")),
|
|
}
|
|
unsigned = "&".join(
|
|
f"{k}={quote_plus(str(v), safe='')}"
|
|
for k, v in sorted(params.items())
|
|
if v is not None and str(v) != ""
|
|
)
|
|
params["sign"] = _sign_content(unsigned, private_key)
|
|
with httpx.Client(timeout=30.0) as client:
|
|
resp = client.post(_gateway(settings), data=params)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
key = "alipay_trade_precreate_response"
|
|
result = data.get(key) or {}
|
|
if result.get("code") != "10000":
|
|
raise RuntimeError(result.get("sub_msg") or result.get("msg") or "支付宝下单失败")
|
|
qr_code = result.get("qr_code")
|
|
if not qr_code:
|
|
raise RuntimeError("支付宝未返回 qr_code")
|
|
return qr_code
|
|
|
|
|
|
def verify_notify(params: dict[str, Any], settings: SystemSettingsData) -> bool:
|
|
sign = params.get("sign")
|
|
if not sign:
|
|
return False
|
|
verify_params = {k: v for k, v in params.items() if k not in ("sign", "sign_type") and v is not None}
|
|
unsigned = "&".join(
|
|
f"{k}={v}"
|
|
for k, v in sorted(verify_params.items())
|
|
if str(v) != ""
|
|
)
|
|
try:
|
|
public_key = _load_public_key(settings.alipay_public_key)
|
|
public_key.verify(
|
|
b64decode(sign),
|
|
unsigned.encode("utf-8"),
|
|
padding.PKCS1v15(),
|
|
hashes.SHA256(),
|
|
)
|
|
return True
|
|
except Exception as exc:
|
|
logger.warning("Alipay notify verify failed: %s", exc)
|
|
return False
|