2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 操作日志记录器
|
|
|
|
|
|
* 支持:
|
|
|
|
|
|
* - 关联请求追踪ID(requestId)
|
|
|
|
|
|
* - 数据库写入失败时降级到文件日志
|
|
|
|
|
|
* - 统一错误处理(使用logger替代console.error)
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-20 17:15:14 +08:00
|
|
|
|
const OperationLog = require('../models/OperationLog');
|
2026-05-07 17:14:54 +08:00
|
|
|
|
const logger = require('./logger');
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
|
|
const logModule = logger.module('OperationLogger');
|
2026-03-20 17:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
const generateRecordId = () => {
|
|
|
|
|
|
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取操作人信息
|
|
|
|
|
|
* @param {Object} req - Express请求对象
|
|
|
|
|
|
* @returns {Object} 操作人信息
|
|
|
|
|
|
*/
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const getOperatorInfo = req => {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
if (!req || !req.user) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
operatorId: 'system',
|
|
|
|
|
|
operatorName: '系统',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
operatorRole: null,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
operatorId: req.user.userId || req.user.id || 'unknown',
|
|
|
|
|
|
operatorName: req.user.realName || req.user.username || '未知用户',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
operatorRole: req.user.roleName || req.user.role || null,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取客户端信息
|
|
|
|
|
|
* @param {Object} req - Express请求对象
|
|
|
|
|
|
* @returns {Object} 客户端信息(含requestId)
|
|
|
|
|
|
*/
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const getClientInfo = req => {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
if (!req) {
|
2026-05-07 17:14:54 +08:00
|
|
|
|
return { ipAddress: null, userAgent: null, requestId: null };
|
2026-03-20 17:15:14 +08:00
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const ipAddress =
|
|
|
|
|
|
req.headers['x-forwarded-for'] ||
|
2026-03-20 17:15:14 +08:00
|
|
|
|
req.headers['x-real-ip'] ||
|
|
|
|
|
|
req.connection?.remoteAddress ||
|
|
|
|
|
|
req.ip ||
|
|
|
|
|
|
null;
|
2026-05-07 17:14:54 +08:00
|
|
|
|
return {
|
|
|
|
|
|
ipAddress,
|
|
|
|
|
|
userAgent: req.headers['user-agent'] || null,
|
|
|
|
|
|
requestId: req.requestId || null,
|
|
|
|
|
|
};
|
2026-03-20 17:15:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const DEVICE_TYPE_MAP = {
|
|
|
|
|
|
server: '服务器',
|
|
|
|
|
|
switch: '交换机',
|
|
|
|
|
|
router: '路由器',
|
|
|
|
|
|
storage: '存储设备',
|
|
|
|
|
|
firewall: '防火墙',
|
|
|
|
|
|
loadbalancer: '负载均衡器',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
other: '其他设备',
|
2026-03-26 14:35:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成设备操作描述
|
|
|
|
|
|
* @param {string} operation - 操作类型描述
|
|
|
|
|
|
* @param {Object} device - 设备信息
|
|
|
|
|
|
* @param {Object} options - 选项
|
|
|
|
|
|
* @returns {string} 操作描述
|
|
|
|
|
|
*/
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const generateDeviceDescription = (operation, device, options = {}) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
includeRack = true,
|
|
|
|
|
|
includePosition = true,
|
|
|
|
|
|
includeSerial = true,
|
|
|
|
|
|
includeIp = true,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
includeModel = true,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
|
|
|
|
|
|
const parts = [`${deviceType}【${device.name}】`];
|
|
|
|
|
|
|
|
|
|
|
|
if (device.deviceId) {
|
|
|
|
|
|
parts.push(`编号:${device.deviceId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (includeModel && device.model) {
|
|
|
|
|
|
parts.push(`型号:${device.model}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (includeSerial && device.serialNumber) {
|
|
|
|
|
|
parts.push(`序列号:${device.serialNumber}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (includeIp && device.ipAddress) {
|
|
|
|
|
|
parts.push(`IP:${device.ipAddress}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (includeRack && device.rackName) {
|
|
|
|
|
|
if (includePosition && device.position !== undefined) {
|
|
|
|
|
|
parts.push(`位置:机柜【${device.rackName}】U${device.position}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
parts.push(`机柜【${device.rackName}】`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${operation}${parts.join(',')}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建设备元数据
|
|
|
|
|
|
* @param {Object} device - 设备信息
|
|
|
|
|
|
* @param {Object} extra - 额外字段
|
|
|
|
|
|
* @returns {Object} 元数据
|
|
|
|
|
|
*/
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const buildDeviceMetadata = (device, extra = {}) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
deviceId: device.deviceId || null,
|
|
|
|
|
|
deviceName: device.name || null,
|
|
|
|
|
|
deviceType: device.type || null,
|
|
|
|
|
|
deviceModel: device.model || null,
|
|
|
|
|
|
serialNumber: device.serialNumber || null,
|
|
|
|
|
|
ipAddress: device.ipAddress || null,
|
|
|
|
|
|
rackId: device.rackId || null,
|
|
|
|
|
|
rackName: device.rackName || null,
|
|
|
|
|
|
position: device.position !== undefined ? device.position : null,
|
|
|
|
|
|
roomId: device.roomId || null,
|
|
|
|
|
|
roomName: device.roomName || null,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
...extra,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 降级写入文件日志
|
|
|
|
|
|
* 当数据库不可用时,将操作日志写入文件作为降级方案
|
|
|
|
|
|
* @param {Object} logData - 日志数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
const fallbackToFile = (logData) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fallbackDir = path.join(process.env.LOG_DIR || './logs', 'fallback');
|
|
|
|
|
|
if (!fs.existsSync(fallbackDir)) {
|
|
|
|
|
|
fs.mkdirSync(fallbackDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
const fallbackPath = path.join(fallbackDir, `operation-fallback-${new Date().toISOString().slice(0, 10)}.log`);
|
|
|
|
|
|
const fallbackEntry = {
|
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
level: 'warn',
|
|
|
|
|
|
module: 'OperationLogger',
|
|
|
|
|
|
message: '操作日志数据库写入失败(降级记录)',
|
|
|
|
|
|
data: logData,
|
|
|
|
|
|
};
|
|
|
|
|
|
fs.appendFileSync(fallbackPath, JSON.stringify(fallbackEntry) + '\n');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 文件写入也失败时静默忽略,避免级联错误
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 记录操作日志(核心方法)
|
|
|
|
|
|
* @param {Object} params - 参数对象
|
|
|
|
|
|
* @param {string} params.module - 模块名称
|
|
|
|
|
|
* @param {string} params.operationType - 操作类型
|
|
|
|
|
|
* @param {string} params.operationDescription - 操作描述
|
|
|
|
|
|
* @param {string} params.targetId - 目标ID
|
|
|
|
|
|
* @param {string} params.targetName - 目标名称
|
|
|
|
|
|
* @param {Object} params.beforeState - 操作前状态
|
|
|
|
|
|
* @param {Object} params.afterState - 操作后状态
|
|
|
|
|
|
* @param {string} params.result - 操作结果
|
|
|
|
|
|
* @param {Object} params.req - 请求对象
|
|
|
|
|
|
* @param {Object} params.metadata - 附加元数据
|
|
|
|
|
|
* @returns {Promise<Object|null>} 创建的日志记录
|
|
|
|
|
|
*/
|
2026-03-20 17:15:14 +08:00
|
|
|
|
async function logOperation({
|
|
|
|
|
|
module,
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
targetId,
|
|
|
|
|
|
targetName,
|
|
|
|
|
|
beforeState,
|
|
|
|
|
|
afterState,
|
|
|
|
|
|
result = 'success',
|
|
|
|
|
|
req,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
metadata = {},
|
2026-03-20 17:15:14 +08:00
|
|
|
|
}) {
|
2026-05-07 17:14:54 +08:00
|
|
|
|
const operatorInfo = getOperatorInfo(req);
|
|
|
|
|
|
const clientInfo = getClientInfo(req);
|
2026-03-20 17:15:14 +08:00
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
const logData = {
|
|
|
|
|
|
module,
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
targetId: targetId || null,
|
|
|
|
|
|
targetName: targetName || null,
|
|
|
|
|
|
operatorId: operatorInfo.operatorId,
|
|
|
|
|
|
operatorName: operatorInfo.operatorName,
|
|
|
|
|
|
operatorRole: operatorInfo.operatorRole,
|
|
|
|
|
|
beforeState: beforeState || null,
|
|
|
|
|
|
afterState: afterState || null,
|
|
|
|
|
|
result,
|
|
|
|
|
|
ipAddress: clientInfo.ipAddress,
|
|
|
|
|
|
userAgent: clientInfo.userAgent,
|
|
|
|
|
|
requestId: clientInfo.requestId,
|
|
|
|
|
|
metadata,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const operationLog = await OperationLog.create({
|
2026-03-20 17:15:14 +08:00
|
|
|
|
recordId: generateRecordId(),
|
2026-05-07 17:14:54 +08:00
|
|
|
|
...logData,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
logModule.debug('操作日志记录成功', {
|
|
|
|
|
|
recordId: operationLog.recordId,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
module,
|
|
|
|
|
|
operationType,
|
2026-05-07 17:14:54 +08:00
|
|
|
|
targetId,
|
|
|
|
|
|
requestId: clientInfo.requestId,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
});
|
2026-05-07 17:14:54 +08:00
|
|
|
|
|
|
|
|
|
|
return operationLog;
|
2026-03-20 17:15:14 +08:00
|
|
|
|
} catch (error) {
|
2026-05-07 17:14:54 +08:00
|
|
|
|
logModule.error('记录操作日志失败', {
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
module,
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
targetId,
|
|
|
|
|
|
requestId: clientInfo.requestId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
fallbackToFile(logData);
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-03-20 17:15:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 记录设备操作日志
|
|
|
|
|
|
* @param {string} operationType - 操作类型
|
|
|
|
|
|
* @param {string} operationDescription - 操作描述
|
|
|
|
|
|
* @param {Object} params - 参数
|
|
|
|
|
|
* @returns {Promise<Object|null>}
|
|
|
|
|
|
*/
|
2026-03-27 19:12:16 +08:00
|
|
|
|
async function logDeviceOperation(
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
|
|
|
|
|
) {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
return logOperation({
|
|
|
|
|
|
module: 'device',
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
targetId,
|
|
|
|
|
|
targetName,
|
|
|
|
|
|
beforeState,
|
|
|
|
|
|
afterState,
|
|
|
|
|
|
result,
|
|
|
|
|
|
req,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
metadata,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 记录用户操作日志
|
|
|
|
|
|
* @param {string} operationType - 操作类型
|
|
|
|
|
|
* @param {string} operationDescription - 操作描述
|
|
|
|
|
|
* @param {Object} params - 参数
|
|
|
|
|
|
* @returns {Promise<Object|null>}
|
|
|
|
|
|
*/
|
2026-03-27 19:12:16 +08:00
|
|
|
|
async function logUserOperation(
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
|
|
|
|
|
) {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
return logOperation({
|
|
|
|
|
|
module: 'user',
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
targetId,
|
|
|
|
|
|
targetName,
|
|
|
|
|
|
beforeState,
|
|
|
|
|
|
afterState,
|
|
|
|
|
|
result,
|
|
|
|
|
|
req,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
metadata,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 17:14:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 记录角色操作日志
|
|
|
|
|
|
* @param {string} operationType - 操作类型
|
|
|
|
|
|
* @param {string} operationDescription - 操作描述
|
|
|
|
|
|
* @param {Object} params - 参数
|
|
|
|
|
|
* @returns {Promise<Object|null>}
|
|
|
|
|
|
*/
|
2026-03-27 19:12:16 +08:00
|
|
|
|
async function logRoleOperation(
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
|
|
|
|
|
) {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
return logOperation({
|
|
|
|
|
|
module: 'role',
|
|
|
|
|
|
operationType,
|
|
|
|
|
|
operationDescription,
|
|
|
|
|
|
targetId,
|
|
|
|
|
|
targetName,
|
|
|
|
|
|
beforeState,
|
|
|
|
|
|
afterState,
|
|
|
|
|
|
result,
|
|
|
|
|
|
req,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
metadata,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
logOperation,
|
|
|
|
|
|
logDeviceOperation,
|
|
|
|
|
|
logUserOperation,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
logRoleOperation,
|
|
|
|
|
|
generateDeviceDescription,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
buildDeviceMetadata,
|
2026-03-20 17:15:14 +08:00
|
|
|
|
};
|