Skip to content

第 19 章:最佳实践与速查表 ​

最后一章不引入新 API,只做一件事:把前 18 章反复出现的取舍固化成规范。前半部分是清单(怎么组织、怎么命名、怎么设计接口),后半部分是速查表(忘了就翻)。


学习目标 ​

  • 掌握一套可直接落地的项目结构规范与目录职责划分
  • 理解 Python 侧 snake_case 与 API 侧 camelCase 的转换方案,能统一实施
  • 能按语义选择 HTTP 状态码,并设计一致的错误响应结构与分页参数
  • 建立两份可自查的清单:性能清单与安全清单
  • 能对照跨章坑点总表定位问题,并通过速查表快速回忆语法

19.1 项目结构规范 ​

目录职责表 ​

目录职责允许依赖禁止出现
app/main.py组装应用:路由、中间件、异常处理器、lifespan所有层业务逻辑、SQL
app/core/配置与安全原语(Settings、密码哈希、JWT)无数据库会话、HTTP 异常
app/db/引擎、会话工厂、DeclarativeBase、get_dbcore业务逻辑
app/models/SQLAlchemy 表定义与关系db校验逻辑、HTTP
app/schemas/Pydantic 输入/输出模型标准库、pydanticSQLAlchemy 查询
app/repositories/数据访问:select()、flush()、分页models业务规则、HTTPException
app/services/业务规则、事务编排、抛领域异常repositories、schemas、modelsfastapi 的任何 import
app/api/路由、依赖、状态码、response_modelservices、schemas直接写 SQL

依赖方向单向向下。可以用一条 grep 自查:

bash
# services / repositories 里出现 fastapi,说明分层被打破
grep -rn "import fastapi\|from fastapi" app/services app/repositories && echo "❌ 分层违规"

模块命名约定 ​

类型约定示例
包与模块全小写、单数、无下划线models/post.py 而不是 models/Posts.py
路由文件按资源拆分,与 URL 前缀同名posts.py 对应 /posts
路由变量模块级 router,一处一个router = APIRouter(prefix="/posts", tags=["posts"])
Service<资源>ServicePostService、CommentService
Repository<资源>RepositoryPostRepository
依赖别名<东西>DepSessionDep、PostServiceDep
异常<原因>ErrorNotFoundError、ConflictError

不要按技术层拆路由文件(api/v1/get_posts.py、api/v1/create_post.py)——一个资源的增删改查应该在同一文件里,否则改一个需求要开五个文件。

💡 当单个资源文件超过 300 行,先考虑把"管理端"与"用户端"拆成两个 router(/admin/posts 与 /posts),而不是按 HTTP 方法拆。


19.2 代码风格与命名 ​

Python 侧 vs API 侧 ​

位置风格示例
变量 / 函数 / 模块snake_casedef get_post_service()
类名PascalCaseclass PostRepository
常量UPPER_SNAKE_CASEMAX_PAGE_SIZE = 100
表名 / 列名snake_case、表名复数posts.author_id
JSON 字段camelCase{"displayName": "...", "createdAt": "..."}

Python 世界用 snake_case、前端世界用 camelCase 是常态。不要在模型里逐字段写 alias=——一个基类 + alias_generator 就能全局转换:

python
from datetime import datetime

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel


class APIModel(BaseModel):
    """所有对外的请求/响应模型的基类。"""

    model_config = ConfigDict(
        alias_generator=to_camel,   # display_name <-> displayName
        populate_by_name=True,      # 用 Python 字段名构造也合法(内部代码友好)
        from_attributes=True,       # 可直接接受 ORM 对象
        str_strip_whitespace=True,
    )


class UserOut(APIModel):
    id: int
    display_name: str        # 对外:displayName
    created_at: datetime     # 对外:createdAt


out = UserOut(id=1, display_name="moqian", created_at=datetime.now())
out.model_dump(by_alias=True)              # 键名转好,但 datetime 仍是 Python 对象
out.model_dump(mode="json", by_alias=True)
# {'id': 1, 'displayName': 'moqian', 'createdAt': '2025-01-01T00:00:00Z'}

🔑 FastAPI 默认 response_model_by_alias=True,所以响应会自动输出 camelCase,不需要在每个路由上写参数。而请求体解析时,alias_generator 生成的是校验别名,客户端必须传 camelCase;populate_by_name=True 只是额外允许服务端内部用 snake_case 构造。

如果只有个别字段需要特殊名字(如外部契约要求 from),再单独用 Field(alias="from")。

模型命名 ​

后缀含义含密码等敏感字段用 from_attributes
XxxCreate创建请求体可以(如 password)否
XxxUpdate更新请求体,字段全可选否否
XxxOut响应体绝不是
XxxInDB数据库内部表示(如带 hashed_password)是是
XxxFilter复杂查询条件封装否否
Page[XxxOut]分页响应容器——

命名本身就是文档:看到 UserOut 就知道能直接返回给客户端,看到 UserInDB 就知道它只在服务端流转。

路由函数命名与 operation_id ​

python
@router.get(
    "/{post_id}",
    response_model=PostOut,
    operation_id="getPost",          # 客户端 SDK 生成的方法名
    summary="查看文章详情",            # 侧边栏一级条目
    description="未发布的文章仅作者本人与管理员可见。",   # 展开后的详细说明
    responses={404: {"description": "文章不存在"}},
)
async def get_post(...) -> PostOut:
    ...
项建议
函数名动词 + 资源:list_posts / get_post / create_post / update_post / delete_post
列表用 list_ 而非 get_all_为分页留出语义空间
operation_id手动指定,避免长路径生成 a_b_c_d_get 这类不可读名字
summary中文短句,就是 /docs 里显示的那行
description只在有非显然行为时写(权限、副作用、幂等性)

19.3 API 设计约定 ​

资源命名 ​

规则✅ 好❌ 差
用复数名词/posts /users/post /getUsers
层级表达从属/posts/{id}/comments/comments?post_id=1(可读,但层级关系丢了)
动作交给 HTTP 方法DELETE /posts/1POST /posts/1/delete
非 CRUD 动作用子资源 + 动词POST /posts/1/publishPOST /publishPost
路径小写、连字符/blog-posts/blogPosts
过滤用查询串/posts?tag=fastapi/posts/tag/fastapi

状态码语义 ​

码含义用在这里
200OKGET 成功、PATCH/PUT 成功
201CreatedPOST 创建资源成功,配 Location 头更佳
204No ContentDELETE 成功(响应体必须为空)
400Bad Request请求格式本身有问题(如非法 JSON 语义)
401Unauthorized未认证:没带 token、token 过期/签名错
403Forbidden已认证但无权限
404Not Found资源不存在或对当前用户不可见
409Conflict唯一约束冲突(slug、邮箱重复)
422Unprocessable Entity参数校验失败(FastAPI 默认)
429Too Many Requests触发限流,带 Retry-After
500Internal Server Error未捕获异常,不要主动抛

两个高频错误:

  • 把 401 和 403 混用。判断依据是"换个身份能不能成功":能 → 403;不能(压根没身份)→ 401。
  • DELETE 返回 204 却带了 body。Starlette 会因为 status_code=204 而丢弃 body,但你会得到一条难以理解的警告;正确做法是路由返回 None。

分页参数统一 ​

选一种,全项目贯彻:

风格参数适合
页码式page + size有 UI 页码控件、需要知道总页数
偏移式limit + offset内部接口、无限滚动
游标式cursor + limit大数据量、要求稳定不跳页

页码式的标准实现(本教程统一用这套):

python
class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int      # 总条数,前端算页数
    page: int
    size: int

约定:page 从 1 开始;size 上限 100(防止 size=100000 拖垮数据库);total 必须与 items 使用同一套过滤条件。

错误响应统一结构 ​

json
{
  "detail": "文章 42 不存在",
  "code": "not_found"
}
字段说明
detail面向开发者的可读信息;字符串或 FastAPI 原生的错误数组
code稳定的机器可读标识,前端据此做分支,不要解析 detail 文案

用 app/errors.py 集中注册处理器(见 第 18 章),保证 4xx 全都长这样。

版本化策略 ​

策略写法取舍
URL 路径版本/api/v1/posts最直观、最好调试、CDN 友好 → 推荐
请求头版本Accept: application/vnd.api+json;version=1URL 干净,但调试与缓存麻烦
查询参数版本/posts?version=1会污染业务参数,不推荐

v1 是路径的一部分,不是文件的一部分:不要建 api/v1/posts.py 和 api/v2/posts.py 两份拷贝。破坏性变更时新建 api/v2/,复用 services/ 层,v1 保留一个弃用周期:

python
app.include_router(posts_v1.router, prefix="/api/v1", deprecated=True)
app.include_router(posts_v2.router, prefix="/api/v2")

19.4 性能清单 ​

#检查项做法反例
1能用异步就用异步I/O 密集(DB、HTTP、Redis)一律 async def + 异步驱动在 async def 里调 requests.get()
2CPU 密集别占事件循环哈希、图像处理放 def(自动走线程池)或 run_in_threadpool在 async def 里做 PBKDF2 10 万次
3列表接口必须分页limit 强制上限,服务端永远不返回全表select(User) 直接返回
4连接池按并发配pool_size + max_overflow 对齐 worker 数与 DB 上限默认值扛 4 worker × 50 并发
5消灭 N+1关系设 lazy="selectin" 或 .options(selectinload(...))循环里访问 post.author.username
6序列化用 ORJSONFastAPI(default_response_class=ORJSONResponse)大列表默认 JSON 编码占满 CPU
7静态资源不经过应用交给 Nginx / CDN,只 mount 开发环境的 StaticFiles用 FastAPI 分发前端构建产物
8慢查询建索引对 WHERE / ORDER BY 列建索引,用 EXPLAIN ANALYZE 验证在 10 万行表上 ILIKE '%x%'
9分页排序键唯一ORDER BY created_at DESC, id DESC只按时间排序,翻页重复
10缓存热点读读多写少的聚合结果放 Redis,设明确 TTL每次请求都 COUNT(*) 全表
python
# 1 + 3 + 6 的组合写法
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)


@router.get("", response_model=Page[PostOut])
async def list_posts(
    service: PostServiceDep,
    size: Annotated[int, Query(ge=1, le=100)] = 20,   # 上限就是性能护栏
) -> Page[PostOut]:
    ...

⚠️ ORJSONResponse 不能序列化任意 Python 对象(比如 Decimal 之外的自定义类)。它依赖 orjson 的类型支持;遇到报错先看返回模型里有没有非标准类型,而不是直接换回默认响应类。


19.5 安全清单 ​

#检查项做法不做的后果
1密钥环境变量化SECRET_KEY 走环境变量,.env 进 .gitignore密钥进 Git 历史,轮换成本极高
2密钥强度校验Field(min_length=32),启动即失败空密钥签发可伪造的 token
3CORS 精确配置列举具体来源;allow_credentials=True 时禁用 "*"任意站点可携带用户 Cookie 调你的 API
4密码哈希用 Argon2pwdlib.PasswordHash.recommended()MD5/SHA1 一秒撞库
5JWT 设过期时间exp 必填,access token 15~60 分钟token 泄露后永久有效
6校验 JWT 算法解码时显式 algorithms=["HS256"]alg=none 伪造
7输入一律过 Pydanticextra="forbid"、长度/范围/正则约束超大字符串、脏数据入库
8响应模型过滤敏感字段输出模型里根本不存在 hashed_password密码哈希随用户对象泄露
9文件上传白名单校验扩展名 + MIME + 大小,重命名存储上传 .py / .html 后被解析执行
10限流slowapi 按 IP/用户限速,返回 429 + Retry-After撞库、刷接口拖垮服务
11依赖漏洞扫描uv lock --upgrade + pip-audit 进 CI长期带着已知 CVE 上线
12生产关闭 /docs内部服务 docs_url=None 或加鉴权接口结构完全暴露

限流的最小可用接法(slowapi 基于 Starlette 中间件,与 FastAPI 天然兼容):

bash
uv add slowapi
python
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
# 路由上:@limiter.limit("5/minute"),登录端点尤其需要

依赖漏洞扫描:

bash
uv sync --frozen           # 保证 CI 与本地依赖完全一致
uv lock --upgrade          # 升级前先看 diff
uv run pip-audit           # 扫描已安装依赖的已知漏洞
uv run pip-audit --fix     # 自动升级到修复版本(需人工复核)

错误示范 vs 正确示范 ​

❌ 不推荐:

python
# 密钥硬编码 + 算法不校验 + token 永不过期
SECRET = "dev"
payload = jwt.decode(token, SECRET)          # 未限定算法
data = {"sub": str(user.id)}                 # 无 exp

✅ 推荐:

python
from datetime import datetime, timedelta, timezone

import jwt

from app.core.config import get_settings

settings = get_settings()


def create_access_token(subject: str) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": subject,
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(minutes=settings.access_token_expire_minutes)).timestamp()),
    }
    return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)


def decode_access_token(token: str) -> dict:
    # 显式限定算法;exp 由 PyJWT 自动校验
    return jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])

19.6 常见坑与排查(跨章总表) ​

坑原因正确做法详见章节
/users/{id} 写在 /users/me 之前,me 被当成 id 解析路由按注册顺序匹配,先命中的先赢静态路径放在动态路径之前第 3 章
函数里参数默认值写 q: str = None类型注解说是 str,实际给了 None写成 q: str | None = None第 3 章
Optional[str] 迁移到 v2 后变成必填v2 中 Optional 只表示"可为 None",不再隐含默认值想要可选就写 = None第 4 章
可变默认值 tags: list[str] = [] 造成请求间串味默认值在模型定义时只求值一次用 default_factory=list第 4 章
前端字段拼错却"创建成功"了extra 默认 "ignore",未知字段被静默丢弃输入模型设 extra="forbid"第 4 章
响应里带出了 hashed_password直接返回 ORM 对象且输出模型里有该字段输出模型不含敏感字段(结构保证)第 5 章
response_model 与函数返回值类型不一致,字段被静默裁剪response_model 才是真正的过滤规则两边注解保持一致,或信任 response_model 并显式声明第 5 章
yield 依赖里 return 而非 yield,清理代码不执行生成器语义未生效确保函数体内有 yield,清理逻辑写在 yield 之后第 6 章
依赖里改了 get_db 却忘记在测试中 dependency_overrides.clear()覆盖是全局字典,跨用例残留fixture 的 yield 之后显式 clear()第 6 章
同时写了 Path() 却把参数名写错,收到 422参数名与路径模板不匹配就会被当成查询参数依赖与路由的参数名必须与 {...} 逐字一致第 7 章
Field(min_length=8) 在去空格前校验," abc " 通过校验顺序早于业务处理配 str_strip_whitespace=True 或在 mode="before" 校验器里 strip()第 7 章
自定义异常处理器里返回了错误的结构,前端解析失败每个处理器各写一套响应体统一 {"detail", "code"} 并集中注册第 8 章
中间件里读 request.body() 后下游拿不到 bodybody 是流,读一次就消费掉了中间件里 await request.body() 后重新构造 Request,或改用依赖第 9 章
@app.on_event("startup") 不执行清理该 API 已废弃,且异常会静默改用 lifespan 上下文管理器第 9 章
MissingGreenlet: greenlet_spawn has not been called异步会话里触发了同步惰性加载lazy="selectin" / selectinload;expire_on_commit=False第 10 章
迁移里全是 DROP TABLE模型没被 import,Base.metadata 里没有它们在 migrations/env.py 或 models/__init__.py 里 import 全部模型第 10 章
路由里直接写 SQL,改个字段要动三层分层被打破SQL 只出现在 repositories/第 11 章
事务提交散落在 Service,异常时半提交没有统一的提交边界提交/回滚放 get_db,Service 只 flush()第 11 章
分页查询 total 与 items 数量对不上两处过滤条件写得不一样用同一个 base 语句构造 count()第 11 章
翻页时出现重复或漏掉的记录排序键不唯一排序追加主键兜底第 11 章
pip install passlib 后 Argon2 报版本不兼容passlib 长期未维护改用 pwdlib[argon2]第 12 章
无 token 得到 403 而不是 401权限依赖先于认证依赖判定先认证(401)再鉴权(403)第 12 章
在 async def 里调 requests.get(),并发数上不去同步阻塞调用占住事件循环用 httpx.AsyncClient,或把函数改成 def 走线程池第 13 章
UploadFile 全量读进内存后 OOMawait file.read() 没有大小限制流式分块读取 + MAX_SIZE 提前校验第 14 章
文件上传按原名存储,被上传 .html 后 XSS未做白名单与重命名扩展名 + MIME 白名单,UUID 重命名第 14 章
WebSocket 广播时遍历连接列表抛 RuntimeError广播过程中集合被修改遍历副本,并对失效连接做 discard第 15 章
测试里 AsyncClient(app=app) 报 TypeErrorhttpx 新版本移除了 app= 简写用 ASGITransport(app=app)第 16 章
测试之间数据互相污染没清表,或测试库与开发库是同一个独立测试库 + fixture 内 drop_all/create_all第 16 章
--workers 4 后内存里缓存的 token / 状态对不上多进程之间不共享内存状态放 Redis / DB,别放模块级变量第 17 章
容器里 docker compose up 后 API 先起,报 relation 不存在depends_on 只保证容器启动顺序db 加 healthcheck + migrate 用 service_completed_successfully第 17 章
allow_origins=["*"] 与 allow_credentials=True 同时开,CORS 失效浏览器规范禁止该组合精确列举来源第 9 章

19.7 速查表 ​

常用命令 ​

场景命令
开发热重载uv run fastapi dev app/main.py
开发热重载(等价)uv run uvicorn app.main:app --reload --port 8000
生产启动uv run fastapi run app/main.py --host 0.0.0.0 --port 8000 --workers 4
添加依赖uv add httpx / uv add --dev pytest
生成迁移uv run alembic revision --autogenerate -m "add tags"
应用迁移uv run alembic upgrade head
回滚一步uv run alembic downgrade -1
查看当前版本uv run alembic current
跑测试uv run pytest -q
带覆盖率uv run pytest --cov=app --cov-report=term-missing
只跑失败过的uv run pytest --lf
依赖漏洞扫描uv run pip-audit
格式化 / 检查uv run ruff format . && uv run ruff check --fix .
类型检查uv run mypy app

参数声明速查(Annotated 一行式) ​

来源声明
路径item_id: Annotated[int, Path(ge=1, description="资源 ID")]
查询(可选)q: Annotated[str | None, Query(max_length=50)] = None
查询(必填)q: Annotated[str, Query(min_length=1)]
查询(枚举)sort: Annotated[Literal["asc", "desc"], Query()] = "desc"
请求体(单模型)payload: PostCreate
请求体(多模型 / 嵌入)payload: Annotated[PostCreate, Body(embed=True)]
请求头x_token: Annotated[str, Header(alias="X-Token")]
Cookiesession_id: Annotated[str | None, Cookie()] = None
表单username: Annotated[str, Form(min_length=3)]
文件upload: Annotated[UploadFile, File(description="≤ 2 MB")]
可复用别名PageSize = Annotated[int, Query(ge=1, le=100)]

状态码速查 ​

码什么时候用FastAPI 写法
200默认成功不用写
201创建成功status_code=status.HTTP_201_CREATED
204删除成功status_code=status.HTTP_204_NO_CONTENT,函数返回 None
400请求语义错误HTTPException(400, ...)
401未认证HTTPException(401, headers={"WWW-Authenticate": "Bearer"})
403已认证无权限HTTPException(403, ...)
404不存在或不可见HTTPException(404, ...)
409唯一约束冲突HTTPException(409, ...)
422校验失败框架自动返回
429限流slowapi 自动返回,附 Retry-After
500未捕获异常不要主动抛

依赖注入速查 ​

需求写法
普通依赖def get_service(db: SessionDep) -> PostService: ...
使用依赖service: Annotated[PostService, Depends(get_service)]
带清理的依赖函数体 yield session,清理写在 yield 之后
带参数的依赖Depends(require_role("admin"))(工厂返回 checker)
类作为依赖Depends(CommonQueryParams),类实例即依赖结果
路由级依赖@router.get("/x", dependencies=[Depends(verify_token)])
全局依赖FastAPI(dependencies=[Depends(verify_token)])
子依赖依赖函数自己的参数同样可以 Depends(...),形成依赖树
同一请求复用依赖按「可调用对象 + 参数」缓存,同一次请求内只执行一次
测试覆盖app.dependency_overrides[get_db] = fake,用完 clear()

Pydantic v2 常用 API 速查 ​

需求写法
dict → 模型UserCreate.model_validate(data)
JSON 字符串 → 模型UserCreate.model_validate_json(raw)
模型 → dictuser.model_dump()
模型 → JSON 字符串user.model_dump_json()
排除未传字段data.model_dump(exclude_unset=True)
排除敏感字段user.model_dump(exclude={"hashed_password"})
用别名输出user.model_dump(by_alias=True)
ORM 对象 → 模型UserOut.model_validate(row)(需 from_attributes=True)
字段级校验器@field_validator("name") + @classmethod
跨字段校验器@model_validator(mode="after"),必须 return self
计算字段@computed_field + @property
生成 JSON SchemaModel.model_json_schema()
局部更新模型Model.model_copy(update=patch)

常用 ConfigDict 选项:

选项作用
extra="forbid"出现未声明字段直接报错
from_attributes=True允许从对象属性读取(ORM 转 schema)
populate_by_name=True字段名与别名两种写法都接受
alias_generator=to_camel全局 snake_case ↔ camelCase
str_strip_whitespace=True字符串自动去首尾空白
validate_assignment=True赋值时也触发校验(有性能开销)
use_enum_values=True枚举字段序列化为其值
frozen=True模型不可变(可哈希)

19.8 学习路线建议 ​

框架生态

方向一句话适合场景
SQLModelPydantic 与 SQLAlchemy 的合并体,一个类既是模型又是 Schema中小项目快速起步;复杂关系仍建议纯 SQLAlchemy
FastStream用 FastAPI 风格写 Kafka / RabbitMQ 消费者需要事件驱动、异步消息
Litestar同类 ASGI 框架,插件与 DI 设计更严格愿意为类型严格性换一点生态
Strawberry / AriadneGraphQL 层,可与 FastAPI 共存前端字段需求多变

工程方向

方向起点
可观测性structlog 结构化日志 + OpenTelemetry 链路追踪 + Prometheus 指标
容器编排把第 17、18 章的 compose 迁移到 K8s:Deployment + Service + Job(迁移)+ HPA
消息队列Celery / ARQ / FastStream 处理邮件、报表等耗时任务;注意幂等与重试
数据库进阶读写分离、pgbouncer 连接池、分区表、EXPLAIN ANALYZE 调优
契约优先用 OpenAPI 生成前端 SDK(openapi-typescript),接口变更走 CI 校验
安全加固OAuth2 授权码流程、刷新令牌轮换、审计日志、密钥托管(Vault / KMS)

💡 最有效的下一步:把第 18 章的博客 API 部署到一台真实服务器上,接上域名与 HTTPS,然后压测一轮。生产环境的报错会立刻告诉你哪一块知识是虚的。


本章小结 ​

要点说明
分层是纪律api → services → repositories → models,靠目录职责表 + grep 自查维持
命名是文档XxxCreate / XxxUpdate / XxxOut / XxxInDB 一眼看出能否对外
大小写转换alias_generator=to_camel 一处配置,全局 API 输出 camelCase
状态码有语义401 与 403 按"换个身份能不能成功"区分;204 不带 body
分页要统一参数名、起始页、上限、total 口径全项目一致
错误要统一{"detail", "code"},前端只依赖 code 做分支
性能靠护栏分页上限、selectinload、连接池、ORJSONResponse
安全靠习惯密钥外置、Argon2、JWT 过期、CORS 精确、限流、依赖扫描
速查表记不住语法是正常的,翻表比搜索快

练习题 ​

  1. 把第 18 章项目的响应统一改成 camelCase:写出 APIModel 基类,改造 UserOut / PostOut,并验证 curl 返回的键名与 /docs 里的 Schema 都变了;同时说明请求体是否也必须用 camelCase。

  2. 为博客 API 制定一份 API_STYLE.md:包含资源命名规则、状态码决策表、分页约定、错误结构,并挑出你项目里三处不符合规范的接口,写出改造方案。

  3. 用 grep 或 CI 脚本实现三条自动校验:(a) services/ 与 repositories/ 中不出现 fastapi;(b) 每个路由都声明了 response_model;(c) 所有 status_code=204 的路由函数返回注解是 None。

  4. 从 19.6 的坑点总表中挑出你实际踩过的三条,写出最小复现代码、报错原文与修复后的代码,作为团队内部的踩坑记录。

  5. 给第 18 章的项目加一轮安全加固:slowapi 限流(登录 5 次/分钟)、依赖漏洞扫描进 CI、生产关闭 /docs,并用 curl 验证 429 响应带 Retry-After。


回到目录 ​

恭喜读完整个教程。建议把第 18 章的实战项目跑通后,回到目录按主题复习。

👉 返回课程目录

📖本文阅读--次|📊全站访问--次|👥访客--人