5. [请求与响应格式](#5-请求与响应格式)
- [5.1 通用请求头](#51-通用请求头) - [5.2 响应格式](#52-响应格式) 6. [错误代码](#6-错误代码) 7. [安全性考虑](#7-安全性考虑)
This commit is contained in:
parent
31b69e318a
commit
c5258233a8
5 changed files with 500 additions and 0 deletions
97
app/Constants/ErrorCode.php
Normal file
97
app/Constants/ErrorCode.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Constants;
|
||||
|
||||
class ErrorCode
|
||||
{
|
||||
// 通用错误 (1000-1999)
|
||||
public const VALIDATION_ERROR = 'E1001';
|
||||
public const UNAUTHORIZED = 'E1002';
|
||||
public const FORBIDDEN = 'E1003';
|
||||
public const RESOURCE_NOT_FOUND = 'E1004';
|
||||
public const METHOD_NOT_ALLOWED = 'E1005';
|
||||
public const TOO_MANY_REQUESTS = 'E1006';
|
||||
public const SERVER_ERROR = 'E1007';
|
||||
public const SERVICE_UNAVAILABLE = 'E1008';
|
||||
|
||||
// 认证相关错误 (2000-2999)
|
||||
public const INVALID_CREDENTIALS = 'E2001';
|
||||
public const TOKEN_EXPIRED = 'E2002';
|
||||
public const TOKEN_INVALID = 'E2003';
|
||||
public const TOKEN_BLACKLISTED = 'E2004';
|
||||
public const TOKEN_NOT_FOUND = 'E2005';
|
||||
|
||||
// 客户相关错误 (3000-3999)
|
||||
public const CLIENT_NOT_FOUND = 'E3001';
|
||||
public const CLIENT_INACTIVE = 'E3002';
|
||||
public const CLIENT_RATE_LIMIT_EXCEEDED = 'E3003';
|
||||
public const CLIENT_QUOTA_EXCEEDED = 'E3004';
|
||||
public const CLIENT_HAS_ACTIVE_TOKENS = 'E3005';
|
||||
|
||||
// LLM 提供商相关错误 (4000-4999)
|
||||
public const PROVIDER_NOT_FOUND = 'E4001';
|
||||
public const PROVIDER_INACTIVE = 'E4002';
|
||||
public const PROVIDER_ERROR = 'E4003';
|
||||
public const PROVIDER_TIMEOUT = 'E4004';
|
||||
public const PROVIDER_RATE_LIMIT = 'E4005';
|
||||
|
||||
// 业务逻辑错误 (5000-5999)
|
||||
public const INVALID_REQUEST_FORMAT = 'E5001';
|
||||
public const INVALID_RESPONSE_FORMAT = 'E5002';
|
||||
public const OPERATION_FAILED = 'E5003';
|
||||
public const RESOURCE_ALREADY_EXISTS = 'E5004';
|
||||
public const RESOURCE_IN_USE = 'E5005';
|
||||
|
||||
/**
|
||||
* 获取错误代码对应的默认消息
|
||||
*
|
||||
* @param string $code 错误代码
|
||||
* @return string 错误消息
|
||||
*/
|
||||
public static function getMessage(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
// 通用错误
|
||||
self::VALIDATION_ERROR => '请求参数验证失败。',
|
||||
self::UNAUTHORIZED => '未经授权的访问。',
|
||||
self::FORBIDDEN => '无权访问该资源。',
|
||||
self::RESOURCE_NOT_FOUND => '请求的资源不存在。',
|
||||
self::METHOD_NOT_ALLOWED => '请求方法不允许。',
|
||||
self::TOO_MANY_REQUESTS => '请求过于频繁,请稍后重试。',
|
||||
self::SERVER_ERROR => '服务器内部错误。',
|
||||
self::SERVICE_UNAVAILABLE => '服务暂时不可用。',
|
||||
|
||||
// 认证相关错误
|
||||
self::INVALID_CREDENTIALS => '无效的认证凭据。',
|
||||
self::TOKEN_EXPIRED => '认证令牌已过期。',
|
||||
self::TOKEN_INVALID => '无效的认证令牌。',
|
||||
self::TOKEN_BLACKLISTED => '认证令牌已被禁用。',
|
||||
self::TOKEN_NOT_FOUND => '认证令牌不存在。',
|
||||
|
||||
// 客户相关错误
|
||||
self::CLIENT_NOT_FOUND => '客户不存在。',
|
||||
self::CLIENT_INACTIVE => '客户状态为非活跃。',
|
||||
self::CLIENT_RATE_LIMIT_EXCEEDED => '已超过请求速率限制。',
|
||||
self::CLIENT_QUOTA_EXCEEDED => '已超过配额限制。',
|
||||
self::CLIENT_HAS_ACTIVE_TOKENS => '客户存在活跃的认证令牌。',
|
||||
|
||||
// LLM 提供商相关错误
|
||||
self::PROVIDER_NOT_FOUND => 'LLM 提供商不存在。',
|
||||
self::PROVIDER_INACTIVE => 'LLM 提供商状态为非活跃。',
|
||||
self::PROVIDER_ERROR => 'LLM 提供商服务错误。',
|
||||
self::PROVIDER_TIMEOUT => 'LLM 提供商服务超时。',
|
||||
self::PROVIDER_RATE_LIMIT => 'LLM 提供商限制请求速率。',
|
||||
|
||||
// 业务逻辑错误
|
||||
self::INVALID_REQUEST_FORMAT => '无效的请求格式。',
|
||||
self::INVALID_RESPONSE_FORMAT => '无效的响应格式。',
|
||||
self::OPERATION_FAILED => '操作失败。',
|
||||
self::RESOURCE_ALREADY_EXISTS => '资源已存在。',
|
||||
self::RESOURCE_IN_USE => '资源正在使用中。',
|
||||
|
||||
default => '未知错误。',
|
||||
};
|
||||
}
|
||||
}
|
||||
181
app/Exceptions/Handler.php
Normal file
181
app/Exceptions/Handler.php
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Constants\ErrorCode;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* API 路由前缀
|
||||
*/
|
||||
protected const API_PREFIX = 'api';
|
||||
|
||||
/**
|
||||
* 不需要报告的异常类型
|
||||
*
|
||||
* @var array<int, class-string<Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
AuthenticationException::class,
|
||||
AuthorizationException::class,
|
||||
ValidationException::class,
|
||||
ModelNotFoundException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* 注册异常处理回调
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->renderable(function (Throwable $e, Request $request) {
|
||||
if ($this->isApiRequest($request)) {
|
||||
return $this->handleApiException($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 API 异常
|
||||
*
|
||||
* @param Throwable $e
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function handleApiException(Throwable $e): JsonResponse
|
||||
{
|
||||
if ($e instanceof ValidationException) {
|
||||
return $this->error(
|
||||
ErrorCode::VALIDATION_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
|
||||
$e->errors(),
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof AuthenticationException) {
|
||||
return $this->error(
|
||||
ErrorCode::UNAUTHORIZED,
|
||||
ErrorCode::getMessage(ErrorCode::UNAUTHORIZED),
|
||||
null,
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof AuthorizationException) {
|
||||
return $this->error(
|
||||
ErrorCode::FORBIDDEN,
|
||||
ErrorCode::getMessage(ErrorCode::FORBIDDEN),
|
||||
null,
|
||||
Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof ModelNotFoundException) {
|
||||
return $this->error(
|
||||
ErrorCode::RESOURCE_NOT_FOUND,
|
||||
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND),
|
||||
null,
|
||||
Response::HTTP_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof NotFoundHttpException) {
|
||||
return $this->error(
|
||||
ErrorCode::RESOURCE_NOT_FOUND,
|
||||
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND),
|
||||
null,
|
||||
Response::HTTP_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof MethodNotAllowedHttpException) {
|
||||
return $this->error(
|
||||
ErrorCode::METHOD_NOT_ALLOWED,
|
||||
ErrorCode::getMessage(ErrorCode::METHOD_NOT_ALLOWED),
|
||||
null,
|
||||
Response::HTTP_METHOD_NOT_ALLOWED
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof TooManyRequestsHttpException) {
|
||||
return $this->error(
|
||||
ErrorCode::TOO_MANY_REQUESTS,
|
||||
ErrorCode::getMessage(ErrorCode::TOO_MANY_REQUESTS),
|
||||
null,
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
if ($e instanceof HttpException) {
|
||||
return $this->error(
|
||||
ErrorCode::SERVER_ERROR,
|
||||
$e->getMessage(),
|
||||
null,
|
||||
$e->getStatusCode()
|
||||
);
|
||||
}
|
||||
|
||||
// 记录未处理的异常
|
||||
$this->report($e);
|
||||
|
||||
return $this->error(
|
||||
ErrorCode::SERVER_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::SERVER_ERROR),
|
||||
config('app.debug') ? [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
] : null,
|
||||
Response::HTTP_INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 API 请求
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function isApiRequest(Request $request): bool
|
||||
{
|
||||
return $request->is(static::API_PREFIX . '/*') || $request->expectsJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误响应
|
||||
*
|
||||
* @param string $error 错误代码
|
||||
* @param string $message 错误消息
|
||||
* @param mixed $errors 详细错误信息
|
||||
* @param int $code HTTP状态码
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function error(string $error, string $message, mixed $errors = null, int $code = Response::HTTP_BAD_REQUEST): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'error' => $error,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($errors !== null) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
}
|
||||
50
app/Http/Kernel.php
Normal file
50
app/Http/Kernel.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
\App\Http\Middleware\ValidateHeaders::class,
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
63
app/Http/Middleware/ValidateHeaders.php
Normal file
63
app/Http/Middleware/ValidateHeaders.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Constants\ErrorCode;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateHeaders
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 检查 Content-Type
|
||||
if ($request->isMethod('POST') || $request->isMethod('PUT') || $request->isMethod('PATCH')) {
|
||||
if (!$request->hasHeader('Content-Type') || !str_contains($request->header('Content-Type'), 'application/json')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
|
||||
'message' => '请求头必须包含 Content-Type: application/json',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 Accept
|
||||
if (!$request->hasHeader('Accept') || !str_contains($request->header('Accept'), 'application/json')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
|
||||
'message' => '请求头必须包含 Accept: application/json',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 检查 API 版本
|
||||
if (!$request->hasHeader('X-API-Version')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
|
||||
'message' => '请求头必须包含 X-API-Version',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 检查客户端标识
|
||||
if (!$request->hasHeader('X-Client-ID')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
|
||||
'message' => '请求头必须包含 X-Client-ID',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
109
app/Traits/ApiResponse.php
Normal file
109
app/Traits/ApiResponse.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
trait ApiResponse
|
||||
{
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param mixed $data 响应数据
|
||||
* @param string|null $message 成功消息
|
||||
* @param int $code HTTP状态码
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function success(mixed $data = null, ?string $message = null, int $code = Response::HTTP_OK): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
if ($message !== null) {
|
||||
$response['message'] = $message;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*
|
||||
* @param string $error 错误代码
|
||||
* @param string $message 错误消息
|
||||
* @param mixed $errors 详细错误信息
|
||||
* @param int $code HTTP状态码
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function error(
|
||||
string $error,
|
||||
string $message,
|
||||
mixed $errors = null,
|
||||
int $code = Response::HTTP_BAD_REQUEST
|
||||
): JsonResponse {
|
||||
$response = [
|
||||
'success' => false,
|
||||
'error' => $error,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($errors !== null) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*
|
||||
* @param array $items 分页数据
|
||||
* @param array $meta 分页元数据
|
||||
* @param string|null $message 成功消息
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function paginate(array $items, array $meta, ?string $message = null): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
'data' => $items,
|
||||
'meta' => $meta,
|
||||
];
|
||||
|
||||
if ($message !== null) {
|
||||
$response['message'] = $message;
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应
|
||||
*
|
||||
* @param mixed $data 创建的资源数据
|
||||
* @param string|null $message 成功消息
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function created(mixed $data, ?string $message = null): JsonResponse
|
||||
{
|
||||
return $this->success($data, $message, Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无内容响应
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function noContent(): JsonResponse
|
||||
{
|
||||
return response()->json(null, Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue