Browse Source

feat:统一异常处理

Yin Bin 4 months ago
parent
commit
3dc184ac87
2 changed files with 79 additions and 43 deletions
  1. 6 2
      app/Exceptions/ApiException.php
  2. 73 41
      app/Exceptions/Handler.php

+ 6 - 2
app/Exceptions/ApiException.php

@@ -1,4 +1,5 @@
 <?php
+
 /**
  * @Name
  *
@@ -13,10 +14,13 @@ namespace App\Exceptions;
 
 use Exception;
 
+/**
+ * 业务异常
+ */
 class ApiException extends Exception
 {
-    public function __construct(array $apiErrorConst, ?\Throwable $previous = null)
+    public function __construct(string $message = "", int $code = 400)
     {
-        // parent::__construct($apiErrorConst['message'], $apiErrorConst['code'], $previous);
+        parent::__construct($message, $code);
     }
 }

+ 73 - 41
app/Exceptions/Handler.php

@@ -4,11 +4,13 @@ namespace App\Exceptions;
 
 use Exception;
 use Illuminate\Auth\AuthenticationException;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
 use Illuminate\Session\TokenMismatchException;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Validation\ValidationException;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Throwable;
 
@@ -25,82 +27,112 @@ class Handler extends ExceptionHandler
         'password_confirmation',
     ];
 
+    /**
+     * 统一的响应格式
+     */
+    private function formatJsonResponse(int $code, string $message, int $httpCode = 200, array $data = null): \Illuminate\Http\JsonResponse
+    {
+        return response()->json([
+            'code' => $code,
+            'message' => $message,
+            'data' => $data,
+        ], $httpCode);
+    }
+
+    /**
+     * 统一的日志记录
+     */
+    private function logException(Throwable $e, string $level = 'error', array $extra = []): void
+    {
+        $context = [
+            'exception' => get_class($e),
+            'file' => $e->getFile(),
+            'line' => $e->getLine(),
+            'trace' => $e->getTraceAsString(),
+            'url' => request()->fullUrl(),
+            'method' => request()->method(),
+            'ip' => request()->ip(),
+            'user_agent' => request()->userAgent(),
+            'user_id' => auth()->id(),
+            ...$extra
+        ];
+
+        Log::{$level}($e->getMessage(), $context);
+    }
+
     /**
      * Register the exception handling callbacks for the application.
      */
     public function register(): void
     {
+        // 记录所有异常
         $this->reportable(function (Throwable $e) {
-            $context = [
-                'exception' => get_class($e),
-                'file' => $e->getFile(),
-                'line' => $e->getLine(),
-                'trace' => $e->getTraceAsString(),
-                'url' => request()->fullUrl(),
-                'method' => request()->method(),
-                'ip' => request()->ip(),
-                'user_agent' => request()->userAgent(),
-            ];
-
-            if ($e instanceof NotFoundHttpException) {
-                Log::error('Route not found: '.request()->fullUrl(), $context);
+            if ($e instanceof ValidationException) {
+                $this->logException($e, 'info');
             } else {
-                Log::error($e->getMessage(), $context);
+                $this->logException($e);
             }
         });
 
+        // 404异常
         $this->renderable(function (NotFoundHttpException $e) {
-            return response()->json([
-                'code' => 404,
-                'msg' => 'Route not found: '.request()->fullUrl(),
-            ], Response::HTTP_NOT_FOUND);
+            return $this->formatJsonResponse(404, '请求的资源不存在', Response::HTTP_NOT_FOUND);
         });
 
+        // 模型未找到异常
+        $this->renderable(function (ModelNotFoundException $e) {
+            return $this->formatJsonResponse(404, '请求的数据不存在', Response::HTTP_NOT_FOUND);
+        });
+
+        // CSRF Token异常
         $this->renderable(function (TokenMismatchException $e) {
-            return response()->json([
-                'code' => $e->getCode() ?: 419,
-                'msg' => $e->getMessage(),
-            ], 419);
+            return $this->formatJsonResponse(419, 'CSRF Token 已过期,请刷新页面重试', 419);
         });
 
+        // 认证异常
         $this->renderable(function (AuthenticationException $e) {
-            return response()->json([
-                'code' => $e->getCode() ?: 401,
-                'msg' => '登录超时!',
-            ], Response::HTTP_UNAUTHORIZED);
+            return $this->formatJsonResponse(401, '请先登录', Response::HTTP_UNAUTHORIZED);
         });
 
+        // 验证异常
         $this->renderable(function (ValidationException $e) {
-            return response()->json([
-                'code' => $e->getCode() ?: 422,
-                'msg' => $e->getMessage(),
-            ], Response::HTTP_UNPROCESSABLE_ENTITY);
+            return $this->formatJsonResponse(422, $e->validator->errors()->first(), Response::HTTP_UNPROCESSABLE_ENTITY);
         });
 
+        // HTTP异常
+        $this->renderable(function (HttpException $e) {
+            return $this->formatJsonResponse($e->getStatusCode(), $e->getMessage(), $e->getStatusCode());
+        });
+
+        // 业务异常
         $this->renderable(function (ApiException $e) {
-            return response()->json([
-                'code' => $e->getCode() ?: 200,
-                'msg' => $e->getMessage(),
-            ], 200);
+            return $this->formatJsonResponse($e->getCode() ?: 400, $e->getMessage());
         });
 
+        // 其他所有异常
         $this->renderable(function (Throwable $e) {
-            return response()->json([
-                'code' => $e->getCode() ?: 500,
-                'msg' => $e->getMessage(),
-            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+            if (config('app.debug')) {
+                throw $e;
+            }
+            return $this->formatJsonResponse(500, '服务器内部错误', Response::HTTP_INTERNAL_SERVER_ERROR);
         });
     }
 
+    /**
+     * 渲染异常,所有异常都返回 统一的JSON 格式
+     * @param mixed $request
+     * @param \Throwable $exception
+     * @return mixed|Response|\Illuminate\Http\JsonResponse
+     */
     public function render($request, Throwable $exception)
     {
         if ($request->expectsJson()) {
-            if ($exception instanceof HttpException) {
+            if ($exception instanceof Exception) {
                 return response()->json([
-                    'code' => $exception->getStatusCode(),
+                    'code' => $exception->getCode() ?: 500,
                     'message' => $exception->getMessage(),
                     'data' => null,
-                ], $exception->getStatusCode());
+                ], status: Response::HTTP_INTERNAL_SERVER_ERROR);
             }
         }