From 7a5a9edde32e68c1ca862d40651843f573652671 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 15 Oct 2025 13:52:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/filters/all-exception.filter.ts | 53 ++++++++++--- src/common/utils/error.util.ts | 89 ++++++++++++++++++++++ src/modules/bot/bot.controller.ts | 55 ++++++++++++- src/modules/bot/bot.dto.ts | 6 +- src/modules/cdn/cdn.controller.ts | 25 ++---- src/modules/meme/meme.controller.ts | 45 +++++------ src/modules/system/systemWeb.controller.ts | 34 +++++---- src/modules/words/words.controller.ts | 62 ++++++++++----- 8 files changed, 274 insertions(+), 95 deletions(-) create mode 100644 src/common/utils/error.util.ts diff --git a/src/common/filters/all-exception.filter.ts b/src/common/filters/all-exception.filter.ts index 7261b62..c5aea0a 100644 --- a/src/common/filters/all-exception.filter.ts +++ b/src/common/filters/all-exception.filter.ts @@ -4,28 +4,63 @@ import { ExceptionFilter, HttpException, HttpStatus, + Logger, } from '@nestjs/common'; /** - * 异常类 + * 全局异常过滤器 */ @Catch() export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - const status = - exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; + const request = ctx.getRequest(); - const message = - exception instanceof HttpException ? exception.message : '服务器内部错误'; + let status: number; + let message: string; + let errorDetails: any = null; - response.status(status).json({ + if (exception instanceof HttpException) { + status = exception.getStatus(); + message = exception.message; + + // 获取HttpException的详细信息 + const exceptionResponse = exception.getResponse(); + if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { + errorDetails = exceptionResponse; + } + } else { + status = HttpStatus.INTERNAL_SERVER_ERROR; + + // 处理非HttpException的错误 + if (exception instanceof Error) { + message = exception.message || '服务器内部错误'; + errorDetails = { + name: exception.name, + stack: process.env.NODE_ENV === 'development' ? exception.stack : undefined, + }; + } else { + message = '服务器内部错误'; + errorDetails = String(exception); + } + } + + // 记录错误日志 + this.logger.error( + `异常捕获 - ${request.method} ${request.url} - ${status} - ${message}`, + exception instanceof Error ? exception.stack : exception, + ); + + const errorResponse = { success: false, data: null, message, - }); + ...(process.env.NODE_ENV === 'development' && errorDetails && { errorDetails }), + }; + + response.status(status).json(errorResponse); } } diff --git a/src/common/utils/error.util.ts b/src/common/utils/error.util.ts new file mode 100644 index 0000000..6bdbde1 --- /dev/null +++ b/src/common/utils/error.util.ts @@ -0,0 +1,89 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +/** + * 错误处理工具类 + */ +export class ErrorUtil { + /** + * 创建业务异常 + * @param message 错误消息 + * @param status HTTP状态码 + * @param details 详细信息 + */ + static createBusinessError( + message: string, + status: HttpStatus = HttpStatus.BAD_REQUEST, + details?: any, + ): HttpException { + return new HttpException( + { + message, + details, + timestamp: new Date().toISOString(), + }, + status, + ); + } + + /** + * 创建服务器内部错误 + * @param message 错误消息 + * @param originalError 原始错误 + */ + static createInternalError(message: string, originalError?: any): HttpException { + return new HttpException( + { + message, + originalError: originalError?.message || originalError, + timestamp: new Date().toISOString(), + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + /** + * 创建资源未找到错误 + * @param resource 资源名称 + * @param identifier 资源标识符 + */ + static createNotFoundError(resource: string, identifier?: string): HttpException { + const message = identifier + ? `${resource} '${identifier}' 不存在` + : `${resource} 不存在`; + + return this.createBusinessError(message, HttpStatus.NOT_FOUND); + } + + /** + * 创建验证错误 + * @param message 错误消息 + * @param field 字段名 + */ + static createValidationError(message: string, field?: string): HttpException { + return new HttpException( + { + message, + field, + timestamp: new Date().toISOString(), + }, + HttpStatus.BAD_REQUEST, + ); + } + + /** + * 处理未知错误并转换为HttpException + * @param error 未知错误 + * @param defaultMessage 默认错误消息 + */ + static handleUnknownError(error: any, defaultMessage: string = '服务器内部错误'): HttpException { + if (error instanceof HttpException) { + return error; + } + + if (error instanceof Error) { + return this.createInternalError(`${defaultMessage}: ${error.message}`, error); + } + + return this.createInternalError(`${defaultMessage}: ${String(error)}`); + } +} diff --git a/src/modules/bot/bot.controller.ts b/src/modules/bot/bot.controller.ts index 9ceefa6..23c68d5 100644 --- a/src/modules/bot/bot.controller.ts +++ b/src/modules/bot/bot.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiTags, ApiBody } from '@nestjs/swagger'; +import { ApiOperation, ApiTags, ApiBody, ApiHeader } from '@nestjs/swagger'; import { BotService } from './bot.service'; import { WsClientManager } from 'src/core/ws/ws-client.manager'; import { TokenAuthGuard } from 'src/core/tools/token-auth.guard'; @@ -23,6 +23,14 @@ export class BotController { @Post('getBotId') @UseGuards(TokenAuthGuard) @ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '获取botId请求', + schema: { + type: 'object', + properties: {}, + }, + }) public async postBotsId(@Body() dto: TokenDto) { return this.botService.getBotId(); } @@ -30,7 +38,17 @@ export class BotController { @Post('getGroupInfo') @UseGuards(TokenAuthGuard) @ApiOperation({ summary: '获取群聊信息' }) - @ApiBody({ type: GroupInfoDto }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '获取群聊信息参数', + schema: { + type: 'object', + properties: { + groupId: { type: 'number', description: '群号', example: 114514 }, + }, + required: ['groupId'], + }, + }) public async postGroupInfo(@Body() dto: GroupInfoDto) { return this.botService.getGroupInfo({ groupId: dto.groupId }); } @@ -38,6 +56,14 @@ export class BotController { @Post('reportBots') @UseGuards(TokenAuthGuard) @ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '广播同步请求', + schema: { + type: 'object', + properties: {}, + }, + }) public async reportBots(@Body() dto: TokenDto) { const sendMessage = { type: 'reportBots', @@ -50,7 +76,18 @@ export class BotController { @Post('sendMessage') @UseGuards(TokenAuthGuard) @ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' }) - @ApiBody({ type: SendMessageDto }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '发送消息参数', + schema: { + type: 'object', + properties: { + groupId: { type: 'number', description: '群号', example: 114514 }, + message: { type: 'string', description: '要发送的消息', example: 'Ciallo~(∠・ω< )⌒★' }, + }, + required: ['groupId', 'message'], + }, + }) public async sendMessage(@Body() dto: SendMessageDto) { const flag = await this.botService.sendMessage(dto.groupId, dto.message); if (!flag) { @@ -62,7 +99,17 @@ export class BotController { @Post('broadcast') @UseGuards(TokenAuthGuard) @ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' }) - @ApiBody({ type: BroadcastDto }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '广播消息参数', + schema: { + type: 'object', + properties: { + message: { type: 'string', description: '要广播的消息', example: '全体目光向我看齐!我宣布个事儿..' }, + }, + required: ['message'], + }, + }) public async smartBroadcast(@Body() dto: BroadcastDto) { await this.botService.broadcastToAllGroups(dto.message); return { message: '广播任务已开始,正在后台执行..' }; diff --git a/src/modules/bot/bot.dto.ts b/src/modules/bot/bot.dto.ts index 9e83776..7794b6e 100644 --- a/src/modules/bot/bot.dto.ts +++ b/src/modules/bot/bot.dto.ts @@ -1,11 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; export class TokenDto { - @ApiProperty({ description: '访问核心的鉴权 token' }) - token: string; } -export class GroupInfoDto extends TokenDto { +export class GroupInfoDto { @ApiProperty({ description: '群号', example: 114514 }) groupId: number; } @@ -15,7 +13,7 @@ export class SendMessageDto extends GroupInfoDto { message: string; } -export class BroadcastDto extends TokenDto { +export class BroadcastDto { @ApiProperty({ description: '要广播的消息', example: '全体目光向我看齐!我宣布个事儿..', diff --git a/src/modules/cdn/cdn.controller.ts b/src/modules/cdn/cdn.controller.ts index 5a76c2e..81cf7f2 100644 --- a/src/modules/cdn/cdn.controller.ts +++ b/src/modules/cdn/cdn.controller.ts @@ -1,17 +1,8 @@ -import { - Controller, - Get, - Param, - Res, - Logger, - HttpException, - HttpStatus, - Inject, - Req, -} from '@nestjs/common'; +import { Controller, Get, Res, Logger, Inject, Req } from '@nestjs/common'; import { CdnService } from './cdn.service'; import { Response } from 'express'; import { ApiOperation } from '@nestjs/swagger'; +import { ErrorUtil } from '../../common/utils/error.util'; @Controller() export class CdnController { @@ -26,26 +17,20 @@ export class CdnController { const filePath = await this.fileService.getFile(relativePath); if (!filePath) { this.logger.warn(`${relativePath}:文件不存在..`); - throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND); + throw ErrorUtil.createNotFoundError('文件', relativePath); } res.sendFile(filePath, (err) => { if (err) { this.logger.error(`文件投递失败: ${err.message}`); - throw new HttpException( - 'Crystelf-CDN处理文件请求时出错..', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw ErrorUtil.createInternalError('文件投递失败', err); } }); this.logger.log(`成功投递文件: ${filePath}`); } catch (error) { this.logger.error('晶灵数据请求处理失败:', error); - throw new HttpException( - 'Crystelf-CDN处理文件请求时出错..', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw ErrorUtil.handleUnknownError(error, 'CDN处理文件请求失败'); } } diff --git a/src/modules/meme/meme.controller.ts b/src/modules/meme/meme.controller.ts index 37e631b..f329af4 100644 --- a/src/modules/meme/meme.controller.ts +++ b/src/modules/meme/meme.controller.ts @@ -5,7 +5,6 @@ import { Body, Query, Res, - HttpException, HttpStatus, Logger, Inject, @@ -35,6 +34,7 @@ import { OpenListService } from '../../core/openlist/openlist.service'; import { PathService } from '../../core/path/path.service'; import { TokenAuthGuard } from '../../core/tools/token-auth.guard'; import { AppConfigService } from '../../config/config.service'; +import { ErrorUtil } from '../../common/utils/error.util'; class MemeRequestDto { character?: string; @@ -64,10 +64,17 @@ export class MemeController { @Post('get') @ApiOperation({ summary: '获取随机表情包' }) - @ApiQuery({ name: 'character', required: false, description: '角色名称' }) - @ApiQuery({ name: 'status', required: false, description: '状态' }) - @ApiQuery({ name: 'token', required: false, description: '可选访问令牌' }) - @ApiBody({ type: MemeRequestDto }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: false }) + @ApiBody({ + description: '获取表情包参数', + schema: { + type: 'object', + properties: { + character: { type: 'string', description: '角色名称' }, + status: { type: 'string', description: '状态' }, + }, + }, + }) public async getRandomMemePost( @Body() dto: MemeRequestDto, @Res() res: Response, @@ -114,10 +121,7 @@ export class MemeController { ); if (!memePath) { - throw new HttpException( - '没有找到符合条件的表情包', - HttpStatus.NOT_FOUND, - ); + throw ErrorUtil.createNotFoundError('表情包'); } const ext = memePath.split('.').pop()?.toLowerCase(); @@ -129,11 +133,8 @@ export class MemeController { res.setHeader('Content-Type', contentType); const stream = fs.createReadStream(memePath); - stream.on('error', () => { - throw new HttpException( - '读取表情包失败', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + stream.on('error', (error) => { + throw ErrorUtil.createInternalError('读取表情包失败', error); }); const fd = await fs.promises.open(memePath, 'r'); @@ -177,7 +178,7 @@ export class MemeController { } } catch (e) { this.logger.error(`获取表情包失败:${e.message}`); - throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); + throw ErrorUtil.handleUnknownError(e, '获取表情包失败'); } } @@ -186,7 +187,6 @@ export class MemeController { * @param file * @param character * @param status - * @param res */ @Post('upload') @ApiOperation({ summary: '上传表情包并同步' }) @@ -211,7 +211,7 @@ export class MemeController { @Body('status') status: string, ) { if (!file) { - throw new HttpException('未检测到上传文件', HttpStatus.BAD_REQUEST); + throw ErrorUtil.createValidationError('未检测到上传文件'); } try { @@ -226,7 +226,7 @@ export class MemeController { const buffer = file.buffer || (await fsp.readFile(file.path)); const imgType = await imageType(buffer); if (!imgType || !['jpg', 'png', 'gif', 'webp'].includes(imgType.ext)) { - throw new HttpException( + throw ErrorUtil.createBusinessError( '不支持的图片格式', HttpStatus.UNSUPPORTED_MEDIA_TYPE, ); @@ -243,10 +243,10 @@ export class MemeController { ) { fileList = listResult.data.content.map((f) => f.name); } else { - this.logger.warn(`目录为空或返回结构异常:${remoteDir}`); + this.logger.warn(`目录为空或返回结构异常:${remoteDir}`); } } catch (err) { - this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`); + this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`); } const usedNumbers = fileList @@ -272,10 +272,7 @@ export class MemeController { return '表情上传成功..'; } catch (error) { this.logger.error('表情包上传失败:', error); - throw new HttpException( - `上传失败: ${error.message || error}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw ErrorUtil.handleUnknownError(error, '上传失败'); } } } diff --git a/src/modules/system/systemWeb.controller.ts b/src/modules/system/systemWeb.controller.ts index 39a45fb..8e85671 100644 --- a/src/modules/system/systemWeb.controller.ts +++ b/src/modules/system/systemWeb.controller.ts @@ -1,16 +1,8 @@ -import { Controller, Post, Inject, UseGuards, Param } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger'; +import { Controller, Post, Inject, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBody, ApiHeader } from '@nestjs/swagger'; import { SystemWebService } from './systemWeb.service'; import { TokenAuthGuard } from '../../core/tools/token-auth.guard'; -class WebServerDto { - @ApiProperty({ - description: '密钥', - example: '1111', - }) - token: string; -} - @ApiTags('System') @Controller('system') export class SystemWebController { @@ -28,8 +20,15 @@ export class SystemWebController { description: '核心执行重启', }) @UseGuards(TokenAuthGuard) - @ApiBody({ type: WebServerDto }) - public async systemRestart(@Param('token') token: string): Promise { + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '系统重启请求', + schema: { + type: 'object', + properties: {}, + }, + }) + public async systemRestart(): Promise { this.systemService.systemRestart(); return '核心正在重启..'; } @@ -43,8 +42,15 @@ export class SystemWebController { description: '返回上次核心重启的耗时', }) @UseGuards(TokenAuthGuard) - @ApiBody({ type: WebServerDto }) - public async getRestartTime(@Param('token') token: string): Promise { + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '获取重启时间请求', + schema: { + type: 'object', + properties: {}, + }, + }) + public async getRestartTime(): Promise { return await this.systemService.getRestartTime(); } } diff --git a/src/modules/words/words.controller.ts b/src/modules/words/words.controller.ts index a75be51..c6b4027 100644 --- a/src/modules/words/words.controller.ts +++ b/src/modules/words/words.controller.ts @@ -1,7 +1,6 @@ import { Controller, Post, - HttpException, HttpStatus, Logger, Inject, @@ -10,7 +9,8 @@ import { } from '@nestjs/common'; import { WordsService } from './words.service'; import { TokenAuthGuard } from '../../core/tools/token-auth.guard'; -import { ApiBody, ApiOperation, ApiProperty } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiProperty, ApiHeader } from '@nestjs/swagger'; +import { ErrorUtil } from '../../common/utils/error.util'; class WordsDto { @ApiProperty({ description: '文案类型', example: 'poke' }) @@ -27,10 +27,7 @@ class WordsDto { name?: string; } -class WordsReloadDto extends WordsDto { - @ApiProperty({ description: '密钥', example: '1111' }) - token: string; -} +class WordsReloadDto extends WordsDto {} @Controller('words') export class WordsController { @@ -45,15 +42,27 @@ export class WordsController { */ @Post('getText') @ApiOperation({ summary: '获取随机文案' }) - @ApiBody({ type: WordsDto }) + @ApiBody({ + description: '获取文案参数', + schema: { + type: 'object', + properties: { + type: { type: 'string', description: '文案类型', example: 'poke' }, + id: { type: 'string', description: '文案名称', example: 'poke' }, + name: { + type: 'string', + description: '可选参数:替换文案中的人名', + example: '坤坤', + }, + }, + required: ['type', 'id'], + }, + }) public async getText(@Body() dto: WordsDto) { try { const texts = await this.wordsService.loadWord(dto.type, dto.id); if (!texts || texts.length === 0) { - throw new HttpException( - `文案 ${dto.type}/${dto.id} 不存在或为空..`, - HttpStatus.NOT_FOUND, - ); + throw ErrorUtil.createNotFoundError('文案', `${dto.type}/${dto.id}`); } const randomIndex = Math.floor(Math.random() * texts.length); @@ -65,7 +74,7 @@ export class WordsController { return text; } catch (e) { this.logger.error(`getText 失败: ${e}`); - throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); + throw ErrorUtil.handleUnknownError(e, '获取文案失败'); } } @@ -75,18 +84,34 @@ export class WordsController { @Post('reloadText') @ApiOperation({ summary: '重载某条文案' }) @UseGuards(TokenAuthGuard) - @ApiBody({ type: WordsReloadDto }) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @ApiBody({ + description: '重载文案参数', + schema: { + type: 'object', + properties: { + type: { type: 'string', description: '文案类型', example: 'poke' }, + id: { type: 'string', description: '文案名称', example: 'poke' }, + name: { + type: 'string', + description: '可选参数:替换文案中的人名', + example: '坤坤', + }, + }, + required: ['type', 'id'], + }, + }) public async reloadWord(@Body() dto: WordsReloadDto) { try { const success = await this.wordsService.reloadWord(dto.type, dto.id); if (success) { return '成功重载..'; } else { - throw new HttpException('重载失败..', HttpStatus.BAD_REQUEST); + throw ErrorUtil.createBusinessError('重载失败', HttpStatus.BAD_REQUEST); } } catch (e) { this.logger.error(`reloadWord 失败: ${e}`); - throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); + throw ErrorUtil.handleUnknownError(e, '重载文案失败'); } } @@ -105,15 +130,12 @@ export class WordsController { try { const names = await this.wordsService.listWordNames(type); if (names.length === 0) { - throw new HttpException( - `类型 ${type} 下没有可用文案或目录不存在..`, - HttpStatus.NOT_FOUND, - ); + throw ErrorUtil.createNotFoundError('文案类型', type); } return names; } catch (e) { this.logger.error(`listWords 失败: ${e}`); - throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); + throw ErrorUtil.handleUnknownError(e, '获取文案列表失败'); } } }