Files
FastapiAdmin/backend/app/utils/payment.py
T
zhangtao 211fddd6e0 refactor: 完成项目大规模代码重构与依赖清理
这是一次综合性的重构更新,包含以下主要变更:
1.  升级Python版本到3.12,更新依赖配置
2.  替换旧的.j2模板为.jinja2格式,新增代码生成模板
3.  重构权限过滤策略,更新权限枚举与模型配置
4.  移除Prefect依赖,替换为自研拓扑并行执行引擎
5.  重构认证与上下文管理,拆分租户/请求上下文
6.  简化响应模型、CRUD与服务层代码
7.  清理废弃的支付网关模块,重构订单定时任务
8.  更新在线用户、监控等模块的接口与路由
9.  优化邮件模板与工具类,新增邮件模板文件
10. 修复数据库会话配置与类型提示
2026-06-21 06:02:05 +08:00

194 lines
7.0 KiB
Python

"""支付网关 — 抽象基类 + 支付宝 + Mock + 工厂"""
from __future__ import annotations
import base64
import json
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
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 app.core.logger import logger
# ── dataclass ──
@dataclass
class PaymentInfo:
pay_url: str | None = None
qr_code_url: str | None = None
trade_no: str = ""
raw: dict[str, Any] = field(default_factory=dict)
@dataclass
class CallbackResult:
verified: bool = False
transaction_id: str | None = None
order_id: int | None = None
amount: int = 0
raw: dict[str, Any] = field(default_factory=dict)
# ── 抽象基类 ──
class BasePaymentGateway(ABC):
@abstractmethod
async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo:
...
@abstractmethod
async def verify_callback(self, data: dict[str, Any]) -> CallbackResult:
...
# ── 支付宝 ──
class AlipayGateway(BasePaymentGateway):
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:
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())
return base64.b64encode(signature).decode("utf-8")
def _verify(self, params: dict[str, Any], signature: str) -> bool:
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
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:
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", "")
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"),
amount=int(float(data.get("total_amount", 0)) * 100),
raw=data,
)
return CallbackResult(verified=False, raw=data)
# ── Mock ──
class MockPaymentGateway(BasePaymentGateway):
def __init__(self) -> None:
self._pending_orders: dict[str, dict] = {}
async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo:
trade_no = f"MOCK{uuid.uuid4().hex[:16].upper()}"
self._pending_orders[order_no] = {"trade_no": trade_no, "amount": amount, "subject": subject}
pay_url = f"/mock-pay?order_no={order_no}&amount={amount}"
qr_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={pay_url}"
return PaymentInfo(pay_url=pay_url, qr_code_url=qr_url, trade_no=trade_no, raw={"order_no": order_no, "amount": amount, "is_mock": True})
async def verify_callback(self, data: dict[str, Any]) -> CallbackResult:
order_no = data.get("order_no", "")
pending = self._pending_orders.pop(order_no, None)
if pending:
return CallbackResult(verified=True, transaction_id=pending["trade_no"], order_id=data.get("order_id"), amount=pending["amount"], raw={**data, **pending})
return CallbackResult(verified=False, raw=data)
def get_mock_callback_data(self, order_id: int, order_no: str) -> dict:
pending = self._pending_orders.get(order_no, {})
return {
"order_id": order_id,
"order_no": order_no,
"trade_no": pending.get("trade_no", f"MOCK{uuid.uuid4().hex[:16].upper()}"),
"total_amount": str(pending.get("amount", 0) / 100),
"trade_status": "TRADE_SUCCESS",
}
# ── 工厂 ──
_mock_gateway: MockPaymentGateway | None = None
def create_payment_gateway(method: str = "") -> BasePaymentGateway:
if method == "alipay":
logger.info("🔔 使用支付宝支付网关")
return AlipayGateway()
if method == "wxpay":
logger.info("🔔 使用微信支付网关(未实现,回退 Mock)")
return MockPaymentGateway()
if settings.PAYMENT_ALIPAY_APP_ID and settings.PAYMENT_ALIPAY_PRIVATE_KEY:
logger.info("🔔 自动选择支付宝支付网关")
return AlipayGateway()
logger.info("🔔 使用 Mock 支付网关(开发模式)")
return MockPaymentGateway()
def get_mock_gateway() -> MockPaymentGateway:
global _mock_gateway
if _mock_gateway is None:
_mock_gateway = MockPaymentGateway()
return _mock_gateway
__all__ = [
"BasePaymentGateway",
"PaymentInfo",
"CallbackResult",
"create_payment_gateway",
"get_mock_gateway",
]