Files
djangoadmin-wiki/DjangoAdmin_Tornado_EleVue/zh/volume3/request-flow.md
T
2026-09-16 10:11:44 +08:00

329 lines
12 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 → Tornado → Handler 生命周期(set_default_headers → prepare → handler_method → on_finish)→ 响应返回浏览器
:::
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
## Tornado 请求处理模型
Tornado 通过 **Handler 生命周期方法** 和 **装饰器** 实现横切关注点:
```
┌─────────────────────────────────────────────────────────────────┐
│ Tornado Handler 生命周期 │
│ │
│ ① set_default_headers() → 设置 CORS 头 + trace_id │
│ ② prepare() → @login_required(JWT 认证) │
│ ③ handler method → @permission_required + @check_demo │
│ + @operation_log + 业务逻辑 │
│ ④ on_finish() → db.close()(释放数据库会话) │
│ │
│ 异常时:write_error() → 统一异常响应 + 日志记录 │
└─────────────────────────────────────────────────────────────────┘
```
## 第一阶段:网络层
### 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:8041;
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 将请求代理到 Tornado 服务,同时附加客户端真实 IP 到 `X-Forwarded-For` 头。
## 第二阶段:Handler 生命周期
### 3. 路由匹配
Tornado 根据 HTTP 方法 + URL 路径匹配到对应的 Handler 类:
```python
# src/api/v1/router.py
def build_routes() -> list:
routes = []
for module in _RESOURCE_MODULES:
routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes])
routes.append((V1_PREFIX + "/health", HealthHandler))
return routes
```
路由表中 `POST /api/v1/position/add` 匹配到 `PositionAddHandler`。
### 4. set_default_headers() — 设置响应头
```python
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def set_default_headers(self):
super().set_default_headers()
cros_required(self) # CORS 跨域头
apply_trace_id(self) # 链路追踪 ID
```
- **CORS**:检查 `Origin` 头是否在白名单中,设置 `Access-Control-Allow-*` 响应头
- **trace_id**:生成唯一追踪 ID,写入响应头和上下文变量
### 5. prepare() — JWT 认证
```python
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
@login_required
def prepare(self):
pass
```
`login_required` 装饰器在 `prepare()` 阶段执行认证:
```python
# src/common/middleware/authentication.py
def login_required(func):
def wrapper(self, *args, **kwargs):
request_url = self.request.path
# 白名单(/login、/captcha)和 OPTIONS 预检直接放行
if request_url not in IGNORE_URL and self.request.method != "OPTIONS":
# 1. 从 Authorization 头提取 Bearer Token
success, access_token, _ = get_access_token(self.request)
if not success or not access_token:
return R.failed(self, code=401, msg="登录过期")
# 2. JWT 解密验证(签名 + 有效期)
result = parse_payload(access_token)
if result["code"] != 0:
return R.failed(self, code=401, msg="登录过期")
# 3. 黑名单校验(Redis,不可用时 fail-open 放行)
if is_token_blacklisted(access_token):
return R.failed(self, code=401, msg="登录过期")
# 4. 将用户信息写入上下文变量
data = result["data"]
current_user_id.set(int(data["userId"]))
current_username.set(data.get("username", ""))
current_realname.set(data.get("realname", ""))
return func(self, *args, **kwargs)
return wrapper
```
认证失败时直接返回 `{"code": 401, "msg": "登录过期"}`,不进入后续 Handler 方法。
### 6. Handler 方法 — 权限校验 + 业务处理
```python
# src/modules/position/handlers.py
class PositionAddHandler(BaseHandler):
@permission_required("sys:position:add") # ① RBAC 权限校验
@check_demo # ② 演示模式拦截
@operation_log("岗位管理", "添加") # ③ 操作日志记录
async def post(self):
return await position_service.add(self) # ④ 调用 Service 层
```
**装饰器执行顺序**(从外到内):
```
① permission_required → 校验用户是否拥有 sys:position:add 权限
├── user_id == 1(admin)→ 直接放行
└── 其他用户 → 查询权限列表(Redis 缓存)→ 匹配则放行,否则返回 403
② check_demo → TORNADO_DEMO=True 时返回 "演示环境,暂无操作权限"
③ operation_log → 记录操作日志(请求参数、操作人、IP、耗时等)
④ handler method → 调用 position_service.add(self)
```
### 7. Service 层 — 业务逻辑
```python
# src/core/base_service.py
class BaseService:
async def add(self, handler) -> R:
try:
# 1. 解析请求体 JSON + Pydantic 校验
form = self._validate(handler, self.create_schema)
data = form.model_dump()
# 2. 唯一性校验
err = self._check_unique(form)
if err:
return R.failed(handler, err)
# 3. 前置扩展钩子
self._before_add(handler, data)
# 4. 组装入库字段
fields = self._build_create_fields(data)
# 5. 调用 Repository 创建记录
self.repo.create(self.model(**fields))
# 6. 返回成功响应
return R.ok(handler, msg="添加成功")
finally:
self.repo.close()
```
### 8. Repository 层 — 数据访问
```python
# src/core/base_repository.py
class BaseRepository:
def create(self, obj: T) -> T:
# 自动写入创建人
if hasattr(obj, "create_user"):
obj.create_user = str(self._uid())
self.db.add(obj)
self.db.commit()
return obj
```
### 9. 响应返回
```python
# src/core/response.py
def ok(self, data=None, msg="操作成功", code=0, **kwargs):
body = {"code": code, "data": data, "msg": msg, "ok": True}
if kwargs:
body.update(kwargs)
self._api_result = body
self.finish(body) # Tornado 写出 JSON 响应
```
`R.ok()` / `R.failed()` 内部调用 `self.finish(body)`,Tornado 自动序列化为 JSON 并发送 HTTP 响应。
### 10. on_finish() — 释放资源
```python
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def on_finish(self):
try:
db.close() # 兜底关闭数据库会话
except Exception:
pass
```
请求结束后关闭数据库会话,避免长连接泄漏。Service 层的 `finally` 块已关闭时此处为幂等 no-op。
### 11. 异常处理
```python
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def write_error(self, status_code, **kwargs):
exc = kwargs.get("exc_info", (None, None, None))[1]
if isinstance(exc, BaseAppError):
# 业务异常:按类型分级记录日志
self.set_status(exc.status_code)
self.finish({"code": exc.code, "data": None, "msg": exc.msg, "ok": False})
else:
# 未预期异常:记录完整堆栈
self.set_status(500)
self.finish({"code": 500, "data": None, "msg": "服务器内部错误", "ok": False})
```
| 异常类型 | 触发场景 | 响应 |
|:---|:---|:---|
| `AuthError` | JWT 过期 / 无权限 | `{"code": 401, "msg": "登录过期"}` |
| `PermissionDeniedError` | 权限不足 | `{"code": 403, "msg": "权限不足"}` |
| `ValidationError` | Pydantic 校验失败 | `{"code": 1, "msg": "field: message"}` |
| `BusinessError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
| `NotFoundError` | 记录不存在 | `{"code": 404, "msg": "记录不存在"}` |
| `Exception` | 未捕获异常 | `{"code": 500, "msg": "服务器内部错误"}` |
## 完整链路流程图
```
[浏览器]
│ POST /api/v1/position/add
│ Authorization: Bearer <token>
▼
[Nginx] ── 反向代理转发
│
▼
[Tornado 路由匹配] ── 匹配 PositionAddHandler
│
▼
[set_default_headers()] ── 设置 CORS 头 + trace_id
│
▼
[prepare() + @login_required] ── JWT 认证
│ ├─ 白名单(/login、/captcha)→ 跳过
│ ├─ Token 无效/过期 → 返回 {code:401, msg:"登录过期"}
│ ├─ Token 在黑名单中 → 返回 {code:401, msg:"登录过期"}
│ └─ 认证通过 → current_user_id / current_username / current_realname 写入上下文
│
▼
[@permission_required("sys:position:add")] ── RBAC 权限校验
│ ├─ user_id == 1 → 直接放行
│ ├─ 权限不足 → 返回 {code:403, msg:"权限不足"}
│ └─ 权限通过 → 继续
│
▼
[@check_demo] ── 演示模式拦截
│ ├─ TORNADO_DEMO=True → 返回 {code:1, msg:"演示环境,暂无操作权限"}
│ └─ 非演示模式 → 继续
│
▼
[@operation_log("岗位管理", "添加")] ── 操作日志装饰器
│
▼
[async def post(self)] ── Handler 方法
│
▼
[position_service.add(self)] ── Service 层
├─ parse_json(handler) ── 解析请求体 JSON
├─ validate(schema, data) ── Pydantic 校验
├─ _check_unique(form) ── 唯一性校验(DB 查询)
├─ _before_add(handler, data) ── 前置扩展钩子
├─ _build_create_fields(data) ── 组装入库字段
└─ repo.create(model(**fields)) ── Repository 层
│
▼
[BaseRepository.create(obj)]
├─ obj.create_user = str(uid) ── 自动写入创建人
├─ db.add(obj) + db.commit() ── 提交事务
└─ return obj
│
▼
[R.ok(handler, msg="添加成功")] ── 统一响应
├─ self._api_result = body ── 保存结果供日志解析
└─ self.finish(body) ── 写出 JSON 响应
│
▼
[on_finish()] ── 释放资源
└─ db.close() ── 关闭数据库会话
│
▼
[浏览器] ← { "code": 0, "data": null, "msg": "添加成功", "ok": true }
```
## 总结
Tornado 的请求处理通过 Handler 生命周期方法(`set_default_headers` → `prepare` → `handler_method` → `on_finish`)和装饰器(`@permission_required`、`@check_demo`、`@operation_log`)实现横切关注点。认证在 `prepare()` 阶段完成,权限/演示模式/操作日志通过装饰器在 Handler 方法执行前校验,数据库会话在 `on_finish()` 中兜底关闭。这种设计使得每一层职责单一、可独立测试。