feat:实现管理员系统核心功能

- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:34 +08:00
parent 0f37130832
commit 6924416bbd
34 changed files with 9481 additions and 199 deletions

View 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条记录
}
}