Files
dy/backend/payments/alipay.py
T
2026-07-23 17:56:25 +08:00

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