From fc2ffeb14573434c2d4a005899545d99d632a1e9 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 15 Oct 2025 15:16:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=9D=E5=AD=98=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interceptors/request-log.interceptor.ts | 108 ++++++++++++++++++ src/main.ts | 3 +- src/modules/words/words.service.ts | 4 +- 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/common/interceptors/request-log.interceptor.ts diff --git a/src/common/interceptors/request-log.interceptor.ts b/src/common/interceptors/request-log.interceptor.ts new file mode 100644 index 0000000..dd793f3 --- /dev/null +++ b/src/common/interceptors/request-log.interceptor.ts @@ -0,0 +1,108 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface RequestLogEntry { + timestamp: string; + ip: string | string[] | undefined; + method: string; + url: string; + controller: string; + handler: string; + userAgent?: string; + params?: any; + query?: any; + body?: any; + statusCode?: number; + durationMs?: number; +} + +@Injectable() +export class RequestLogInterceptor implements NestInterceptor { + private readonly logger = new Logger(RequestLogInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const http = context.switchToHttp(); + const req = http.getRequest(); + const res = http.getResponse(); + + const now = Date.now(); + const controller = context.getClass().name; + const handler = context.getHandler().name; + const ip = req.headers['x-forwarded-for'] || req.ip; + const method = req.method; + const url = req.originalUrl || req.url; + const userAgent = req.headers['user-agent']; + this.logger.log( + `${method} ${url} - ${controller}.${handler} - ip=${Array.isArray(ip) ? ip[0] : ip}`, + ); + + return next.handle().pipe( + tap({ + next: () => { + this.writeLog({ + timestamp: new Date(now).toISOString(), + ip, + method, + url, + controller, + handler, + userAgent, + params: req.params, + query: req.query, + body: req.body, + statusCode: res.statusCode, + durationMs: Date.now() - now, + }); + }, + error: () => { + this.writeLog({ + timestamp: new Date(now).toISOString(), + ip, + method, + url, + controller, + handler, + userAgent, + params: req.params, + query: req.query, + body: req.body, + statusCode: res.statusCode, + durationMs: Date.now() - now, + }); + }, + }), + ); + } + + private writeLog(entry: RequestLogEntry) { + try { + const baseDir = path.resolve(process.cwd(), 'logs'); + if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir, { recursive: true }); + } + const fileName = `access-${this.getDateStr()}.jsonl`; + const filePath = path.join(baseDir, fileName); + const serialized = JSON.stringify(entry) + '\n'; + fs.appendFile(filePath, serialized, { encoding: 'utf8' }, () => {}); + } catch (err) { + this.logger.warn(`写入访问日志失败: ${err?.message || err}`); + } + } + + private getDateStr(): string { + const d = new Date(); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; + } +} diff --git a/src/main.ts b/src/main.ts index 4951931..fbdfe2d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { AppModule } from './app.module'; import { Logger, RequestMethod } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; +import { RequestLogInterceptor } from './common/interceptors/request-log.interceptor'; import { AllExceptionsFilter } from './common/filters/all-exception.filter'; import { SystemService } from './core/system/system.service'; import { WsAdapter } from '@nestjs/platform-ws'; @@ -35,7 +36,7 @@ async function bootstrap() { { path: 'public/(.*)', method: RequestMethod.ALL }, ], }); - app.useGlobalInterceptors(new ResponseInterceptor()); + app.useGlobalInterceptors(new RequestLogInterceptor(), new ResponseInterceptor()); app.useGlobalFilters(new AllExceptionsFilter()); const systemService = app.get(SystemService); const restartDuration = systemService.checkRestartTime(); diff --git a/src/modules/words/words.service.ts b/src/modules/words/words.service.ts index 11859e1..6357303 100644 --- a/src/modules/words/words.service.ts +++ b/src/modules/words/words.service.ts @@ -101,7 +101,7 @@ export class WordsService { const safeType = this.safePathSegment(type); const safeName = this.safePathSegment(name); const cacheKey = `${safeType}/${safeName}`; - this.logger.log(`重载文案: ${cacheKey}..`); + //this.logger.log(`重载文案: ${cacheKey}..`); const filePath = path.join( this.paths.get('words'), safeType, @@ -136,7 +136,7 @@ export class WordsService { const names = files .filter((f) => f.isFile() && f.name.endsWith('.json')) .map((f) => f.name.replace(/\.json$/, '')); - this.logger.log(`扫描文案类型 ${safeType} 下的文件: ${names.join(', ')}`); + //this.logger.log(`扫描文案类型 ${safeType} 下的文件: ${names.join(', ')}`); return names; } catch (e) { this.logger.error(`读取文案目录失败: ${safeType}`, e);