- src/ 按域组织骨架,示例域 items 双后端实现(SQLAlchemy2.0 async / Beanie 2.x) - DB_BACKEND 环境变量切换,router 只依赖 Repo Protocol - alembic 迁移(含初始 items 迁移)、uv 依赖管理、ruff、pytest 异步测试 - Dockerfile + docker-compose(app/mysql/mongo) - AGENTS.md 重写为本模板开发规范;原最佳实践文档归档 docs/ - 验证:mongodb/mysql 双后端 pytest 全绿 + uvicorn 真实冒烟通过
6.2 KiB
6.2 KiB
AGENTS.md — fastapi-template 开发规范
本文件是 AI Agent / 开发者在本仓库工作的必读规范。先读这个,再动手。 通用 FastAPI 最佳实践参考 docs/AGENTS_GENERIC.md 与 docs/BEST_PRACTICES_ZH.md。
0. 这个模板是什么
FastAPI 生产级项目骨架。核心特性:DB_BACKEND 一个环境变量切换 MySQL / MongoDB,业务代码零改动。
任何修改都不得破坏这个特性——router/schemas 永远不允许 import 具体 DB 实现。
1. 技术栈基线
| 项 | 版本 | 说明 |
|---|---|---|
| Python | ≥3.11 | 用 X | Y、StrEnum |
| FastAPI | ≥0.115 | 依赖注入一律 Annotated[T, Depends(...)] |
| Pydantic | v2 | 禁用 v1 API(.dict() / json_encoders) |
| SQLAlchemy | 2.0 async | AsyncSession + async_sessionmaker,禁 sync session |
| Beanie | ≥1.27 | MongoDB ODM |
| 包管理 | uv | 禁 pip/poetry/conda;加依赖 uv add xxx |
| Lint | ruff | 提交前 uv run ruff check --fix . 必须零告警 |
| 测试 | pytest + pytest-asyncio + httpx | asyncio_mode=auto |
2. 目录结构铁律
按**域(domain)**组织,不按文件类型。一个域一个包,照抄 src/items/:
src/{domain}/
├── router.py # 路由:只做参数校验和调用 repo,禁写 SQL/查询
├── schemas.py # Pydantic 契约:与 DB 无关,前后端共用语义
├── dependencies.py # 定义 Repo Protocol + 按 DB_BACKEND 选实现
├── mysql.py # MySQL ORM 模型 + MySQLXxxRepo
├── mongo.py # Beanie Document + MongoXxxRepo
├── service.py # (可选)跨 repo 的复杂业务逻辑
├── constants.py # (可选)常量、错误码
└── exceptions.py # (可选)域内异常,继承 src.exceptions.AppError
- 新增域后必做三件事:①
src/mongo.py::_collect_documents()登记 Document; ②alembic/env.pyimport mysql 模型;③src/main.pyinclude_router。 - 跨域 import 必须显式模块名:
from src.items import constants as item_constants,禁import *。 - 全局共享的东西才放
src/根(config / exceptions / database / mongo)。
3. 双数据库后端规范(本模板灵魂)
# ✅ router 只依赖 Protocol
async def get_item(item_id: str, repo: ItemRepoDep): ...
# ✅ dependencies.py 模块加载时选定实现
get_item_repo = _get_mysql_repo if settings.DB_BACKEND == "mysql" else _get_mongo_repo
# ❌ 禁止在 router/schemas/service 里出现
from src.items.mysql import Item # 不许!
from src.items.mongo import ItemDoc # 不许!
- 两个 repo 实现同一 Protocol,方法签名逐字一致。
- schemas 的
id统一用str(MySQL int 自增 / Mongo ObjectId 都转 str), 转换在各 repo 的to_out()里完成。 - MySQL 改表结构 → 必须
alembic revision --autogenerate生成迁移,禁手改生产库。 - Mongo 新增 Document → 只登记
_collect_documents(),beanie 自动建索引。 src/database.py、src/mongo.py只在 lifespan / dependencies 里被引用; 非对应后端模式下它们可以 import 但不得产生连接(engine 惰性,别主动 connect)。
4. 异步纪律
| 场景 | 写法 |
|---|---|
| 可 await 的 I/O | async def |
| 只有同步 SDK | def(FastAPI 自动进线程池)或 run_in_threadpool |
| CPU 密集 >50ms | 扔任务队列,别放请求路径 |
# ❌ async 路由里调用阻塞库 = 冻结整个事件循环
@router.get("/bad")
async def bad():
time.sleep(5)
# ✅
@router.get("/ok")
def ok():
time.sleep(5)
5. 配置与异常
- 配置只走
src/config.py的settings(pydantic-settings);域级配置放{domain}/config.py,env_prefix="XXX_"。 - 禁在代码里硬编码 DSN / 密钥;
.env不入库,.env.example必须同步更新。 - 业务异常继承
AppError,404 用NotFoundError;统一由register_exception_handlers输出{"detail": ...}。 - 禁裸
except:,禁except Exception: pass。
6. Pydantic 规范
# ✅ 约束写清楚;required 和 optional 二选一,别自相矛盾
name: str = Field(min_length=1, max_length=128)
age: int | None = Field(default=None, ge=0) # 可选
age: int = Field(ge=18) # 必填
# ❌ 矛盾写法
age: int = Field(ge=18, default=None)
- 序列化定制用
@field_serializer,不用json_encoders。 - 入参模型(XxxCreate/XxxUpdate)与出参模型(XxxOut)分开;
XxxUpdate全字段可选 +model_dump(exclude_unset=True)。
7. 测试规范
- 测试从第一天就是异步:
httpx.AsyncClient+ASGITransport,见tests/conftest.py。 - 测试库与开发库隔离:Mongo 用
app_test,MySQL 用app_test。 - 每个域至少覆盖:create / get / list / update / delete + 404 路径。
- 跑 MySQL 后端测试:
DB_BACKEND=mysql uv run pytest。
8. Git 规范
- 提交信息:Conventional Commits ——
feat: xxx/fix: xxx/refactor:/docs:/test:/chore:。 - 主分支
main;功能开发开feature/xxx分支,提 PR 合并。 - 提交前自检三连:
uv run ruff check --fix . && uv run pytest,全绿才推。
9. 常用命令速查
uv sync # 装依赖
uv add fastapi # 加依赖(dev 依赖:uv add --dev xxx)
uv run uvicorn src.main:app --reload # 开发起服务
uv run pytest # 测试
uv run ruff check --fix . # lint
uv run alembic revision --autogenerate -m "msg" # MySQL 迁移
docker compose up -d --build # 容器化整套
10. 反模式清单(Review 时逐条核对)
- router 里出现 SQL /
ItemDoc.find()等具体 DB 调用 - async 路由里调用 requests / time.sleep 等阻塞库
print调试残留(用logging)- 硬编码连接串 / 密钥
from x import *、裸 except- 改了 MySQL 模型没生成 alembic 迁移
- 新增 Mongo Document 没登记
_collect_documents() - 新域只在单一后端实现(两个 repo 必须成对)
- 提交信息 freestyle(非 Conventional Commits)