import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, } from '@nestjs/common'; import { Response } from 'express'; import { Prisma } from '../generated/prisma/client.js'; import { MESSAGES } from './messages.js'; /** * 全局异常过滤器:统一异常返回结构,并保证 msg 为中文。 */ @Catch() export class HttpExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(HttpExceptionFilter.name); catch(exception: unknown, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); // 非 HttpException 统一记录堆栈,便于定位 500 根因。 if (!(exception instanceof HttpException)) { const error = exception as { message?: string; stack?: string }; this.logger.error( error?.message ?? 'Unhandled exception', error?.stack, ); } const status = this.resolveStatus(exception); const msg = this.resolveMessage(exception, status); response.status(status).json({ code: status, msg, data: null, }); } /** * 解析 HTTP 状态码,非 HttpException 统一按 500 处理。 */ private resolveStatus(exception: unknown): number { if (exception instanceof Prisma.PrismaClientInitializationError) { return HttpStatus.SERVICE_UNAVAILABLE; } if (exception instanceof Prisma.PrismaClientKnownRequestError) { switch (exception.code) { case 'P2002': return HttpStatus.CONFLICT; case 'P2025': return HttpStatus.NOT_FOUND; default: return HttpStatus.INTERNAL_SERVER_ERROR; } } if (exception instanceof HttpException) { return exception.getStatus(); } return HttpStatus.INTERNAL_SERVER_ERROR; } /** * 解析异常消息:优先使用业务抛出的 message;否则按状态码兜底中文。 */ private resolveMessage(exception: unknown, status: number): string { if (exception instanceof Prisma.PrismaClientInitializationError) { return MESSAGES.DB.CONNECTION_FAILED; } if (exception instanceof Prisma.PrismaClientKnownRequestError) { switch (exception.code) { case 'P2021': return MESSAGES.DB.TABLE_MISSING; case 'P2022': return MESSAGES.DB.COLUMN_MISSING; case 'P2002': return MESSAGES.DEFAULT_CONFLICT; case 'P2025': return MESSAGES.DEFAULT_NOT_FOUND; default: return MESSAGES.DEFAULT_INTERNAL_ERROR; } } if (exception instanceof HttpException) { const payload = exception.getResponse(); if (typeof payload === 'string') { return payload; } if (payload && typeof payload === 'object') { const body = payload as Record; const message = body.message; if (Array.isArray(message)) { return message.join(';'); } if (typeof message === 'string' && message.trim()) { return message; } } } switch (status) { case HttpStatus.BAD_REQUEST: return MESSAGES.DEFAULT_BAD_REQUEST; case HttpStatus.UNAUTHORIZED: return MESSAGES.DEFAULT_UNAUTHORIZED; case HttpStatus.FORBIDDEN: return MESSAGES.DEFAULT_FORBIDDEN; case HttpStatus.NOT_FOUND: return MESSAGES.DEFAULT_NOT_FOUND; case HttpStatus.CONFLICT: return MESSAGES.DEFAULT_CONFLICT; default: return MESSAGES.DEFAULT_INTERNAL_ERROR; } } }