forked from datawhale/whale-town-end
203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
/**
|
||
* 管理员操作日志拦截器
|
||
*
|
||
* 功能描述:
|
||
* - 自动拦截管理员操作并记录日志
|
||
* - 记录操作前后的数据状态
|
||
* - 监控操作性能和错误
|
||
* - 支持敏感操作的特殊处理
|
||
*
|
||
* 职责分离:
|
||
* - 操作拦截:拦截控制器方法的执行
|
||
* - 数据捕获:记录请求参数和响应数据
|
||
* - 日志记录:调用日志服务记录操作
|
||
* - 错误处理:记录操作异常信息
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||
* - 2026-01-08: 功能新增 - 创建管理员操作日志拦截器 (修改者: assistant)
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.1
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-08
|
||
*/
|
||
|
||
import {
|
||
Injectable,
|
||
NestInterceptor,
|
||
ExecutionContext,
|
||
CallHandler,
|
||
Logger,
|
||
} from '@nestjs/common';
|
||
import { Reflector } from '@nestjs/core';
|
||
import { Observable, throwError } from 'rxjs';
|
||
import { tap, catchError } from 'rxjs/operators';
|
||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||
import { LOG_ADMIN_OPERATION_KEY, LogAdminOperationOptions } from './log_admin_operation.decorator';
|
||
import { SENSITIVE_FIELDS } from './admin_constants';
|
||
import { extractClientIp, generateRequestId, sanitizeRequestBody } from './admin_utils';
|
||
|
||
@Injectable()
|
||
export class AdminOperationLogInterceptor implements NestInterceptor {
|
||
private readonly logger = new Logger(AdminOperationLogInterceptor.name);
|
||
|
||
constructor(
|
||
private readonly reflector: Reflector,
|
||
private readonly logService: AdminOperationLogService,
|
||
) {}
|
||
|
||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||
const logOptions = this.reflector.get<LogAdminOperationOptions>(
|
||
LOG_ADMIN_OPERATION_KEY,
|
||
context.getHandler(),
|
||
);
|
||
|
||
// 如果没有日志配置,直接执行
|
||
if (!logOptions) {
|
||
return next.handle();
|
||
}
|
||
|
||
const request = context.switchToHttp().getRequest();
|
||
const response = context.switchToHttp().getResponse();
|
||
const startTime = Date.now();
|
||
|
||
// 提取请求信息
|
||
const adminUser = request.user;
|
||
const clientIp = extractClientIp(request);
|
||
const userAgent = request.headers['user-agent'] || 'unknown';
|
||
const httpMethodPath = `${request.method} ${request.route?.path || request.url}`;
|
||
const requestId = generateRequestId();
|
||
|
||
// 提取请求参数
|
||
const requestParams = logOptions.captureRequestParams !== false ? {
|
||
params: request.params,
|
||
query: request.query,
|
||
body: sanitizeRequestBody(request.body)
|
||
} : undefined;
|
||
|
||
// 提取目标ID(如果存在)
|
||
const targetId = request.params?.id || request.body?.id || request.query?.id;
|
||
|
||
let beforeData: any = undefined;
|
||
let operationError: any = null;
|
||
|
||
return next.handle().pipe(
|
||
tap((responseData) => {
|
||
// 操作成功,记录日志
|
||
this.recordLog({
|
||
logOptions,
|
||
adminUser,
|
||
clientIp,
|
||
userAgent,
|
||
httpMethodPath,
|
||
requestId,
|
||
requestParams,
|
||
targetId,
|
||
beforeData,
|
||
afterData: logOptions.captureAfterData !== false ? responseData : undefined,
|
||
operationResult: 'SUCCESS',
|
||
durationMs: Date.now() - startTime,
|
||
affectedRecords: this.extractAffectedRecords(responseData),
|
||
});
|
||
}),
|
||
catchError((error) => {
|
||
// 操作失败,记录错误日志
|
||
operationError = error;
|
||
this.recordLog({
|
||
logOptions,
|
||
adminUser,
|
||
clientIp,
|
||
userAgent,
|
||
httpMethodPath,
|
||
requestId,
|
||
requestParams,
|
||
targetId,
|
||
beforeData,
|
||
operationResult: 'FAILED',
|
||
errorMessage: error.message || String(error),
|
||
errorCode: error.code || error.status || 'UNKNOWN_ERROR',
|
||
durationMs: Date.now() - startTime,
|
||
});
|
||
|
||
return throwError(() => error);
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 记录操作日志
|
||
*/
|
||
private async recordLog(params: {
|
||
logOptions: LogAdminOperationOptions;
|
||
adminUser: any;
|
||
clientIp: string;
|
||
userAgent: string;
|
||
httpMethodPath: string;
|
||
requestId: string;
|
||
requestParams?: any;
|
||
targetId?: string;
|
||
beforeData?: any;
|
||
afterData?: any;
|
||
operationResult: 'SUCCESS' | 'FAILED';
|
||
errorMessage?: string;
|
||
errorCode?: string;
|
||
durationMs: number;
|
||
affectedRecords?: number;
|
||
}) {
|
||
try {
|
||
await this.logService.createLog({
|
||
adminUserId: params.adminUser?.id || 'unknown',
|
||
adminUsername: params.adminUser?.username || 'unknown',
|
||
operationType: params.logOptions.operationType,
|
||
targetType: params.logOptions.targetType,
|
||
targetId: params.targetId,
|
||
operationDescription: params.logOptions.description,
|
||
httpMethodPath: params.httpMethodPath,
|
||
requestParams: params.requestParams,
|
||
beforeData: params.beforeData,
|
||
afterData: params.afterData,
|
||
operationResult: params.operationResult,
|
||
errorMessage: params.errorMessage,
|
||
errorCode: params.errorCode,
|
||
durationMs: params.durationMs,
|
||
clientIp: params.clientIp,
|
||
userAgent: params.userAgent,
|
||
requestId: params.requestId,
|
||
isSensitive: params.logOptions.isSensitive || false,
|
||
affectedRecords: params.affectedRecords || 0,
|
||
});
|
||
} catch (error) {
|
||
this.logger.error('记录操作日志失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
adminUserId: params.adminUser?.id,
|
||
operationType: params.logOptions.operationType,
|
||
targetType: params.logOptions.targetType,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提取影响的记录数量
|
||
*/
|
||
private extractAffectedRecords(responseData: any): number {
|
||
if (!responseData || typeof responseData !== 'object') {
|
||
return 0;
|
||
}
|
||
|
||
// 从响应数据中提取影响的记录数
|
||
if (responseData.data) {
|
||
if (Array.isArray(responseData.data.items)) {
|
||
return responseData.data.items.length;
|
||
}
|
||
if (responseData.data.total !== undefined) {
|
||
return responseData.data.total;
|
||
}
|
||
if (responseData.data.success !== undefined && responseData.data.failed !== undefined) {
|
||
return responseData.data.success + responseData.data.failed;
|
||
}
|
||
}
|
||
|
||
return 1; // 默认为1条记录
|
||
}
|
||
} |