📡 接口响应与异常
SunAdmin 接口统一返回:
{
"code": 200,
"message": "",
"data": {}
}前端应优先判断业务 code,不要只依赖 HTTP 状态码。
🎯 本页目标
本页用于约定后端接口“怎么返回、失败怎么表达、分页怎么组织、前端怎么消费”。新增或调整接口时,应优先保证响应结构稳定,避免同一类接口在不同页面返回不同字段名。
📘 OpenAPI 导出
项目通过 Artisan 命令从当前 Laravel 路由表导出 OpenAPI 3.0 JSON:
php artisan openapi:export默认输出路径为:
storage/app/openapi.json可通过 --output 指定临时路径:
php artisan openapi:export --output=storage/app/openapi-test.json导出规则:
- 仅导出
adminapi/*与api/*路由。 adminapi/*接口归入后台端分组。api/*接口归入用户端分组。- tag 目录按
端 / 模块标题 / 控制器分组生成,例如后台端 / 系统模块 / 文章、用户端 / 系统模块 / 装修。模块标题优先读取module.json.title,缺失时使用模块目录名;控制器分组由控制器类上的#[ApiDoc(tag: '...')]声明。 - 接口名称读取控制器方法上的
#[ApiDoc(summary: '...')];未声明时会导出Controller@method,用于快速发现漏标接口。 - 未使用 FormRequest 的简单接口,会尝试从
$this->request()->pick([...])推导 query 或 body 参数;复杂参数可通过#[ApiDoc(parameters: [...])]显式补充。
示例:
use App\Core\Attributes\ApiDoc;
use App\Core\Attributes\OpenGet;
#[ApiDoc(tag: '装修')]
class DecorateController extends ApiController
{
#[OpenGet('page')]
#[ApiDoc(summary: '按 Key 获取装修页')]
public function page()
{
$params = $this->request()->pick([
['key', 'home'],
]);
return success($this->decorateAction->page((string) $params['key']));
}
}上面接口会归入 用户端 / 系统模块 / 装修 分组,接口名称导出为“按 Key 获取装修页”,并自动把 key 识别为 query 参数。
生成的 storage/app/openapi.json 可导入 Apifox、Postman、Swagger 等接口管理工具。该文件是运行时生成产物,通常不提交到仓库。
🧑💻 响应助手
app/Support/helpers.php 提供:
success($data = null, $message = '', $code = 200);
error($message = '', $code = 400, $status = 200, $data = null);底层实现位于 app/Core/Support/ApiResponse.php。
成功响应示例
返回详情:
return success([
'id' => 1,
'name' => '系统管理员',
]);接口输出:
{
"code": 200,
"message": "",
"data": {
"id": 1,
"name": "系统管理员"
}
}返回操作成功提示:
以下两种写法均可,因为第一个参数如果传递字符串,会被当做提示消息返回
return success('保存成功');
return success(null, '保存成功');业务失败示例
return error('账号已被禁用');接口输出:
{
"code": 400,
"message": "账号已被禁用",
"data": null
}业务失败通常仍然使用 HTTP 200,由业务 code 表达失败原因。只有下载、上传底层失败、服务不可用等特殊场景,才需要结合 HTTP 状态码处理。
🔢 状态码
| code | 含义 | 前端处理 |
|---|---|---|
200 | 成功 | 使用 data |
400 | 通用业务失败 | 展示 message |
10001 | 系统异常 / 数据库异常 | 展示 message,开发环境可返回真实错误 |
10002 | 未登录或登录失效 | 清理登录态并跳转登录页 |
10003 | 数据不存在 | 展示 message |
10004 | 无权限访问 | 展示 message |
10005 | 参数校验失败 | 展示 message |
20001 | 登录黑名单 | 预留 |
20002 | 账号禁用 | 预留 |
2 | 打开外部页面 | Admin 特殊处理 |
code=2 是后台特殊跳转码,用于让 Admin 端在收到响应后跳转到 data.url 指定的外部页面,而不是按业务数据展示。典型用法是后端拼好第三方授权链接、外部支付页或文档地址后直接抛出去,前端不需要再手动判断。
示例:
return success(['url' => 'https://example.com/auth/callback'], '', 2);接口输出:
{
"code": 2,
"message": "",
"data": {
"url": "https://example.com/auth/callback"
}
}Admin 响应拦截器读到 code=2 后会自动执行跳转,业务页面无需额外编码。该约定只在后台 Admin 端生效,用户端 uni-app 不识别 code=2,需要外部跳转的用户端业务应直接由后端返回 data.url,前端按普通字段处理。
🧨 异常类层级
项目定义了三个异常类,均位于 app/Core/Exceptions/,统一由全局异常处理器捕获并转换为标准响应。
BusinessException
app/Core/Exceptions/BusinessException.php基类业务异常,所有业务抛错都应继承此类或直接使用。构造函数:
public function __construct(
string $message = '操作失败',
int $errorCode = 400,
int $httpStatus = 200,
mixed $data = null,
?Throwable $previous = null
)| 参数 | 说明 |
|---|---|
$message | 前端展示的提示文案 |
$errorCode | 业务 code,对应响应中的 code 字段 |
$httpStatus | HTTP 状态码,默认 200(业务错误不中断 HTTP) |
$data | 附带数据,会写入响应的 data 字段 |
SystemException
app/Core/Exceptions/SystemException.php系统级异常,用于数据库错误、外部服务不可用等非业务故障。默认 code 10001。
throw new SystemException(); // "系统繁忙,请稍后再试",code 10001
throw new SystemException('数据库连接失败'); // 自定义文案DataNotFoundException
app/Core/Exceptions/DataNotFoundException.php数据不存在异常,BaseDao::getOrFail() 默认抛出此类。默认 code 10003。
throw new DataNotFoundException(); // "数据不存在",code 10003
throw new DataNotFoundException('用户不存在'); // 自定义文案
throw new DataNotFoundException('商品已下架', 10003, 200, ['id' => 1]); // 附带数据自定义业务异常
业务模块可继承 BusinessException 定义领域异常:
use App\Core\Exceptions\BusinessException;
class InsufficientBalanceException extends BusinessException
{
public function __construct(float $balance)
{
parent::__construct(
message: '余额不足,当前余额 ' . $balance . ' 元',
errorCode: 30001,
data: ['balance' => $balance]
);
}
}前端可通过 code 做差异化处理,通过 data 获取附带信息。
💻 Admin 端响应处理
Admin 统一在:
frontend/admin/src/utils/request/index.ts处理逻辑:
- 使用 Axios,基础地址来自
VITE_APP_BASE_URL,并自动补齐末尾/。 - 默认接口前缀为
adminapi,请求时自动拼接baseURL + urlPrefix + url。 - 例如
VITE_APP_BASE_URL=http://example.com,接口调用传入url: '/system/login/account',最终请求地址为http://example.com/adminapi/system/login/account。 - 请求时自动追加
Authorization: Bearer {token}。 - POST 默认把
params转成data。 code=200返回data。code=2跳转data.url。code=10002清理登录态并跳转/login。- 其他错误展示
message并 reject。
📲 uni-app 端响应处理
uni-app 统一在:
frontend/uniapp/src/utils/request.ts处理逻辑:
- 使用
uni.request,基础地址来自VITE_APP_BASE_URL,并自动补齐末尾/。 - 默认接口前缀为
api,请求时自动拼接baseUrl + urlPrefix + url。 - 例如
VITE_APP_BASE_URL=http://example.com,接口调用传入url: '/system/login/account',最终请求地址为http://example.com/api/system/login/account。 - 如果传入的是
http://或https://开头的完整地址,则直接使用该地址,不再拼接baseUrl和api前缀。 - 默认携带
Authorization,可通过withToken: false关闭。 - 小程序 / App 要求
VITE_APP_BASE_URL是完整域名。 - 只对网络错误做有限重试,业务错误不重试。
10002会清理用户登录态并reLaunch到登录页。
🚦 错误处理端到端
下表把常见错误码在“后端怎么返回、Admin 怎么处理、uni-app 怎么处理”三个角度串起来,便于排查和复现。
| code | 触发场景 | 后端写法 | Admin 行为 | uni-app 行为 |
|---|---|---|---|---|
10001 | 系统异常、数据库异常等未受控错误 | 通常由全局异常处理器返回,业务一般不主动抛 | 默认弹出 message 并 reject | 默认弹出 message 并 reject |
10002 | Token 过期、未登录、登录态被踢出 | 由认证中间件统一抛出 | 清理登录信息,跳转 /login,保留原 URL 作为 redirect | 清理 token / 用户信息,reLaunch 到登录页,登录成功后回跳原页面 |
10003 | 数据不存在 | error('记录不存在', 10003) | 弹 message,可在页面层处理“返回列表”或“关闭弹窗” | 弹 message,可在页面层做“返回上一页”等处理 |
10004 | 当前身份无权限访问资源 | error('暂无权限', 10004) | 弹 message,按钮可结合 v-perm 提前隐藏 | 弹 message,敏感操作页可主动跳转 |
10005 | 参数校验失败 | FormRequest 自动抛出 | 弹 message,必要时在表单字段下展示 errors | 同 Admin,请求封装从 message/msg/errors 中选最适合的文案 |
2 | 后台需要跳转外部页面 | success(['url' => '...'], '', 2) | 自动跳转 data.url | 不识别,按普通字段处理 |
落地建议:
- 业务模块对外抛错误时优先使用稳定的错误码(
10003、10004、10005),不要全部塞到400。这样前端能按错误码做差异化处理,而不是只能弹message。 - 当某个页面的“未授权”需要做特殊跳转(例如踢回首页或开通页),优先在页面层判断
code=10004,不要修改全局请求拦截器的默认行为。 - 新增错误码时,需要同步:后端
app/Core/Support/ApiResponse.php的常量、Admin 请求拦截器、uni-app 请求封装、以及本页表格。
📄 分页约定
后端 getPageConfig() 支持:
| 参数 | 说明 |
|---|---|
page_no | 当前页 |
page_size | 每页数量 |
当 page_no 为 0 时,表示解除分页,返回完整列表。默认最大 page_size 由 config/sunadmin.php 的 list.page_size_max 控制,当前为 200。
列表接口统一返回格式:
{
"code": 200,
"message": "",
"data": {
"list": [
{
"id": 1,
"name": "系统管理员"
}
],
"count": 1,
"page_no": 1,
"page_size": 20
}
}后端侧推荐通过 Dao / Builder 输出该结构:
return $this->dao->getList(
$where,
'*',
['id' => 'desc']
);或者
return $this->query()
->filter($where)
->latest('id')
->listData();🛠️ 表单验证
字段校验失败会返回 10005。Laravel 验证错误可能包含 errors 字段,uni-app 请求封装会从 message、msg、errors 中提取最适合展示的文案。
FormRequest 示例:
public function rules(): array
{
return [
'keyword' => ['nullable', 'string', 'max:100'],
'status' => ['nullable', 'integer', 'in:0,1'],
'page_no' => ['nullable', 'integer', 'min:1'],
'page_size' => ['nullable', 'integer', 'min:1', 'max:200'],
];
}校验失败响应示例:
{
"code": 10005,
"message": "状态值不正确",
"data": null
}🧩 字段命名建议
| 类型 | 建议 |
|---|---|
| ID 字段 | 使用 id、user_id、role_id 保持下划线风格 |
| 时间字段 | 使用 created_at、updated_at、deleted_at 或明确业务时间字段 |
| 状态字段 | 使用 status 等稳定字段,不在不同接口随意改名 |
| 分页字段 | 入参使用 page_no、page_size,返回使用 list、count、page_no、page_size |
| 金额字段 | 明确单位和精度,避免同一含义同时出现分、元两个字段 |
字段一旦被 Admin 或 uni-app 使用,调整前需要同步检查前端接口类型、表格列、表单字段和详情页展示。
