阶段3(上):Python/asyncio/FastAPI 专题的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:44:13 +08:00
1 parent b8010a74f3
commit 7f52513ffc
24 files changed
+2817 -33

No files matched your search

@@ -0,0 +1,116 @@
---
id: python.fastapi.routing-validation
title: 路由和参数校验:路径 / 查询 / 请求体、pydantic 模型、响应模型
type: lesson
domain: python
stack: [fastapi]
tags: [路由, pydantic, 参数校验, 响应模型, OpenAPI]
level: basic
prerequisites: [python.python.typing-errors-testing]
est_hours: 2
related: [python.fastapi.deps-auth-errors, backend.gin.routing-binding, python.fastapi.interview]
week: [4]
order: 1
---
# 路由和参数校验:路径 / 查询 / 请求体、pydantic 模型、响应模型
## 解释说明
- **参数来源由类型和位置决定**:路径里出现的参数 → 路径参数;简单类型(int、str)→ 查询参数;pydantic 模型 → JSON 请求体;`Header()`、`Cookie()` 显式声明。
- **校验**:`Field(ge=1, le=100, min_length=1, pattern=...)`、`Query(...)`、`Path(...)`;`Literal` 限定枚举值;校验失败自动返回 **422**,带出错字段和原因。
- **响应模型**:返回类型标注或 `response_model=`,会过滤掉模型之外的字段——防止把密码哈希等内部字段返回出去。
- **自动文档**:启动后访问 `/docs`(Swagger UI),前端联调直接用。
- **`async def` 还是 `def`**:路由里调用的都是异步库 → `async def`;调用同步阻塞库 → 普通 `def`(FastAPI 会放到线程池执行)。**不要在 `async def` 里调用阻塞函数。**
- **APIRouter**:按模块拆分路由,`app.include_router(router, prefix="/v1/docs")`。
和 Gin 对照:`c.Param` ↔ 路径参数,`c.Query` ↔ 查询参数,`ShouldBindJSON` + `binding` 标签 ↔ pydantic 模型 + `Field`。
## 案例
知识库的文档接口:创建文档(标题 1~200 字、标签最多 10 个、可见范围只能是 `private / team / public`)、按 ID 查询、分页列表(页码 ≥ 1,每页 1~100)。用户传了 `"visibility": "everyone"`,FastAPI 自动返回 422 和具体字段,不用手写校验代码。
## 代码示例
```python
from datetime import datetime, timezone
from typing import Annotated, Literal
from fastapi import APIRouter, FastAPI, HTTPException, Path, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
class DocCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
content: str = Field(min_length=1)
tags: list[str] = Field(default_factory=list, max_length=10)
visibility: Literal["private", "team", "public"] = "team"
class DocOut(BaseModel): # 响应模型:只暴露这些字段
id: int
title: str
tags: list[str]
visibility: str
created_at: datetime
class Page(BaseModel):
items: list[DocOut]
total: int
router = APIRouter(prefix="/v1/docs", tags=["文档"])
DB: dict[int, dict] = {}
@router.post("", status_code=201)
async def create_doc(body: DocCreate) -> DocOut:
doc_id = len(DB) + 1
DB[doc_id] = {**body.model_dump(), "id": doc_id, "owner_secret": "内部字段",
"created_at": datetime.now(timezone.utc)}
return DocOut(**DB[doc_id]) # 只取模型里声明的字段
@router.get("/{doc_id}")
async def get_doc(doc_id: Annotated[int, Path(ge=1)]) -> DocOut:
if doc_id not in DB:
raise HTTPException(status_code=404, detail="文档不存在")
return DocOut(**DB[doc_id])
@router.get("")
async def list_docs(page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 20,
tag: str | None = None) -> Page:
docs = [d for d in DB.values() if tag is None or tag in d["tags"]]
start = (page - 1) * size
return Page(items=[DocOut(**d) for d in docs[start:start + size]], total=len(docs))
app = FastAPI()
app.include_router(router)
c = TestClient(app)
r = c.post("/v1/docs", json={"title": "年假制度", "content": "……", "tags": ["hr"]})
print(r.status_code, r.json()["id"], "owner_secret" in r.json()) # 201 1 False
r = c.post("/v1/docs", json={"title": "", "content": "x", "visibility": "everyone"})
print(r.status_code, [e["loc"] for e in r.json()["detail"]]) # 422 [['body', 'title'], ['body', 'visibility']]
print(c.get("/v1/docs", params={"size": 500}).status_code) # 422
print(c.get("/v1/docs/99").json()) # {'detail': '文档不存在'}
print(c.get("/v1/docs", params={"tag": "hr"}).json()["total"]) # 1
```
## 易错点
- **在 `async def` 路由里调用同步数据库驱动或 `requests`**:阻塞事件循环,吞吐骤降。
- **直接返回 ORM 对象或字典而不声明响应模型**:内部字段(密码哈希、内部备注)泄露。
- **可变默认值**:`tags: list[str] = []` 在 pydantic 里是安全的(会复制),但养成 `default_factory` 的习惯更清晰。
- **路由顺序**:`/docs/{doc_id}` 写在 `/docs/stats` 之前,`stats` 会被当成 doc_id 去解析,返回 422。固定路径放在前面。
- **422 的错误格式直接给前端**:默认格式偏技术化,通常用异常处理器转换成团队统一的格式(见下一课)。
- **pydantic v1 和 v2 写法混用**:v2 用 `model_dump()`、`model_validate()`、`field_validator`;网上很多旧教程是 v1 写法(`dict()`、`validator`)。
## 面试常问
见 [FastAPI 面试题](interview.md):FastAPI 为什么快、参数校验原理、async def 和 def 的区别、响应模型的作用。