Files
FastapiAdmin/backend/app/utils/payment.py
T
zhangtao 5ff72b086f refactor: 重构项目模块结构与代码细节优化
1.  调整后端模块路由与插件文件结构,迁移部分模块代码至api/v1目录
2.  优化代码注释格式与文档字符串,简化冗余代码
3.  添加监控仪表盘、工单评论等新模块功能
4.  修复前端部分组件交互逻辑与类型定义
5.  清理冗余的初始化数据与废弃配置文件
6.  新增租户注册表单字段与国际化支持
7.  优化HTTP请求拦截器与快捷键功能
2026-07-10 00:18:11 +08:00

186 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",
"CallbackResult",
"PaymentInfo",
"create_payment_gateway",
"get_mock_gateway",
]