Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp6_EleVue/zh/architecture/request-flow.md
T
2026-09-29 09:49:04 +08:00

532 lines
15 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.
# 4.2 请求处理链路
:::tip 链路概览
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS 中间件 → Auth 中间件(JWT 认证 + RBAC 鉴权)→ Tenant 中间件 → Demo 中间件 → Log 中间件 → 控制器 → 逻辑层 → 模型层 → 数据库 → 响应序列化 → 浏览器
:::
本节以 `POST /api/example/add` 添加案例为例,逐步拆解完整链路。
## 第一阶段:网络层
### 1. 浏览器发起请求
```typescript
// 前端 Axios 调用(ui/src/api/tool/example.ts)
export function exampleAdd(data: any) {
return http.request({
url: '/example/add',
method: 'POST',
data, // { name: '测试案例', status: 1, sort: 0 }
});
}
```
前端通过 Axios 发起 HTTP POST 请求,携带:
- `Authorization: Bearer <JWT Token>` 请求头(由请求拦截器自动添加)
- `Content-Type: application/json` 请求头
- JSON 请求体
### 2. Nginx 反向代理(生产环境)
```nginx
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
开发环境下,Vite 开发服务器将 `/api` 请求代理到后端:
```typescript
// ui/vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
}
```
## 第二阶段:路由解析
### 3. ThinkPHP 入口
```text
public/index.php → 引导框架 → 路由解析 route/app.php
```
### 4. 路由匹配
```php
// route/app.php
// 公开接口(无需认证)
Route::group('api', function () {
Route::post('login', 'LoginController/login');
Route::get('captcha', 'LoginController/captcha');
});
// 需认证接口
Route::group('api', function () {
Route::group('example', function () {
Route::post('add', 'ExampleController/add'); // ← 匹配到此处
});
})->middleware([
\app\middleware\AuthMiddleware::class, // JWT 认证 + 权限校验
\app\middleware\TenantMiddleware::class, // 租户上下文
\app\middleware\DemoMiddleware::class, // 演示环境拦截
\app\middleware\LogMiddleware::class, // 操作日志
]);
```
路由匹配到 `POST api/example/add` → `ExampleController::add()`,同时绑定四个中间件。
## 第三阶段:中间件链(按顺序执行)
### 5. CorsMiddleware(全局中间件)
```php
// app/middleware/CorsMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 处理 OPTIONS 预检请求
if ($request->isOptions()) {
$response = Response::create('', 'html', 204);
} else {
$response = $next($request);
}
// 设置跨域响应头
$response->header([
'Access-Control-Allow-Origin' => $origin,
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, ...',
'Access-Control-Allow-Credentials' => 'true',
]);
return $response;
}
```
- 每个请求都经过(全局中间件)
- OPTIONS 预检请求直接返回 204
- 普通请求附加 CORS 响应头
### 6. AuthMiddleware(JWT 认证 + 权限校验)
```php
// app/middleware/AuthMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 1. 检查排除路由(login/captcha 等直接放行)
$path = $request->pathinfo();
foreach ($exceptList as $except) {
if (strpos($path, $except) !== false) {
return $next($request);
}
}
// 2. 从请求头获取 Token
$token = Jwt::getTokenFromHeader();
if (empty($token)) {
return json(['code' => 401, 'msg' => '未提供认证令牌']);
}
// 3. 解析 Token,校验签名和有效期
try {
$decoded = Jwt::decode($token);
} catch (\RuntimeException $e) {
return json(['code' => 401, 'msg' => $e->getMessage()]);
}
// 4. 校验 token 类型必须为 access
if (($decoded->type ?? '') !== 'access') {
return json(['code' => 401, 'msg' => '无效的认证令牌']);
}
// 5. 注入用户信息到 Request
$request->userInfo = $decoded;
// 6. 读取 #[Permission] 注解,校验权限
$permission = AttributeService::getPermission($controllerClass, $action);
if ($permission !== null) {
$hasPermission = $this->checkPermission($decoded->uid, $permission->code);
if (!$hasPermission) {
return json(['code' => 403, 'msg' => '无访问权限']);
}
}
return $next($request);
}
```
处理流程:
```text
请求进入
│
├─ 路径在排除列表? → 直接放行
│
├─ 提取 Authorization: Bearer <token>
│ └─ 为空 → 返回 401
│
├─ Jwt::decode($token) 解析验证
│ └─ 失败(过期/签名错误)→ 返回 401
│
├─ 校验 type === 'access'
│ └─ 不是 → 返回 401
│
├─ 注入 $request->userInfo = {uid, username, type, exp}
│
├─ 读取 #[Permission] 注解
│ ├─ 无注解 → 跳过权限校验
│ └─ 有注解 → checkPermission()
│ ├─ uid === 1 → 直接放行(超级管理员)
│ └─ 查询 UserRole → RoleMenu → Menu.permission
│ ├─ 包含 → 放行
│ └─ 不包含 → 返回 403
│
└─ 放行 → 进入下一个中间件
```
### 7. TenantMiddleware(租户上下文)
```php
// app/middleware/TenantMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
$userInfo = $request->userInfo ?? null;
$userId = $userInfo->uid ?? 0;
$user = User::find($userId);
$tenantId = $user->tenant_id ?? 0;
// 注入租户上下文
$request->tenantId = $tenantId;
$request->isSuperAdmin = ($tenantId == 0);
// 非超级管理员校验租户状态
if ($tenantId > 0) {
$tenant = Tenant::find($tenantId);
if (!$tenant || $tenant->status != 1) {
return json(['code' => 403, 'msg' => '租户已被禁用']);
}
if ($tenant->expire_time && strtotime($tenant->expire_time) < time()) {
return json(['code' => 403, 'msg' => '租户已过期']);
}
$request->tenantInfo = $tenant;
}
return $next($request);
}
```
注入内容:
- `$request->tenantId` — 租户 ID(0=超级管理员)
- `$request->isSuperAdmin` — 是否超级管理员
- `$request->tenantInfo` — 租户信息对象
### 8. DemoMiddleware(演示环境拦截)
```php
// app/middleware/DemoMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 非演示环境直接放行
if (!env('app_demo', false)) {
return $next($request);
}
// GET 请求放行(读操作)
if (strtoupper($request->method()) === 'GET') {
return $next($request);
}
// 检查 #[DemoAllow] 注解
if (AttributeService::hasDemoAllow($controllerClass, $action)) {
return $next($request);
}
// 拦截写操作
return json(['code' => 403, 'msg' => '演示环境,禁止操作']);
}
```
### 9. LogMiddleware(操作日志)
```php
// app/middleware/LogMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
$startTime = microtime(true);
// 执行后续中间件和控制器
$response = $next($request);
// 写操作始终记录
if (in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
// 读取 #[Log] 注解
$logAttr = AttributeService::getLog($controllerClass, $action);
// 组装日志数据
$logData = [
'title' => $logAttr?->title ?: $path,
'type' => $logAttr?->type ?: $this->guessType($method),
'method' => $method,
'url' => $request->url(),
'param' => json_encode($request->param()),
'ip' => $requestInfo->getIp(),
'location' => $requestInfo->getIpLocation(),
'os' => $requestInfo->getOs(),
'browser' => $requestInfo->getBrowser(),
'consume_time'=> round((microtime(true) - $startTime) * 1000),
'status' => $this->detectStatus($response->getContent()),
];
// 写入数据库(失败不影响主业务)
OperationLog::create($logData);
}
return $response;
}
```
## 第四阶段:控制器层
### 10. ExampleController::add()
```php
// app/controller/ExampleController.php
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
#[Permission('sys:example:add', '添加案例演示')]
public function add(): Json
{
// getJsonBody() 获取前端 POST 提交的 JSON 数据
// _add() 内部调用 $this->logic->add($data)
return parent::_add($this->logic, $this->getJsonBody());
}
```
控制器层职责:
- 通过 `#[Permission]` 声明所需权限
- 通过 `#[Log]` 声明日志信息
- 获取参数,调用 Logic 层,返回响应
## 第五阶段:逻辑层
### 11. ExampleLogic::add()
```php
// app/logic/ExampleLogic.php → 继承 BaseLogic
// BaseLogic::add() 的完整流程:
public function add(array $data): int
{
// 1. 钩子:新增前(可修改数据,抛异常可拦截)
$data = $this->beforeAdd($data);
// 2. 保留原始数据供 afterAdd 使用
$originalData = $data;
// 3. 入库前转换:camelCase → snake_case
$data = array_camel_to_snake($data);
// 4. 租户ID自动填充
$data = $this->processTenantId($data);
// 5. 文件字段:迁移临时文件,去掉域名
$data = $this->processFileFieldsOnSave($data);
// 6. 富文本字段:迁移临时文件
$data = $this->processContentFieldsOnSave($data);
// 7. 过滤非数据库字段
$saveData = $this->filterTableFields($data);
// 8. 唯一性校验
$this->checkUnique($saveData);
// 9. 写入数据库
$model = $this->getModel()->create($saveData);
$id = $model->id;
// 10. 钩子:新增后
$this->afterAdd($id, $originalData);
return $id;
}
```
## 第六阶段:模型层
### 12. BaseModel 自动处理
```php
// app/model/Example.php
class Example extends BaseModel
{
protected $name = 'example';
}
// BaseModel 提供的能力:
// 1. 全局查询范围 soft_delete → WHERE is_delete = 0
// 2. onBeforeInsert → 自动填充 create_user, create_time
// 3. onBeforeUpdate → 自动填充 update_user, update_time
```
### 13. 数据库执行
ThinkPHP ORM 将操作转换为 SQL:
```sql
INSERT INTO think_example (name, status, sort, avatar, create_user, create_time)
VALUES ('测试案例', 1, 0, '', 'admin', '2026-09-20 12:00:00');
```
## 第七阶段:响应返回
### 14. 响应序列化
```php
// BaseController::_add()
protected function _add(BaseLogic $logic, array $data): Json
{
$id = $logic->add($data);
return $this->success(['id' => $id], '添加成功');
}
// Result::success()
public static function success($data, string $msg = '操作成功'): Json
{
return json([
'code' => 0,
'ok' => true,
'msg' => $msg,
'data' => self::convertData($data), // snake_case → camelCase
]);
}
```
### 15. 浏览器接收响应
```json
{
"code": 0,
"ok": true,
"msg": "添加成功",
"data": {
"id": 42
}
}
```
前端 Axios 拦截器统一处理:`code === 0` 时展示成功提示,否则展示错误信息。
## 完整链路流程图
```text
[浏览器]
│ POST /api/example/add
│ Authorization: Bearer <token>
▼
[Nginx / Vite Proxy] ── 反向代理转发
│
▼
[ThinkPHP 入口] ── public/index.php
│
▼
[路由解析] ── route/app.php 匹配 api/example/add
│
▼
[CorsMiddleware] ── 跨域校验通过(全局)
│
▼
[AuthMiddleware]
├─ 提取 Token → Jwt::decode()
├─ 校验 type === 'access'
├─ 注入 $request->userInfo
├─ 读取 #[Permission] 注解
├─ checkPermission() 权限校验
│ ├─ uid=1 → 跳过(超级管理员)
│ └─ UserRole → RoleMenu → Menu.permission
└─ 放行
│
▼
[TenantMiddleware]
├─ 查询用户 tenant_id
├─ 注入 $request->tenantId
└─ 校验租户状态(禁用/过期 → 403)
│
▼
[DemoMiddleware]
├─ 非演示环境 → 放行
├─ GET 请求 → 放行
└─ 有 #[DemoAllow] → 放行
│
▼
[LogMiddleware] ── 记录开始时间
│
▼
[ExampleController::add()]
├─ #[Log] 注解元数据
├─ #[Permission] 注解元数据
└─ parent::_add($this->logic, $data)
│
▼
[ExampleLogic::add()]
├─ beforeAdd() 钩子
├─ array_camel_to_snake() 键名转换
├─ processTenantId() 租户填充
├─ processFileFieldsOnSave() 文件处理
├─ filterTableFields() 字段过滤
├─ checkUnique() 唯一性校验
├─ Model::create() 写入数据库
└─ afterAdd() 钩子
│
▼
[BaseModel]
├─ onBeforeInsert → create_user, create_time
└─ 执行 SQL INSERT
│
▼
[数据库] ── 返回自增 ID
│
▼ 逐层返回响应
[Result::success()] ── {code:0, ok:true, msg:"添加成功", data:{id:42}}
│
▼
[LogMiddleware] ── 记录操作日志(写操作)
│
▼
[浏览器] ← JSON 响应
```
## 异常处理链路
当链路中发生异常时,ExceptionHandle 统一捕获并返回标准响应:
| 异常类型 | 触发场景 | 响应 |
|---------|---------|------|
| `ValidateException` | 参数验证失败 | `{code:1, msg:"参数验证失败", data:{错误明细}}` |
| `ModelNotFoundException` | 数据不存在 | `{code:1, msg:"数据不存在"}` |
| `RuntimeException (401)` | JWT 过期/无效 | `{code:1, msg:"token已过期"}` |
| `HttpException` | HTTP 错误 | `{code:1, msg:"请求错误"}` |
| `Exception` | 未捕获异常 | `{code:1, msg:"操作失败"}`(生产环境隐藏详情) |
:::warning 异常处理原则
所有异常均返回 HTTP 200 状态码,通过响应体 `code` 字段区分业务成功(0)与失败(1/401/403/404/422)。前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
:::
## 总结
一个 API 请求从浏览器到数据库经过 7 个阶段、15+ 个处理节点。中间件链负责横切关注点(认证、权限、租户、日志),控制器层负责参数获取和响应返回,逻辑层负责业务处理和数据加工,模型层负责数据库操作。这种分层设计使得每一层职责单一、可独立测试。