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

@@ -3,7 +3,7 @@
from app.core.config import settings
from app.features.plan.schemas import PlanResponse
ENABLE_TEST_PLAN = (settings.enable_test_plan or "").lower() in ("1", "true", "yes")
ENABLE_TEST_PLAN = settings.enable_test_plan
AVAILABLE_PLANS = [
PlanResponse(

View File

@@ -1,13 +1,22 @@
"""Plan feature dependencies: get_plan_service."""
from typing import Annotated
from fastapi import Depends
from app.core.dependencies import get_current_user
from app.features.plan.service import PlanService
from app.features.quota.deps import get_quota_service
from app.features.quota.service import QuotaService
from app.features.user.models import User
from app.core.deps_types import DbDep
def get_plan_service(
quota_service: QuotaService = Depends(get_quota_service),
quota_service: Annotated[QuotaService, Depends(get_quota_service)],
) -> PlanService:
return PlanService(quota_service=quota_service)
PlanServiceDep = Annotated[PlanService, Depends(get_plan_service)]
CurrentUserDep = Annotated[User, Depends(get_current_user)]

View File

@@ -2,36 +2,29 @@
订阅计划路由。
"""
from typing import List
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from app.core.dependencies import get_current_user
from app.features.plan.deps import get_plan_service
from app.core.openapi import error_responses
from app.features.plan.deps import CurrentUserDep, PlanServiceDep
from app.features.plan.schemas import CurrentPlanResponse, PlanResponse
from app.features.plan.service import PlanService, get_plans_for_api
from app.features.user.models import User
router = APIRouter(
prefix="/api/plans",
tags=["plans"],
responses={
401: {"description": "认证失败"},
404: {"description": "资源不存在"},
},
responses=error_responses(401, 404),
)
@router.get("", response_model=List[PlanResponse])
async def get_plans():
@router.get("", response_model=list[PlanResponse])
def get_plans(service: PlanServiceDep) -> list[PlanResponse]:
"""获取所有可用的订阅计划(开发环境 ENABLE_TEST_PLAN=1 时包含「一分钱测试版」)。"""
return get_plans_for_api()
return service.get_plans_for_api()
@router.get("/current", response_model=CurrentPlanResponse)
async def get_current_plan(
current_user: User = Depends(get_current_user),
service: PlanService = Depends(get_plan_service),
):
current_user: CurrentUserDep,
service: PlanServiceDep,
) -> CurrentPlanResponse:
"""获取当前用户的订阅计划信息"""
return await service.get_current_plan_response(current_user)

View File

@@ -1,5 +1,3 @@
from typing import List, Optional
from pydantic import BaseModel
@@ -9,17 +7,24 @@ class PlanResponse(BaseModel):
display_name: str
price: float
currency: str
features: List[str]
max_conversations: Optional[int] = None
max_chapters: Optional[int] = None
max_words: Optional[int] = None
features: list[str]
max_conversations: int | None = None
max_chapters: int | None = None
max_words: int | None = None
is_popular: bool = False
class PlanUsageResponse(BaseModel):
conversations: int
chapters: int
max_conversations: int | None = None
max_chapters: int | None = None
class CurrentPlanResponse(BaseModel):
plan_id: str
plan_name: str
subscription_type: str
expires_at: Optional[str] = None
features: List[str]
usage: dict
expires_at: str | None = None
features: list[str]
usage: PlanUsageResponse

View File

@@ -7,7 +7,11 @@ from app.features.plan.catalog import (
get_plan_by_type,
get_plans_for_api,
)
from app.features.plan.schemas import CurrentPlanResponse, PlanResponse
from app.features.plan.schemas import (
CurrentPlanResponse,
PlanResponse,
PlanUsageResponse,
)
from app.features.quota.service import QuotaService
from app.features.user.models import User
@@ -32,12 +36,12 @@ class PlanService:
async def get_current_plan_response(self, user: User) -> CurrentPlanResponse:
plan = get_plan_by_type(user.subscription_type)
segment_count, chapter_count = await self._quota.get_usage(user.id)
usage = {
"conversations": segment_count,
"chapters": chapter_count,
"max_conversations": plan.max_conversations,
"max_chapters": plan.max_chapters,
}
usage = PlanUsageResponse(
conversations=segment_count,
chapters=chapter_count,
max_conversations=plan.max_conversations,
max_chapters=plan.max_chapters,
)
expires_at = None
if user.subscription_expires_at:
expires_at = user.subscription_expires_at.isoformat()