121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
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<Response>();
|
||
|
||
// 非 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<string, unknown>;
|
||
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;
|
||
}
|
||
}
|
||
}
|