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