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

21 KiB
Raw Blame History

4.2 请求处理链路

:::tip 链路概览 一个典型的 API 请求经过以下阶段:浏览器 → Nginx → Proxy 中间件(真实 IP)→ CORS → 强制 JSON → auth.jwt(JWT 认证 + RBAC 鉴权)→ tenant(租户上下文)→ log.operation(操作日志采集)→ FormRequest 校验 → 控制器 → 服务层 → 模型层 → 数据库 → 响应序列化 → 浏览器 →(响应后)操作日志落库 :::

本节以 POST /api/example/add 添加案例为例,逐步拆解完整链路(代码摘自项目实际源码)。

第一阶段:网络层

1. 浏览器发起请求

// 前端 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)

# docker/nginx.conf(本项目实际配置)
location / {
    try_files $uri $uri/ /index.php?$query_string;
}

开发环境下,Vite 开发服务器将 /api 请求代理到后端:

// ui/vite.config.ts
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8000',
      changeOrigin: true,
    },
  },
}

第二阶段:路由解析

3. Laravel 入口

public/index.php → bootstrap/app.php(中间件别名/分组、健康检查)→ 路由解析 routes/api.php

4. 路由匹配

// 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 三个前置中间件:

// 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)

// 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(全局,跨域处理)

// 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)

// 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 认证 + 权限校验)

// 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);
}

处理流程:

请求进入
    │
    ├─ 提取 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,租户上下文)

// 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 阶段在响应完成后落库:

// 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 参数校验(进入方法前)

// 控制器方法签名声明 ExampleRequest,框架在校验通过后才调用方法
public function add(ExampleRequest $request): JsonResponse
  • rules() 未通过 → BaseRequest::failedValidation() 直接返回 {"code":1,"ok":false,"msg":"参数验证失败","data":{"name":["案例名称不能为空"]}}
  • 校验通过 → 控制器方法执行,$request->validated() 取已校验数据(键名转换在 Service 写入入口完成)

12. ExampleController::add()

// 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 主流程)

$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 执行

// 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:

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. 响应序列化

// 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. 浏览器接收响应 + 日志落库

{
    "code": 0,
    "ok": true,
    "msg": "添加成功",
    "data": { "id": 42 }
}

响应发送后,Laravel 调用 LogOperation::terminate() 将本次操作写入 laravel_operation_log(耗时、状态、归属地等)。

前端 Axios 拦截器统一处理:code === 0 时展示成功提示,否则展示错误信息;401 触发登出跳转。

完整链路流程图

[浏览器]
   │ 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 负责参数格式,控制器层负责编排与响应封装,服务层负责业务处理与事务,模型层负责数据持久化。这种分层设计使得每一层职责单一、可独立测试。