- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
575 lines
17 KiB
TypeScript
575 lines
17 KiB
TypeScript
/**
|
||
* 管理员操作日志服务
|
||
*
|
||
* 功能描述:
|
||
* - 记录管理员的所有数据库操作
|
||
* - 提供操作日志的查询和统计功能
|
||
* - 支持敏感操作的特殊标记
|
||
* - 实现日志的自动清理和归档
|
||
*
|
||
* 职责分离:
|
||
* - 日志记录:记录操作的详细信息
|
||
* - 日志查询:提供灵活的日志查询接口
|
||
* - 日志统计:生成操作统计报告
|
||
* - 日志管理:自动清理和归档功能
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-09: 代码质量优化 - 使用常量替代硬编码字符串,提高代码一致性 (修改者: moyin)
|
||
* - 2026-01-09: 代码质量优化 - 拆分getStatistics长方法为多个私有方法,提高可读性 (修改者: moyin)
|
||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||
* - 2026-01-08: 注释规范优化 - 为接口添加注释,完善文档说明 (修改者: moyin)
|
||
* - 2026-01-08: 注释规范优化 - 添加类注释,完善服务文档说明 (修改者: moyin)
|
||
* - 2026-01-08: 代码质量优化 - 提取魔法数字为常量,重构长方法,补充导入 (修改者: moyin)
|
||
* - 2026-01-08: 功能新增 - 创建管理员操作日志服务 (修改者: assistant)
|
||
*
|
||
* @author moyin
|
||
* @version 1.4.0
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-09
|
||
*/
|
||
|
||
import { Injectable, Logger } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import { AdminOperationLog } from './admin_operation_log.entity';
|
||
import { LOG_QUERY_LIMITS, USER_QUERY_LIMITS, LOG_RETENTION, OPERATION_TYPES, OPERATION_RESULTS } from './admin_constants';
|
||
|
||
/**
|
||
* 创建日志参数接口
|
||
*
|
||
* 功能描述:
|
||
* 定义创建管理员操作日志所需的所有参数
|
||
*
|
||
* 使用场景:
|
||
* - AdminOperationLogService.createLog()方法的参数类型
|
||
* - 记录管理员操作的详细信息
|
||
*/
|
||
export interface CreateLogParams {
|
||
adminUserId: string;
|
||
adminUsername: string;
|
||
operationType: keyof typeof OPERATION_TYPES;
|
||
targetType: string;
|
||
targetId?: string;
|
||
operationDescription: string;
|
||
httpMethodPath: string;
|
||
requestParams?: Record<string, any>;
|
||
beforeData?: Record<string, any>;
|
||
afterData?: Record<string, any>;
|
||
operationResult: keyof typeof OPERATION_RESULTS;
|
||
errorMessage?: string;
|
||
errorCode?: string;
|
||
durationMs: number;
|
||
clientIp?: string;
|
||
userAgent?: string;
|
||
requestId: string;
|
||
context?: Record<string, any>;
|
||
isSensitive?: boolean;
|
||
affectedRecords?: number;
|
||
batchId?: string;
|
||
}
|
||
|
||
/**
|
||
* 日志查询参数接口
|
||
*
|
||
* 功能描述:
|
||
* 定义查询管理员操作日志的过滤条件
|
||
*
|
||
* 使用场景:
|
||
* - AdminOperationLogService.queryLogs()方法的参数类型
|
||
* - 支持多维度的日志查询和过滤
|
||
*/
|
||
export interface LogQueryParams {
|
||
adminUserId?: string;
|
||
operationType?: string;
|
||
targetType?: string;
|
||
operationResult?: string;
|
||
startDate?: Date;
|
||
endDate?: Date;
|
||
isSensitive?: boolean;
|
||
limit?: number;
|
||
offset?: number;
|
||
}
|
||
|
||
/**
|
||
* 日志统计信息接口
|
||
*
|
||
* 功能描述:
|
||
* 定义管理员操作日志的统计数据结构
|
||
*
|
||
* 使用场景:
|
||
* - AdminOperationLogService.getStatistics()方法的返回类型
|
||
* - 提供操作统计和分析数据
|
||
*/
|
||
export interface LogStatistics {
|
||
totalOperations: number;
|
||
successfulOperations: number;
|
||
failedOperations: number;
|
||
operationsByType: Record<string, number>;
|
||
operationsByTarget: Record<string, number>;
|
||
operationsByAdmin: Record<string, number>;
|
||
averageDuration: number;
|
||
sensitiveOperations: number;
|
||
uniqueAdmins: number;
|
||
}
|
||
|
||
/**
|
||
* 管理员操作日志服务
|
||
*
|
||
* 功能描述:
|
||
* - 记录管理员的所有数据库操作
|
||
* - 提供操作日志的查询和统计功能
|
||
* - 支持敏感操作的特殊标记
|
||
* - 实现日志的自动清理和归档
|
||
*
|
||
* 职责分离:
|
||
* - 日志记录:记录操作的详细信息
|
||
* - 日志查询:提供灵活的日志查询接口
|
||
* - 日志统计:生成操作统计报告
|
||
* - 日志管理:自动清理和归档功能
|
||
*
|
||
* 主要方法:
|
||
* - createLog() - 创建操作日志记录
|
||
* - queryLogs() - 查询操作日志
|
||
* - getLogById() - 获取单个日志详情
|
||
* - getStatistics() - 获取操作统计
|
||
* - getSensitiveOperations() - 获取敏感操作日志
|
||
* - getAdminOperationHistory() - 获取管理员操作历史
|
||
* - cleanupExpiredLogs() - 清理过期日志
|
||
*
|
||
* 使用场景:
|
||
* - 管理员操作审计
|
||
* - 安全监控和异常检测
|
||
* - 系统操作统计分析
|
||
*/
|
||
@Injectable()
|
||
export class AdminOperationLogService {
|
||
private readonly logger = new Logger(AdminOperationLogService.name);
|
||
|
||
constructor(
|
||
@InjectRepository(AdminOperationLog)
|
||
private readonly logRepository: Repository<AdminOperationLog>,
|
||
) {
|
||
this.logger.log('AdminOperationLogService初始化完成');
|
||
}
|
||
|
||
/**
|
||
* 创建操作日志
|
||
*
|
||
* @param params 日志参数
|
||
* @returns 创建的日志记录
|
||
*/
|
||
async createLog(params: CreateLogParams): Promise<AdminOperationLog> {
|
||
try {
|
||
const log = this.logRepository.create({
|
||
admin_user_id: params.adminUserId,
|
||
admin_username: params.adminUsername,
|
||
operation_type: params.operationType,
|
||
target_type: params.targetType,
|
||
target_id: params.targetId,
|
||
operation_description: params.operationDescription,
|
||
http_method_path: params.httpMethodPath,
|
||
request_params: params.requestParams,
|
||
before_data: params.beforeData,
|
||
after_data: params.afterData,
|
||
operation_result: params.operationResult,
|
||
error_message: params.errorMessage,
|
||
error_code: params.errorCode,
|
||
duration_ms: params.durationMs,
|
||
client_ip: params.clientIp,
|
||
user_agent: params.userAgent,
|
||
request_id: params.requestId,
|
||
context: params.context,
|
||
is_sensitive: params.isSensitive || false,
|
||
affected_records: params.affectedRecords || 0,
|
||
batch_id: params.batchId,
|
||
});
|
||
|
||
const savedLog = await this.logRepository.save(log);
|
||
|
||
this.logger.log('操作日志记录成功', {
|
||
logId: savedLog.id,
|
||
adminUserId: params.adminUserId,
|
||
operationType: params.operationType,
|
||
targetType: params.targetType,
|
||
operationResult: params.operationResult
|
||
});
|
||
|
||
return savedLog;
|
||
} catch (error) {
|
||
this.logger.error('操作日志记录失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
params
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建查询条件
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @param params 查询参数
|
||
*/
|
||
private buildQueryConditions(queryBuilder: any, params: LogQueryParams): void {
|
||
if (params.adminUserId) {
|
||
queryBuilder.andWhere('log.admin_user_id = :adminUserId', { adminUserId: params.adminUserId });
|
||
}
|
||
|
||
if (params.operationType) {
|
||
queryBuilder.andWhere('log.operation_type = :operationType', { operationType: params.operationType });
|
||
}
|
||
|
||
if (params.targetType) {
|
||
queryBuilder.andWhere('log.target_type = :targetType', { targetType: params.targetType });
|
||
}
|
||
|
||
if (params.operationResult) {
|
||
queryBuilder.andWhere('log.operation_result = :operationResult', { operationResult: params.operationResult });
|
||
}
|
||
|
||
if (params.startDate && params.endDate) {
|
||
queryBuilder.andWhere('log.created_at BETWEEN :startDate AND :endDate', {
|
||
startDate: params.startDate,
|
||
endDate: params.endDate
|
||
});
|
||
}
|
||
|
||
if (params.isSensitive !== undefined) {
|
||
queryBuilder.andWhere('log.is_sensitive = :isSensitive', { isSensitive: params.isSensitive });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询操作日志
|
||
*
|
||
* @param params 查询参数
|
||
* @returns 日志列表和总数
|
||
*/
|
||
async queryLogs(params: LogQueryParams): Promise<{ logs: AdminOperationLog[]; total: number }> {
|
||
try {
|
||
const queryBuilder = this.logRepository.createQueryBuilder('log');
|
||
|
||
// 构建查询条件
|
||
this.buildQueryConditions(queryBuilder, params);
|
||
|
||
// 排序
|
||
queryBuilder.orderBy('log.created_at', 'DESC');
|
||
|
||
// 分页
|
||
const limit = params.limit || LOG_QUERY_LIMITS.DEFAULT_LOG_QUERY_LIMIT;
|
||
const offset = params.offset || 0;
|
||
queryBuilder.limit(limit).offset(offset);
|
||
|
||
const [logs, total] = await queryBuilder.getManyAndCount();
|
||
|
||
this.logger.log('操作日志查询成功', {
|
||
total,
|
||
returned: logs.length,
|
||
params
|
||
});
|
||
|
||
return { logs, total };
|
||
} catch (error) {
|
||
this.logger.error('操作日志查询失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
params
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取操作日志详情
|
||
*
|
||
* @param id 日志ID
|
||
* @returns 日志详情
|
||
*/
|
||
async getLogById(id: string): Promise<AdminOperationLog | null> {
|
||
try {
|
||
const log = await this.logRepository.findOne({ where: { id } });
|
||
|
||
if (log) {
|
||
this.logger.log('操作日志详情获取成功', { logId: id });
|
||
} else {
|
||
this.logger.warn('操作日志不存在', { logId: id });
|
||
}
|
||
|
||
return log;
|
||
} catch (error) {
|
||
this.logger.error('操作日志详情获取失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
logId: id
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取基础统计数据
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @returns 基础统计数据
|
||
*/
|
||
private async getBasicStatistics(queryBuilder: any): Promise<{
|
||
totalOperations: number;
|
||
successfulOperations: number;
|
||
failedOperations: number;
|
||
sensitiveOperations: number;
|
||
}> {
|
||
const totalOperations = await queryBuilder.getCount();
|
||
|
||
const successfulOperations = await queryBuilder
|
||
.clone()
|
||
.andWhere('log.operation_result = :result', { result: OPERATION_RESULTS.SUCCESS })
|
||
.getCount();
|
||
|
||
const failedOperations = totalOperations - successfulOperations;
|
||
|
||
const sensitiveOperations = await queryBuilder
|
||
.clone()
|
||
.andWhere('log.is_sensitive = :sensitive', { sensitive: true })
|
||
.getCount();
|
||
|
||
return {
|
||
totalOperations,
|
||
successfulOperations,
|
||
failedOperations,
|
||
sensitiveOperations
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取操作类型统计
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @returns 操作类型统计
|
||
*/
|
||
private async getOperationTypeStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||
const operationTypeStats = await queryBuilder
|
||
.clone()
|
||
.select('log.operation_type', 'type')
|
||
.addSelect('COUNT(*)', 'count')
|
||
.groupBy('log.operation_type')
|
||
.getRawMany();
|
||
|
||
return operationTypeStats.reduce((acc, stat) => {
|
||
acc[stat.type] = parseInt(stat.count);
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
}
|
||
|
||
/**
|
||
* 获取目标类型统计
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @returns 目标类型统计
|
||
*/
|
||
private async getTargetTypeStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||
const targetTypeStats = await queryBuilder
|
||
.clone()
|
||
.select('log.target_type', 'type')
|
||
.addSelect('COUNT(*)', 'count')
|
||
.groupBy('log.target_type')
|
||
.getRawMany();
|
||
|
||
return targetTypeStats.reduce((acc, stat) => {
|
||
acc[stat.type] = parseInt(stat.count);
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
}
|
||
|
||
/**
|
||
* 获取管理员统计
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @returns 管理员统计
|
||
*/
|
||
private async getAdminStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||
const adminStats = await queryBuilder
|
||
.clone()
|
||
.select('log.admin_user_id', 'admin')
|
||
.addSelect('COUNT(*)', 'count')
|
||
.groupBy('log.admin_user_id')
|
||
.getRawMany();
|
||
|
||
if (!adminStats || !Array.isArray(adminStats)) {
|
||
return {};
|
||
}
|
||
|
||
return adminStats.reduce((acc, stat) => {
|
||
acc[stat.admin] = parseInt(stat.count);
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
}
|
||
|
||
/**
|
||
* 获取性能统计
|
||
*
|
||
* @param queryBuilder 查询构建器
|
||
* @returns 性能统计
|
||
*/
|
||
private async getPerformanceStatistics(queryBuilder: any): Promise<{
|
||
averageDuration: number;
|
||
uniqueAdmins: number;
|
||
}> {
|
||
// 平均耗时
|
||
const avgDurationResult = await queryBuilder
|
||
.clone()
|
||
.select('AVG(log.duration_ms)', 'avgDuration')
|
||
.getRawOne();
|
||
|
||
const averageDuration = parseFloat(avgDurationResult?.avgDuration || '0');
|
||
|
||
// 唯一管理员数量
|
||
const uniqueAdminsResult = await queryBuilder
|
||
.clone()
|
||
.select('COUNT(DISTINCT log.admin_user_id)', 'uniqueAdmins')
|
||
.getRawOne();
|
||
|
||
const uniqueAdmins = parseInt(uniqueAdminsResult?.uniqueAdmins || '0');
|
||
|
||
return { averageDuration, uniqueAdmins };
|
||
}
|
||
|
||
/**
|
||
* 获取操作统计信息
|
||
*
|
||
* @param startDate 开始日期
|
||
* @param endDate 结束日期
|
||
* @returns 统计信息
|
||
*/
|
||
async getStatistics(startDate?: Date, endDate?: Date): Promise<LogStatistics> {
|
||
try {
|
||
const queryBuilder = this.logRepository.createQueryBuilder('log');
|
||
|
||
if (startDate && endDate) {
|
||
queryBuilder.where('log.created_at BETWEEN :startDate AND :endDate', {
|
||
startDate,
|
||
endDate
|
||
});
|
||
}
|
||
|
||
// 获取各类统计数据
|
||
const basicStats = await this.getBasicStatistics(queryBuilder);
|
||
const operationsByType = await this.getOperationTypeStatistics(queryBuilder);
|
||
const operationsByTarget = await this.getTargetTypeStatistics(queryBuilder);
|
||
const operationsByAdmin = await this.getAdminStatistics(queryBuilder);
|
||
const performanceStats = await this.getPerformanceStatistics(queryBuilder);
|
||
|
||
const statistics: LogStatistics = {
|
||
...basicStats,
|
||
operationsByType,
|
||
operationsByTarget,
|
||
operationsByAdmin,
|
||
...performanceStats
|
||
};
|
||
|
||
this.logger.log('操作统计获取成功', statistics);
|
||
|
||
return statistics;
|
||
} catch (error) {
|
||
this.logger.error('操作统计获取失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
startDate,
|
||
endDate
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过期日志
|
||
*
|
||
* @param daysToKeep 保留天数
|
||
* @returns 清理的记录数
|
||
*/
|
||
async cleanupExpiredLogs(daysToKeep: number = LOG_RETENTION.DEFAULT_DAYS): Promise<number> {
|
||
try {
|
||
const cutoffDate = new Date();
|
||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||
|
||
const result = await this.logRepository
|
||
.createQueryBuilder()
|
||
.delete()
|
||
.where('created_at < :cutoffDate', { cutoffDate })
|
||
.andWhere('is_sensitive = :sensitive', { sensitive: false }) // 保留敏感操作日志
|
||
.execute();
|
||
|
||
const deletedCount = result.affected || 0;
|
||
|
||
this.logger.log('过期日志清理完成', {
|
||
deletedCount,
|
||
cutoffDate,
|
||
daysToKeep
|
||
});
|
||
|
||
return deletedCount;
|
||
} catch (error) {
|
||
this.logger.error('过期日志清理失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
daysToKeep
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取管理员操作历史
|
||
*
|
||
* @param adminUserId 管理员用户ID
|
||
* @param limit 限制数量
|
||
* @returns 操作历史
|
||
*/
|
||
async getAdminOperationHistory(adminUserId: string, limit: number = USER_QUERY_LIMITS.ADMIN_HISTORY_DEFAULT_LIMIT): Promise<AdminOperationLog[]> {
|
||
try {
|
||
const logs = await this.logRepository.find({
|
||
where: { admin_user_id: adminUserId },
|
||
order: { created_at: 'DESC' },
|
||
take: limit
|
||
});
|
||
|
||
this.logger.log('管理员操作历史获取成功', {
|
||
adminUserId,
|
||
count: logs.length
|
||
});
|
||
|
||
return logs;
|
||
} catch (error) {
|
||
this.logger.error('管理员操作历史获取失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
adminUserId
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取敏感操作日志
|
||
*
|
||
* @param limit 限制数量
|
||
* @param offset 偏移量
|
||
* @returns 敏感操作日志
|
||
*/
|
||
async getSensitiveOperations(limit: number = LOG_QUERY_LIMITS.SENSITIVE_LOG_DEFAULT_LIMIT, offset: number = 0): Promise<{ logs: AdminOperationLog[]; total: number }> {
|
||
try {
|
||
const [logs, total] = await this.logRepository.findAndCount({
|
||
where: { is_sensitive: true },
|
||
order: { created_at: 'DESC' },
|
||
take: limit,
|
||
skip: offset
|
||
});
|
||
|
||
this.logger.log('敏感操作日志获取成功', {
|
||
total,
|
||
returned: logs.length
|
||
});
|
||
|
||
return { logs, total };
|
||
} catch (error) {
|
||
this.logger.error('敏感操作日志获取失败', {
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
} |