mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
|
|
"""支付回调接口。"""
|
||
|
|
|
||
|
|
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,
|
||
|
|
)
|