447 lines
13 KiB
Markdown
447 lines
13 KiB
Markdown
# 请求完整链路
|
||
|
||
本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。
|
||
|
||
## 请求链路总览
|
||
|
||
:::tip 链路概览
|
||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → Redis 注入 → DB 会话 → 操作日志 → 路由匹配 → JWT 认证 → RBAC 鉴权 → WTForms 校验 → Router → 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:8021;
|
||
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 将请求代理到 Flask 服务,同时附加客户端真实 IP 到 `X-Forwarded-For` 头。
|
||
|
||
## 第二阶段:中间件链(请求/响应钩子)
|
||
|
||
Flask 的中间件通过 `before_request` / `after_request` 钩子实现。请求从最外层进入,逐层穿透到路由处理函数,响应沿反向链返回。
|
||
|
||
### 3. CORS 跨域中间件(最外层)
|
||
|
||
```python
|
||
# src/middleware/cors.py
|
||
register_cors(app) # 注册 CORS 中间件
|
||
```
|
||
|
||
- 检查 `Origin` 头是否在允许列表中
|
||
- 预检请求(OPTIONS)直接返回 CORS 响应头
|
||
- 普通请求附加 `Access-Control-Allow-*` 响应头
|
||
|
||
### 4. 请求限流中间件
|
||
|
||
```python
|
||
# src/middleware/rate_limit.py
|
||
@app.before_request
|
||
def rate_limit():
|
||
if not RATE_LIMIT_ENABLED:
|
||
return
|
||
# 滑动窗口计数器(Redis Lua 原子操作)
|
||
count = sliding_window_incr(redis, current_key, prev_key, ...)
|
||
if count > RATE_LIMIT_LIMIT:
|
||
return R.failed("请求过于频繁,请稍后再试")
|
||
```
|
||
|
||
- 基于客户端 IP 的滑动窗口计数器
|
||
- 使用 Redis Lua 脚本保证原子性
|
||
- 超限直接返回统一 JSON 响应,不进入后续链路
|
||
- Redis 不可用时降级放行(fail-open)
|
||
|
||
### 5. Redis 上下文注入中间件
|
||
|
||
```python
|
||
# src/core/redis_client.py
|
||
@app.before_request
|
||
def redis_context():
|
||
# 将 Redis 客户端注入到 g 对象
|
||
g.redis = redis_client
|
||
```
|
||
|
||
- 将 Redis 客户端注入 `g` 对象
|
||
- 后续业务代码通过 `get_redis()` 获取当前请求的 Redis 客户端
|
||
|
||
### 6. 数据库会话中间件
|
||
|
||
```python
|
||
# src/middleware/transaction.py
|
||
@app.before_request
|
||
def _transaction_begin():
|
||
"""请求开始前:标记事务活跃"""
|
||
g.transaction_active = True
|
||
|
||
@app.after_request
|
||
def _transaction_commit(response):
|
||
"""请求结束后:根据响应状态决定提交或回滚"""
|
||
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
|
||
if response.status_code == 200:
|
||
body = response.get_json()
|
||
if body and body.get('code') == 0:
|
||
db.session.commit()
|
||
else:
|
||
db.session.rollback()
|
||
return response
|
||
|
||
@app.teardown_request
|
||
def _transaction_rollback(exception):
|
||
"""请求异常时:回滚事务"""
|
||
if exception:
|
||
db.session.rollback()
|
||
db.session.remove()
|
||
```
|
||
|
||
- 为每个请求管理数据库会话
|
||
- 写操作根据响应体 `code` 字段自动 commit/rollback
|
||
- 异常时自动 rollback,finally 中关闭会话
|
||
|
||
### 7. 操作日志中间件
|
||
|
||
```python
|
||
# src/middleware/operation_log.py
|
||
@app.after_request
|
||
def operation_log(response):
|
||
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
|
||
# 记录操作日志到 flask_operation_log 表
|
||
# 包括:操作人、操作IP、请求路径、请求参数、响应状态、耗时等
|
||
...
|
||
return response
|
||
```
|
||
|
||
- 在 DB 会话外层,记录写操作日志
|
||
- 包含请求方法、URL、参数、响应码、耗时、操作人等信息
|
||
|
||
### 8. 请求计时中间件
|
||
|
||
```python
|
||
# src/app.py
|
||
@app.before_request
|
||
def start_timer():
|
||
g.start_time = time.time()
|
||
|
||
@app.after_request
|
||
def add_process_time_header(response):
|
||
if hasattr(g, 'start_time'):
|
||
process_time = time.time() - g.start_time
|
||
response.headers["X-Process-Time"] = str(round(process_time, 5))
|
||
return response
|
||
```
|
||
|
||
- 记录请求处理耗时
|
||
- 通过 `X-Process-Time` 响应头返回
|
||
|
||
## 第三阶段:路由与认证
|
||
|
||
### 9. 路由匹配
|
||
|
||
```python
|
||
# src/routes/v1.py
|
||
def register_v1(app):
|
||
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
|
||
for blue in BLUEPRINTS:
|
||
v1.register_blueprint(blue)
|
||
app.register_blueprint(v1)
|
||
```
|
||
|
||
- Flask 根据 HTTP 方法 + URL 路径匹配到 `POST /api/v1/position/add` 端点
|
||
- 该端点挂载在 `v1` 蓝图上
|
||
- `v1` 蓝图在 `src/routes/v1.py` 中集中注册所有模块路由
|
||
|
||
### 10. JWT 认证(login_required)
|
||
|
||
```python
|
||
# src/middleware/auth.py
|
||
@app.before_request
|
||
def login_required():
|
||
# 白名单路径跳过认证
|
||
if request.path in LOGIN_WHITELIST:
|
||
return
|
||
|
||
# 1. 从 Authorization 头提取 Bearer Token
|
||
# 2. 验证 Token 签名、有效期
|
||
# 3. 检查 Token 是否在 Redis 黑名单中
|
||
# 4. 解析出 user_id、username、realname
|
||
# 5. 注入到 g 对象
|
||
g.user_id = user_id
|
||
g.username = username
|
||
g.realname = realname
|
||
```
|
||
|
||
- 认证失败返回 `{"code": 401, "msg": "..."}` 响应
|
||
- 认证通过后,用户信息注入 `g` 对象供后续使用
|
||
|
||
### 11. RBAC 鉴权(check_permission)
|
||
|
||
```python
|
||
# src/modules/system/position/router.py
|
||
@position_blue.route('/add', methods=['POST'])
|
||
@check_permission('sys:position:add')
|
||
def add():
|
||
from src.modules.system.position.service import add_position
|
||
from src.modules.system.position.schema import PositionForm
|
||
data = parse_request_body(request, PositionForm)
|
||
return add_position(data)
|
||
```
|
||
|
||
```python
|
||
# src/core/permission.py
|
||
def check_permission(permission: str):
|
||
def decorator(func):
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
user_id = get_user_id()
|
||
if user_id == 1:
|
||
return func(*args, **kwargs)
|
||
from src.modules.system.menu.service import get_permissions_list
|
||
permission_list = get_permissions_list(user_id)
|
||
if permission not in permission_list:
|
||
return R.failed("权限不足")
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
return decorator
|
||
```
|
||
|
||
- 权限字符串格式:`sys:{module}:{action}`(如 `sys:position:add`)
|
||
- 用户 ID=1(超级管理员)跳过所有权限检查
|
||
- 使用懒加载 `from src.modules.system.menu.service import get_permissions_list` 避免 core → 业务模块顶层循环依赖
|
||
|
||
## 第四阶段:参数校验
|
||
|
||
### 12. WTForms 请求校验
|
||
|
||
```python
|
||
# src/modules/system/position/schema.py
|
||
class PositionForm(BaseForm):
|
||
name = StringField('岗位名称', validators=[
|
||
DataRequired(message='岗位名称不能为空'),
|
||
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
|
||
])
|
||
status = IntegerField('岗位状态', validators=[
|
||
DataRequired(message='岗位状态不能为空'),
|
||
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
||
])
|
||
sort = IntegerField('岗位排序', validators=[
|
||
DataRequired(message='岗位排序不能为空'),
|
||
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
|
||
])
|
||
```
|
||
|
||
- `parse_request_body` 函数解析 JSON 请求体
|
||
- WTForms 按 `validators` 约束校验每个字段
|
||
- 校验失败返回:
|
||
|
||
```json
|
||
{"code": 1, "data": null, "msg": "岗位名称不能为空"}
|
||
```
|
||
|
||
- 校验通过后,`data` 参数为已验证的字典
|
||
|
||
## 第五阶段:业务处理
|
||
|
||
### 13. Router(HTTP 层)
|
||
|
||
```python
|
||
# src/modules/system/position/router.py
|
||
def add():
|
||
from src.modules.system.position.service import add_position
|
||
from src.modules.system.position.schema import PositionForm
|
||
data = parse_request_body(request, PositionForm)
|
||
return add_position(data)
|
||
```
|
||
|
||
- HTTP 层仅做请求转发,不含业务逻辑
|
||
- 直接调用 Service 层函数
|
||
|
||
### 14. Service(业务逻辑层)
|
||
|
||
```python
|
||
# src/modules/system/position/service.py
|
||
class PositionService(BaseService):
|
||
repository = PositionRepository
|
||
form = PositionForm
|
||
status_form = PositionStatusForm
|
||
model_name = "岗位"
|
||
|
||
@classmethod
|
||
def before_delete(cls, ids):
|
||
"""删除前校验"""
|
||
for pid in ids:
|
||
if UserRepository.count_by({'position_id': pid}) > 0:
|
||
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
|
||
|
||
# 模块级代理函数
|
||
add_position = PositionService.add
|
||
```
|
||
|
||
### 15. Repository(数据访问层)
|
||
|
||
```python
|
||
# src/common/base_repository.py
|
||
class BaseRepository:
|
||
def create(self, **fields):
|
||
# 清洗数据:camelCase → snake_case,过滤非模型字段
|
||
obj = cls.model(**cls._clean_form_data(fields))
|
||
obj.save() # add + flush(不 commit)
|
||
return obj
|
||
```
|
||
|
||
### 16. Model(模型层)
|
||
|
||
```python
|
||
# src/common/base_db.py
|
||
class BaseDB:
|
||
def save(self):
|
||
db.session.add(self)
|
||
db.session.flush() # 发送 SQL 到数据库,获取自增 ID
|
||
return self
|
||
```
|
||
|
||
### 17. 数据库执行
|
||
|
||
- SQLAlchemy 将 ORM 操作转换为 SQL:`INSERT INTO flask_position (name, status, sort, create_user, ...) VALUES (...)`
|
||
- `flush()` 将 SQL 发送到数据库,获取自增主键
|
||
- 事务由中间件在响应返回后统一 commit
|
||
|
||
## 第六阶段:响应返回
|
||
|
||
### 18. 响应序列化
|
||
|
||
```python
|
||
# Service 返回 R.ok(msg='添加成功')
|
||
# src/core/response.py
|
||
R.ok(msg='添加成功')
|
||
# → jsonify({"code": 0, "data": None, "msg": "添加成功", "ok": True})
|
||
```
|
||
|
||
### 19. 中间件反向链
|
||
|
||
响应沿中间件链反向返回:
|
||
|
||
1. **请求计时**:附加 `X-Process-Time` 响应头
|
||
2. **操作日志**:记录写操作日志到数据库
|
||
3. **DB 会话**:检测 `code==0`,执行 `db.session.commit()`
|
||
4. **Redis 上下文**:无操作(g 对象自动清理)
|
||
5. **限流**:无操作(响应直接透传)
|
||
6. **CORS**:附加 `Access-Control-Allow-*` 响应头
|
||
|
||
### 20. 浏览器接收响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": null,
|
||
"msg": "添加成功",
|
||
"ok": true
|
||
}
|
||
```
|
||
|
||
前端 Axios 拦截器统一处理响应,`code===0` 时展示成功提示,否则展示错误信息。
|
||
|
||
## 完整链路流程图
|
||
|
||
```
|
||
请求链路(自上而下)
|
||
|
||
[浏览器]
|
||
│ POST /api/v1/position/add
|
||
▼
|
||
[Nginx] ── 反向代理转发
|
||
│
|
||
▼
|
||
[CORS 中间件] ── 跨域校验通过
|
||
│
|
||
▼
|
||
[限流中间件] ── 滑动窗口计数(Redis)
|
||
│ ├─ 超限 → 直接返回失败
|
||
│ └─ 通过 ▼
|
||
▼
|
||
[DB 会话中间件] ── 标记事务活跃
|
||
│
|
||
▼
|
||
[操作日志中间件] ── 记录写操作(不影响主流程)
|
||
│
|
||
▼
|
||
[路由层] ── 匹配 /api/v1/position/add
|
||
│
|
||
▼
|
||
[认证中间件 login_required] ── JWT 验证 + 黑名单检查(Redis)
|
||
│ ├─ 失败 → 返回 401
|
||
│ └─ 通过 → 解析 user_id/username 注入 g 对象 ▼
|
||
▼
|
||
[Router 层]
|
||
├─ @check_permission ── 权限校验(Redis 缓存)
|
||
├─ parse_request_body ── WTForms 参数校验
|
||
└─ 调用 Service ▼
|
||
▼
|
||
[Service 层]
|
||
├─ _validate_form ── WTForms 表单校验
|
||
├─ before_add ── 前置扩展钩子
|
||
├─ _clean_form_data ── 过滤无效字段
|
||
└─ 调用 Repository.create ▼
|
||
▼
|
||
[Repository 层]
|
||
└─ model(**kwargs).save() ── add + flush(获取自增 ID,不提交)▼
|
||
▼
|
||
[数据库] ── 执行 INSERT,返回自增 ID
|
||
│
|
||
▼ 逐层返回响应
|
||
[Repository] → [Service] → [Router] → [OpLog] → [DBSession]
|
||
│
|
||
├─ 业务成功(code=0)→ commit 提交事务
|
||
└─ 业务失败(code≠0)→ rollback 回滚事务
|
||
│
|
||
▼
|
||
[限流] → [CORS] → [Nginx]
|
||
│
|
||
▼
|
||
[浏览器] ← JSON 响应({ code, data, msg, ok })
|
||
```
|
||
|
||
## 异常处理链路
|
||
|
||
当请求链路中发生异常时,全局异常处理器统一捕获并返回标准响应:
|
||
|
||
| 异常类型 | 触发场景 | 响应 |
|
||
|:---|:---|:---|
|
||
| `BizError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
|
||
| `ValidationError` | WTForms 校验失败 | `{"code": 1, "msg": "field: message"}` |
|
||
| `Exception` | 未捕获异常 | `{"code": 1, "msg": "服务器内部错误"}` |
|
||
|
||
:::warning 异常处理原则
|
||
所有异常均返回 HTTP 200 状态码,通过 `code` 字段区分业务成功(0)与失败(1)。这是前后端约定,前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
|
||
:::
|
||
|
||
## 总结
|
||
|
||
一个 API 请求从浏览器到数据库经过 7 个阶段、20+ 个处理节点。中间件链负责横切关注点(安全、限流、日志、事务),路由层负责认证鉴权,WTForms 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。
|