Skip to content

📡 接口响应与异常

SunAdmin 接口统一返回:

json
{
  "code": 200,
  "message": "",
  "data": {}
}

前端应优先判断业务 code,不要只依赖 HTTP 状态码。

🎯 本页目标

本页用于约定后端接口“怎么返回、失败怎么表达、分页怎么组织、前端怎么消费”。新增或调整接口时,应优先保证响应结构稳定,避免同一类接口在不同页面返回不同字段名。

📘 OpenAPI 导出

项目通过 Artisan 命令从当前 Laravel 路由表导出 OpenAPI 3.0 JSON:

bash
php artisan openapi:export

默认输出路径为:

text
storage/app/openapi.json

可通过 --output 指定临时路径:

bash
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: [...])] 显式补充。

示例:

php
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 提供:

php
success($data = null, $message = '', $code = 200);
error($message = '', $code = 400, $status = 200, $data = null);

底层实现位于 app/Core/Support/ApiResponse.php

成功响应示例

返回详情:

php
return success([
    'id' => 1,
    'name' => '系统管理员',
]);

接口输出:

json
{
  "code": 200,
  "message": "",
  "data": {
    "id": 1,
    "name": "系统管理员"
  }
}

返回操作成功提示:

以下两种写法均可,因为第一个参数如果传递字符串,会被当做提示消息返回

php
return success('保存成功');
return success(null, '保存成功');

业务失败示例

php
return error('账号已被禁用');

接口输出:

json
{
  "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 指定的外部页面,而不是按业务数据展示。典型用法是后端拼好第三方授权链接、外部支付页或文档地址后直接抛出去,前端不需要再手动判断。

示例:

php
return success(['url' => 'https://example.com/auth/callback'], '', 2);

接口输出:

json
{
  "code": 2,
  "message": "",
  "data": {
    "url": "https://example.com/auth/callback"
  }
}

Admin 响应拦截器读到 code=2 后会自动执行跳转,业务页面无需额外编码。该约定只在后台 Admin 端生效,用户端 uni-app 不识别 code=2,需要外部跳转的用户端业务应直接由后端返回 data.url,前端按普通字段处理。

🧨 异常类层级

项目定义了三个异常类,均位于 app/Core/Exceptions/,统一由全局异常处理器捕获并转换为标准响应。

BusinessException

php
app/Core/Exceptions/BusinessException.php

基类业务异常,所有业务抛错都应继承此类或直接使用。构造函数:

php
public function __construct(
    string $message = '操作失败',
    int $errorCode = 400,
    int $httpStatus = 200,
    mixed $data = null,
    ?Throwable $previous = null
)
参数说明
$message前端展示的提示文案
$errorCode业务 code,对应响应中的 code 字段
$httpStatusHTTP 状态码,默认 200(业务错误不中断 HTTP)
$data附带数据,会写入响应的 data 字段

SystemException

php
app/Core/Exceptions/SystemException.php

系统级异常,用于数据库错误、外部服务不可用等非业务故障。默认 code 10001

php
throw new SystemException();                        // "系统繁忙,请稍后再试",code 10001
throw new SystemException('数据库连接失败');           // 自定义文案

DataNotFoundException

php
app/Core/Exceptions/DataNotFoundException.php

数据不存在异常,BaseDao::getOrFail() 默认抛出此类。默认 code 10003

php
throw new DataNotFoundException();                  // "数据不存在",code 10003
throw new DataNotFoundException('用户不存在');        // 自定义文案
throw new DataNotFoundException('商品已下架', 10003, 200, ['id' => 1]); // 附带数据

自定义业务异常

业务模块可继承 BusinessException 定义领域异常:

php
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 统一在:

text
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 统一在:

text
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:// 开头的完整地址,则直接使用该地址,不再拼接 baseUrlapi 前缀。
  • 默认携带 Authorization,可通过 withToken: false 关闭。
  • 小程序 / App 要求 VITE_APP_BASE_URL 是完整域名。
  • 只对网络错误做有限重试,业务错误不重试。
  • 10002 会清理用户登录态并 reLaunch 到登录页。

🚦 错误处理端到端

下表把常见错误码在“后端怎么返回、Admin 怎么处理、uni-app 怎么处理”三个角度串起来,便于排查和复现。

code触发场景后端写法Admin 行为uni-app 行为
10001系统异常、数据库异常等未受控错误通常由全局异常处理器返回,业务一般不主动抛默认弹出 message 并 reject默认弹出 message 并 reject
10002Token 过期、未登录、登录态被踢出由认证中间件统一抛出清理登录信息,跳转 /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不识别,按普通字段处理

落地建议:

  • 业务模块对外抛错误时优先使用稳定的错误码(100031000410005),不要全部塞到 400。这样前端能按错误码做差异化处理,而不是只能弹 message
  • 当某个页面的“未授权”需要做特殊跳转(例如踢回首页或开通页),优先在页面层判断 code=10004,不要修改全局请求拦截器的默认行为。
  • 新增错误码时,需要同步:后端 app/Core/Support/ApiResponse.php 的常量、Admin 请求拦截器、uni-app 请求封装、以及本页表格。

📄 分页约定

后端 getPageConfig() 支持:

参数说明
page_no当前页
page_size每页数量

page_no0 时,表示解除分页,返回完整列表。默认最大 page_sizeconfig/sunadmin.phplist.page_size_max 控制,当前为 200

列表接口统一返回格式:

json
{
  "code": 200,
  "message": "",
  "data": {
    "list": [
      {
        "id": 1,
        "name": "系统管理员"
      }
    ],
    "count": 1,
    "page_no": 1,
    "page_size": 20
  }
}

后端侧推荐通过 Dao / Builder 输出该结构:

php
return $this->dao->getList(
    $where,
    '*',
    ['id' => 'desc']
);

或者

php
return $this->query()
    ->filter($where)
    ->latest('id')
    ->listData();

🛠️ 表单验证

字段校验失败会返回 10005。Laravel 验证错误可能包含 errors 字段,uni-app 请求封装会从 messagemsgerrors 中提取最适合展示的文案。

FormRequest 示例:

php
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'],
    ];
}

校验失败响应示例:

json
{
  "code": 10005,
  "message": "状态值不正确",
  "data": null
}

🧩 字段命名建议

类型建议
ID 字段使用 iduser_idrole_id 保持下划线风格
时间字段使用 created_atupdated_atdeleted_at 或明确业务时间字段
状态字段使用 status 等稳定字段,不在不同接口随意改名
分页字段入参使用 page_nopage_size,返回使用 listcountpage_nopage_size
金额字段明确单位和精度,避免同一含义同时出现分、元两个字段

字段一旦被 Admin 或 uni-app 使用,调整前需要同步检查前端接口类型、表格列、表单字段和详情页展示。