优化更新
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
## 请求链路总览
|
||||
|
||||
:::tip 链路概览
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → Redis 注入 → DB 会话 → 操作日志 → Token 续签 → 路由匹配 → JWT 认证 → RBAC 鉴权 → Pydantic 校验 → Endpoint → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → 上传体积限制 → 请求计时 → Token 续签 → 操作日志 → DB 会话 → Redis 注入 → 路由匹配 → JWT 认证 → RBAC 鉴权 → Pydantic 校验 → Endpoint → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
|
||||
:::
|
||||
|
||||
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
|
||||
@@ -52,28 +52,19 @@ FastAPI 的中间件按注册顺序形成洋葱模型。请求从最外层进入
|
||||
### 3. CORS 跨域中间件(最外层)
|
||||
|
||||
```python
|
||||
# src/middleware/cors.py
|
||||
register_cors(app) # 注册 CORS 中间件
|
||||
# src/middleware/cors.py — 最后注册,位于洋葱最外层
|
||||
register_cors(app)
|
||||
```
|
||||
|
||||
- 检查 `Origin` 头是否在允许列表中
|
||||
- 预检请求(OPTIONS)直接返回 CORS 响应头
|
||||
- 普通请求附加 `Access-Control-Allow-*` 响应头
|
||||
- 确保限流等直接返回的响应也带 CORS 头
|
||||
|
||||
### 4. 上传体积限制中间件
|
||||
### 4. 请求限流中间件
|
||||
|
||||
```python
|
||||
# src/middleware/upload_size.py
|
||||
app.middleware("http")(upload_size_limit_middleware)
|
||||
```
|
||||
|
||||
- 按 `Content-Length` 提前拒绝超大请求体
|
||||
- 避免超大 multipart 落临时盘后再由业务层校验
|
||||
|
||||
### 5. 请求限流中间件
|
||||
|
||||
```python
|
||||
# src/middleware/rate_limit.py
|
||||
# src/middleware/rate_limit.py — 倒数第二注册,仅 CORS 内层
|
||||
async def rate_limit_middleware(request: Request, call_next):
|
||||
if not RATE_LIMIT_ENABLED:
|
||||
return await call_next(request)
|
||||
@@ -92,29 +83,73 @@ async def rate_limit_middleware(request: Request, call_next):
|
||||
|
||||
- 基于客户端 IP 的滑动窗口计数器
|
||||
- 使用 Redis Lua 脚本保证原子性
|
||||
- 超限直接返回统一 JSON 响应,不进入后续链路
|
||||
- 超限直接返回 HTTP 200 + `{"code": 1}` 统一 JSON 响应,不进入后续链路
|
||||
- 被限流的请求不进入 DB 会话与操作日志,避免 DoS 攻击时日志表与 DB 连接不降反增
|
||||
- Redis 不可用时降级放行(fail-open)
|
||||
- CORS 预检请求不计数
|
||||
|
||||
### 6. Redis 上下文注入中间件
|
||||
### 5. 上传体积限制中间件
|
||||
|
||||
```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)
|
||||
# src/middleware/upload_size_limit.py — 按 Content-Length 提前拒绝超大请求体
|
||||
app.middleware("http")(upload_size_limit_middleware)
|
||||
```
|
||||
|
||||
- 将 Redis 异步客户端注入 `contextvars`
|
||||
- 后续业务代码通过 `get_redis()` 获取当前请求的 Redis 客户端
|
||||
- 按 `Content-Length` 提前拒绝超大请求体
|
||||
- 避免超大 multipart 落临时盘后再由业务层校验
|
||||
- 位于限流内层,已被限流放行的请求才检查体积
|
||||
|
||||
### 7. 数据库会话中间件
|
||||
### 6. 请求计时中间件
|
||||
|
||||
```python
|
||||
# src/middleware/db_session.py
|
||||
# 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
|
||||
```
|
||||
|
||||
- 记录请求处理耗时(不含限流/CORS 等外层开销)
|
||||
- 通过 `X-Process-Time` 响应头返回
|
||||
|
||||
### 7. Token 续签中间件
|
||||
|
||||
```python
|
||||
# src/core/app.py — 续签 token 写入响应头
|
||||
async def token_refresh_middleware(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` 响应头返回
|
||||
|
||||
### 8. 操作日志中间件
|
||||
|
||||
```python
|
||||
# src/middleware/operation_log.py — 在 DB 会话外层,记录写操作日志
|
||||
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. 数据库会话中间件
|
||||
|
||||
```python
|
||||
# src/middleware/db_session.py — 后注册→外层,DB 会话包裹业务逻辑
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
# 创建数据库会话
|
||||
db = SessionLocal()
|
||||
@@ -144,52 +179,22 @@ async def db_session_middleware(request: Request, call_next):
|
||||
- 写操作根据响应体 `code` 字段自动 commit/rollback
|
||||
- 异常时自动 rollback,finally 中关闭会话
|
||||
|
||||
### 8. 操作日志中间件
|
||||
### 10. Redis 上下文注入中间件(最内层)
|
||||
|
||||
```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
|
||||
# src/core/redis_client.py — 先注册→最内层:先注入 Redis 再开 DB 会话
|
||||
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)
|
||||
```
|
||||
|
||||
- 在 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` 响应头返回
|
||||
- 将 Redis 异步客户端注入 `contextvars`
|
||||
- 后续业务代码通过 `get_redis()` 获取当前请求的 Redis 客户端
|
||||
- 位于最内层,确保 DB 会话和操作日志都能使用 Redis
|
||||
|
||||
## 第三阶段:路由与认证
|
||||
|
||||
@@ -301,15 +306,25 @@ def permission_required(permission: str):
|
||||
```python
|
||||
# src/core/access_decorators.py
|
||||
def check_demo(func):
|
||||
"""演示模式拦截装饰器,放在 @permission_required 之后、路由函数之前"""
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
if FASTAPI_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
return await func(*args, **kwargs)
|
||||
return async_wrapper
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
if FASTAPI_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
return func(*args, **kwargs)
|
||||
return sync_wrapper
|
||||
```
|
||||
|
||||
- `FASTAPI_DEMO=True` 时,所有写操作返回失败响应
|
||||
- 根据被装饰函数类型自动选择同步/异步包装
|
||||
- 保护演示环境数据不被修改
|
||||
|
||||
## 第四阶段:参数校验
|
||||
@@ -440,7 +455,7 @@ R.ok(msg='添加成功')
|
||||
## 完整链路流程图
|
||||
|
||||
```
|
||||
请求链路(自上而下)
|
||||
请求链路(自上而下,洋葱模型外层 → 内层)
|
||||
|
||||
[浏览器]
|
||||
│ POST /api/v1/position/add
|
||||
@@ -448,29 +463,41 @@ R.ok(msg='添加成功')
|
||||
[Nginx] ── 反向代理转发
|
||||
│
|
||||
▼
|
||||
[CORS 中间件] ── 跨域校验通过
|
||||
[CORS 中间件] ── 跨域校验通过(最外层)
|
||||
│
|
||||
▼
|
||||
[限流中间件] ── 滑动窗口计数(Redis)
|
||||
│ ├─ 超限 → 直接返回 429
|
||||
[限流中间件] ── 滑动窗口计数(Redis Lua)
|
||||
│ ├─ 超限 → 直接返回 HTTP 200 + {code:1, msg:"请求过于频繁"}
|
||||
│ └─ 通过 ▼
|
||||
▼
|
||||
[上传体积限制] ── 按 Content-Length 拦截超大请求
|
||||
│
|
||||
▼
|
||||
[请求计时] ── 记录起始时间,响应时附加 X-Process-Time
|
||||
│
|
||||
▼
|
||||
[Token 续签] ── 检查是否需要续签,响应时附加 X-Refresh-Token
|
||||
│
|
||||
▼
|
||||
[操作日志中间件] ── 异步记录写操作(不影响主流程)
|
||||
│
|
||||
▼
|
||||
[DB 会话中间件] ── 创建 SessionLocal(),注入 contextvars
|
||||
│
|
||||
▼
|
||||
[操作日志中间件] ── 异步记录写操作(不影响主流程)
|
||||
[Redis 注入中间件] ── 将 Redis 客户端注入 contextvars(最内层)
|
||||
│
|
||||
▼
|
||||
[路由层] ── 匹配 /api/v1/position/add
|
||||
│
|
||||
▼
|
||||
[认证依赖 login_required] ── JWT 验证 + 黑名单检查(Redis)
|
||||
│ ├─ 失败 → 返回 401
|
||||
│ ├─ 失败 → 返回 HTTP 200 + {code:401, msg:"..."}
|
||||
│ └─ 通过 → 解析 user_id/username 注入 request.state ▼
|
||||
▼
|
||||
[Endpoint 层]
|
||||
├─ @permission_required ── 权限校验(同步直查DB / 异步Redis缓存)
|
||||
├─ @check_demo ── 演示模式拦截
|
||||
├─ @permission_required ── 权限校验(Redis 缓存)
|
||||
└─ 调用 Service ▼
|
||||
▼
|
||||
[Service 层]
|
||||
@@ -485,14 +512,14 @@ R.ok(msg='添加成功')
|
||||
▼
|
||||
[数据库] ── 执行 INSERT,返回自增 ID
|
||||
│
|
||||
▼ 逐层返回响应
|
||||
[Repository] → [Service] → [Endpoint] → [Router] → [OpLog] → [DBSession]
|
||||
▼ 逐层返回响应(内层 → 外层)
|
||||
[Redis] → [DBSession] → [OpLog] → [Token续签] → [请求计时] → [上传体积] → [限流] → [CORS]
|
||||
│
|
||||
├─ 业务成功(code=0)→ commit 提交事务
|
||||
└─ 业务失败(code≠0)→ rollback 回滚事务
|
||||
├─ 业务成功(code=0)→ DB 会话 commit 提交事务
|
||||
└─ 业务失败(code≠0)→ DB 会话 rollback 回滚事务
|
||||
│
|
||||
▼
|
||||
[限流] → [CORS] → [Nginx]
|
||||
[Nginx]
|
||||
│
|
||||
▼
|
||||
[浏览器] ← JSON 响应({ code, data, msg, ok })
|
||||
@@ -502,7 +529,7 @@ R.ok(msg='添加成功')
|
||||
|
||||
当请求链路中发生异常时,全局异常处理器统一捕获并返回标准响应:
|
||||
|
||||
| 异常类型 | 触发场景 | 响应 |
|
||||
| 异常类型 | 触发场景 | 响应(均为 HTTP 200) |
|
||||
|:---|:---|:---|
|
||||
| `AuthorizationException` | JWT 过期 / 无权限 | `{"code": 401, "msg": "..."}` |
|
||||
| `BusinessException` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
|
||||
@@ -511,7 +538,7 @@ R.ok(msg='添加成功')
|
||||
| `Exception` | 未捕获异常 | `{"code": 1, "msg": "服务器内部错误"}` |
|
||||
|
||||
:::warning 异常处理原则
|
||||
所有异常均返回 HTTP 200 状态码,通过 `code` 字段区分业务成功(0)与失败(1)。这是前后端约定,前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
|
||||
所有异常均返回 **HTTP 200** 状态码,通过响应体 `code` 字段区分业务成功(0)与失败(1/401 等)。注意:`AuthorizationException` 的 `code=401` 是响应体中的业务码,不是 HTTP 状态码。这是前后端约定,前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
Reference in New Issue
Block a user