Files
djangoadmin-wiki/DjangoAdmin_FastAPI_NaiveVue/zh/volume3/request-flow.md
T
2026-09-12 23:12:15 +08:00

520 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 请求完整链路
本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。
## 请求链路总览
:::tip 链路概览
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → Redis 注入 → DB 会话 → 操作日志 → Token 续签 → 路由匹配 → JWT 认证 → RBAC 鉴权 → Pydantic 校验 → Endpoint → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
:::
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
## 第一阶段:网络层
### 1. 浏览器发起请求
```javascript
// 前端 Axios 调用
const res = await request({
url: '/api/v1/position/add',
method: 'post',
data: {
name: '高级工程师',
status: 1,
sort: 10
}
});
```
前端通过 Axios 发起 HTTP POST 请求,携带:
- `Authorization: Bearer <JWT Token>` 请求头
- `Content-Type: application/json` 请求头
- JSON 请求体
### 2. Nginx 反向代理(生产环境)
```nginx
location /api/ {
proxy_pass http://127.0.0.1:8031;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
Nginx 将请求代理到 FastAPI 服务,同时附加客户端真实 IP 到 `X-Forwarded-For` 头。
## 第二阶段:中间件链(洋葱模型外层 → 内层)
FastAPI 的中间件按注册顺序形成洋葱模型。请求从最外层进入,逐层穿透到路由处理函数,响应沿反向链返回。
### 3. CORS 跨域中间件(最外层)
```python
# src/middleware/cors.py
register_cors(app) # 注册 CORS 中间件
```
- 检查 `Origin` 头是否在允许列表中
- 预检请求(OPTIONS)直接返回 CORS 响应头
- 普通请求附加 `Access-Control-Allow-*` 响应头
### 4. 上传体积限制中间件
```python
# src/middleware/upload_size.py
app.middleware("http")(upload_size_limit_middleware)
```
- 按 `Content-Length` 提前拒绝超大请求体
- 避免超大 multipart 落临时盘后再由业务层校验
### 5. 请求限流中间件
```python
# src/middleware/rate_limit.py
async def rate_limit_middleware(request: Request, call_next):
if not RATE_LIMIT_ENABLED:
return await call_next(request)
# 滑动窗口计数器(Redis Lua 原子操作)
count = await sliding_window_incr(redis, current_key, prev_key, ...)
if count > RATE_LIMIT_LIMIT:
return JSONResponse(
status_code=200,
content={
"code": 1,
"msg": "请求过于频繁,请稍后再试"
}
)
return await call_next(request)
```
- 基于客户端 IP 的滑动窗口计数器
- 使用 Redis Lua 脚本保证原子性
- 超限直接返回统一 JSON 响应,不进入后续链路
- Redis 不可用时降级放行(fail-open)
### 6. Redis 上下文注入中间件
```python
# src/core/redis_client.py
async def redis_middleware(request: Request, call_next):
# 从应用状态中获取 Redis 客户端实例
redis = getattr(request.app.state, 'redis', None)
if redis is not None:
# 将 Redis 客户端注入到 contextvars,供业务层通过依赖注入获取
_redis_ctx.set(redis)
return await call_next(request)
```
- 将 Redis 异步客户端注入 `contextvars`
- 后续业务代码通过 `get_redis()` 获取当前请求的 Redis 客户端
### 7. 数据库会话中间件
```python
# src/middleware/db_session.py
async def db_session_middleware(request: Request, call_next):
# 创建数据库会话
db = SessionLocal()
# 注入到 contextvars,供业务层通过依赖注入获取
_db_ctx.set(db)
try:
# 执行后续请求处理(路由、业务逻辑等)
response = await call_next(request)
# 对于写操作,根据业务返回码决定提交或回滚
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
body = await read_response_body(response)
if _is_business_success(body): # code == 0 表示业务成功
db.commit()
else:
db.rollback()
return response
except Exception:
# 发生异常时回滚事务
db.rollback()
raise
finally:
# 无论成功或异常,最终关闭会话释放连接
db.close()
```
- 为每个请求创建独立的数据库会话
- 写操作根据响应体 `code` 字段自动 commit/rollback
- 异常时自动 rollback,finally 中关闭会话
### 8. 操作日志中间件
```python
# src/middleware/operation_log.py
async def operation_log_middleware(request: Request, call_next):
response = await call_next(request)
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
# 记录操作日志到 fastapi_operation_log 表
# 包括:操作人、操作IP、请求路径、请求参数、响应状态、耗时等
...
return response
```
- 在 DB 会话外层,记录写操作日志
- 包含请求方法、URL、参数、响应码、耗时、操作人等信息
### 9. Token 续签中间件
```python
# src/core/app.py
async def token_refresh_middleware(request: Request, call_next):
response = await call_next(request)
refresh_token = getattr(request.state, '_refresh_token', None)
if refresh_token:
response.headers['X-Refresh-Token'] = refresh_token
return response
```
- 最外层中间件,检查是否需要续签 Token
- 续签后的 Token 通过 `X-Refresh-Token` 响应头返回
### 10. 请求计时中间件
```python
# src/core/app.py
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(round(process_time, 5))
return response
```
- 记录请求处理耗时
- 通过 `X-Process-Time` 响应头返回
## 第三阶段:路由与认证
### 11. 路由匹配
```python
# src/api/__init__.py
def register_router(app: FastAPI):
# 登录和验证码无需认证,直接挂载到 app(同样带 /api/v1 前缀)
app.include_router(login_view, prefix="/api/v1", tags=["系统登录"])
# 需要认证的路由集合
app.include_router(
v1,
prefix="/api/v1",
dependencies=[Depends(login_required)],
)
```
- FastAPI 根据 HTTP 方法 + URL 路径匹配到 `POST /api/v1/position/add` 端点
- 该端点挂载在 `v1` 路由器上,自动应用 `login_required` 依赖
- `v1` 路由器在 `src/api/v1/router.py` 中集中注册所有模块路由
### 12. JWT 认证(login_required)
```python
# src/api/deps.py
async def login_required(request: Request):
# 1. 从 Authorization 头提取 Bearer Token
# 2. 验证 Token 签名、有效期
# 3. 检查 Token 是否在 Redis 黑名单中
# 4. 解析出 user_id、username、realname
# 5. 注入到 request.state
request.state.user_id = user_id
request.state.username = username
request.state.realname = realname
```
- 认证失败抛出 `AuthorizationException`,由全局异常处理器返回统一响应
- 认证通过后,用户信息注入 `request.state` 供后续使用
### 13. RBAC 鉴权(permission_required)
```python
# src/api/v1/endpoints/position.py
@router.post('/add', summary='添加岗位')
@permission_required("sys:position:add") # 权限校验:验证当前用户是否拥有该权限节点
@check_demo # 演示模式校验:演示环境下禁止写操作
def add(request: Request, data: PositionForm):
return position_service.add(request, data)
```
```python
# src/core/access_decorators.py
def permission_required(permission: str):
"""节点权限鉴权装饰器,userId==1(管理员)直接放行"""
def decorator(func):
def _check(request):
"""同步权限校验(直查DB,无Redis依赖,供线程池/回退场景使用)"""
userId = get_user_id(request)
if userId == 1:
return None
from modules.system.menu import service as menu
permission_list = menu.get_permissions_list_sync(userId)
if permission not in permission_list:
return R.failed("权限不足")
return None
async def _check_cached(request):
"""异步权限校验(走 Redis 缓存,未命中才回源DB)"""
userId = get_user_id(request)
if userId == 1:
return None
from modules.system.menu import service as menu
permission_list = await menu.get_permissions_list(userId)
if permission not in permission_list:
return R.failed("权限不足")
return None
if asyncio.iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
request = _extract_request(*args, **kwargs)
denied = await _check_cached(request)
if denied:
return denied
return await func(*args, **kwargs)
return async_wrapper
@wraps(func)
def sync_wrapper(*args, **kwargs):
request = _extract_request(*args, **kwargs)
denied = _check(request)
if denied:
return denied
return func(*args, **kwargs)
return sync_wrapper
return decorator
```
- 权限字符串格式:`sys:{module}:{action}`(如 `sys:position:add`)
- 用户 ID=1(超级管理员)跳过所有权限检查
- 同步端点走 `_check` 直查 DB(在线程池中执行,不阻塞事件循环)
- 异步端点走 `_check_cached` 走 Redis 缓存查询,变更时主动失效
- 使用懒加载 `from modules.system.menu import service as menu` 避免 core → 业务模块顶层循环依赖
### 14. 演示模式校验(check_demo)
```python
# src/core/access_decorators.py
def check_demo(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if FASTAPI_DEMO:
return R.failed("演示环境,暂无操作权限")
return await func(*args, **kwargs)
return wrapper
```
- `FASTAPI_DEMO=True` 时,所有写操作返回失败响应
- 保护演示环境数据不被修改
## 第四阶段:参数校验
### 15. Pydantic 请求校验
```python
# src/modules/system/position/schemas.py
class PositionForm(BaseForm):
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
sort: int = Field(..., ge=0, le=99999, description="岗位排序")
```
- FastAPI 自动解析 JSON 请求体
- Pydantic 按 `Field` 约束校验每个字段
- 校验失败触发 `RequestValidationError`,由全局异常处理器返回:
```json
{"code": 1, "data": null, "msg": "name: 确保该值至少有 1 个字符"}
```
- 校验通过后,`data` 参数为已验证的 `PositionForm` 实例
## 第五阶段:业务处理
### 16. Endpoint(HTTP 层)
```python
# src/api/v1/endpoints/position.py
def add(request: Request, data: PositionForm):
return position_service.add(request, data)
```
- HTTP 层仅做请求转发,不含业务逻辑
- 直接调用 Service 层方法
### 17. Service(业务逻辑层)
```python
# src/modules/system/position/service.py
class PositionService(BaseService[Position]):
repo = position_repo
model = Position
unique_fields = {'name': '岗位名称不能重复'}
def add(self, request, data) -> R:
# 1. 唯一性校验
err = self._check_unique(data)
if err:
return R.failed(err)
# 2. 前置扩展钩子
self._before_add(request, data)
# 3. 组装创建字段
fields = self._build_create_fields(request, data)
# 4. 调用 Repository 创建记录
self.repo.create(**fields)
# 5. 返回成功响应
return R.ok(msg='添加成功')
```
### 18. Repository(数据访问层)
```python
# src/core/base_repository.py
class BaseRepository(Generic[M]):
def create(self, **fields) -> M:
# 清洗数据:camelCase → snake_case,过滤非模型字段
obj = self.model(**self._clean_model_data(fields))
obj.save() # add + flush(不 commit)
return obj
```
### 19. Model(模型层)
```python
# src/core/base_db.py
class base_db:
def save(self):
db = get_db_session()
db.add(self)
db.flush() # 发送 SQL 到数据库,获取自增 ID
return self
```
### 20. 数据库执行
- SQLAlchemy 将 ORM 操作转换为 SQL:`INSERT INTO fastapi_position (name, status, sort, create_user, ...) VALUES (...)`
- `flush()` 将 SQL 发送到数据库,获取自增主键
- 事务由中间件在响应返回后统一 commit
## 第六阶段:响应返回
### 21. 响应序列化
```python
# Service 返回 R.ok(msg='添加成功')
# src/core/response.py
R.ok(msg='添加成功')
# → JSONResponse(status_code=200, content={"code": 0, "data": None, "msg": "添加成功", "ok": true})
```
### 22. 中间件反向链
响应沿中间件链反向返回:
1. **请求计时**:附加 `X-Process-Time` 响应头
2. **Token 续签**:检查并附加 `X-Refresh-Token` 响应头
3. **操作日志**:记录写操作日志到数据库
4. **DB 会话**:检测 `code==0`,执行 `db.commit()`
5. **Redis 上下文**:无操作(contextvars 自动清理)
6. **限流**:无操作(响应直接透传)
7. **CORS**:附加 `Access-Control-Allow-*` 响应头
### 23. 浏览器接收响应
```json
{
"code": 0,
"data": null,
"msg": "添加成功",
"ok": true
}
```
前端 Axios 拦截器统一处理响应,`code===0` 时展示成功提示,否则展示错误信息。
## 完整链路流程图
```
请求链路(自上而下)
[浏览器]
│ POST /api/v1/position/add
▼
[Nginx] ── 反向代理转发
│
▼
[CORS 中间件] ── 跨域校验通过
│
▼
[限流中间件] ── 滑动窗口计数(Redis)
│ ├─ 超限 → 直接返回 429
│ └─ 通过 ▼
▼
[DB 会话中间件] ── 创建 SessionLocal(),注入 contextvars
│
▼
[操作日志中间件] ── 异步记录写操作(不影响主流程)
│
▼
[路由层] ── 匹配 /api/v1/position/add
│
▼
[认证依赖 login_required] ── JWT 验证 + 黑名单检查(Redis)
│ ├─ 失败 → 返回 401
│ └─ 通过 → 解析 user_id/username 注入 request.state ▼
▼
[Endpoint 层]
├─ @check_demo ── 演示模式拦截
├─ @permission_required ── 权限校验(Redis 缓存)
└─ 调用 Service ▼
▼
[Service 层]
├─ _check_unique ── 唯一性校验(DB 查询)
├─ _before_add ── 前置扩展钩子
├─ _build_create_fields ── 组装创建字段
└─ 调用 Repository ▼
▼
[Repository 层]
├─ _clean_model_data ── camelCase → snake_case,过滤非法字段
└─ model.save() ── add + flush(获取自增 ID,不提交)▼
▼
[数据库] ── 执行 INSERT,返回自增 ID
│
▼ 逐层返回响应
[Repository] → [Service] → [Endpoint] → [Router] → [OpLog] → [DBSession]
│
├─ 业务成功(code=0)→ commit 提交事务
└─ 业务失败(code≠0)→ rollback 回滚事务
│
▼
[限流] → [CORS] → [Nginx]
│
▼
[浏览器] ← JSON 响应({ code, data, msg, ok })
```
## 异常处理链路
当请求链路中发生异常时,全局异常处理器统一捕获并返回标准响应:
| 异常类型 | 触发场景 | 响应 |
|:---|:---|:---|
| `AuthorizationException` | JWT 过期 / 无权限 | `{"code": 401, "msg": "..."}` |
| `BusinessException` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
| `RequestValidationError` | Pydantic 校验失败 | `{"code": 1, "msg": "field: message"}` |
| `StarletteHTTPException` | 404 / 405 等 | `{"code": 1, "msg": "请求错误"}` |
| `Exception` | 未捕获异常 | `{"code": 1, "msg": "服务器内部错误"}` |
:::warning 异常处理原则
所有异常均返回 HTTP 200 状态码,通过 `code` 字段区分业务成功(0)与失败(1)。这是前后端约定,前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
:::
## 总结
一个 API 请求从浏览器到数据库经过 7 个阶段、20+ 个处理节点。中间件链负责横切关注点(安全、限流、日志、事务),路由层负责认证鉴权,Pydantic 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。