tyt-api-nest/src/common/http-exception.filter.ts
EL 73082225f6 "1. 新增系统字典与全局植入目录相关表结构及迁移
2. 扩展患者手术与材料模型,更新种子数据
3. 新增字典模块,增强设备植入目录管理能力
4. 重构患者后台服务与表单链路,统一权限与参数校验
5. 管理台新增字典页面并改造患者/设备页面与路由权限
6. 补充字典及相关领域 e2e 测试并更新文档"
2026-03-19 20:42:17 +08:00

118 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}