# 5.16 统一响应与异常抛出 :::tip 概述 本页从**后端开发视角**讲解两件事:接口如何返回统一响应(`Result` 类 + Controller 快捷方法),业务异常如何抛出并被 `Handler` 统一渲染为 JSON。前端报文格式见 [8.1 统一响应格式](/zh/api-spec/response.html),规范约束见 [12.1 API 响应规范](/zh/standards/api-response.html)。 ::: ## 响应结构 所有接口(含成功与失败)返回统一 JSON 结构,HTTP 状态码默认 200,前端以 body 中的 `code` 判断结果: ```json // 成功 {"code": 0, "ok": true, "msg": "操作成功", "data": {...}} // 失败 {"code": 1, "ok": false, "msg": "操作失败", "data": null} // 分页 {"code": 0, "ok": true, "msg": "操作成功", "data": {"records": [...], "total": 100, "current": 1, "size": 20, "pages": 5}} ``` ## 统一响应类(Result) `App\Support\Result` 是所有响应的构造入口——Controller 快捷方法与全局异常处理器都委托给它,保证全站结构一致。 ### 业务码常量 | 常量 | 值 | 含义 | HTTP 状态码 | |------|----|------|-------------| | `Result::SUCCESS` | 0 | 成功 | 200 | | `Result::FAIL` | 1 | 一般失败(业务逻辑失败) | 200 | | `Result::UNAUTHORIZED` | 401 | 未授权(Token 缺失/无效/过期) | 200 | | `Result::FORBIDDEN` | 403 | 禁止访问(权限不足 / 租户异常 / 演示环境) | **403** | | `Result::NOT_FOUND` | 404 | 数据不存在 / 路由不存在 | 200 | | `Result::VALID_ERROR` | 422 | 参数验证错误 | 200 | :::tip HTTP 状态码约定 除 `forbidden()` 同时设置 HTTP 403(便于网关按 HTTP 状态码处理)外,其余响应 HTTP 状态码恒为 200,结果一律看 body 的 `code`(`code=401` 时前端跳转登录)。 ::: ### 方法速查 ```php Result::success($data, $msg); // 成功响应(data 自动驼峰转换) Result::success($data, $msg, true); // 成功响应,$raw=true 跳过驼峰转换 Result::page($records, $total, $current, $size, $pages = null); // 分页响应 Result::fail($msg, $code, $data); // 失败响应($code 默认 1) Result::unauthorized($msg); // 401 未授权(HTTP 200 + body code=401) Result::forbidden($msg); // 403 禁止访问(HTTP 403 + body code=403) Result::notFound($msg); // 404 数据不存在(body code=404) Result::validateError($errors); // 422 验证错误(data=字段错误明细) ``` ### Controller 快捷方法 控制器基类把常用方法转发到 `Result`,在控制器中直接用 `$this->` 调用: ```php // app/Http/Controllers/Controller.php $this->success($data, '操作成功'); // → Result::success() $this->paginate($result); // → Result::page(),$result 来自 Service::pageList() $this->fail('操作失败'); // → Result::fail(),$code 默认 1 $this->fail('数据不存在', 404); // → 自定义 code // 通用 CRUD 快捷方法(内部已处理响应与异常,控制器一行调用) $this->_page($service); // 分页 $this->_detail($service, $id); // 详情(空返回 404) $this->_list($service); // 全量列表 $this->_add($service, $data); // 新增(返回 id) $this->_treeList($service); // 树形列表 $this->_update($service, $id, $data); $this->_delete($service, $id); // 删除 $this->_batchDelete($service, $ids); ``` ## 抛出业务异常(BizException) 业务规则校验失败时,在 **Service 层**抛出 `App\Exceptions\BizException`,无须自己构造响应: ```php use App\Exceptions\BizException; // 典型场景:唯一性校验、状态依赖校验 if ($this->uniqueCheck('code', $data, $id)) { throw new BizException('编码已存在'); } if ($model->status !== 1) { throw new BizException('该状态下不允许删除'); } ``` **工作机制:** - `BizException` 继承 `RuntimeException`,属于"预期内"的业务异常,默认 `code=1`; - 走 `_add() / _update() / _delete()` 等快捷方法时,被 Controller 捕获并转为 `$this->fail($e->getMessage(), $e->getCode())`; - 没被捕获时由全局 `Handler` 兜底处理,仍返回 `code=1` 的失败响应——**任何路径下前端拿到的结构都一致**。 :::warning 使用边界 `BizException` 仅用于**业务规则失败**(消息直接展示给用户,不含敏感信息)。程序错误(空指针、SQL 异常等)应让其自然冒泡,由 `Handler` 统一转为 `code=500`,生产环境隐藏真实消息——不要把未知异常包装成业务异常掩盖问题。 ::: ## 异常处理流程(Handler) `app/Exceptions/Handler.php` 拦截 `api/*` 或期望 JSON 的请求,按异常类型分派: ```text 任意异常抛出 │ ▼ Handler::render() ── 非 api/* 且非 JSON 请求 → 交回框架默认处理(如网页 404) │ ▼(api/* 请求) renderJson() 按类型分派: ├─ BizException → Result::fail(消息, 异常 code ?: 1) ├─ ValidationException → Result::validateError(字段明细) code=422 ├─ AuthenticationException → Result::unauthorized() code 401(HTTP 200) ├─ NotFoundHttpException → Result::notFound() code=404 └─ 其它异常 → Result::fail(消息, 500) APP_DEBUG=true 时消息=异常原文, 生产环境消息固定为「操作失败」 ``` 对应代码: ```php // app/Exceptions/Handler.php(节选) protected function renderJson($request, Throwable $e): JsonResponse { if ($e instanceof BizException) { return Result::fail($e->getMessage(), $e->getCode() ?: 1); } if ($e instanceof ValidationException) { return Result::validateError($e->errors()); } if ($e instanceof AuthenticationException) { return Result::unauthorized(); } if ($e instanceof NotFoundHttpException) { return Result::notFound(); } // 其他异常:生产环境隐藏细节,调试模式返回真实消息 $msg = config('app.debug') ? $e->getMessage() : '操作失败'; return Result::fail($msg ?: '操作失败', 500); } ``` :::danger 生产环境安全 `APP_DEBUG` 必须为 `false`,否则未知异常的真实消息(可能包含 SQL 语句、文件路径等敏感信息)会通过 `msg` 返回给前端。详见 [11.8 安全加固清单](/zh/deploy/security.html)。 ::: ## 参数验证的两条路径 参数验证失败有两条路径,**code 不同**,联调时注意区分: | 路径 | 触发方式 | 响应 | |------|----------|------| | FormRequest(控制器入参) | 路由类型注入 `XxxRequest`,`rules()` 不通过 | `code=1`,`msg=参数验证失败`,`data=字段明细`(`BaseRequest::failedValidation()` 统一处理,与 `fail()` 默认 code 一致) | | ValidationException(代码内手动校验) | `Validator::validate()` / `$request->validate()` 抛出 | `code=422`(`Handler` 分派到 `Result::validateError()`) | FormRequest 失败响应示例: ```json {"code": 1, "ok": false, "msg": "参数验证失败", "data": {"username": ["用户名不能为空"], "password": ["密码不能为空"]}} ``` ## 完整示例 ```php // Controller:参数由 FormRequest 验证,响应交给快捷方法 public function add(ExampleAddRequest $request) { return $this->_add($this->service, $request->validated()); } // Service:业务规则失败直接抛异常 public function add(array $data): int { if ($this->uniqueCheck('code', $data)) { throw new BizException('编码已存在'); } return $this->doAdd($data); // DB::transaction 由基类钩子处理 } // 手动构造响应(非 CRUD 场景) public function custom() { $data = $this->service->stats(); if (! $data) { return $this->fail('暂无统计数据', 404); } return $this->success($data); } ``` ## 驼峰转换 `Result` 构造响应时自动把 `snake_case` 键转为 `camelCase`(由 `config/api.php` 的 `API_CAMEL_SNAKE_CONVERT` 控制,请求方向反向转换): - 响应:`create_time` → `createTime` - 需要原样返回时:`Result::success($data, $msg, true)`(`$raw=true`) ## 常见问题 | 问题 | 说明 | |------|------| | 为什么 HTTP 200 却是失败? | 约定 body `code` 判断结果,HTTP 状态码仅 403 例外(便于网关处理);`code=401` 由前端跳转登录 | | 前端拿到 `code=500` 且 `msg=操作失败` | 未知异常 + 生产模式;排查服务端日志 `storage/logs/` | | 抛了 BizException 却返回 500? | 检查是否被外层 `try/catch (\Exception)` 吞掉后重新抛出,或消息中混入了敏感信息 | | 验证失败有时 1 有时 422? | 见上文「参数验证的两条路径」,前端两种都要兼容(项目以 `code !== 0` 判失败) | | 分页字段顺序为何固定? | `Result::page()` 固定 `records / total / current / size / pages` 五项,与 `Service::pageList()` 返回结构对应 | ## 相关页面 - [8.1 统一响应格式](/zh/api-spec/response.html) — 前端对接视角的报文格式 - [8.2 错误码对照表](/zh/api-spec/error-code.html) — 全部错误码速查 - [12.1 API 响应规范](/zh/standards/api-response.html) — 必须遵守的规范约束 - [3.6 控制器开发](/zh/guide/controller.html) — 快捷方法的完整用法