Bootstrap

Laravel 开发Api规范

一,修改时区

配置 config/app.php 文件

// 时区修改,感觉两者皆可,自己根据实际情况定义
'timezone' => 'PRC', // 大陆时间

二,设置 Accept 头中间件

accept头即为客户端请求头,做成中间件来使用。Accept 决定了响应返回的格式,设置为 application/json, 遇到的所有报错 Laravel 会默认处理为 JSON 格式。

  1. 生成中间件
php artisan make:middleware AcceptHeader
<?php
namespace App\Http\Middleware;
use Closure;
class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}
  1. 添加到全局中间件
    修改文件 app/http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        ...
    ],
    'api' => [
        \App\Http\Middleware\AcceptHeader::class,
        ...
    ],
];

二,统一封装响应码

Enum 枚举,新建app/Enums/ResponseEnum.php

<?php

namespace App\Enums;

class ResponseEnum
{
    // sevming/laravel-response 默认以 '|' 作为分割错误码与错误信息的字符串
    public const INVALID_REQUEST = '无效请求|21001';
    // 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务

    /*-------------------------------------------------------------------------------------------*/
    // 100开头的表示 信息提示,这类状态表示临时的响应
    // 100 - 继续
    // 101 - 切换协议


    /*-------------------------------------------------------------------------------------------*/
    // 200表示服务器成功地接受了客户端请求
    const HTTP_OK = [200001, '操作成功'];
    const HTTP_ERROR = [200002, '操作失败'];
    const HTTP_ACTION_COUNT_ERROR = [200302, '操作频繁'];
    const USER_SERVICE_LOGIN_SUCCESS = [200200, '登录成功'];
    const USER_SERVICE_LOGIN_ERROR = [200201, '登录失败'];
    const USER_SERVICE_LOGOUT_SUCCESS = [200202, '退出登录成功'];
    const USER_SERVICE_LOGOUT_ERROR = [200203, '退出登录失败'];
    const USER_SERVICE_REGISTER_SUCCESS = [200104, '注册成功'];
    const USER_SERVICE_REGISTER_ERROR = [200105, '注册失败'];
    const USER_ACCOUNT_REGISTERED = [23001, '账号已注册'];


    /*-------------------------------------------------------------------------------------------*/
    // 300开头的表示服务器重定向,指向的别的地方,客户端浏览器必须采取更多操作来实现请求
    // 302 - 对象已移动。
    // 304 - 未修改。
    // 307 - 临时重定向。


    /*-------------------------------------------------------------------------------------------*/
    // 400开头的表示客户端错误请求错误,请求不到数据,或者找不到等等
    // 400 - 错误的请求
    const CLIENT_NOT_FOUND_HTTP_ERROR = [400001, '请求失败'];
    const CLIENT_PARAMETER_ERROR = [400200, '参数错误'];
    const CLIENT_CREATED_ERROR = [400201, '数据已存在'];
    const CLIENT_DELETED_ERROR = [400202, '数据不存在'];
    // 401 - 访问被拒绝
    const CLIENT_HTTP_UNAUTHORIZED = [401001, '授权失败,请先登录'];
    const CLIENT_HTTP_UNAUTHORIZED_EXPIRED = [401200, '账号信息已过期,请重新登录'];
    const CLIENT_HTTP_UNAUTHORIZED_BLACKLISTED = [401201, '账号在其他设备登录,请重新登录'];
    // 403 - 禁止访问
    // 404 - 没有找到文件或目录
    const CLIENT_NOT_FOUND_ERROR = [404001, '没有找到该页面'];
    // 405 - 用来访问本页面的 HTTP 谓词不被允许(方法不被允许)
    const CLIENT_METHOD_HTTP_TYPE_ERROR = [405001, 'HTTP请求类型错误'];
    // 406 - 客户端浏览器不接受所请求页面的 MIME 类型
    // 407 - 要求进行代理身份验证
    // 412 - 前提条件失败
    // 413 – 请求实体太大
    // 414 - 请求 URI 太长
    // 415 – 不支持的媒体类型
    // 416 – 所请求的范围无法满足
    // 417 – 执行失败
    // 423 – 锁定的错误


    /*-------------------------------------------------------------------------------------------*/
    // 500开头的表示服务器错误,服务器因为代码,或者什么原因终止运行
    // 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
    // 500 - 内部服务器错误
    const SYSTEM_ERROR = [500001, '服务器错误'];
    const SYSTEM_UNAVAILABLE = [500002, '服务器正在维护,暂不可用'];
    const SYSTEM_CACHE_CONFIG_ERROR = [500003, '缓存配置错误'];
    const SYSTEM_CACHE_MISSED_ERROR = [500004, '缓存未命中'];
    const SYSTEM_CONFIG_ERROR = [500005, '系统配置错误'];

    // 业务操作错误码(外部服务或内部服务调用)
    const SERVICE_REGISTER_ERROR = [500101, '注册失败'];
    const SERVICE_LOGIN_ERROR = [500102, '登录失败'];
    const SERVICE_LOGIN_ACCOUNT_ERROR = [500103, '账号或密码错误'];
    const SERVICE_USER_INTEGRAL_ERROR = [500200, '积分不足'];

    //501 - 页眉值指定了未实现的配置
    //502 - Web 服务器用作网关或代理服务器时收到了无效响应
    //503 - 服务不可用。这个错误代码为 IIS 6.0 所专用
    //504 - 网关超时
    //505 - HTTP 版本不受支持
    /*-------------------------------------------------------------------------------------------*/
}

三,封装 API 返回的统一消息(ApiResponse)

在 app/Helpers 目录下创建 ApiResponse.php 文件

<?php

namespace App\Helpers;

use App\Enum\ResponseEnum;
use App\Exceptions\BaseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;


trait ApiResponse
{
    /**
     * 成功
     * @param null $data
     * @param array $codeResponse
     * @return JsonResponse
     */
    public function success($data = null, $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
    {
        return $this->jsonResponse('success', $codeResponse, $data);
    }

    /**
     * 失败
     * @param array $codeResponse
     * @param null $data
     * @param null $error
     * @return JsonResponse
     */
    public function fail($codeResponse = ResponseEnum::HTTP_ERROR, $data = null): JsonResponse
    {
        return $this->jsonResponse('fail', $codeResponse, $data);
    }

    /**
     * json响应
     * @param $status
     * @param $codeResponse
     * @param $data
     * @param $error
     * @return JsonResponse
     */
    private function jsonResponse($status, $codeResponse, $data): JsonResponse
    {
        list($code, $message) = $codeResponse;
        return response()->json([
            'status' => $status,
            'code' => $code,
            'message' => $message,
            'data' => $data ?? null,
        ]);
    }


    /**
     * 成功分页返回
     * @param $page
     * @return JsonResponse
     */
    protected function successPaginate($page): JsonResponse
    {
        return $this->success($this->paginate($page));
    }

    private function paginate($page)
    {
        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 array $codeResponse
     * @param string $info
     * @throws BaseException
     */
    public function throwBaseException(array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = '')
    {
        throw new BaseException($codeResponse, $info);
    }
}


四,创建项目异常捕获 Exception 文件

异常分为两种,一种是要给前端返回展示的,比如表单验证,一种是不需要给前端展示的,比如服务器内部错误。

  1. 在 app/Exceptions 目录下创建 BaseException.php 文件用于服务器内部异常的抛出
<?php

namespace App\Exceptions;

use Exception;
class BaseException extends Exception
{
    /**
     * 基础异常构造函数
     * @param array $codeResponse 状态码
     * @param string $info 自定义返回信息,不为空时会替换掉codeResponse 里面的message文字信息
     */
    public function __construct(array $codeResponse, $info = '')
    {
        [$code, $message] = $codeResponse;
        parent::__construct($info ?: $message, $code);
    }
}

  1. 自定义返回异常
    修改 app/Exceptions 目录下的 Handler.php 文件
<?php

namespace App\Exceptions;

use App\Enum\ResponseEnum;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;
use App\Helpers\ApiResponse;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class Handler extends ExceptionHandler
{
    use ApiResponse;

    /**
     * A list of exception types with their corresponding custom log levels.
     *
     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
     */
    protected $levels = [
        //
    ];

    /**
     * A list of the exception types that are not reported.
     *
     * @var array<int, class-string<\Throwable>>
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }


    public function render($request, Throwable $exception)
    {   
        //判断只接管api部分的异常
        if ($request->is('api/*')) {
            // 请求类型错误异常抛出
            if ($exception instanceof MethodNotAllowedHttpException) {
                $this->throwBaseException(ResponseEnum::CLIENT_METHOD_HTTP_TYPE_ERROR);
            } // 参数校验错误异常抛出
            elseif ($exception instanceof ValidationException) {
                $this->throwBaseException(ResponseEnum::CLIENT_PARAMETER_ERROR, $exception->getMessage());
            } // 路由不存在异常抛出
            elseif ($exception instanceof NotFoundHttpException) {
                $this->throwBaseException(ResponseEnum::CLIENT_NOT_FOUND_ERROR);
            } // 自定义错误异常抛出
            elseif ($exception instanceof BaseException) {
                return response()->json([
                    'status' => 'fail',
                    'code' => $exception->getCode(),
                    'message' => $exception->getMessage(),
                    'data' => null,
                ]);
                //系统异常
            } else {
                //生产模式
                if (config('app.debug')) {
                    return response()->json([
                        'status' => 'fail',
                        'code' => 500,
                        'message' => "服务器内部错误!",
                        'data' => null,
                    ]);
                } else {
                    return response()->json([
                        'status' => 'fail',
                        'code' => 500,
                        'message' => $exception->getMessage(),
                        'data' => null
                    ]);
                }

            }
        }

        return parent::render($request, $exception);

    }
}


五,使用

路由定义如下

Route::prefix('v1')->name('api.v1.')->group(function() {
    Route::get('version', function() {
        return 'this is version v1';
    })->name('version');
    
    //限速,每分钟只能请求10次
    Route::get('test', [TestController::class,'test'])->middleware(['throttle:10,1']);
    
    Route::post('login', [TestController::class,'login']);
    Route::post('register', [TestController::class,'register']);
    Route::group(['middleware' => 'auth.jwt'], function () {
        Route::get('user', [TestController::class,'user']);
        Route::get('logout', [TestController::class,'logout']);
        Route::get('refresh', [TestController::class,'refresh']);
    });
});
;