feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
203
src/business/admin/admin_operation_log.interceptor.ts
Normal file
203
src/business/admin/admin_operation_log.interceptor.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 管理员操作日志拦截器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 自动拦截管理员操作并记录日志
|
||||
* - 记录操作前后的数据状态
|
||||
* - 监控操作性能和错误
|
||||
* - 支持敏感操作的特殊处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 操作拦截:拦截控制器方法的执行
|
||||
* - 数据捕获:记录请求参数和响应数据
|
||||
* - 日志记录:调用日志服务记录操作
|
||||
* - 错误处理:记录操作异常信息
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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条记录
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user