9.3 KiB
5.16 统一响应与异常抛出
:::tip 概述
本页从后端开发视角讲解两件事:接口如何返回统一响应(Result 类 + Controller 快捷方法),业务异常如何抛出并被 Handler 统一渲染为 JSON。前端报文格式见 8.1 统一响应格式,规范约束见 12.1 API 响应规范。
:::
响应结构
所有接口(含成功与失败)返回统一 JSON 结构,HTTP 状态码默认 200,前端以 body 中的 code 判断结果:
// 成功
{"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 时前端跳转登录)。
:::
方法速查
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-> 调用:
// 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,无须自己构造响应:
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 的请求,按异常类型分派:
任意异常抛出
│
▼
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 时消息=异常原文,
生产环境消息固定为「操作失败」
对应代码:
// 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 安全加固清单。
:::
参数验证的两条路径
参数验证失败有两条路径,code 不同,联调时注意区分:
| 路径 | 触发方式 | 响应 |
|---|---|---|
| FormRequest(控制器入参) | 路由类型注入 XxxRequest,rules() 不通过 |
code=1,msg=参数验证失败,data=字段明细(BaseRequest::failedValidation() 统一处理,与 fail() 默认 code 一致) |
| ValidationException(代码内手动校验) | Validator::validate() / $request->validate() 抛出 |
code=422(Handler 分派到 Result::validateError()) |
FormRequest 失败响应示例:
{"code": 1, "ok": false, "msg": "参数验证失败",
"data": {"username": ["用户名不能为空"], "password": ["密码不能为空"]}}
完整示例
// 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 统一响应格式 — 前端对接视角的报文格式
- 8.2 错误码对照表 — 全部错误码速查
- 12.1 API 响应规范 — 必须遵守的规范约束
- 3.6 控制器开发 — 快捷方法的完整用法