签入版本
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
# 请求完整链路
|
||||
|
||||
本章以一个具体的 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()` 中兜底关闭。这种设计使得每一层职责单一、可独立测试。
|
||||
Reference in New Issue
Block a user