123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- <?php
- namespace App\Exceptions;
- use Exception;
- use Throwable;
- use Illuminate\Support\Facades\Log;
- use App\Support\AsyncExceptionRecorder;
- use Illuminate\Auth\AuthenticationException;
- use Illuminate\Session\TokenMismatchException;
- use Illuminate\Validation\ValidationException;
- use Symfony\Component\HttpFoundation\Response;
- use Illuminate\Database\Eloquent\ModelNotFoundException;
- use Symfony\Component\HttpKernel\Exception\HttpException;
- use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
- use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
- class Handler extends ExceptionHandler
- {
- /**
- * The 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',
- ];
- /**
- * 统一的响应格式
- */
- 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) {
- // 预处理请求数据,确保可以被 JSON 序列化
- $requestData = [
- 'url' => request()->fullUrl(),
- 'method' => request()->method(),
- 'params' => json_decode(json_encode(request()->all(), JSON_INVALID_UTF8_SUBSTITUTE), true) ?? [],
- 'headers' => array_map(function ($item) {
- return is_array($item) ? implode(', ', $item) : $item;
- }, request()->headers->all()),
- ];
- // 异步分发任务
- AsyncExceptionRecorder::dispatch(
- get_class($e),
- $e->getMessage(),
- $e->getFile(),
- $e->getLine(),
- $e->getTraceAsString(),
- $requestData, // 使用处理过的数据
- auth()->id()
- );
- // 记录日志
- if ($e instanceof ValidationException) {
- $this->logException($e, 'info');
- } else {
- $this->logException($e);
- }
- });
- // 404异常
- $this->renderable(function (NotFoundHttpException $e) {
- return $this->formatJsonResponse(Response::HTTP_NOT_FOUND, '请求的资源不存在', 200);
- });
- // 模型未找到异常
- $this->renderable(function (ModelNotFoundException $e) {
- return $this->formatJsonResponse(404, '请求的数据不存在', Response::HTTP_NOT_FOUND);
- });
- // CSRF Token异常
- $this->renderable(function (TokenMismatchException $e) {
- return $this->formatJsonResponse(419, 'CSRF Token 已过期,请刷新页面重试', 419);
- });
- // 认证异常
- $this->renderable(function (AuthenticationException $e) {
- return $this->formatJsonResponse(401, '请先登录', Response::HTTP_UNAUTHORIZED);
- });
- // 验证异常
- $this->renderable(function (ValidationException $e) {
- return $this->formatJsonResponse(Response::HTTP_UNPROCESSABLE_ENTITY, $e->validator->errors()->first(), 200);
- });
- // HTTP异常
- $this->renderable(function (HttpException $e) {
- return $this->formatJsonResponse($e->getStatusCode(), $e->getMessage(), 200);
- });
- // 业务异常
- $this->renderable(function (ApiException $e) {
- return $this->formatJsonResponse($e->getCode() ?: 400, $e->getMessage());
- });
- // 其他所有异常
- $this->renderable(function (Throwable $e) {
- 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 Exception) {
- return response()->json([
- 'code' => $exception->getCode() ?: 500,
- 'message' => $exception->getMessage(),
- 'data' => null,
- ], status: Response::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- return parent::render($request, $exception);
- }
- }
|