Files
FastapiAdmin/backend/app/utils/payment.py
T
zhangtao 7d2367e34e refactor: 统一项目代码风格并修复多处类型与调用问题
本次提交包含多项优化:
1.  移除大量冗余的文件头注释与过时的from __future__导入
2.  将CRUD的list方法统一重命名为get_list保持接口一致
3.  修复前后端状态字段类型不匹配问题,将string类型status改为number
4.  修正前端文案错别字,将"代办事项"修正为标准写法
5.  更新sqlalchemy版本并调整依赖配置
6.  新增缓存工具类替代fastapi-cache2,重构缓存调用逻辑
7.  新增开源授权函生成相关工具与数据库字段支持
8.  为多个业务模块添加防重复提交loading状态
9.  修复邮件模型的外键关联缺失问题
10. 优化pdf生成工具的导入时机与文档注释
2026-06-21 17:34:11 +08:00

190 lines
6.9 KiB
Python

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",
]