Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/api-spec/response.md
T
2026-10-08 17:50:29 +08:00

346 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |