Response 响应
这里不使用 dingo 进行开发,个人感觉不怎好用,我们下面自己定义
首先,我们需要在 app
目录下创建一个 Helpers
目录
[upl-image-preview url=https://wyz-xyz.oss-cn-huhehaote.aliyuncs.com/2024-06-23/1719133224-587814-image.png]
一、封装统一状态码(ResponseEnum)
在 app/Helpers
目录下创建 ResponseEnum.php
文件
<?php
namespace App\Helpers;
/**
* API 响应枚举
*
* @method array getResponse() 获取 [code, message] 数组
*/
enum ResponseEnum: int
{
//------------------------- 通用状态码 -------------------------
// 成功
case HTTP_OK = 200001;
// 失败
case HTTP_ERROR = 200002;
// 客户端错误
case CLIENT_PARAMETER_ERROR = 400200;
case CLIENT_NOT_FOUND_ERROR = 404001;
case CLIENT_METHOD_HTTP_TYPE_ERROR = 405001;
case CLIENT_VALIDATION_ERROR = 422001;
case CLIENT_TOO_MANY_REQUESTS = 429001;
// 授权与认证 (401, 403)
case AUTH_UNAUTHORIZED = 401001;
case AUTH_TOKEN_EXPIRED = 401200;
case AUTH_TOKEN_BLACKLISTED = 401201;
case AUTH_FORBIDDEN = 403001; // 权限不足
// 系统/服务器错误
case SYSTEM_ERROR = 500001;
case SYSTEM_UNAVAILABLE = 500002;
case DATABASE_ERROR = 500006;
case WECHAT_API_ERROR = 500301;
//------------------------- 模块一:身份与组织管理 (100xxx) -------------------------
case IDENTITY_APPLICATION_SUBMITTED = 100001; // 申请已提交,等待审核
case IDENTITY_NOT_FOUND = 100404; // 未找到指定的身份
case IDENTITY_ALREADY_EXISTS = 100409; // 该身份已存在,请勿重复申请
case IDENTITY_APPROVAL_REJECTED = 100410; // 身份申请被拒绝
case IDENTITY_HEADCOUNT_EXCEEDED = 100411; // 职位编制已满
case BUILDING_NOT_FOUND = 101404; // 认证时楼栋信息不存在
case UNIT_CREATION_FORBIDDEN = 102403; // 无权创建子组织
case UNIT_POSITION_NOT_FOUND = 103404; // 申请的职位不存在
//------------------------- 模块二:内容与工作流 (200xxx) -------------------------
case POST_NOT_FOUND = 200404; // 帖子不存在
case POST_ACTION_FORBIDDEN = 200403; // 无权操作此帖子(如编辑、删除)
case POST_IS_LOCKED = 200423; // 帖子已办结/锁定,无法评论或修改
case VOTE_IS_CLOSED = 201410; // 投票已截止
case ACTIVITY_REGISTRATION_CLOSED = 202410; // 活动报名已截止
case ACTIVITY_PARTICIPANTS_FULL = 202411; // 活动报名人数已满
//------------------------- 模块三:多维激励系统 (300xxx) -------------------------
case POINTS_INSUFFICIENT = 300402; // 用户积分不足
case POINT_RULE_NOT_FOUND = 301404; // 积分规则未配置
case POINT_DAILY_LIMIT_REACHED = 301429; // 今日获取该项积分已达上限
//------------------------- 模块四:消息通知中心 (400xxx - 业务码) -------------------------
case NOTIFICATION_SEND_FAILED = 400500; // 消息发送失败
/**
* 获取状态码对应的默认消息
* @return string
*/
public function message(): string
{
return match ($this) {
// 通用
self::HTTP_OK => '操作成功',
self::HTTP_ERROR => '操作失败',
self::CLIENT_PARAMETER_ERROR => '参数错误',
self::CLIENT_NOT_FOUND_ERROR => '请求的资源不存在',
self::CLIENT_METHOD_HTTP_TYPE_ERROR => '请求方法不被允许',
self::CLIENT_VALIDATION_ERROR => '输入数据验证失败',
self::CLIENT_TOO_MANY_REQUESTS => '请求过于频繁,请稍后再试',
self::AUTH_UNAUTHORIZED => '授权失败,请先登录',
self::AUTH_TOKEN_EXPIRED => '账号信息已过期,请重新登录',
self::AUTH_TOKEN_BLACKLISTED => '账号在其他设备登录,请重新登录',
self::AUTH_FORBIDDEN => '无权访问此内容或执行此操作',
self::SYSTEM_ERROR => '服务器内部错误,请稍后重试',
self::SYSTEM_UNAVAILABLE => '服务器正在维护,暂不可用',
self::DATABASE_ERROR => '数据库操作失败',
self::WECHAT_API_ERROR => '微信服务调用失败',
// 身份与组织
self::IDENTITY_APPLICATION_SUBMITTED => '您的身份认证申请已提交,请等待管理员审核',
self::IDENTITY_NOT_FOUND => '未找到指定的身份信息',
self::IDENTITY_ALREADY_EXISTS => '您已拥有该身份,请勿重复申请',
self::IDENTITY_APPROVAL_REJECTED => '抱歉,您的身份申请未通过审核',
self::IDENTITY_HEADCOUNT_EXCEEDED => '该职位编制已满,无法申请',
self::BUILDING_NOT_FOUND => '您选择的楼栋信息不存在,请联系管理员',
self::UNIT_CREATION_FORBIDDEN => '您没有权限在此组织下创建子单元',
self::UNIT_POSITION_NOT_FOUND => '您申请的职位不存在或已关闭申请',
// 内容与工作流
self::POST_NOT_FOUND => '您要访问的内容不存在或已被删除',
self::POST_ACTION_FORBIDDEN => '您没有权限操作此内容',
self::POST_IS_LOCKED => '该事项已处理完毕,当前已锁定',
self::VOTE_IS_CLOSED => '抱歉,投票已经截止',
self::ACTIVITY_REGISTRATION_CLOSED => '抱歉,活动报名已经截止',
self::ACTIVITY_PARTICIPANTS_FULL => '抱歉,活动报名人数已满',
// 积分
self::POINTS_INSUFFICIENT => '您的积分不足',
self::POINT_RULE_NOT_FOUND => '该操作的积分奖励规则未配置',
self::POINT_DAILY_LIMIT_REACHED => '今日获取该项积分已达到上限',
// 通知
self::NOTIFICATION_SEND_FAILED => '消息发送失败',
};
}
/**
* 获取完整的响应数组 [code, message]
* @return array
*/
public function getResponse(): array
{
return [$this->value, $this->message()];
}
}
二、创建业务异常捕获 Exception 文件
在 app/Exceptions
目录下创建 BusinessException.php
文件用于业务异常的抛出
<?php
namespace App\Exceptions;
use Exception;
class BusinessException extends Exception
{
/**
* 业务异常构造函数
* @param array $codeResponse 状态码
* @param string $info 自定义返回信息,不为空时会替换掉codeResponse 里面的message文字信息
*/
public function __construct(array $codeResponse, $info = '')
{
[$code, $message] = $codeResponse;
parent::__construct($info ?: $message, $code);
}
}
三、自定义返回异常
修改 bootstrap\app.php
文件
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\ForceJsonResponse;
use App\Exceptions\BusinessException;
use App\Helpers\ApiResponse;
use App\Helpers\ResponseEnum;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->prependToGroup('api', [
ForceJsonResponse::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
// 实例化一个匿名类来使用 ApiResponse Trait
$apiResponse = new class {
use ApiResponse;
};
// 注册全局 API 异常渲染器
$exceptions->render(function (Throwable $e, Request $request) use ($apiResponse) {
// 由于有 ForceJsonResponse 中间件, api 路由会始终进入此逻辑
if ($request->expectsJson()) {
// 1. 自定义业务异常
if ($e instanceof BusinessException) {
// 从异常中获取 code 和 message,并使用统一的 fail 方法返回
return $apiResponse->fail([$e->getCode(), $e->getMessage()]);
}
// 2. 参数校验异常
if ($e instanceof ValidationException) {
// 获取详细的验证错误信息
$errors = $e->validator->errors()->getMessages();
return $apiResponse->fail(ResponseEnum::CLIENT_VALIDATION_ERROR, null, $errors);
}
// 3. 路由未找到异常
if ($e instanceof NotFoundHttpException) {
return $apiResponse->fail(ResponseEnum::CLIENT_NOT_FOUND_ERROR);
}
// 4. 请求方法不允许异常
if ($e instanceof MethodNotAllowedHttpException) {
return $apiResponse->fail(ResponseEnum::CLIENT_METHOD_HTTP_TYPE_ERROR);
}
// 5. 在生产环境下,将所有其他未捕获的异常视为系统错误,避免暴露敏感信息
if (!config('app.debug')) {
// 可以在这里记录详细的错误日志
// Log::error($e);
return $apiResponse->fail(ResponseEnum::SYSTEM_ERROR);
}
}
// 对于非 JSON 请求或在 debug 模式下未被捕获的异常, 交给 Laravel 默认渲染器处理
// 这样可以显示 Whoops 错误页面,方便调试
return null;
});
})->create();
四、封装 API 返回的统一消息(ApiResponse)
在 app/Helpers
目录下创建 ApiResponse.php
文件
<?php
namespace App\Helpers;
use App\Exceptions\BusinessException;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
trait ApiResponse
{
/**
* 成功响应
*
* @param mixed $data 返回的数据
* @param ResponseEnum|array $codeResponse 状态码枚举或数组 [code, message]
* @return JsonResponse
*/
public function success(mixed $data = null, ResponseEnum|array $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
{
return $this->jsonResponse('success', $codeResponse, $data, null);
}
/**
* 失败响应
*
* @param ResponseEnum|array $codeResponse 状态码枚举或数组 [code, message]
* @param mixed $data 返回的数据
* @param mixed|null $error 详细的错误信息
* @return JsonResponse
*/
public function fail(ResponseEnum|array $codeResponse = ResponseEnum::HTTP_ERROR, mixed $data = null, mixed $error = null): JsonResponse
{
return $this->jsonResponse('fail', $codeResponse, $data, $error);
}
/**
* 构建 JSON 响应
*
* @param string $status 状态 'success' 或 'fail'
* @param ResponseEnum|array $codeResponse 状态码枚举或数组 [code, message]
* @param mixed $data 返回的数据
* @param mixed $error 详细的错误信息
* @return JsonResponse
*/
private function jsonResponse(string $status, ResponseEnum|array $codeResponse, mixed $data, mixed $error): JsonResponse
{
// 核心修改:如果传入的是 ResponseEnum 枚举实例,自动调用 getResponse() 转换为数组
if ($codeResponse instanceof ResponseEnum) {
$codeResponse = $codeResponse->getResponse();
}
[$code, $message] = $codeResponse;
return response()->json([
'status' => $status,
'code' => $code,
'message' => $message,
'data' => $data ?? null,
'error' => $error,
]);
}
/**
* 成功的分页响应
*
* @param LengthAwarePaginator|Collection|array $page 分页器实例或集合
* @return JsonResponse
*/
protected function successPaginate(LengthAwarePaginator|Collection|array $page): JsonResponse
{
return $this->success($this->paginate($page));
}
/**
* 格式化分页数据
*
* @param mixed $page
* @return mixed
*/
private function paginate(mixed $page): mixed
{
if ($page instanceof LengthAwarePaginator) {
return [
'total' => $page->total(),
'page' => $page->currentPage(),
'limit' => $page->perPage(),
'pages' => $page->lastPage(),
'list' => $page->items()
];
}
if ($page instanceof Collection) {
$page = $page->toArray();
}
if (!is_array($page)) {
return $page;
}
$total = count($page);
return [
'total' => $total, //数据总数
'page' => 1, // 当前页码
'limit' => $total, // 每页的数据条数
'pages' => 1, // 最后一页的页码
'list' => $page // 数据
];
}
/**
* 抛出业务异常
*
* @param ResponseEnum|array $codeResponse 状态码枚举或数组 [code, message]
* @param string $info 自定义补充信息,会覆盖默认 message
* @throws BusinessException
*/
public function throwBusinessException(ResponseEnum|array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = ''): void
{
// 如果传入的是 ResponseEnum 枚举实例,自动调用 getResponse() 转换为数组
if ($codeResponse instanceof ResponseEnum) {
$codeResponse = $codeResponse->getResponse();
}
throw new BusinessException($codeResponse, $info);
}
}
五、创建控制器基类
1、在 app/Http/controller
目录下创建一个 BaseController.php
作为 Api 的基类,然后在 BaseController.php
这个基类中继承 Controller,并引入封装 API 返回的统一消息(ApiResponse)
<?php
namespace App\Http\Controllers;
use App\Helpers\ApiResponse;
class BaseController extends Controller
{
// API接口响应
use ApiResponse;
}
六、使用
1、返回成功信息
return $this->success($data);
2、返回失败信息
return $this->fail($codeResponse);
3、抛出异常
$this->throwBusinessException($codeResponse);
4、分页
return $this->successPaginate($data);
创建 API 表单请求验证基类
每当接收用户提交的参数时,都需要对数据做验证,以保证数据的准确性,接下来创建属于 Api 的表单请求验证类:
需要创建一个基类,方便做一些统一方法的封装,对于接口的验证类,我们也统一放在 Api 目录中。
$ php artisan make:request Api/FormRequest
app/Http/Requests/Api/FormRequest.php
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
class FormRequest extends BaseFormRequest
{
public function authorize()
{
return true;
}
}
题外话
我自己在写api的时候,为了区分api的用户。往往会将api的目录设置为以下这样。用不同的命名空间在区分接口。比如小程序所有的api接口会放在Api/Weapp
中,会让代码结构更清晰。
Api/Weapp //代表的是小程序的api接口
Api/Mobile //代表的是移动端App的api接口
Api/Web //代表的是网站的api接口
如果这样的话,那么上面的基类路径需要稍微修改一下。这样更加合理一些。
然后创建一个我们的控制器基类
php artisan make:controller Api/Weapp/Controller
修改一下
app/Http/Controllers/Api/Weapp/Controller.php
<?php
namespace App\Http\Controllers\Api\Weapp;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller as BaseController;
use App\Helpers\ApiResponse;
class Controller extends BaseController
{
// API接口响应
use ApiResponse;
}