149 lines
6.4 KiB
Markdown
149 lines
6.4 KiB
Markdown
# AGENTS.md — fastapi-template 开发规范(mysql 分支)
|
||
|
||
> 本分支为 **MySQL 单后端**:ORM 模型写 `src/{domain}/mysql.py`,
|
||
> 改表结构必须 `uv run alembic revision --autogenerate` 生成迁移 + `alembic upgrade head` 执行。
|
||
|
||
|
||
本文件是 AI Agent / 开发者在本仓库工作的**必读规范**。先读这个,再动手。
|
||
通用 FastAPI 最佳实践参考 [docs/AGENTS_GENERIC.md](./docs/AGENTS_GENERIC.md) 与 [docs/BEST_PRACTICES_ZH.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.py` import mysql 模型;③ `src/main.py` include_router。
|
||
- 跨域 import 必须显式模块名:`from src.items import constants as item_constants`,禁 `import *`。
|
||
- 全局共享的东西才放 `src/` 根(config / exceptions / database / mongo)。
|
||
|
||
## 3. 双数据库后端规范(本模板灵魂)
|
||
|
||
```python
|
||
# ✅ 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 | 扔任务队列,别放请求路径 |
|
||
|
||
```python
|
||
# ❌ 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 规范
|
||
|
||
```python
|
||
# ✅ 约束写清楚;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. 常用命令速查
|
||
|
||
```bash
|
||
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)
|