# 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 ` 请求头(由请求拦截器自动添加) - `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 │ └─ 为空 → 返回 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 ▼ [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+ 个处理节点。中间件链负责横切关注点(认证、权限、租户、日志),控制器层负责参数获取和响应返回,逻辑层负责业务处理和数据加工,模型层负责数据库操作。这种分层设计使得每一层职责单一、可独立测试。