diff --git a/app/Constants/ErrorCode.php b/app/Constants/ErrorCode.php new file mode 100644 index 0000000..9047a1d --- /dev/null +++ b/app/Constants/ErrorCode.php @@ -0,0 +1,97 @@ + '请求参数验证失败。', + 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 => '未知错误。', + }; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..78e0092 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,181 @@ +> + */ + 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); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..1987fe6 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,50 @@ + + */ + 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> + */ + 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, + ], + ]; +} diff --git a/app/Http/Middleware/ValidateHeaders.php b/app/Http/Middleware/ValidateHeaders.php new file mode 100644 index 0000000..fadc272 --- /dev/null +++ b/app/Http/Middleware/ValidateHeaders.php @@ -0,0 +1,63 @@ +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); + } +} diff --git a/app/Traits/ApiResponse.php b/app/Traits/ApiResponse.php new file mode 100644 index 0000000..51f4cb2 --- /dev/null +++ b/app/Traits/ApiResponse.php @@ -0,0 +1,109 @@ + 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); + } +}