Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_AntdVue/zh/core/exception.md
T
2026-10-08 17:50:29 +08:00

9.3 KiB
Raw Blame History

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() 返回结构对应

相关页面