refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)

配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
This commit is contained in:
Sully
2026-05-22 13:44:50 +08:00
committed by GitHub
parent f09ae248f9
commit 53e0065e3e
298 changed files with 15247 additions and 4344 deletions

View File

@@ -1,12 +1,11 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_async_db
from app.features.payment.order_service import PaymentOrderService
from app.features.payment.payment_config import PaymentConfig
from app.features.payment.payment_facade import PaymentService
from app.features.plan.deps import get_plan_service
from app.features.plan.service import PlanService
from app.core.deps_types import DbDep
_payment_service = None
@@ -21,7 +20,7 @@ def get_payment_service() -> PaymentService:
def get_payment_order_service(
db: AsyncSession = Depends(get_async_db),
db: DbDep,
plan_service: PlanService = Depends(get_plan_service),
) -> PaymentOrderService:
"""Payment order facade: create_order, callbacks, list/status."""

View File

@@ -7,11 +7,17 @@ import time
import uuid
from datetime import timedelta
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import utc_now
from app.core.db import transactional, utc_now
from app.core.errors import (
AppError,
BadRequestError,
GatewayTimeoutError,
NotFoundError,
ServiceUnavailableError,
)
from app.core.logging import get_logger
from app.features.payment.models import Order
from app.features.payment.schemas import (
@@ -47,6 +53,11 @@ class PaymentOrderService:
self._db = db
self._plan_service = plan_service
async def _persist_failed_order(self, order: Order) -> None:
async with transactional(self._db):
self._db.add(order)
order.status = "failed"
async def create_order(
self,
user_id: str,
@@ -62,23 +73,18 @@ class PaymentOrderService:
plans = self._plan_service.get_plans_for_api()
plan = next((p for p in plans if p.id == plan_id), None)
if plan is None:
raise HTTPException(status_code=400, detail="无效的套餐 ID")
raise BadRequestError("无效的套餐 ID")
if plan.price <= 0:
raise HTTPException(status_code=400, detail="免费套餐无需支付")
raise BadRequestError("免费套餐无需支付")
if payment_method not in ("wechat", "alipay"):
raise HTTPException(
status_code=400, detail="不支持的支付方式,仅支持 wechat / alipay"
)
raise BadRequestError("不支持的支付方式,仅支持 wechat / alipay")
client = _get_payment_service_client()
if not client.is_method_available(payment_method):
if payment_method == "alipay":
raise HTTPException(
status_code=503, detail="支付宝支付接口正在开发中,暂时不可用"
)
raise HTTPException(
status_code=503,
detail=f"{payment_method} 支付暂不可用,请选择其他支付方式",
raise ServiceUnavailableError("支付宝支付接口正在开发中,暂时不可用")
raise ServiceUnavailableError(
f"{payment_method} 支付暂不可用,请选择其他支付方式"
)
amount_fen = int(plan_price * 100)
@@ -96,8 +102,6 @@ class PaymentOrderService:
created_at=now,
expired_at=now + timedelta(minutes=ORDER_EXPIRE_MINUTES),
)
self._db.add(order)
await self._db.flush()
if payment_method == "wechat":
try:
@@ -106,16 +110,12 @@ class PaymentOrderService:
timeout=WECHAT_INIT_TIMEOUT_SEC,
)
except asyncio.TimeoutError:
order.status = "failed"
await self._db.flush()
raise HTTPException(
status_code=504, detail="微信支付初始化超时,请稍后重试。"
)
await self._persist_failed_order(order)
raise GatewayTimeoutError("微信支付初始化超时,请稍后重试。") from None
except Exception as e:
order.status = "failed"
await self._db.flush()
await self._persist_failed_order(order)
logger.exception("微信支付客户端初始化失败: {}", e)
raise HTTPException(status_code=503, detail=f"微信支付暂不可用: {e!s}")
raise ServiceUnavailableError(f"微信支付暂不可用: {e!s}") from e
try:
payment_result = await asyncio.wait_for(
@@ -124,32 +124,33 @@ class PaymentOrderService:
payment_method,
order_no,
amount_fen,
f"岁月书 - {plan_display_name}",
f"岁月书 - {plan_display_name}",
),
timeout=PREPAY_TIMEOUT_SEC,
)
except asyncio.TimeoutError:
order.status = "failed"
await self._db.flush()
raise HTTPException(
status_code=504,
detail="创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。",
)
await self._persist_failed_order(order)
raise GatewayTimeoutError(
"创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。"
) from None
except PaymentError as e:
order.status = "failed"
await self._db.flush()
raise HTTPException(
status_code=500, detail=f"创建支付订单失败: {e.message}"
)
await self._persist_failed_order(order)
raise AppError(
f"创建支付订单失败: {e.message}",
status_code=500,
error_code="PAYMENT_FAILED",
) from e
except Exception as e:
order.status = "failed"
await self._db.flush()
await self._persist_failed_order(order)
logger.exception("创建支付订单异常: {}", e)
raise HTTPException(
status_code=500, detail=f"创建支付订单异常: {type(e).__name__}: {e!s}"
)
raise AppError(
f"创建支付订单异常: {type(e).__name__}: {e!s}",
status_code=500,
error_code="INTERNAL_ERROR",
) from e
await self._db.commit()
async with transactional(self._db):
self._db.add(order)
logger.info(
"订单创建成功: order_no={}, payment_method={}, amount_fen={}",
order_no,
@@ -173,29 +174,29 @@ class PaymentOrderService:
logger.info("支付回调: 订单已处理过 {}", out_trade_no)
return
now = utc_now()
order.status = "paid"
order.trade_no = trade_no
order.paid_at = now
user_result = await self._db.execute(
select(User).where(User.id == order.user_id)
)
user = user_result.scalar_one_or_none()
if user:
duration_days = SUBSCRIPTION_DURATION_DAYS.get(order.plan_id, 365)
if user.subscription_expires_at and user.subscription_expires_at > now:
user.subscription_expires_at = user.subscription_expires_at + timedelta(
days=duration_days
)
else:
user.subscription_expires_at = now + timedelta(days=duration_days)
user.subscription_type = order.plan_id
logger.info(
"用户 {} 订阅已升级为 {},到期: {}",
user.id,
order.plan_id,
user.subscription_expires_at,
async with transactional(self._db):
order.status = "paid"
order.trade_no = trade_no
order.paid_at = now
user_result = await self._db.execute(
select(User).where(User.id == order.user_id)
)
await self._db.commit()
user = user_result.scalar_one_or_none()
if user:
duration_days = SUBSCRIPTION_DURATION_DAYS.get(order.plan_id, 365)
if user.subscription_expires_at and user.subscription_expires_at > now:
user.subscription_expires_at = user.subscription_expires_at + timedelta(
days=duration_days
)
else:
user.subscription_expires_at = now + timedelta(days=duration_days)
user.subscription_type = order.plan_id
logger.info(
"用户 {} 订阅已升级为 {},到期: {}",
user.id,
order.plan_id,
user.subscription_expires_at,
)
logger.info(
"支付成功处理完成: 订单 {}, 第三方交易号 {}", out_trade_no, trade_no
)
@@ -232,7 +233,7 @@ class PaymentOrderService:
)
order = result.scalar_one_or_none()
if order is None:
raise HTTPException(status_code=404, detail="订单不存在")
raise NotFoundError("订单不存在")
return OrderStatusResponse(
order_id=order.id,
plan_id=order.plan_id,

View File

@@ -72,6 +72,8 @@ class PaymentConfig:
@classmethod
def from_settings(cls, settings) -> "PaymentConfig":
from app.core.runtime_constants import misc_defaults
wechat_private_key = (
getattr(settings, "wechat_pay_private_key", "") or ""
).strip()
@@ -90,9 +92,7 @@ class PaymentConfig:
wechat_private_key_path = (
getattr(settings, "wechat_pay_private_key_path", "") or ""
).strip()
alipay_under = (
getattr(settings, "alipay_under_development", "true") or "true"
).lower()
alipay_under = misc_defaults.alipay_under_development.lower()
config = cls(
wechat=WeChatPayConfig(
app_id=getattr(settings, "wechat_pay_app_id", "") or "",
@@ -115,7 +115,7 @@ class PaymentConfig:
private_key=getattr(settings, "alipay_private_key", "") or "",
alipay_public_key=getattr(settings, "alipay_public_key", "") or "",
notify_url=getattr(settings, "alipay_notify_url", "") or "",
sign_type=getattr(settings, "alipay_sign_type", "RSA2") or "RSA2",
sign_type=misc_defaults.alipay_sign_type,
),
alipay_under_development=alipay_under in ("true", "1", "yes"),
)

View File

@@ -1,11 +1,21 @@
"""支付模块异常定义(从 payment 迁入 app"""
"""支付模块异常定义(继承 AppError由全局 handler 统一映射)。"""
from app.core.errors import AppError
_PAYMENT_CODE_MAP: dict[str, tuple[int, str]] = {
"PAYMENT_CONFIG_ERROR": (502, "PROVIDER_ERROR"),
"PAYMENT_CREATE_ERROR": (502, "PROVIDER_ERROR"),
"PAYMENT_NOTIFY_ERROR": (400, "BAD_REQUEST"),
"PAYMENT_QUERY_ERROR": (502, "PROVIDER_ERROR"),
"PAYMENT_ERROR": (400, "BAD_REQUEST"),
}
class PaymentError(Exception):
class PaymentError(AppError):
def __init__(self, message: str = "支付异常", code: str = "PAYMENT_ERROR"):
self.message = message
status_code, error_code = _PAYMENT_CODE_MAP.get(code, (400, code))
super().__init__(message, status_code=status_code, error_code=error_code)
self.code = code
super().__init__(self.message)
class PaymentConfigError(PaymentError):

View File

@@ -1,10 +1,12 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, Request
from fastapi.responses import PlainTextResponse
from app.core.dependencies import get_current_user
from app.core.deps_types import CurrentUserDep
from app.core.errors import BadRequestError
from app.core.logging import get_logger
from app.core.openapi import error_responses
from app.features.payment.deps import get_payment_order_service
from app.features.payment.order_service import PaymentOrderService
from app.features.payment.schemas import (
@@ -15,25 +17,20 @@ from app.features.payment.schemas import (
)
from app.features.plan.deps import get_plan_service
from app.features.plan.service import PlanService
from app.features.user.models import User
logger = get_logger(__name__)
router = APIRouter(
prefix="/api/payment",
tags=["payment"],
responses={
401: {"description": "认证失败"},
404: {"description": "订单不存在"},
503: {"description": "支付服务暂不可用"},
},
responses=error_responses(401, 404, 500, 502, 503, 504),
)
@router.post("/create-order", response_model=CreateOrderResponse)
async def create_order(
request: CreateOrderRequest,
current_user: User = Depends(get_current_user),
current_user: CurrentUserDep,
service: PaymentOrderService = Depends(get_payment_order_service),
plan_service: PlanService = Depends(get_plan_service),
):
@@ -41,7 +38,7 @@ async def create_order(
(p for p in plan_service.get_plans_for_api() if p.id == request.plan_id), None
)
if plan is None:
raise HTTPException(status_code=400, detail="无效的套餐 ID")
raise BadRequestError("无效的套餐 ID")
return await service.create_order(
user_id=current_user.id,
user_subscription_type=current_user.subscription_type,
@@ -87,7 +84,7 @@ async def alipay_notify(
@router.get("/order/{order_id}/status", response_model=OrderStatusResponse)
async def get_order_status(
order_id: str,
current_user: User = Depends(get_current_user),
current_user: CurrentUserDep,
service: PaymentOrderService = Depends(get_payment_order_service),
):
return await service.get_order_status(order_id, current_user.id)
@@ -95,7 +92,7 @@ async def get_order_status(
@router.get("/orders", response_model=List[OrderListResponse])
async def list_orders(
current_user: User = Depends(get_current_user),
current_user: CurrentUserDep,
service: PaymentOrderService = Depends(get_payment_order_service),
):
return await service.list_orders(current_user.id)