FastAPI路径操作、查询参数与请求体:构建高效API的完整指南

目录

  • FastAPI路径操作、查询参数与请求体:构建高效API的完整指南
    • [1. 引言](#1. 引言)
    • [2. 路径操作基础](#2. 路径操作基础)
      • [2.1 什么是路径操作](#2.1 什么是路径操作)
      • [2.2 基本路径操作示例](#2.2 基本路径操作示例)
      • [2.3 HTTP方法详解](#2.3 HTTP方法详解)
    • [3. 路径参数详解](#3. 路径参数详解)
      • [3.1 基本路径参数](#3.1 基本路径参数)
      • [3.2 路径参数验证](#3.2 路径参数验证)
    • [4. 查询参数详解](#4. 查询参数详解)
      • [4.1 基本查询参数](#4.1 基本查询参数)
      • [4.2 查询参数验证](#4.2 查询参数验证)
    • [5. 请求体详解](#5. 请求体详解)
      • [5.1 基本请求体](#5.1 基本请求体)
      • [5.2 多个请求体参数](#5.2 多个请求体参数)
      • [5.3 请求体验证](#5.3 请求体验证)
    • [6. 路径参数、查询参数和请求体的组合使用](#6. 路径参数、查询参数和请求体的组合使用)
    • [7. 完整示例:博客API系统](#7. 完整示例:博客API系统)
    • [8. 最佳实践与常见问题](#8. 最佳实践与常见问题)
      • [8.1 最佳实践](#8.1 最佳实践)
      • [8.2 常见问题与解决方案](#8.2 常见问题与解决方案)
      • [8.3 代码自查清单](#8.3 代码自查清单)
    • [9. 总结](#9. 总结)

『宝藏代码胶囊开张啦!』------ 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 "白菜价"+"量身定制 "!无论是卡脖子的毕设/课设/文献复现 ,需要灵光一现的算法改进 ,还是想给项目加个"外挂",这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

FastAPI路径操作、查询参数与请求体:构建高效API的完整指南

1. 引言

在现代Web开发中,构建清晰、健壮且易于维护的API是至关重要的。FastAPI作为基于Python的现代Web框架,凭借其卓越的性能、直观的API设计和强大的类型系统,已成为构建API的首选框架之一。本博客将深入探讨FastAPI中的三个核心概念:路径操作、查询参数和请求体,并通过实际示例展示如何高效地使用这些功能。
匹配路径 不匹配 客户端请求 FastAPI应用 路由匹配 提取路径参数 返回404 提取查询参数 解析请求体 执行路径操作函数 返回响应 序列化响应数据 客户端接收响应

2. 路径操作基础

2.1 什么是路径操作

路径操作是指与API端点关联的函数,这些函数处理特定的HTTP请求(如GET、POST、PUT、DELETE等)。在FastAPI中,路径操作装饰器用于将Python函数转换为API端点。

2.2 基本路径操作示例

python 复制代码
from fastapi import FastAPI
from datetime import datetime

app = FastAPI(
    title="用户管理系统API",
    description="一个完整的用户管理系统示例",
    version="1.0.0"
)

@app.get("/")
async def root():
    """根端点,返回API基本信息"""
    return {
        "message": "欢迎使用用户管理系统API",
        "version": app.version,
        "timestamp": datetime.now().isoformat(),
        "docs_url": "/docs",
        "redoc_url": "/redoc"
    }

@app.get("/health")
async def health_check():
    """健康检查端点"""
    return {
        "status": "healthy",
        "timestamp": datetime.now().isoformat()
    }

@app.get("/api/v1/status")
async def api_status():
    """API状态检查"""
    return {
        "api_version": "v1",
        "status": "operational",
        "uptime": "99.9%",
        "timestamp": datetime.now().isoformat()
    }

2.3 HTTP方法详解

FastAPI支持所有标准的HTTP方法:

python 复制代码
from fastapi import FastAPI, HTTPException, status
from typing import Optional
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    """物品模型"""
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# 内存存储模拟数据库
items_db = {}

@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    """
    创建新物品 - POST方法
    POST通常用于创建新资源
    """
    if item.name in items_db:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"物品 '{item.name}' 已存在"
        )
    
    items_db[item.name] = item
    return {
        "message": "物品创建成功",
        "item": item,
        "location": f"/items/{item.name}"
    }

@app.get("/items/{item_name}")
async def read_item(item_name: str):
    """
    获取物品 - GET方法
    GET用于检索资源,不应有副作用
    """
    if item_name not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"物品 '{item_name}' 不存在"
        )
    
    return items_db[item_name]

@app.put("/items/{item_name}")
async def update_item(item_name: str, item: Item):
    """
    更新物品 - PUT方法
    PUT用于完全替换资源
    """
    if item_name not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"物品 '{item_name}' 不存在"
        )
    
    items_db[item_name] = item
    return {
        "message": "物品更新成功",
        "item": item
    }

@app.patch("/items/{item_name}")
async def partial_update_item(item_name: str, item: Item):
    """
    部分更新物品 - PATCH方法
    PATCH用于部分更新资源
    """
    if item_name not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"物品 '{item_name}' 不存在"
        )
    
    # 获取现有物品
    existing_item = items_db[item_name]
    
    # 更新提供的字段
    update_data = item.dict(exclude_unset=True)
    updated_item = existing_item.copy(update=update_data)
    
    items_db[item_name] = updated_item
    return {
        "message": "物品部分更新成功",
        "item": updated_item
    }

@app.delete("/items/{item_name}")
async def delete_item(item_name: str):
    """
    删除物品 - DELETE方法
    DELETE用于删除资源
    """
    if item_name not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"物品 '{item_name}' 不存在"
        )
    
    deleted_item = items_db.pop(item_name)
    return {
        "message": "物品删除成功",
        "deleted_item": deleted_item
    }

@app.head("/items/{item_name}")
async def head_item(item_name: str):
    """
    HEAD方法 - 获取资源头部信息
    与GET相同但不返回响应体
    """
    if item_name not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"物品 '{item_name}' 不存在"
        )
    
    # HEAD请求不返回响应体,FastAPI会自动处理
    return {"message": "HEAD请求成功"}

@app.options("/items/")
async def options_items():
    """
    OPTIONS方法 - 获取资源支持的HTTP方法
    """
    return {
        "allowed_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
        "description": "物品资源端点"
    }

3. 路径参数详解

3.1 基本路径参数

路径参数是URL路径的一部分,用于标识特定资源:

python 复制代码
from fastapi import FastAPI, Path
from typing import Optional
from enum import Enum

app = FastAPI()

class UserRole(str, Enum):
    """用户角色枚举"""
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    """
    基本的路径参数
    类型提示会自动将路径参数转换为指定类型
    """
    return {
        "user_id": user_id,
        "message": f"获取用户 {user_id} 的信息"
    }

@app.get("/users/{user_id}/posts/{post_id}")
async def read_user_post(user_id: int, post_id: str):
    """
    多个路径参数
    """
    return {
        "user_id": user_id,
        "post_id": post_id,
        "message": f"获取用户 {user_id} 的帖子 {post_id}"
    }

@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    """
    包含路径分隔符的路径参数
    使用 :path 转换器来匹配包含斜杠的路径
    """
    return {
        "file_path": file_path,
        "message": f"访问文件路径: {file_path}"
    }

@app.get("/roles/{role_name}")
async def get_role_info(role_name: UserRole):
    """
    枚举类型的路径参数
    FastAPI会自动验证参数是否为有效枚举值
    """
    if role_name == UserRole.ADMIN:
        description = "管理员,拥有所有权限"
    elif role_name == UserRole.USER:
        description = "普通用户,拥有基本权限"
    else:
        description = "访客,拥有只读权限"
    
    return {
        "role": role_name,
        "description": description
    }

3.2 路径参数验证

使用Path类为路径参数添加验证:

python 复制代码
from fastapi import FastAPI, Path, HTTPException
from typing import Optional
from pydantic import BaseModel, Field

app = FastAPI()

class User(BaseModel):
    """用户模型"""
    id: int
    username: str
    email: str

# 模拟用户数据库
users_db = {
    1: User(id=1, username="alice", email="alice@example.com"),
    2: User(id=2, username="bob", email="bob@example.com"),
    3: User(id=3, username="charlie", email="charlie@example.com"),
}

@app.get("/users/{user_id}")
async def read_user_with_validation(
    user_id: int = Path(
        ...,
        title="用户ID",
        description="要获取的用户ID",
        gt=0,  # 大于0
        le=1000,  # 小于等于1000
        example=1
    )
):
    """
    带有验证的路径参数
    """
    if user_id not in users_db:
        raise HTTPException(
            status_code=404,
            detail=f"用户ID {user_id} 不存在"
        )
    
    return users_db[user_id]

@app.get("/users/{user_id}/orders/{order_id}")
async def read_user_order(
    user_id: int = Path(..., gt=0, description="用户ID"),
    order_id: str = Path(
        ...,
        min_length=10,
        max_length=20,
        regex=r'^ORD\d{7,}$',
        description="订单ID,格式为ORD后跟至少7位数字"
    ),
    detail: bool = False
):
    """
    多个带有验证的路径参数
    """
    # 模拟数据
    order_info = {
        "order_id": order_id,
        "user_id": user_id,
        "status": "completed",
        "amount": 150.75
    }
    
    if detail:
        order_info.update({
            "items": [
                {"product": "Laptop", "quantity": 1, "price": 1200},
                {"product": "Mouse", "quantity": 1, "price": 25}
            ],
            "shipping_address": "123 Main St, City, Country",
            "created_at": "2024-01-15T10:30:00Z"
        })
    
    return order_info

@app.get("/products/{category}/{product_id}")
async def get_product(
    category: str = Path(..., min_length=2, max_length=50),
    product_id: int = Path(..., gt=0),
    include_reviews: bool = False
):
    """
    带验证的多段路径参数
    """
    # 模拟产品数据
    product_data = {
        "category": category,
        "product_id": product_id,
        "name": f"{category} Product {product_id}",
        "price": 99.99,
        "in_stock": True
    }
    
    if include_reviews:
        product_data["reviews"] = [
            {"user": "user1", "rating": 5, "comment": "Excellent!"},
            {"user": "user2", "rating": 4, "comment": "Good quality"}
        ]
    
    return product_data

4. 查询参数详解

4.1 基本查询参数

查询参数是URL中?后面的键值对,用于过滤、排序和分页等操作:

python 复制代码
from fastapi import FastAPI, Query
from typing import Optional, List
from datetime import date
from enum import Enum

app = FastAPI()

class ItemSortBy(str, Enum):
    """排序字段枚举"""
    NAME = "name"
    PRICE = "price"
    DATE = "date"

class SortOrder(str, Enum):
    """排序顺序枚举"""
    ASC = "asc"
    DESC = "desc"

@app.get("/items/")
async def read_items(
    skip: int = 0,  # 默认值
    limit: int = 10,  # 默认值
    name_filter: Optional[str] = None,  # 可选参数
    min_price: Optional[float] = None,
    max_price: Optional[float] = None,
    in_stock: bool = True
):
    """
    基本查询参数示例
    """
    # 模拟数据过滤逻辑
    all_items = [
        {"id": i, "name": f"Item {i}", "price": i * 10.5, "in_stock": i % 2 == 0}
        for i in range(1, 101)
    ]
    
    # 应用过滤器
    filtered_items = all_items
    
    if name_filter:
        filtered_items = [item for item in filtered_items if name_filter.lower() in item["name"].lower()]
    
    if min_price is not None:
        filtered_items = [item for item in filtered_items if item["price"] >= min_price]
    
    if max_price is not None:
        filtered_items = [item for item in filtered_items if item["price"] <= max_price]
    
    if in_stock:
        filtered_items = [item for item in filtered_items if item["in_stock"]]
    
    # 分页
    paginated_items = filtered_items[skip:skip + limit]
    
    return {
        "items": paginated_items,
        "total": len(filtered_items),
        "skip": skip,
        "limit": limit,
        "has_more": len(filtered_items) > skip + limit
    }

@app.get("/search/")
async def search_items(
    q: Optional[str] = Query(
        None,
        min_length=2,
        max_length=50,
        description="搜索关键词"
    ),
    categories: Optional[List[str]] = Query(
        None,
        description="按分类筛选,可多选"
    ),
    sort_by: ItemSortBy = ItemSortBy.NAME,
    sort_order: SortOrder = SortOrder.ASC,
    created_after: Optional[date] = None,
    created_before: Optional[date] = None
):
    """
    复杂的查询参数示例
    """
    # 模拟搜索逻辑
    results = {
        "query": q,
        "categories": categories,
        "sort_by": sort_by,
        "sort_order": sort_order,
        "created_after": created_after,
        "created_before": created_before,
        "results": [
            {"id": 1, "name": "Sample Item 1", "category": "electronics"},
            {"id": 2, "name": "Sample Item 2", "category": "books"}
        ],
        "total_results": 2
    }
    
    return results

4.2 查询参数验证

使用Query类为查询参数添加验证:

python 复制代码
from fastapi import FastAPI, Query
from typing import Optional, List
import re

app = FastAPI()

@app.get("/users/")
async def list_users(
    username: Optional[str] = Query(
        None,
        min_length=3,
        max_length=20,
        regex=r'^[a-zA-Z][a-zA-Z0-9_]*$',
        description="用户名,只能包含字母、数字和下划线"
    ),
    email: Optional[str] = Query(
        None,
        regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
        description="邮箱地址"
    ),
    age_min: Optional[int] = Query(
        None,
        ge=0,
        le=150,
        description="最小年龄"
    ),
    age_max: Optional[int] = Query(
        None,
        ge=0,
        le=150,
        description="最大年龄"
    ),
    page: int = Query(
        1,
        ge=1,
        description="页码,从1开始"
    ),
    page_size: int = Query(
        10,
        ge=1,
        le=100,
        description="每页数量,最大100"
    ),
    tags: Optional[List[str]] = Query(
        None,
        min_items=1,
        max_items=5,
        description="标签筛选,最多5个"
    )
):
    """
    带有详细验证的查询参数
    """
    # 验证年龄范围
    if age_min is not None and age_max is not None and age_min > age_max:
        return {
            "error": "年龄范围无效",
            "detail": "最小年龄不能大于最大年龄"
        }
    
    # 模拟用户数据
    all_users = [
        {
            "id": i,
            "username": f"user{i}",
            "email": f"user{i}@example.com",
            "age": 20 + (i % 50),
            "tags": ["active", "premium"] if i % 3 == 0 else ["active"]
        }
        for i in range(1, 101)
    ]
    
    # 应用过滤
    filtered_users = all_users
    
    if username:
        filtered_users = [u for u in filtered_users if username.lower() in u["username"].lower()]
    
    if email:
        filtered_users = [u for u in filtered_users if email.lower() in u["email"].lower()]
    
    if age_min is not None:
        filtered_users = [u for u in filtered_users if u["age"] >= age_min]
    
    if age_max is not None:
        filtered_users = [u for u in filtered_users if u["age"] <= age_max]
    
    if tags:
        filtered_users = [u for u in filtered_users if any(tag in u["tags"] for tag in tags)]
    
    # 分页
    start_index = (page - 1) * page_size
    end_index = start_index + page_size
    paginated_users = filtered_users[start_index:end_index]
    
    return {
        "users": paginated_users,
        "pagination": {
            "page": page,
            "page_size": page_size,
            "total": len(filtered_users),
            "total_pages": (len(filtered_users) + page_size - 1) // page_size,
            "has_next": len(filtered_users) > end_index,
            "has_previous": page > 1
        },
        "filters": {
            "username": username,
            "email": email,
            "age_min": age_min,
            "age_max": age_max,
            "tags": tags
        }
    }

@app.get("/products/stats")
async def get_product_stats(
    category: str = Query(..., min_length=2, description="产品分类"),
    start_date: str = Query(
        ...,
        regex=r'^\d{4}-\d{2}-\d{2}$',
        description="开始日期,格式:YYYY-MM-DD"
    ),
    end_date: str = Query(
        ...,
        regex=r'^\d{4}-\d{2}-\d{2}$',
        description="结束日期,格式:YYYY-MM-DD"
    ),
    interval: str = Query(
        "day",
        regex=r'^(day|week|month|year)$',
        description="统计间隔:day/week/month/year"
    )
):
    """
    带有复杂验证的查询参数
    """
    # 这里应该实现实际的统计逻辑
    # 以下是模拟数据
    
    return {
        "category": category,
        "start_date": start_date,
        "end_date": end_date,
        "interval": interval,
        "stats": {
            "total_sales": 15000.50,
            "total_units": 450,
            "average_price": 33.33,
            "top_products": [
                {"name": "Product A", "sales": 5000},
                {"name": "Product B", "sales": 4500},
                {"name": "Product C", "sales": 3000}
            ]
        }
    }

5. 请求体详解

5.1 基本请求体

请求体是客户端发送给API的数据,通常用于创建或更新资源:

python 复制代码
from fastapi import FastAPI, Body
from typing import Optional, List
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

app = FastAPI()

class UserCreate(BaseModel):
    """创建用户的请求体模型"""
    username: str = Field(..., min_length=3, max_length=20)
    email: EmailStr
    password: str = Field(..., min_length=8)
    full_name: Optional[str] = None
    age: Optional[int] = Field(None, ge=0, le=150)
    
    class Config:
        schema_extra = {
            "example": {
                "username": "john_doe",
                "email": "john@example.com",
                "password": "SecurePass123",
                "full_name": "John Doe",
                "age": 30
            }
        }

class Product(BaseModel):
    """产品模型"""
    name: str = Field(..., max_length=100)
    description: Optional[str] = Field(None, max_length=500)
    price: float = Field(..., gt=0)
    category: str
    tags: List[str] = Field(default_factory=list)
    in_stock: bool = True
    
    class Config:
        schema_extra = {
            "example": {
                "name": "Wireless Mouse",
                "description": "Ergonomic wireless mouse",
                "price": 29.99,
                "category": "Electronics",
                "tags": ["wireless", "ergonomic", "computer"],
                "in_stock": True
            }
        }

@app.post("/users/")
async def create_user(user: UserCreate):
    """
    基本请求体示例
    """
    # 在实际应用中,这里应该将用户保存到数据库
    # 现在,我们只是返回创建的用户(不包含密码)
    
    created_user = {
        "id": 1,  # 模拟ID
        "username": user.username,
        "email": user.email,
        "full_name": user.full_name,
        "age": user.age,
        "created_at": datetime.now(),
        "status": "active"
    }
    
    return {
        "message": "用户创建成功",
        "user": created_user,
        "timestamp": datetime.now().isoformat()
    }

@app.post("/products/")
async def create_product(product: Product):
    """
    创建产品
    """
    # 模拟保存产品
    product_data = product.dict()
    product_data["id"] = 1001  # 模拟ID
    product_data["created_at"] = datetime.now()
    
    return {
        "message": "产品创建成功",
        "product": product_data,
        "inventory_status": "in_stock" if product.in_stock else "out_of_stock"
    }

5.2 多个请求体参数

FastAPI支持在同一个操作中使用多个请求体参数:

python 复制代码
from fastapi import FastAPI, Body
from typing import Optional
from pydantic import BaseModel, Field

app = FastAPI()

class User(BaseModel):
    """用户模型"""
    username: str
    email: str

class Item(BaseModel):
    """物品模型"""
    name: str
    description: Optional[str] = None
    price: float

class Order(BaseModel):
    """订单模型"""
    item_id: int
    quantity: int

@app.post("/user-item/")
async def create_user_item(
    user: User,  # 第一个请求体
    item: Item,  # 第二个请求体
    priority: int = Body(1, ge=1, le=5)  # 单独的请求体参数
):
    """
    多个请求体参数
    """
    return {
        "user": user,
        "item": item,
        "priority": priority,
        "combined": {
            "user_email": user.email,
            "item_name": item.name,
            "total_price": item.price
        }
    }

@app.post("/orders/complex")
async def create_complex_order(
    user: User = Body(..., embed=True),  # 嵌入的请求体
    order: Order = Body(...),
    note: Optional[str] = Body(None, max_length=500),
    discount_code: Optional[str] = Body(None, max_length=20)
):
    """
    使用Body参数控制请求体嵌入
    """
    # 模拟订单处理
    order_total = order.quantity * 10  # 假设每个物品10元
    
    if discount_code == "SAVE10":
        order_total *= 0.9
    
    return {
        "user": user.username,
        "order": order,
        "note": note,
        "discount_code": discount_code,
        "order_total": order_total,
        "status": "processing"
    }

@app.post("/orders/with-extra-data")
async def create_order_with_extras(
    # 主要请求体
    order: Order,
    # 额外的请求体参数
    shipping_address: str = Body(...),
    billing_address: Optional[str] = Body(None),
    gift_message: Optional[str] = Body(None),
    gift_wrapping: bool = Body(False)
):
    """
    混合使用模型和单个Body参数
    """
    order_details = {
        "order": order.dict(),
        "shipping": {
            "address": shipping_address,
            "gift_wrapping": gift_wrapping,
            "gift_message": gift_message
        },
        "billing_address": billing_address or shipping_address,
        "calculated_total": order.quantity * 10,
        "estimated_delivery": "3-5 business days"
    }
    
    return order_details

5.3 请求体验证

Pydantic模型提供强大的验证功能:

python 复制代码
from fastapi import FastAPI, HTTPException
from typing import Optional, List
from pydantic import BaseModel, Field, validator, root_validator
from datetime import date, datetime

app = FastAPI()

class EmployeeCreate(BaseModel):
    """创建员工请求体"""
    employee_id: str = Field(
        ...,
        min_length=6,
        max_length=10,
        regex=r'^EMP\d+$',
        description="员工ID,格式:EMP后跟数字"
    )
    full_name: str = Field(..., min_length=2, max_length=100)
    email: str = Field(..., regex=r'^[a-zA-Z0-9._%+-]+@company\.com$')
    department: str = Field(..., description="部门名称")
    position: str = Field(..., description="职位")
    salary: float = Field(..., gt=0, description="月薪")
    hire_date: date = Field(..., description="入职日期")
    manager_id: Optional[str] = Field(None, regex=r'^EMP\d+$')
    
    @validator('hire_date')
    def validate_hire_date(cls, v):
        """验证入职日期"""
        if v > date.today():
            raise ValueError('入职日期不能是未来日期')
        if v < date(2000, 1, 1):
            raise ValueError('入职日期不能早于2000-01-01')
        return v
    
    @validator('salary')
    def validate_salary(cls, v, values):
        """验证薪水合理性"""
        position = values.get('position')
        
        if position == 'Intern' and v > 5000:
            raise ValueError('实习生薪水不能超过5000')
        elif position == 'Manager' and v < 10000:
            raise ValueError('经理薪水不能低于10000')
        
        return v
    
    @root_validator
    def validate_manager_relationship(cls, values):
        """验证经理关系"""
        employee_id = values.get('employee_id')
        manager_id = values.get('manager_id')
        
        if manager_id and employee_id == manager_id:
            raise ValueError('员工不能是自己的经理')
        
        return values
    
    class Config:
        schema_extra = {
            "example": {
                "employee_id": "EMP1001",
                "full_name": "张三",
                "email": "zhangsan@company.com",
                "department": "技术部",
                "position": "软件工程师",
                "salary": 15000.00,
                "hire_date": "2023-01-15",
                "manager_id": "EMP1000"
            }
        }

class ProjectAssignment(BaseModel):
    """项目分配请求体"""
    project_id: str
    employee_ids: List[str] = Field(..., min_items=1, max_items=10)
    start_date: date
    end_date: date
    allocation_percentage: float = Field(..., ge=0, le=100)
    
    @validator('end_date')
    def validate_end_date(cls, v, values):
        """验证结束日期"""
        start_date = values.get('start_date')
        
        if start_date and v < start_date:
            raise ValueError('结束日期不能早于开始日期')
        
        # 项目时长不能超过1年
        if start_date:
            from datetime import timedelta
            max_end_date = start_date + timedelta(days=365)
            if v > max_end_date:
                raise ValueError('项目时长不能超过1年')
        
        return v
    
    @validator('allocation_percentage')
    def validate_allocation(cls, v, values):
        """验证分配比例"""
        if v > 100:
            raise ValueError('分配比例不能超过100%')
        return v

@app.post("/employees/")
async def create_employee(employee: EmployeeCreate):
    """
    创建员工,演示复杂的请求体验证
    """
    # 模拟保存到数据库
    employee_data = employee.dict()
    employee_data["created_at"] = datetime.now()
    employee_data["status"] = "active"
    
    return {
        "message": "员工创建成功",
        "employee": employee_data,
        "next_steps": [
            "发送欢迎邮件",
            "分配公司邮箱",
            "安排入职培训"
        ]
    }

@app.post("/projects/assign")
async def assign_to_project(assignment: ProjectAssignment):
    """
    分配员工到项目
    """
    # 模拟项目分配逻辑
    total_allocation = len(assignment.employee_ids) * assignment.allocation_percentage
    
    if total_allocation > 300:  # 简单验证总分配量
        raise HTTPException(
            status_code=400,
            detail=f"总分配比例({total_allocation}%)过高"
        )
    
    return {
        "message": "项目分配成功",
        "assignment": assignment.dict(),
        "total_allocation": total_allocation,
        "estimated_completion": assignment.end_date.isoformat()
    }

6. 路径参数、查询参数和请求体的组合使用

在实际应用中,我们经常需要同时使用路径参数、查询参数和请求体:

python 复制代码
from fastapi import FastAPI, Path, Query, Body, HTTPException
from typing import Optional, List
from pydantic import BaseModel, Field
from datetime import datetime

app = FastAPI()

class UserUpdate(BaseModel):
    """用户更新请求体"""
    full_name: Optional[str] = Field(None, min_length=2, max_length=100)
    email: Optional[str] = Field(None, regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    bio: Optional[str] = Field(None, max_length=500)
    tags: Optional[List[str]] = Field(None, max_items=10)

class PostCreate(BaseModel):
    """创建帖子请求体"""
    title: str = Field(..., min_length=5, max_length=200)
    content: str = Field(..., min_length=10, max_length=5000)
    tags: List[str] = Field(default_factory=list, max_items=10)
    is_published: bool = True

class CommentCreate(BaseModel):
    """创建评论请求体"""
    content: str = Field(..., min_length=1, max_length=1000)
    parent_id: Optional[int] = Field(None, gt=0)

# 模拟数据存储
users_db = {}
posts_db = {}
comments_db = {}
next_id = {"user": 1, "post": 1, "comment": 1}

@app.put("/users/{user_id}")
async def update_user(
    user_id: int = Path(..., gt=0, description="用户ID"),
    update_data: UserUpdate = Body(...),
    update_reason: Optional[str] = Query(None, description="更新原因"),
    notify_user: bool = Query(True, description="是否通知用户")
):
    """
    组合使用路径参数、查询参数和请求体
    路径参数:user_id
    查询参数:update_reason, notify_user
    请求体:update_data
    """
    if user_id not in users_db:
        raise HTTPException(
            status_code=404,
            detail=f"用户ID {user_id} 不存在"
        )
    
    # 获取现有用户
    user = users_db[user_id]
    
    # 更新用户数据
    update_dict = update_data.dict(exclude_unset=True)
    for key, value in update_dict.items():
        setattr(user, key, value)
    
    user.updated_at = datetime.now()
    
    # 模拟通知
    notification = None
    if notify_user and update_data.email and update_data.email != getattr(user, 'email', None):
        notification = f"已发送邮件到 {update_data.email} 通知用户更新"
    
    return {
        "message": "用户更新成功",
        "user_id": user_id,
        "update_reason": update_reason,
        "notification": notification,
        "updated_fields": list(update_dict.keys()),
        "timestamp": datetime.now().isoformat()
    }

@app.post("/users/{user_id}/posts")
async def create_user_post(
    user_id: int = Path(..., gt=0, description="用户ID"),
    post: PostCreate = Body(...),
    draft: bool = Query(False, description="是否保存为草稿"),
    schedule_publish: Optional[datetime] = Query(None, description="定时发布时间")
):
    """
    为用户创建帖子
    """
    if user_id not in users_db:
        raise HTTPException(
            status_code=404,
            detail=f"用户ID {user_id} 不存在"
        )
    
    # 生成帖子ID
    post_id = next_id["post"]
    next_id["post"] += 1
    
    # 创建帖子数据
    post_data = post.dict()
    post_data["id"] = post_id
    post_data["user_id"] = user_id
    post_data["created_at"] = datetime.now()
    post_data["is_draft"] = draft
    
    if schedule_publish:
        post_data["scheduled_publish"] = schedule_publish
        post_data["is_published"] = False
    
    # 保存到模拟数据库
    posts_db[post_id] = post_data
    
    response_data = {
        "message": "帖子创建成功",
        "post_id": post_id,
        "user_id": user_id,
        "status": "draft" if draft else ("scheduled" if schedule_publish else "published"),
        "created_at": post_data["created_at"].isoformat()
    }
    
    if schedule_publish:
        response_data["scheduled_publish"] = schedule_publish.isoformat()
    
    return response_data

@app.get("/users/{user_id}/posts/{post_id}/comments")
async def get_post_comments(
    user_id: int = Path(..., gt=0, description="用户ID"),
    post_id: int = Path(..., gt=0, description="帖子ID"),
    page: int = Query(1, ge=1, description="页码"),
    page_size: int = Query(20, ge=1, le=100, description="每页数量"),
    sort_by: str = Query("created_at", regex=r'^(created_at|likes)$', description="排序字段"),
    sort_order: str = Query("desc", regex=r'^(asc|desc)$', description="排序顺序"),
    include_replies: bool = Query(True, description="是否包含回复")
):
    """
    获取帖子评论,演示复杂的参数组合
    """
    # 验证用户和帖子是否存在
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="用户不存在")
    
    if post_id not in posts_db:
        raise HTTPException(status_code=404, detail="帖子不存在")
    
    # 模拟评论数据
    all_comments = [
        {
            "id": i,
            "post_id": post_id,
            "user_id": user_id,
            "content": f"评论内容 {i}",
            "likes": i * 10,
            "created_at": datetime.now(),
            "parent_id": None if i % 3 != 0 else i - 1
        }
        for i in range(1, 101)
    ]
    
    # 过滤回复
    if not include_replies:
        all_comments = [c for c in all_comments if c["parent_id"] is None]
    
    # 排序
    reverse = sort_order == "desc"
    all_comments.sort(key=lambda x: x[sort_by], reverse=reverse)
    
    # 分页
    start_idx = (page - 1) * page_size
    end_idx = start_idx + page_size
    paginated_comments = all_comments[start_idx:end_idx]
    
    return {
        "comments": paginated_comments,
        "pagination": {
            "page": page,
            "page_size": page_size,
            "total": len(all_comments),
            "total_pages": (len(all_comments) + page_size - 1) // page_size,
            "has_next": len(all_comments) > end_idx,
            "has_prev": page > 1
        },
        "post_info": {
            "post_id": post_id,
            "title": posts_db[post_id].get("title", "Unknown"),
            "comment_count": len(all_comments)
        }
    }

@app.post("/users/{user_id}/posts/{post_id}/comments")
async def create_post_comment(
    user_id: int = Path(..., gt=0, description="用户ID"),
    post_id: int = Path(..., gt=0, description="帖子ID"),
    comment: CommentCreate = Body(...),
    anonymous: bool = Query(False, description="是否匿名评论"),
    notify_author: bool = Query(True, description="是否通知帖子作者")
):
    """
    为帖子创建评论
    """
    # 验证用户和帖子是否存在
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="用户不存在")
    
    if post_id not in posts_db:
        raise HTTPException(status_code=404, detail="帖子不存在")
    
    # 验证父评论是否存在
    if comment.parent_id and comment.parent_id not in comments_db:
        raise HTTPException(status_code=404, detail="父评论不存在")
    
    # 生成评论ID
    comment_id = next_id["comment"]
    next_id["comment"] += 1
    
    # 创建评论数据
    comment_data = comment.dict()
    comment_data["id"] = comment_id
    comment_data["post_id"] = post_id
    comment_data["user_id"] = None if anonymous else user_id
    comment_data["created_at"] = datetime.now()
    comment_data["is_anonymous"] = anonymous
    
    # 保存到模拟数据库
    comments_db[comment_id] = comment_data
    
    # 更新帖子评论数
    if post_id in posts_db:
        if "comment_count" not in posts_db[post_id]:
            posts_db[post_id]["comment_count"] = 0
        posts_db[post_id]["comment_count"] += 1
    
    response = {
        "message": "评论创建成功",
        "comment_id": comment_id,
        "post_id": post_id,
        "anonymous": anonymous,
        "created_at": comment_data["created_at"].isoformat()
    }
    
    if notify_author:
        response["notification"] = "已通知帖子作者"
    
    return response

7. 完整示例:博客API系统

下面是一个完整的博客API系统示例,展示了路径操作、查询参数和请求体的综合应用:

python 复制代码
"""
完整的博客API系统
演示FastAPI中路径操作、查询参数和请求体的综合应用
"""

from fastapi import FastAPI, HTTPException, Path, Query, Body, status
from typing import Optional, List, Dict, Any
from datetime import datetime, date
from pydantic import BaseModel, Field, validator, EmailStr
from enum import Enum
import uuid

# 创建FastAPI应用
app = FastAPI(
    title="博客系统API",
    description="一个完整的博客系统API示例",
    version="2.0.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    openapi_url="/api/openapi.json"
)

# 数据模型
class UserRole(str, Enum):
    """用户角色枚举"""
    ADMIN = "admin"
    AUTHOR = "author"
    READER = "reader"
    MODERATOR = "moderator"

class PostStatus(str, Enum):
    """帖子状态枚举"""
    DRAFT = "draft"
    PUBLISHED = "published"
    ARCHIVED = "archived"
    DELETED = "deleted"

class Category(BaseModel):
    """分类模型"""
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    name: str = Field(..., max_length=50)
    slug: str = Field(..., max_length=50, regex=r'^[a-z0-9]+(?:-[a-z0-9]+)*$')
    description: Optional[str] = Field(None, max_length=200)
    parent_id: Optional[str] = None
    
    class Config:
        schema_extra = {
            "example": {
                "name": "编程技术",
                "slug": "programming",
                "description": "关于编程和技术的文章"
            }
        }

class UserCreate(BaseModel):
    """用户创建模型"""
    username: str = Field(
        ...,
        min_length=3,
        max_length=30,
        regex=r'^[a-zA-Z][a-zA-Z0-9_]*$',
        description="用户名,只能包含字母、数字和下划线"
    )
    email: EmailStr
    password: str = Field(..., min_length=8)
    full_name: Optional[str] = Field(None, max_length=100)
    bio: Optional[str] = Field(None, max_length=500)
    
    @validator('password')
    def validate_password(cls, v):
        """验证密码强度"""
        if not any(c.isupper() for c in v):
            raise ValueError('密码必须包含至少一个大写字母')
        if not any(c.isdigit() for c in v):
            raise ValueError('密码必须包含至少一个数字')
        return v
    
    class Config:
        schema_extra = {
            "example": {
                "username": "john_doe",
                "email": "john@example.com",
                "password": "SecurePass123",
                "full_name": "John Doe",
                "bio": "软件工程师,热爱编程"
            }
        }

class UserResponse(BaseModel):
    """用户响应模型"""
    id: str
    username: str
    email: str
    full_name: Optional[str]
    bio: Optional[str]
    role: UserRole
    created_at: datetime
    updated_at: Optional[datetime]
    
    class Config:
        orm_mode = True

class PostCreate(BaseModel):
    """帖子创建模型"""
    title: str = Field(..., min_length=5, max_length=200)
    content: str = Field(..., min_length=100)
    excerpt: Optional[str] = Field(None, max_length=300)
    category_id: str
    tags: List[str] = Field(default_factory=list, max_items=10)
    status: PostStatus = PostStatus.DRAFT
    is_featured: bool = False
    allow_comments: bool = True
    
    @validator('excerpt')
    def generate_excerpt_if_empty(cls, v, values):
        """如果摘要为空,自动从内容生成"""
        if v is None and 'content' in values:
            content = values['content']
            # 取前150个字符作为摘要
            return content[:147] + '...' if len(content) > 150 else content
        return v
    
    class Config:
        schema_extra = {
            "example": {
                "title": "FastAPI入门指南",
                "content": "FastAPI是一个现代、快速(高性能)的Web框架...",
                "category_id": "programming",
                "tags": ["Python", "FastAPI", "Web开发"],
                "status": "draft"
            }
        }

class PostUpdate(BaseModel):
    """帖子更新模型"""
    title: Optional[str] = Field(None, min_length=5, max_length=200)
    content: Optional[str] = Field(None, min_length=100)
    excerpt: Optional[str] = Field(None, max_length=300)
    category_id: Optional[str] = None
    tags: Optional[List[str]] = Field(None, max_items=10)
    status: Optional[PostStatus] = None
    is_featured: Optional[bool] = None
    allow_comments: Optional[bool] = None

class PostResponse(BaseModel):
    """帖子响应模型"""
    id: str
    title: str
    content: str
    excerpt: Optional[str]
    author_id: str
    category_id: str
    tags: List[str]
    status: PostStatus
    is_featured: bool
    allow_comments: bool
    view_count: int
    like_count: int
    comment_count: int
    created_at: datetime
    published_at: Optional[datetime]
    updated_at: Optional[datetime]

class CommentCreate(BaseModel):
    """评论创建模型"""
    content: str = Field(..., min_length=1, max_length=1000)
    parent_id: Optional[str] = None
    
    class Config:
        schema_extra = {
            "example": {
                "content": "这是一篇很棒的文章!",
                "parent_id": None
            }
        }

class CommentResponse(BaseModel):
    """评论响应模型"""
    id: str
    content: str
    author_id: str
    post_id: str
    parent_id: Optional[str]
    like_count: int
    created_at: datetime
    updated_at: Optional[datetime]

# 模拟数据库
class Database:
    """模拟数据库"""
    
    def __init__(self):
        self.users: Dict[str, Dict] = {}
        self.posts: Dict[str, Dict] = {}
        self.comments: Dict[str, Dict] = {}
        self.categories: Dict[str, Dict] = {}
    
    def add_user(self, user_data: Dict) -> str:
        """添加用户"""
        user_id = str(uuid.uuid4())
        user_data["id"] = user_id
        user_data["created_at"] = datetime.now()
        user_data["role"] = UserRole.READER
        self.users[user_id] = user_data
        return user_id
    
    def get_user(self, user_id: str) -> Optional[Dict]:
        """获取用户"""
        return self.users.get(user_id)
    
    def update_user(self, user_id: str, update_data: Dict) -> bool:
        """更新用户"""
        if user_id not in self.users:
            return False
        
        for key, value in update_data.items():
            if value is not None:
                self.users[user_id][key] = value
        
        self.users[user_id]["updated_at"] = datetime.now()
        return True
    
    def add_post(self, post_data: Dict) -> str:
        """添加帖子"""
        post_id = str(uuid.uuid4())
        post_data["id"] = post_id
        post_data["created_at"] = datetime.now()
        post_data["view_count"] = 0
        post_data["like_count"] = 0
        post_data["comment_count"] = 0
        
        if post_data["status"] == PostStatus.PUBLISHED:
            post_data["published_at"] = datetime.now()
        
        self.posts[post_id] = post_data
        return post_id
    
    def get_post(self, post_id: str) -> Optional[Dict]:
        """获取帖子"""
        return self.posts.get(post_id)
    
    def update_post(self, post_id: str, update_data: Dict) -> bool:
        """更新帖子"""
        if post_id not in self.posts:
            return False
        
        for key, value in update_data.items():
            if value is not None:
                self.posts[post_id][key] = value
        
        # 如果状态变为发布,设置发布时间
        if update_data.get("status") == PostStatus.PUBLISHED:
            self.posts[post_id]["published_at"] = datetime.now()
        
        self.posts[post_id]["updated_at"] = datetime.now()
        return True
    
    def delete_post(self, post_id: str) -> bool:
        """删除帖子(软删除)"""
        if post_id not in self.posts:
            return False
        
        self.posts[post_id]["status"] = PostStatus.DELETED
        self.posts[post_id]["updated_at"] = datetime.now()
        return True

# 初始化数据库
db = Database()

# API端点
@app.post("/api/v1/users/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
    """
    创建新用户
    """
    # 检查用户名是否已存在
    for existing_user in db.users.values():
        if existing_user["username"] == user.username:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="用户名已存在"
            )
        
        if existing_user["email"] == user.email:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="邮箱已存在"
            )
    
    # 创建用户(不保存密码)
    user_data = user.dict(exclude={"password"})
    user_id = db.add_user(user_data)
    
    return db.get_user(user_id)

@app.get("/api/v1/users/{user_id}", response_model=UserResponse)
async def get_user(
    user_id: str = Path(..., description="用户ID")
):
    """
    获取用户信息
    """
    user = db.get_user(user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    return user

@app.put("/api/v1/users/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: str = Path(..., description="用户ID"),
    user_update: UserCreate = Body(...),
    update_reason: Optional[str] = Query(None, description="更新原因")
):
    """
    更新用户信息
    """
    if not db.get_user(user_id):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 检查用户名和邮箱是否被其他用户使用
    for uid, existing_user in db.users.items():
        if uid != user_id:
            if existing_user["username"] == user_update.username:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="用户名已被其他用户使用"
                )
            
            if existing_user["email"] == user_update.email:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="邮箱已被其他用户使用"
                )
    
    # 更新用户
    update_data = user_update.dict(exclude={"password"})
    db.update_user(user_id, update_data)
    
    user = db.get_user(user_id)
    user["update_reason"] = update_reason
    
    return user

@app.post("/api/v1/posts/", response_model=PostResponse, status_code=status.HTTP_201_CREATED)
async def create_post(
    post: PostCreate,
    author_id: str = Query(..., description="作者ID"),
    publish_now: bool = Query(False, description="是否立即发布")
):
    """
    创建新帖子
    """
    # 验证作者存在
    if not db.get_user(author_id):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="作者不存在"
        )
    
    # 准备帖子数据
    post_data = post.dict()
    post_data["author_id"] = author_id
    
    if publish_now:
        post_data["status"] = PostStatus.PUBLISHED
    
    # 创建帖子
    post_id = db.add_post(post_data)
    
    return db.get_post(post_id)

@app.get("/api/v1/posts/", response_model=List[PostResponse])
async def list_posts(
    category_id: Optional[str] = Query(None, description="按分类筛选"),
    author_id: Optional[str] = Query(None, description="按作者筛选"),
    tag: Optional[str] = Query(None, description="按标签筛选"),
    status: Optional[PostStatus] = Query(None, description="按状态筛选"),
    is_featured: Optional[bool] = Query(None, description="是否精选"),
    search: Optional[str] = Query(None, description="搜索关键词"),
    page: int = Query(1, ge=1, description="页码"),
    page_size: int = Query(10, ge=1, le=50, description="每页数量"),
    sort_by: str = Query("created_at", regex=r'^(created_at|published_at|view_count|like_count)$'),
    sort_order: str = Query("desc", regex=r'^(asc|desc)$')
):
    """
    获取帖子列表
    """
    # 过滤帖子
    filtered_posts = []
    
    for post in db.posts.values():
        # 跳过已删除的帖子
        if post["status"] == PostStatus.DELETED:
            continue
        
        # 应用过滤器
        if category_id and post["category_id"] != category_id:
            continue
        
        if author_id and post["author_id"] != author_id:
            continue
        
        if tag and tag not in post["tags"]:
            continue
        
        if status and post["status"] != status:
            continue
        
        if is_featured is not None and post["is_featured"] != is_featured:
            continue
        
        if search and search.lower() not in post["title"].lower() and search.lower() not in post["content"].lower():
            continue
        
        filtered_posts.append(post)
    
    # 排序
    reverse = sort_order == "desc"
    filtered_posts.sort(key=lambda x: x.get(sort_by, x["created_at"]), reverse=reverse)
    
    # 分页
    start_idx = (page - 1) * page_size
    end_idx = start_idx + page_size
    
    return filtered_posts[start_idx:end_idx]

@app.get("/api/v1/posts/{post_id}", response_model=PostResponse)
async def get_post(
    post_id: str = Path(..., description="帖子ID"),
    increment_view: bool = Query(True, description="是否增加浏览量")
):
    """
    获取单个帖子
    """
    post = db.get_post(post_id)
    if not post or post["status"] == PostStatus.DELETED:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="帖子不存在"
        )
    
    # 增加浏览量
    if increment_view:
        post["view_count"] += 1
    
    return post

@app.put("/api/v1/posts/{post_id}", response_model=PostResponse)
async def update_post(
    post_id: str = Path(..., description="帖子ID"),
    post_update: PostUpdate = Body(...),
    publish_now: bool = Query(False, description="是否立即发布")
):
    """
    更新帖子
    """
    post = db.get_post(post_id)
    if not post or post["status"] == PostStatus.DELETED:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="帖子不存在"
        )
    
    # 准备更新数据
    update_data = post_update.dict(exclude_unset=True)
    
    if publish_now:
        update_data["status"] = PostStatus.PUBLISHED
    
    # 更新帖子
    db.update_post(post_id, update_data)
    
    return db.get_post(post_id)

@app.delete("/api/v1/posts/{post_id}")
async def delete_post(
    post_id: str = Path(..., description="帖子ID"),
    permanent: bool = Query(False, description="是否永久删除")
):
    """
    删除帖子
    """
    if not db.get_post(post_id):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="帖子不存在"
        )
    
    if permanent:
        # 永久删除
        db.posts.pop(post_id, None)
        return {"message": "帖子已永久删除"}
    else:
        # 软删除
        db.update_post(post_id, {"status": PostStatus.DELETED})
        return {"message": "帖子已标记为删除"}

@app.post("/api/v1/posts/{post_id}/like")
async def like_post(
    post_id: str = Path(..., description="帖子ID"),
    user_id: str = Query(..., description="用户ID")
):
    """
    点赞帖子
    """
    post = db.get_post(post_id)
    if not post or post["status"] != PostStatus.PUBLISHED:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="帖子不存在或未发布"
        )
    
    if not db.get_user(user_id):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 增加点赞数
    post["like_count"] += 1
    
    return {
        "message": "点赞成功",
        "post_id": post_id,
        "like_count": post["like_count"]
    }

@app.get("/api/v1/stats")
async def get_system_stats(
    start_date: Optional[date] = Query(None, description="开始日期"),
    end_date: Optional[date] = Query(None, description="结束日期")
):
    """
    获取系统统计信息
    """
    # 计算统计信息
    total_users = len(db.users)
    total_posts = len([p for p in db.posts.values() if p["status"] != PostStatus.DELETED])
    total_published_posts = len([p for p in db.posts.values() if p["status"] == PostStatus.PUBLISHED])
    
    # 计算日期范围内的数据
    if start_date and end_date:
        filtered_posts = [
            p for p in db.posts.values()
            if p["status"] == PostStatus.PUBLISHED
            and p.get("published_at")
            and start_date <= p["published_at"].date() <= end_date
        ]
    else:
        filtered_posts = [p for p in db.posts.values() if p["status"] == PostStatus.PUBLISHED]
    
    total_views = sum(p["view_count"] for p in filtered_posts)
    total_likes = sum(p["like_count"] for p in filtered_posts)
    
    return {
        "total_users": total_users,
        "total_posts": total_posts,
        "total_published_posts": total_published_posts,
        "total_views": total_views,
        "total_likes": total_likes,
        "average_views_per_post": total_views / len(filtered_posts) if filtered_posts else 0,
        "average_likes_per_post": total_likes / len(filtered_posts) if filtered_posts else 0,
        "date_range": {
            "start_date": start_date.isoformat() if start_date else None,
            "end_date": end_date.isoformat() if end_date else None
        }
    }

# 健康检查端点
@app.get("/health")
async def health_check():
    """健康检查"""
    return {
        "status": "healthy",
        "timestamp": datetime.now().isoformat(),
        "version": app.version,
        "database": {
            "users": len(db.users),
            "posts": len(db.posts),
            "comments": len(db.comments)
        }
    }

# 根端点
@app.get("/")
async def root():
    """API根端点"""
    return {
        "message": "欢迎使用博客系统API",
        "version": app.version,
        "documentation": {
            "swagger": "/api/docs",
            "redoc": "/api/redoc",
            "openapi": "/api/openapi.json"
        },
        "endpoints": {
            "users": "/api/v1/users/",
            "posts": "/api/v1/posts/",
            "health": "/health"
        }
    }

8. 最佳实践与常见问题

8.1 最佳实践

  1. 命名规范

    • 使用复数名词表示资源集合(如 /users
    • 使用小写字母和连字符(-)分隔单词
    • 保持URL结构一致
  2. 版本控制

    • 在URL中包含API版本(如 /api/v1/
    • 为重大更改创建新版本
  3. 错误处理

    • 使用适当的HTTP状态码
    • 提供清晰的错误信息
    • 记录详细的错误日志
  4. 文档化

    • 为每个端点添加详细的文档字符串
    • 使用Pydantic模型的示例
    • 保持OpenAPI文档的准确性

8.2 常见问题与解决方案

  1. 路径参数与查询参数混淆

    • 路径参数标识特定资源(如 /users/{id}
    • 查询参数用于过滤、排序和分页
  2. 请求体过大

    • 对大文件使用文件上传而不是Base64编码
    • 分页处理大数据集
    • 考虑使用流式处理
  3. 参数验证失败

    • 提供清晰的验证错误信息
    • 使用Pydantic的自定义验证器
    • 在请求体模型中使用Field约束
  4. API性能问题

    • 使用异步处理
    • 实现缓存机制
    • 优化数据库查询

8.3 代码自查清单

在部署API之前,请检查以下项目:

  1. 所有端点都有适当的HTTP方法
  2. 路径参数有类型提示和验证
  3. 查询参数有合理的默认值和约束
  4. 请求体模型有完整的验证
  5. 错误处理覆盖了所有可能的异常
  6. API文档完整且准确
  7. 性能关键端点有适当的优化
  8. 安全考虑(认证、授权、输入消毒)

9. 总结

FastAPI通过强大的类型系统和直观的API设计,使得构建健壮的Web API变得简单而高效。本文深入探讨了路径操作、查询参数和请求体的核心概念,展示了如何:

  1. 定义清晰的API端点:使用适当的HTTP方法和URL结构
  2. 处理各种参数类型:路径参数、查询参数和请求体
  3. 实现强大的验证:利用Pydantic进行类型检查和数据验证
  4. 构建复杂的API系统:通过组合使用各种参数类型

通过遵循本文中的最佳实践和示例,您可以构建出既强大又易于维护的API系统。FastAPI的自动文档生成、编辑器支持和异步能力进一步提升了开发体验。

记住,良好的API设计不仅仅是技术实现,更是对用户体验和开发者友好性的深入思考。通过精心设计的端点、清晰的错误消息和完整的文档,您可以创建出真正优秀的API服务。


进一步学习资源

  1. FastAPI官方文档
  2. Pydantic文档
  3. OpenAPI规范
  4. REST API设计最佳实践

示例代码仓库 :本文所有完整示例代码可在 GitHub仓库 中找到。

相关推荐
_一路向北_17 小时前
爬虫框架:Feapder使用心得
爬虫·python
皇族崛起17 小时前
【3D标注】- Unreal Engine 5.7 与 Python 交互基础
python·3d·ue5
你想知道什么?17 小时前
Python基础篇(上) 学习笔记
笔记·python·学习
Swizard18 小时前
速度与激情:Android Python + CameraX 零拷贝实时推理指南
android·python·ai·移动开发
一直跑18 小时前
Liunx服务器centos7离线升级内核(Liunx服务器centos7.9离线/升级系统内核)
python
leocoder18 小时前
大模型基础概念入门 + 代码实战(实现一个多轮会话机器人)
前端·人工智能·python
Buxxxxxx18 小时前
DAY 37 深入理解SHAP图
python
ada7_18 小时前
LeetCode(python)108.将有序数组转换为二叉搜索树
数据结构·python·算法·leetcode
请一直在路上18 小时前
python文件打包成exe(虚拟环境打包,减少体积)
开发语言·python
浩瀚地学18 小时前
【Arcpy】入门学习笔记(五)-矢量数据
经验分享·笔记·python·arcgis·arcpy