FastAPI: Phase 2 综合练习 — PriceTracker 核心功能闭环

最后更新:2026-08-26

Phase 2 就像汽车总装——引擎(数据库)、方向盘(路由)、安全带(认证)、仪表盘(中间件)都已造好,现在组装成一辆能上路的完整汽车。

1. 你将学到


2. Alice 的真实故事

(1) 痛点:功能分散无法联调

Alice 分别实现了中间件、依赖注入、数据库、CRUD、JWT 认证,但每个功能都是独立测试的。当她尝试把所有功能整合到一起时,发现 CORS 中间件和 JWT 认证的执行顺序冲突、DB Session 在 DI 链中被创建了两次、Pro 用户的限流逻辑和权限检查互相覆盖。

(2) 系统化整合的解法

Phase 2 综合练习将所有模块按正确的层级和顺序整合:CORS(最外层)→ 限流 → 认证 → 业务逻辑 → 数据库(最内层),每层职责清晰、互不干扰。

(3) 收益

整合后的 PriceTracker 核心服务可以完整运行:Bob 前端跨域访问成功、所有受保护端点需要 JWT Token、Pro 用户批量导入顺畅、Free 用户被正确限流。


3. Phase 2 架构总览

(1) 完整请求链路

100%
flowchart TD
    Client[Bob Frontend] --> CORS[CORS Middleware]
    CORS --> RateLimit[Rate Limit Middleware]
    RateLimit --> Router[FastAPI Router]
    Router --> AuthDI{Auth Required?}
    AuthDI -->|Yes| GetDB[get_db Dependency]
    AuthDI -->|No| Handler[Handler]
    GetDB --> GetUser[get_current_user]
    GetUser --> CheckSub[require_subscription]
    CheckSub --> Handler
    Handler --> Repo[Repository Layer]
    Repo --> DB[(PostgreSQL)]
    
    RateLimit -.->|429| Reject[Rate Limited]
    GetUser -.->|401| Unauth[Unauthorized]
    CheckSub -.->|403| Forbid[Forbidden]

(2) 路由分组策略

路由组 前缀 认证 说明
public / health, docs
api_v1 /api/v1 JWT 必选 所有业务端点
admin /api/v1/admin JWT + Admin 管理端点

4. 完整代码整合

(1) 应用入口与中间件配置

▶ 示例:main.py 完整配置

PYTHON
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from app.api.routes import products, prices, auth
from app.core.config import settings

app = FastAPI(
    title=settings.app_name,
    version="1.0.0",
    description="PriceTracker SaaS API - E-commerce price tracking service",
)

# Middleware order: last registered = innermost (executes first on request)
# CORS should be outermost (registered last)
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers - public first, then protected
app.include_router(auth.router, tags=["auth"])
app.include_router(
    products.router,
    prefix="/api/v1",
    dependencies=[Depends(get_current_user)],
    tags=["products"],
)
app.include_router(
    prices.router,
    prefix="/api/v1",
    dependencies=[Depends(get_current_user)],
    tags=["prices"],
)

@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "pricetracker"}

输出:

TEXT 📖 仅展示
# 函数定义成功

(2) 三层 DI 链完整实现

▶ 示例:app/core/deps.py

PYTHON
from fastapi import Depends, HTTPException, Header
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import jwt, JWTError

from app.db import async_session
from app.core.config import settings
from app.core.security import verify_password
from app.models import User
from sqlalchemy import select

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

# Level 1: Database session
async def get_db():
    async with async_session() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

# Level 2: Current user
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
):
    credentials_exception = HTTPException(
        status_code=401,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(
            token, settings.secret_key, algorithms=[settings.algorithm]
        )
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    stmt = select(User).where(User.email == username)
    result = await db.execute(stmt)
    user = result.scalar_one_or_none()
    if user is None:
        raise credentials_exception
    return user

# Level 3: Subscription check
def require_subscription(min_level: str = "free"):
    LEVELS = {"free": 0, "pro": 1, "enterprise": 2}

    async def check(user: User = Depends(get_current_user)):
        if LEVELS.get(user.subscription, 0) < LEVELS.get(min_level, 0):
            raise HTTPException(
                status_code=403,
                detail=f"Requires {min_level} plan. Current: {user.subscription}",
            )
        return user
    return check

输出:

TEXT 📖 仅展示
# 函数定义成功

(3) 权限校验流程

100%
flowchart LR
    Request[Request with Token] --> ParseToken[Parse JWT Token]
    ParseToken --> ValidToken{Token Valid?}
    ValidToken -->|No| Return401[Return 401]
    ValidToken -->|Yes| QueryUser[Query User from DB]
    QueryUser --> UserExists{User Found?}
    UserExists -->|No| Return401
    UserExists -->|Yes| CheckSub{Subscription Level?}
    CheckSub -->|Free| AllowBasic[Allow Basic Endpoints]
    CheckSub -->|Pro| AllowPro[Allow Pro Endpoints]
    CheckSub -->|Enterprise| AllowAll[Allow All Endpoints]

▶ 示例:受保护的商品路由

PYTHON
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional

from app.core.deps import get_db, get_current_user, require_subscription
from app.schemas import ProductCreate, ProductResponse, ProductUpdate
from app.services import ProductService

router = APIRouter()

@router.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[str] = Query(None),
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    return await service.list_products(category=category, skip=skip, limit=limit)

@router.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(
    product: ProductCreate,
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    return await service.create_product(product, user_id=user.id)

@router.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(
    product_id: int = Path(gt=0),
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    product = await service.get_product(product_id)
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    return product

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:批量导入端点(含订阅限制)

PYTHON
@router.post("/products/{product_id}/prices/bulk")
async def bulk_import_prices(
    product_id: int = Path(gt=0),
    prices: list[PriceCreate] = ...,
    db: AsyncSession = Depends(get_db),
    user=Depends(require_subscription("free")),  # Even free users can import
):
    # Free plan: max 1000 prices per import
    limits = {"free": 1000, "pro": 100000, "enterprise": 1000000}
    max_import = limits.get(user.subscription, 1000)
    
    if len(prices) > max_import:
        raise HTTPException(
            status_code=403,
            detail=f"Import limit: {max_import} for {user.subscription} plan. Upgrade at /pricing",
        )
    
    service = PriceService(db)
    return await service.bulk_import_prices(product_id, prices)

输出:

TEXT 📖 仅展示
# 函数定义成功

❓ 常见问题

Q 中间件注册顺序为什么 CORS 要最后?
A CORS 需要给所有响应(包括 4xx 错误)添加 CORS 头。如果 CORS 不是最外层,限流中间件返回的 429 响应可能缺少 CORS 头,前端无法读取错误信息。
Q 路由组依赖和端点依赖会重复执行吗?
A 不会。FastAPI 对同一依赖做请求级缓存。路由组的 Depends(get_current_user) 和端点的 Depends(get_current_user) 共享同一次执行结果。
Q DI 链中 get_db 被多次声明会有多个连接吗?
A 不会。同请求内 get_db 只执行一次,所有依赖共享同一个 AsyncSession。
Q Free 用户如何升级到 Pro?
A Stripe 支付集成(超出本课范围)。这里用 User.subscription 字段模拟,生产环境由支付回调更新。
Q 如何测试完整的请求链路?
A 用 TestClient + dependency_overrides 替换 get_db,逐步测试:无 Token → 401,Free Token → 200 但受限,Pro Token → 完整权限。
Q Phase 2 完成后下一步做什么?
A Phase 3 引入高级特性——WebSocket 实时推送、Celery 异步任务、Redis 缓存、文件上传、pytest 测试、OpenAPI 定制。

📖 小节


📝 作业

  1. 基础题(难度⭐):将 Phase 1 的内存存储 CRUD 替换为异步 SQLAlchemy 实现,确保所有端点通过 Depends(get_db) 注入数据库会话。提示:Repository 模式 + AsyncSession
  2. 进阶题(难度⭐⭐):实现路由分组——public(/health, /auth/login)无认证,api/v1 所有业务端点 JWT 保护,用 Swagger UI 测试登录+访问受保护端点。提示:APIRouter(dependencies=[Depends(get_current_user)])
  3. 挑战题(难度⭐⭐⭐):实现完整的三层 DI 链 + 批量导入端点:Pro 用户可导入 100000 条价格,Free 用户超 1000 条返回 403,同时在 Swagger UI 完整演示注册→登录→导入流程。提示:require_subscription("free") + limits 字典

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏