# 8.1 统一响应格式 :::tip 概述 所有接口返回统一的 JSON 结构,前端可据此判断请求结果。响应由 `App\Support\Result` 类构造,保证全系统格式一致;HTTP 状态码默认 200(仅 403 例外),结果一律以 body 的 `code` 判断。 ::: ## 响应结构 ```typescript interface ApiResponse { code: number; // 状态码:0=成功,其他=失败 ok: boolean; // 是否成功 msg: string; // 提示信息 data: any; // 响应数据 } ``` ## 状态码定义 | code | 含义 | ok | HTTP 状态 | 说明 | |------|------|-----|-----|------| | `0` | 成功 | `true` | 200 | 业务成功 | | `1` | 业务失败 | `false` | 200 | 通用业务错误(业务校验失败、FormRequest 格式错误等) | | `401` | 未授权 | `false` | 200 | Token 缺失/无效/过期(前端据 body code=401 跳转登录) | | `403` | 禁止访问 | `false` | **403** | 权限不足 / 租户异常 / 演示环境拦截 | | `404` | 数据不存在 | `false` | 200 | 查询的记录不存在 / 路由不存在 | | `422` | 参数验证失败 | `false` | 200 | 代码内手动 `Validator::validate()` 校验不通过 | :::tip HTTP 状态码约定 仅 `forbidden()` 同时设置 HTTP 403(便于网关按 HTTP 状态处理),其余响应 HTTP 恒为 200——前端以 `code` 为准,`code=401` 时跳转登录。 ::: ## 成功响应 ### 普通成功 ```json { "code": 0, "ok": true, "msg": "操作成功", "data": { "id": 1, "username": "admin", "realname": "管理员", "createTime": "2026-01-01 00:00:00" } } ``` ### 带自定义消息 ```json { "code": 0, "ok": true, "msg": "添加成功", "data": { "id": 42 } } ``` ### data 为 null ```json { "code": 0, "ok": true, "msg": "修改成功", "data": null } ``` ### data 为数组 ```json { "code": 0, "ok": true, "msg": "操作成功", "data": [ { "id": 1, "name": "选项一" }, { "id": 2, "name": "选项二" } ] } ``` ## 分页响应 ```json { "code": 0, "ok": true, "msg": "操作成功", "data": { "records": [ { "id": 1, "username": "admin", "createTime": "2026-01-01 00:00:00" }, { "id": 2, "username": "user1", "createTime": "2026-01-02 00:00:00" } ], "total": 100, "current": 1, "size": 20, "pages": 5 } } ``` ### 分页字段 | 字段 | 类型 | 说明 | |------|------|------| | `records` | array | 当前页数据列表 | | `total` | number | 总记录数 | | `current` | number | 当前页码 | | `size` | number | 每页条数 | | `pages` | number | 总页数 | ## 失败响应 ### 通用业务失败(BizException) ```json { "code": 1, "ok": false, "msg": "名称已存在", "data": null } ``` ### FormRequest 参数验证失败 ```json { "code": 1, "ok": false, "msg": "参数验证失败", "data": { "username": ["用户名不能为空"], "password": ["密码不能为空"] } } ``` :::info data 为字段明细对象 FormRequest 失败时 `data` 为 `{字段名: [错误消息数组]}`(`BaseRequest::failedValidation()`),前端可按字段回显。 ::: ### 代码内手动校验失败(ValidationException) ```json { "code": 422, "ok": false, "msg": "参数验证失败", "data": { "name": ["案例名称不能为空"] } } ``` ### 未授权(Token 问题,code 401) ```json { "code": 401, "ok": false, "msg": "未提供认证令牌", "data": null } ``` ### 权限不足(HTTP 403) ```json { "code": 403, "ok": false, "msg": "无访问权限:sys:user:add", "data": null } ``` ### 数据不存在(404) ```json { "code": 404, "ok": false, "msg": "数据不存在", "data": null } ``` ### 未知异常(500) ```json { "code": 500, "ok": false, "msg": "操作失败", "data": null } ``` (调试模式 `APP_DEBUG=true` 时 `msg` 为异常原文,生产环境固定"操作失败"。) ## 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `code` | number | 状态码,0=成功 | | `ok` | boolean | 是否成功 | | `msg` | string | 提示信息 | | `data` | any | 响应数据,失败时通常为 null 或错误详情 | ## 后端 Result 类 **文件:** `app/Support/Result.php` ### 方法清单 | 方法 | 说明 | code | HTTP | |------|------|------|------| | `success($data, $msg, $raw)` | 成功响应(`$raw=true` 跳过驼峰转换) | 0 | 200 | | `page($records, $total, $current, $size, $pages = null)` | 分页响应 | 0 | 200 | | `fail($msg, $code, $data)` | 失败响应(code 默认 1) | 自定义 | 200 | | `unauthorized($msg)` | 未授权 | 401 | 200 | | `forbidden($msg)` | 禁止访问 | 403 | **403** | | `notFound($msg)` | 数据不存在 | 404 | 200 | | `validateError($errors)` | 验证失败 | 422 | 200 | ### 使用示例 ```php // Controller 中通过基类调用(推荐) $this->success($data, '操作成功'); $this->success(['id' => $id], '添加成功'); $this->paginate($pageListResult); // 分页(records/total/current/size/pages) $this->fail('名称已存在'); $this->fail('缺少id参数', 1); // 直接调用 Result 类 Result::success($data); Result::fail('错误信息'); Result::unauthorized('未授权'); Result::forbidden('无权限'); Result::notFound('数据不存在'); Result::validateError(['name' => ['名称不能为空']]); ``` 响应构造与异常渲染的完整机制见 [5.16 统一响应与异常抛出](/zh/core/exception.html)。 ## 驼峰自动转换 响应数据自动从 snake_case 转为 camelCase(`API_CAMEL_SNAKE_CONVERT=true` 时): | 数据库字段 | 响应字段 | |-----------|---------| | `create_time` | `createTime` | | `user_name` | `userName` | | `is_delete` | `isDelete` | | `dept_id` | `deptId` | 请求方向同时自动转换(排除 `pageNo/pageSize/orderField/orderType`),可通过 `config/api.php` 的 `camel_snake_convert` 关闭。需要原样返回时用 `Result::success($data, $msg, true)`。 ## 前端响应处理 ### Axios 响应拦截器 ```typescript // ui/src/utils/http/axios/index.ts axios.interceptors.response.use( response => { const { code, data, msg } = response.data; // 成功 if (code === 0) { return data; } // Token 过期(code 401) if (code === 401 || response.status === 401) { localStorage.clear(); router.push('/login'); return Promise.reject(new Error(msg)); } // 权限不足 if (code === 403 || response.status === 403) { ElMessage.error(msg || '无访问权限'); return Promise.reject(new Error(msg)); } // 其他业务错误 ElMessage.error(msg || '操作失败'); return Promise.reject(new Error(msg)); }, error => { ElMessage.error('网络错误'); return Promise.reject(error); } ); ``` ### 处理分页数据 ```typescript const fetchData = async () => { const res = await getUserPage({ pageNo: 1, pageSize: 20 }); // res 已经是 data 部分(拦截器已解包) tableData.value = res.records; total.value = res.total; // 总页数:res.pages }; ``` ### 处理导入结果 ```typescript const handleImport = async (file) => { const res = await importUsers(file); // res = { count: 8, errors: [{ row: 3, name: "...", reason: "..." }] } if (res.errors.length === 0) { ElMessage.success(`成功导入 ${res.count} 条`); } else { ElMessage.warning(`成功 ${res.count} 条,失败 ${res.errors.length} 条`); } }; ``` ## 常见响应场景汇总 | 场景 | code | msg | data | |------|------|-----|------| | 查询成功 | 0 | 操作成功 | 数据对象/数组 | | 分页成功 | 0 | 操作成功 | {records, total, current, size, pages} | | 新增成功 | 0 | 添加成功 | {id: 42} | | 修改成功 | 0 | 修改成功 | null | | 删除成功 | 0 | 删除成功 | null | | 业务失败 | 1 | 具体错误信息 | null | | FormRequest 验证失败 | 1 | 参数验证失败 | {字段: [错误数组]} | | 手动校验失败 | 422 | 参数验证失败 | {字段: [错误数组]} | | Token 过期 | 401 | 未提供认证令牌 / Expired token | null | | 无权限 | 403(HTTP 403) | 无访问权限:xxx | null | | 数据不存在 | 404 | 数据不存在 | null | | 未知异常 | 500 | 操作失败(生产) | null |