优化更新

This commit is contained in:
2026-09-14 09:44:33 +08:00
parent b3a9e21254
commit 5b8e178970
60 changed files with 1243 additions and 1022 deletions
@@ -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 状态码差异。
:::
## 总结