Files
Aether/_deprecated_py_src/api/payment/routes.py

98 lines
3.5 KiB
Python
Raw Normal View History

"""支付回调接口。"""
from __future__ import annotations
import secrets
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from src.config import config
from src.database import get_db
from src.services.payment import PaymentService
router = APIRouter(prefix="/api/payment", tags=["Payment"])
CALLBACK_TOKEN_HEADER = "x-payment-callback-token"
CALLBACK_SIGNATURE_HEADER = "x-payment-callback-signature"
class PaymentCallbackPayload(BaseModel):
callback_key: str = Field(..., min_length=1, max_length=128)
order_no: str | None = Field(default=None, max_length=64)
gateway_order_id: str | None = Field(default=None, max_length=128)
amount_usd: float = Field(..., gt=0, allow_inf_nan=False)
pay_amount: float | None = Field(default=None, gt=0, allow_inf_nan=False)
pay_currency: str | None = Field(default=None, min_length=3, max_length=3)
exchange_rate: float | None = Field(default=None, gt=0, allow_inf_nan=False)
payload: dict[str, Any] | None = None
def _verify_callback_request_auth(request: Request) -> None:
expected_token = config.payment_callback_secret
if not expected_token:
raise HTTPException(status_code=503, detail="payment callback is disabled")
provided_token = (request.headers.get(CALLBACK_TOKEN_HEADER) or "").strip()
if not provided_token or not secrets.compare_digest(provided_token, expected_token):
raise HTTPException(status_code=401, detail="invalid payment callback token")
async def _process_callback(
*,
payment_method: str,
request: Request,
payload: PaymentCallbackPayload,
db: Session = Depends(get_db),
) -> dict[str, Any]:
if not payment_method:
raise HTTPException(status_code=400, detail="payment_method is required")
_verify_callback_request_auth(request)
callback_signature = (request.headers.get(CALLBACK_SIGNATURE_HEADER) or "").strip()
if not callback_signature:
raise HTTPException(status_code=401, detail="missing payment callback signature")
try:
callback_payload = payload.payload if payload.payload is not None else payload.model_dump()
result = PaymentService.handle_callback(
db,
payment_method=payment_method,
callback_key=payload.callback_key,
payload=callback_payload,
callback_signature=callback_signature,
callback_secret=config.payment_callback_secret,
order_no=payload.order_no,
gateway_order_id=payload.gateway_order_id,
amount_usd=payload.amount_usd,
pay_amount=payload.pay_amount,
pay_currency=payload.pay_currency,
exchange_rate=payload.exchange_rate,
)
db.commit()
return {
**result,
"payment_method": payment_method,
"request_path": request.url.path,
}
except Exception as exc:
db.rollback()
if isinstance(exc, HTTPException):
raise
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/callback/{payment_method}")
async def handle_payment_callback(
payment_method: str,
request: Request,
payload: PaymentCallbackPayload,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return await _process_callback(
payment_method=payment_method,
request=request,
payload=payload,
db=db,
)