Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/architecture/request-flow.md
T
2026-10-08 17:50:29 +08:00

587 lines
21 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 → Proxy 中间件(真实 IP)→ CORS → 强制 JSON → auth.jwt(JWT 认证 + RBAC 鉴权)→ tenant(租户上下文)→ log.operation(操作日志采集)→ FormRequest 校验 → 控制器 → 服务层 → 模型层 → 数据库 → 响应序列化 → 浏览器 →(响应后)操作日志落库
:::
本节以 `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 反向代理(生产环境 / Docker)
```nginx
# docker/nginx.conf(本项目实际配置)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
```
开发环境下,Vite 开发服务器将 `/api` 请求代理到后端:
```typescript
// ui/vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
}
```
## 第二阶段:路由解析
### 3. Laravel 入口
```text
public/index.php → bootstrap/app.php(中间件别名/分组、健康检查)→ 路由解析 routes/api.php
```
### 4. 路由匹配
```php
// routes/api.php
// 公开接口(无需认证)——放在中间件组外
Route::post('login', [Api\AuthController::class, 'login']);
// 认证接口
Route::middleware(['auth.jwt', 'tenant', 'demo', 'log.operation'])->group(function () {
// --- 案例演示 ---
Route::post('example/add', [Api\ExampleController::class, 'add']); // ← 匹配到此处
});
```
同时,`bootstrap/app.php` 的全局 `api` 中间件组已 prepend 三个前置中间件:
```php
// bootstrap/app.php
$middleware->alias([
'proxy' => ProxyMiddleware::class,
'cors' => Cors::class,
'force.json' => ForceJsonResponse::class,
'auth.jwt' => Authenticate::class,
'tenant' => ResolveTenant::class,
'demo' => DemoMiddleware::class,
'log.operation' => LogOperation::class,
]);
// API 中间件组(按顺序:Proxy → Cors → ForceJson)
$middleware->api(prepend: [
ProxyMiddleware::class,
Cors::class,
ForceJsonResponse::class,
]);
```
完整链路:`ProxyMiddleware → Cors → ForceJsonResponse → auth.jwt → tenant → log.operation → 控制器`。
## 第三阶段:中间件链(按顺序执行)
### 5. ProxyMiddleware(全局,真实 IP)
```php
// app/Http/Middleware/ProxyMiddleware.php(节选)
public function handle(Request $request, Closure $next): Response
{
$remoteAddr = $request->server('REMOTE_ADDR', '');
$proxyIps = $this->getTrustedProxies();
// 当前连接来源不在受信列表中,跳过(防止伪造代理头)
if (! $this->isTrustedProxy($remoteAddr, $proxyIps)) {
return $next($request);
}
// 按优先级提取真实 IP(X-Real-IP > X-Forwarded-For 第一个合法值)
$realIp = $this->extractRealIp($request);
if (! empty($realIp) && $realIp !== $remoteAddr) {
// 通过 server() 方法替换 REMOTE_ADDR,后续 ip() 即返回真实 IP
$server = $request->server->all();
$server['REMOTE_ADDR'] = $realIp;
$request->server->replace($server);
}
return $next($request);
}
```
- 部署在 CDN / 负载均衡后时保证 `request()->ip()` 正确(日志、限流依赖)
- 受信代理由 `.env` 的 `TRUSTED_PROXIES` 配置,未配置默认信任 `127.0.0.1` 与 `::1`
### 6. Cors(全局,跨域处理)
```php
// app/Http/Middleware/Cors.php(节选)
public function handle(Request $request, Closure $next): Response
{
// 预检请求直接返回,不进入业务逻辑(Authenticate 等中间件不会执行)
if ($request->isMethod('OPTIONS')) {
$response = response('', 204);
} else {
$response = $next($request);
}
$config = config('cors');
$origin = $request->header('Origin', '');
$allowOrigins = $config['allow_origin'] ?? ['*'];
$allowCredentials = $config['allow_credentials'] ?? true;
$originAllowed = false;
if (in_array('*', $allowOrigins)) {
if ($allowCredentials && $origin) {
// 凭证模式下不能用 *,回显具体 Origin
$response->headers->set('Access-Control-Allow-Origin', $origin);
$response->headers->set('Vary', 'Origin'); // 防止 CDN 缓存错误的 CORS 响应
} else {
$response->headers->set('Access-Control-Allow-Origin', '*');
}
$originAllowed = true;
} elseif ($origin && in_array($origin, $allowOrigins)) {
// 白名单匹配:仅回显请求的 Origin
$response->headers->set('Access-Control-Allow-Origin', $origin);
$response->headers->set('Vary', 'Origin');
$originAllowed = true;
}
// 仅在 Origin 允许时设置其余 CORS 头
if ($originAllowed) {
$response->headers->set('Access-Control-Allow-Methods', $config['allow_methods'] ?? 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', $config['allow_headers'] ?? 'Content-Type, Authorization');
$response->headers->set('Access-Control-Expose-Headers', $config['expose_headers'] ?? 'Authorization');
$response->headers->set('Access-Control-Max-Age', $config['max_age'] ?? 3600);
if ($allowCredentials) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
}
return $response;
}
```
### 7. ForceJsonResponse(全局,强制 JSON)
```php
// app/Http/Middleware/ForceJsonResponse.php
public function handle(Request $request, Closure $next): Response
{
// 强制声明期望 JSON,确保异常与验证失败均以 JSON 返回
$request->headers->set('Accept', 'application/json');
return $next($request);
}
```
使后续 `Handler::render()` 的 `$request->wantsJson()` 恒为 true,框架错误页不会以 HTML 形式返回。
### 8. Authenticate(auth.jwt,JWT 认证 + 权限校验)
```php
// app/Http/Middleware/Authenticate.php(节选)
public function handle(Request $request, Closure $next): Response
{
// 获取 token:带 Bearer 前缀则剥离,裸 token 直接使用
$header = $request->header('Authorization', '');
if ($header === '') {
return response()->json([
'code' => 401, 'ok' => false,
'msg' => '未提供认证令牌', 'data' => null,
]);
}
$prefix = config('jwt.header_prefix', 'Bearer ');
$token = str_starts_with($header, $prefix) ? substr($header, strlen($prefix)) : $header;
// 解析 token
$jwtService = app(JwtService::class);
try {
$decoded = $jwtService->parseToken($token);
} catch (\Exception $e) {
// 令牌解析失败:body code=401,消息取自异常
return response()->json([
'code' => 401, 'ok' => false,
'msg' => $e->getMessage(), 'data' => null,
]);
}
// 注入用户信息,供控制器通过 $request->attributes->get('userInfo') 读取
$request->attributes->set('userInfo', $decoded);
// 权限校验:仅当方法标注了 #[Permission] 时才校验
$permissionCode = $this->getPermissionCode($request);
if ($permissionCode !== null) {
$uid = $decoded->uid ?? 0;
if (! $this->checkPermission($uid, $permissionCode)) {
return response()->json([
'code' => 403, 'ok' => false,
'msg' => '无访问权限:'.$permissionCode, 'data' => null,
], 403);
}
}
return $next($request);
}
```
处理流程:
```text
请求进入
│
├─ 提取 Authorization 头(带 Bearer 前缀则剥离,裸 token 亦可)
│ └─ 头为空 → 401「未提供认证令牌」
│
├─ JwtService::parseToken($token) 解析验证
│ └─ 失败(过期/签名错误)→ 401(消息取自异常)
│
├─ 注入 $request->attributes['userInfo'] = {uid, username, ...}
│
├─ 反射读取控制器方法的 #[Permission] 注解(ReflectionMethod,微秒级)
│ ├─ 无注解 → 跳过权限校验,仅完成认证
│ └─ 有注解 → checkPermission(uid, code)
│ ├─ uid === 1 → 直接放行(超级管理员)
│ ├─ Redis 读 perms:{uid}
│ │ ├─ 命中 *:*:* 或含目标编码 → 通过
│ │ └─ Redis 不可用 → 降级
│ ├─ Laravel Cache 读 perms_fallback:{uid}(300 秒)
│ └─ 未命中 → 查库 user_role → role_menu → menu.permission
│ ├─ 包含 → 放行(回填二级缓存)
│ └─ 不包含 → 403「无访问权限:sys:example:add」
│
└─ 放行 → 进入下一个中间件
```
:::tip 三级权限缓存
Redis(`perms:{uid}`,一级)→ Laravel Cache(`perms_fallback:{uid}` 5 分钟,二级)→ 数据库(回填)。菜单/角色菜单变更时 Observer 清除相关缓存,保证权限调整即时生效。
:::
### 9. ResolveTenant(tenant,租户上下文)
```php
// app/Http/Middleware/ResolveTenant.php(节选)
public function handle(Request $request, Closure $next): Response
{
$userInfo = $request->attributes->get('userInfo');
if (! $userInfo) {
// 未认证请求:不注入租户上下文,直接放行
return $next($request);
}
$uid = $userInfo->uid ?? 0;
// 超级管理员(uid=1):跳过租户校验,直接注入 tenantId=0
if ($uid === 1) {
$request->attributes->set('tenantId', 0);
$request->attributes->set('isSuperAdmin', true);
return $next($request);
}
// 非超管:查库取 tenant_id(JWT 不含 tenant_id,需查库)
$user = User::find($uid);
if (! $user) {
return response()->json([
'code' => 401, 'ok' => false,
'msg' => '用户不存在,请重新登录', 'data' => null,
]);
}
$tenantId = $user->tenant_id ?? 0;
$request->attributes->set('tenantId', $tenantId);
$request->attributes->set('isSuperAdmin', false);
// 平台用户(tenantId=0):跳过租户校验
if ($tenantId > 0) {
$tenant = Tenant::find($tenantId);
// 租户不存在 / 被禁用(status != 1)/ 已过期(expire_time->isPast())
// → 均返回 403 对应提示
$request->attributes->set('tenantInfo', $tenant);
}
return $next($request);
}
```
注入内容:
- `tenantId` — 租户 ID(0=平台用户)
- `isSuperAdmin` — 是否超级管理员(uid=1)
- `tenantInfo` — 租户模型实例(`tenantId > 0` 时)
### 10. LogOperation(log.operation,操作日志采集)
`handle` 阶段**只收集不写入**,`terminate` 阶段在响应完成后落库:
```php
// app/Http/Middleware/LogOperation.php(节选)
public function handle(Request $request, Closure $next): Response
{
$this->logContext = [
'source' => 0, // 来源端:0=后台,1=前台
'method' => strtoupper($request->method()), // HTTP 方法
'request_method' => $request->route()?->getActionMethod() ?? '', // 控制器方法名
'url' => $request->fullUrl(), // 完整请求地址
'param' => mb_substr(json_encode($request->except(['password', 'token']), JSON_UNESCAPED_UNICODE), 0, 2000), // 排除敏感字段,截断 2000 字
'ip' => $request->ip(),
];
$this->logAttr = $this->getLogAttribute($request); // 反射读取 #[OperationLog]
$this->shouldLog = $this->determineIfShouldLog($request->method(), $this->logAttr);
// 预创建请求信息服务(handle 阶段 request 信息完整,terminate 阶段可能已丢失)
if ($this->shouldLog) {
$this->requestInfo = RequestInfoService::create($request);
}
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
if (! $this->shouldLog) {
return;
}
try {
// 补充响应内容、IP 归属地、操作系统、浏览器、耗时、状态
$this->logContext['result'] = mb_substr($response->getContent() ?? '', 0, 2000);
$this->logContext['location'] = $this->requestInfo->getIpLocation();
$this->logContext['os'] = $this->requestInfo->getOs();
$this->logContext['browser'] = $this->requestInfo->getBrowser();
$this->logContext['consume_time'] = defined('LARAVEL_START')
? round((microtime(true) - LARAVEL_START) * 1000) : 0;
$this->logContext['status'] = $this->detectStatus($response->getContent()); // code=0/200 → 0,其余 → 1
$this->logContext['create_user'] = $request->attributes->get('userInfo')->username ?? '';
OperationLog::create($this->logContext);
} catch (\Exception $e) {
// 日志写入失败不影响业务
}
}
```
记录规则(`determineIfShouldLog`):
- `POST/PUT/DELETE/PATCH` 一律记录
- `GET` 仅在注解类型为 `EXPORT / DOWNLOAD / IMPORT` 时记录
- 其它方法(OPTIONS/HEAD)不记录
:::tip DemoMiddleware 说明
演示模式中间件(`demo` 别名)已挂载于认证路由组(`auth.jwt` → `tenant` → `demo` → `log.operation`):非演示模式(`DEMO_MODE=false`)直接放行;演示模式下 GET 放行、写操作需 `#[DemoAllow]` 注解,否则 403。拦截发生在 `LogOperation` 之前,被拦截请求不产生操作日志。
:::
## 第四阶段:控制器层
### 11. FormRequest 参数校验(进入方法前)
```php
// 控制器方法签名声明 ExampleRequest,框架在校验通过后才调用方法
public function add(ExampleRequest $request): JsonResponse
```
- `rules()` 未通过 → `BaseRequest::failedValidation()` 直接返回
`{"code":1,"ok":false,"msg":"参数验证失败","data":{"name":["案例名称不能为空"]}}`
- 校验通过 → 控制器方法执行,`$request->validated()` 取已校验数据(键名转换在 Service 写入入口完成)
### 12. ExampleController::add()
```php
// app/Http/Controllers/Api/ExampleController.php
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
#[Permission('sys:example:add', '添加示例')]
public function add(ExampleRequest $request): JsonResponse
{
return $this->_add($this->exampleService, $request->validated());
}
```
控制器层职责:注解声明权限与日志 → 参数由 FormRequest 保证 → 调用快捷方法 → 返回统一响应。
## 第五阶段:服务层
### 13. ExampleService::add()(HasCrudOperations 主流程)
```text
$this->_add($service, $data)
│
▼
Service::add($data) ← HasCrudOperations
├─ beforeAdd($data) 钩子 ← 可改写数据、抛 BizException 拦截
├─ checkUnique($data) ← uniqueFields 唯一性校验,重复抛 BizException
├─ DB::transaction { ← 事务包裹(任一异常自动回滚)
│ Model::create($data) ← Eloquent 写入
│ }
├─ afterAdd($id, $data) 钩子 ← 关联处理(抛异常同样回滚)
└─ return $id
```
:::tip 异常即控制流
`BizException` 在钩子/唯一性校验中抛出 → 中断事务闭包回滚 → 由 `_add()` 捕获转为 `fail(消息, code)`,或由全局 `Handler` 兜底——任何路径前端拿到的结构一致。
:::
## 第六阶段:模型层
### 14. HasAuditColumns 自动填充 + SQL 执行
```php
// app/Models/Concerns/HasAuditColumns.php(节选)
static::creating(function ($model) {
$model->create_user = $model->create_user ?? Auth::username();
$model->create_time = $model->create_time ?? now();
});
```
Eloquent 将操作转换为 SQL:
```sql
INSERT INTO laravel_example (name, status, sort, create_user, create_time)
VALUES ('测试案例', 1, 0, 'admin', '2026-10-02 12:00:00');
```
返回自增 ID(MySQL `lastInsertId`;Oracle 走序列 CURRVAL,见 `OracleProcessor`)。
## 第七阶段:响应返回
### 15. 响应序列化
```php
// Controller::_add() 内部
$id = $service->add($data);
return $this->success(['id' => $id], '添加成功');
// Support\Result::success()(节选)
return response()->json([
'code' => self::SUCCESS, // 0
'ok' => true,
'msg' => $msg,
'data' => convert_to_camel($data), // snake_case → camelCase
]);
```
### 16. 浏览器接收响应 + 日志落库
```json
{
"code": 0,
"ok": true,
"msg": "添加成功",
"data": { "id": 42 }
}
```
响应发送后,Laravel 调用 `LogOperation::terminate()` 将本次操作写入 `laravel_operation_log`(耗时、状态、归属地等)。
前端 Axios 拦截器统一处理:`code === 0` 时展示成功提示,否则展示错误信息;401 触发登出跳转。
## 完整链路流程图
```text
[浏览器]
│ POST /api/example/add
│ Authorization: Bearer <token>
▼
[Nginx / Vite Proxy] ── 反向代理转发
▼
[public/index.php] ── bootstrap/app.php 引导
▼
[路由解析] ── routes/api.php 匹配 api/example/add(挂 auth.jwt + tenant + demo + log.operation)
▼
[ProxyMiddleware] ── 受信代理校验,替换真实 REMOTE_ADDR(全局)
▼
[Cors] ── OPTIONS 直接 204;响应追加 CORS 头(全局)
▼
[ForceJsonResponse] ── Accept 强制 application/json(全局)
▼
[Authenticate]
├─ 提取 Authorization token(头为空 → 401)
├─ JwtService::parseToken()(过期/签名错 → 401)
├─ 注入 attributes['userInfo']
├─ 反射读取 #[Permission] 注解
└─ checkPermission():uid=1 跳过 → Redis perms:{uid} → Cache 降级 → 查库
└─ 不通过 → 403「无访问权限:sys:example:add」
▼
[ResolveTenant]
├─ 无 userInfo → 放行
├─ uid=1 → tenantId=0, isSuperAdmin=true
└─ 非超管 → 查库 tenant_id → 状态/过期校验(禁用/过期/不存在 → 403)
▼
[DemoMiddleware]
├─ 非演示模式(DEMO_MODE=false)→ 放行
├─ GET 请求 → 放行
└─ 写操作:有 #[DemoAllow] 放行,否则 403「演示模式下禁止操作」
▼
[LogOperation::handle] ── 收集上下文、读 #[OperationLog]、判断是否记录
▼
[ExampleRequest] ── rules() 校验(失败 → code=1 + 字段明细)
▼
[ExampleController::add()]
└─ $this->_add($service, $request->validated())
▼
[ExampleService::add()]
├─ beforeAdd() 钩子
├─ checkUnique()(重复 → BizException → 回滚 → code=1)
├─ DB::transaction { Model::create() }
│ └─ HasAuditColumns:create_user / create_time 自动填充
└─ afterAdd() 钩子
▼
[数据库] ── INSERT INTO laravel_example ... → 自增 ID
▼ 逐层返回
[Result::success] ── {code:0, ok:true, msg:"添加成功", data:{id:42}}(驼峰转换)
▼
[浏览器] ← JSON 响应
▼(响应完成后)
[LogOperation::terminate] ── 写入操作日志(失败静默,不影响业务)
```
## 异常处理链路
当链路中发生异常时,`Handler` 统一捕获并返回标准响应:
| 异常类型 | 触发场景 | 响应 |
|---------|---------|------|
| `HttpResponseException`(FormRequest) | 表单格式校验失败 | `{code:1, msg:"参数验证失败", data:{字段=>错误数组}}` |
| `BizException` | 业务规则失败 | `{code:1(或自定义), msg:"业务提示"}` |
| `ValidationException` | 代码内手动校验失败 | `{code:422, msg:"参数验证失败", data:{错误明细}}` |
| `AuthenticationException` | 未登录 / 令牌失效 | `{code:401, msg:"未授权,请先登录"}` |
| `NotFoundHttpException` | 路由不存在 | `{code:404, msg:"数据不存在"}` |
| 其它异常 | 未捕获异常 | `{code:500, msg:调试模式为原文, 生产为"操作失败"}` |
:::warning 异常处理原则
除 403(中间件直接返回时同时设置 HTTP 状态码)外,所有响应 HTTP 状态码为 200,通过响应体 `code` 字段区分业务成功(0)与失败。前端统一按 `code` 判断,`code=401` 时跳转登录,无需处理 HTTP 状态码差异。
:::
## 总结
一个 API 请求从浏览器到数据库经过 7 个阶段、16+ 个处理节点。中间件链负责横切关注点(真实 IP、跨域、JSON 协商、认证、权限、租户、日志),FormRequest 负责参数格式,控制器层负责编排与响应封装,服务层负责业务处理与事务,模型层负责数据持久化。这种分层设计使得每一层职责单一、可独立测试。