mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-24 05:26:58 +00:00
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
121 lines
4.3 KiB
Python
121 lines
4.3 KiB
Python
"""支付宝支付网关
|
||
|
||
依赖 cryptography(已在 pyproject.toml 中)进行 RSA 签名验签。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
from typing import Any
|
||
from urllib.parse import urlencode
|
||
|
||
from cryptography.hazmat.primitives import hashes, serialization
|
||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||
|
||
from app.config.setting import settings
|
||
|
||
from .base import BasePaymentGateway, CallbackResult, PaymentInfo
|
||
|
||
|
||
class AlipayGateway(BasePaymentGateway):
|
||
"""支付宝支付网关(App 支付 / 网站支付 / Native 扫码)"""
|
||
|
||
GATEWAY_URL = "https://openapi.alipay.com/gateway.do"
|
||
DEV_GATEWAY_URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
||
|
||
def __init__(self) -> None:
|
||
self.app_id = settings.PAYMENT_ALIPAY_APP_ID or ""
|
||
self._private_key = (settings.PAYMENT_ALIPAY_PRIVATE_KEY or "").encode()
|
||
self._alipay_public_key = (settings.PAYMENT_ALIPAY_PUBLIC_KEY or "").encode()
|
||
self.is_sandbox = settings.PAYMENT_ALIPAY_SANDBOX
|
||
self._gateway = self.DEV_GATEWAY_URL if self.is_sandbox else self.GATEWAY_URL
|
||
|
||
def _sign(self, params: dict[str, Any]) -> str:
|
||
"""RSA2 签名"""
|
||
sorted_params = sorted((k, v) for k, v in params.items() if v != "" and v is not None)
|
||
sign_str = "&".join(f"{k}={v}" for k, v in sorted_params)
|
||
|
||
private_key_obj = serialization.load_pem_private_key(
|
||
self._private_key,
|
||
password=None,
|
||
)
|
||
if not isinstance(private_key_obj, rsa.RSAPrivateKey):
|
||
raise TypeError("私钥类型不是 RSA")
|
||
|
||
signature = private_key_obj.sign(
|
||
sign_str.encode("utf-8"),
|
||
padding.PKCS1v15(),
|
||
hashes.SHA256(),
|
||
)
|
||
import base64
|
||
return base64.b64encode(signature).decode("utf-8")
|
||
|
||
def _verify(self, params: dict[str, Any], signature: str) -> bool:
|
||
"""验证 RSA2 签名"""
|
||
sorted_params = sorted(
|
||
(k, v) for k, v in params.items()
|
||
if k != "sign" and k != "sign_type" and v != "" and v is not None
|
||
)
|
||
sign_str = "&".join(f"{k}={v}" for k, v in sorted_params)
|
||
|
||
try:
|
||
public_key_obj = serialization.load_pem_public_key(self._alipay_public_key)
|
||
if not isinstance(public_key_obj, rsa.RSAPublicKey):
|
||
return False
|
||
|
||
import base64
|
||
public_key_obj.verify(
|
||
base64.b64decode(signature),
|
||
sign_str.encode("utf-8"),
|
||
padding.PKCS1v15(),
|
||
hashes.SHA256(),
|
||
)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
async def create_payment(
|
||
self, order_no: str, amount: int, subject: str, notify_url: str
|
||
) -> PaymentInfo:
|
||
"""创建支付宝支付(返回 H5 支付页面 URL)"""
|
||
biz_content = json.dumps({
|
||
"out_trade_no": order_no,
|
||
"total_amount": f"{amount / 100:.2f}",
|
||
"subject": subject,
|
||
"product_code": "FAST_INSTANT_TRADE_PAY",
|
||
}, ensure_ascii=False)
|
||
|
||
params = {
|
||
"app_id": self.app_id,
|
||
"method": "alipay.trade.page.pay",
|
||
"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": biz_content,
|
||
}
|
||
params["sign"] = self._sign(params)
|
||
|
||
pay_url = f"{self._gateway}?{urlencode(params)}"
|
||
return PaymentInfo(pay_url=pay_url, trade_no="", raw=params)
|
||
|
||
async def verify_callback(self, data: dict[str, Any]) -> CallbackResult:
|
||
"""验证支付宝异步通知回调"""
|
||
sign = data.pop("sign", "")
|
||
_ = data.pop("sign_type", "") # noqa
|
||
|
||
verified = self._verify(data, sign) if sign else False
|
||
|
||
if verified and data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
||
return CallbackResult(
|
||
verified=True,
|
||
transaction_id=data.get("trade_no"),
|
||
order_id=None, # 由调用方从 out_trade_no 解析
|
||
amount=int(float(data.get("total_amount", 0)) * 100),
|
||
raw=data,
|
||
)
|
||
|
||
return CallbackResult(verified=False, raw=data)
|