Handler.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. <?php
  2. namespace App\Exceptions;
  3. use Exception;
  4. use Throwable;
  5. use Illuminate\Support\Facades\Log;
  6. use App\Support\AsyncExceptionRecorder;
  7. use Illuminate\Auth\AuthenticationException;
  8. use Illuminate\Session\TokenMismatchException;
  9. use Illuminate\Validation\ValidationException;
  10. use Symfony\Component\HttpFoundation\Response;
  11. use Illuminate\Database\Eloquent\ModelNotFoundException;
  12. use Symfony\Component\HttpKernel\Exception\HttpException;
  13. use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
  14. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  15. class Handler extends ExceptionHandler
  16. {
  17. /**
  18. * The list of the inputs that are never flashed to the session on validation exceptions.
  19. *
  20. * @var array<int, string>
  21. */
  22. protected $dontFlash = [
  23. 'current_password',
  24. 'password',
  25. 'password_confirmation',
  26. ];
  27. /**
  28. * 统一的响应格式
  29. */
  30. private function formatJsonResponse(int $code, string $message, int $httpCode = 200, array $data = null): \Illuminate\Http\JsonResponse
  31. {
  32. return response()->json([
  33. 'code' => $code,
  34. 'message' => $message,
  35. 'data' => $data,
  36. ], $httpCode);
  37. }
  38. /**
  39. * 统一的日志记录
  40. */
  41. private function logException(Throwable $e, string $level = 'error', array $extra = []): void
  42. {
  43. $context = [
  44. 'exception' => get_class($e),
  45. 'file' => $e->getFile(),
  46. 'line' => $e->getLine(),
  47. 'trace' => $e->getTraceAsString(),
  48. 'url' => request()->fullUrl(),
  49. 'method' => request()->method(),
  50. 'ip' => request()->ip(),
  51. 'user_agent' => request()->userAgent(),
  52. 'user_id' => auth()->id(),
  53. ...$extra
  54. ];
  55. Log::{$level}($e->getMessage(), $context);
  56. }
  57. /**
  58. * Register the exception handling callbacks for the application.
  59. */
  60. public function register(): void
  61. {
  62. // 记录所有异常到数据库(异步)
  63. $this->reportable(function (Throwable $e) {
  64. // 预处理请求数据,确保可以被 JSON 序列化
  65. $requestData = [
  66. 'url' => request()->fullUrl(),
  67. 'method' => request()->method(),
  68. 'params' => json_decode(json_encode(request()->all(), JSON_INVALID_UTF8_SUBSTITUTE), true) ?? [],
  69. 'headers' => array_map(function ($item) {
  70. return is_array($item) ? implode(', ', $item) : $item;
  71. }, request()->headers->all()),
  72. ];
  73. // 异步分发任务
  74. AsyncExceptionRecorder::dispatch(
  75. get_class($e),
  76. $e->getMessage(),
  77. $e->getFile(),
  78. $e->getLine(),
  79. $e->getTraceAsString(),
  80. $requestData, // 使用处理过的数据
  81. auth()->id()
  82. );
  83. // 记录日志
  84. if ($e instanceof ValidationException) {
  85. $this->logException($e, 'info');
  86. } else {
  87. $this->logException($e);
  88. }
  89. });
  90. // 404异常
  91. $this->renderable(function (NotFoundHttpException $e) {
  92. return $this->formatJsonResponse(Response::HTTP_NOT_FOUND, '请求的资源不存在', 200);
  93. });
  94. // 模型未找到异常
  95. $this->renderable(function (ModelNotFoundException $e) {
  96. return $this->formatJsonResponse(404, '请求的数据不存在', Response::HTTP_NOT_FOUND);
  97. });
  98. // CSRF Token异常
  99. $this->renderable(function (TokenMismatchException $e) {
  100. return $this->formatJsonResponse(419, 'CSRF Token 已过期,请刷新页面重试', 419);
  101. });
  102. // 认证异常
  103. $this->renderable(function (AuthenticationException $e) {
  104. return $this->formatJsonResponse(401, '请先登录', Response::HTTP_UNAUTHORIZED);
  105. });
  106. // 验证异常
  107. $this->renderable(function (ValidationException $e) {
  108. return $this->formatJsonResponse(Response::HTTP_UNPROCESSABLE_ENTITY, $e->validator->errors()->first(), 200);
  109. });
  110. // HTTP异常
  111. $this->renderable(function (HttpException $e) {
  112. return $this->formatJsonResponse($e->getStatusCode(), $e->getMessage(), 200);
  113. });
  114. // 业务异常
  115. $this->renderable(function (ApiException $e) {
  116. return $this->formatJsonResponse($e->getCode() ?: 400, $e->getMessage());
  117. });
  118. // 其他所有异常
  119. $this->renderable(function (Throwable $e) {
  120. if (config('app.debug')) {
  121. throw $e;
  122. }
  123. return $this->formatJsonResponse(500, '服务器内部错误', Response::HTTP_INTERNAL_SERVER_ERROR);
  124. });
  125. }
  126. /**
  127. * 渲染异常,所有异常都返回 统一的JSON 格式
  128. * @param mixed $request
  129. * @param \Throwable $exception
  130. * @return mixed|Response|\Illuminate\Http\JsonResponse
  131. */
  132. public function render($request, Throwable $exception)
  133. {
  134. if ($request->expectsJson()) {
  135. if ($exception instanceof Exception) {
  136. return response()->json([
  137. 'code' => $exception->getCode() ?: 500,
  138. 'message' => $exception->getMessage(),
  139. 'data' => null,
  140. ], status: Response::HTTP_INTERNAL_SERVER_ERROR);
  141. }
  142. }
  143. return parent::render($request, $exception);
  144. }
  145. }