8.6 KiB
8.6 KiB
8.1 统一响应格式
:::tip 概述
所有接口返回统一的 JSON 结构,前端可据此判断请求结果。响应由 App\Support\Result 类构造,保证全系统格式一致;HTTP 状态码默认 200(仅 403 例外),结果一律以 body 的 code 判断。
:::
响应结构
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 时跳转登录。
:::
成功响应
普通成功
{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": {
"id": 1,
"username": "admin",
"realname": "管理员",
"createTime": "2026-01-01 00:00:00"
}
}
带自定义消息
{
"code": 0,
"ok": true,
"msg": "添加成功",
"data": { "id": 42 }
}
data 为 null
{
"code": 0,
"ok": true,
"msg": "修改成功",
"data": null
}
data 为数组
{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": [
{ "id": 1, "name": "选项一" },
{ "id": 2, "name": "选项二" }
]
}
分页响应
{
"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)
{
"code": 1,
"ok": false,
"msg": "名称已存在",
"data": null
}
FormRequest 参数验证失败
{
"code": 1,
"ok": false,
"msg": "参数验证失败",
"data": {
"username": ["用户名不能为空"],
"password": ["密码不能为空"]
}
}
:::info data 为字段明细对象
FormRequest 失败时 data 为 {字段名: [错误消息数组]}(BaseRequest::failedValidation()),前端可按字段回显。
:::
代码内手动校验失败(ValidationException)
{
"code": 422,
"ok": false,
"msg": "参数验证失败",
"data": { "name": ["案例名称不能为空"] }
}
未授权(Token 问题,code 401)
{
"code": 401,
"ok": false,
"msg": "未提供认证令牌",
"data": null
}
权限不足(HTTP 403)
{
"code": 403,
"ok": false,
"msg": "无访问权限:sys:user:add",
"data": null
}
数据不存在(404)
{
"code": 404,
"ok": false,
"msg": "数据不存在",
"data": null
}
未知异常(500)
{
"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 |
使用示例
// 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 统一响应与异常抛出。
驼峰自动转换
响应数据自动从 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 响应拦截器
// ui/src/utils/http/axios/index.ts
import { useMessage } from '@/hooks/web/useMessage';
const $message = useMessage();
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) {
$message.error(msg || '无访问权限');
return Promise.reject(new Error(msg));
}
// 其他业务错误
$message.error(msg || '操作失败');
return Promise.reject(new Error(msg));
},
error => {
$message.error('网络错误');
return Promise.reject(error);
}
);
处理分页数据
const fetchData = async () => {
const res = await getUserPage({ pageNo: 1, pageSize: 20 });
// res 已经是 data 部分(拦截器已解包)
tableData.value = res.records;
total.value = res.total;
// 总页数:res.pages
};
处理导入结果
import { useMessage } from '@/hooks/web/useMessage';
const $message = useMessage();
const handleImport = async (file) => {
const res = await importUsers(file);
// res = { count: 8, errors: [{ row: 3, name: "...", reason: "..." }] }
if (res.errors.length === 0) {
$message.success(`成功导入 ${res.count} 条`);
} else {
$message.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 |