Files
nxxmdata/backend/middleware/autoOperationLogger.js

317 lines
8.6 KiB
JavaScript
Raw Normal View History

2025-09-12 20:08:42 +08:00
/**
* 自动操作日志中间件
* @file autoOperationLogger.js
* @description 自动记录所有API操作日志的中间件
*/
const { OperationLog } = require('../models');
/**
* 自动操作日志中间件
* 自动记录所有经过认证的API操作
*/
const autoOperationLogger = async (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
let responseBody = null;
// 拦截响应数据
res.send = function(data) {
responseBody = data;
return originalSend.call(this, data);
};
// 在响应完成后记录日志
res.on('finish', async () => {
try {
// 只记录经过认证的操作
if (!req.user) {
console.log(`[操作日志] 跳过记录 - 无用户信息: ${req.method} ${req.originalUrl}`);
return;
}
const executionTime = Date.now() - startTime;
const operationType = getOperationType(req.method, req.originalUrl);
if (!operationType) {
console.log(`[操作日志] 跳过记录 - 不支持的操作类型: ${req.method} ${req.originalUrl}`);
return; // 不记录的操作类型
}
console.log(`[操作日志] 开始记录: ${req.user.username} ${operationType} ${req.method} ${req.originalUrl}`);
// 获取模块名称
const moduleName = getModuleName(req.originalUrl);
// 获取表名
const tableName = getTableName(req.originalUrl);
// 获取记录ID
const recordId = getRecordId(req);
// 生成操作描述
const operationDesc = generateOperationDesc(req, responseBody);
// 获取操作前后数据
const { oldData, newData } = await getOperationData(req, res, operationType);
// 获取IP地址
const ipAddress = getClientIP(req);
// 记录操作日志
await OperationLog.recordOperation({
userId: req.user.id,
username: req.user.username,
userRole: req.user.role || 'unknown',
operationType,
moduleName,
tableName,
recordId,
operationDesc,
oldData,
newData,
ipAddress,
userAgent: req.get('User-Agent'),
requestUrl: req.originalUrl,
requestMethod: req.method,
responseStatus: res.statusCode,
executionTime,
errorMessage: res.statusCode >= 400 ? (responseBody?.message || '操作失败') : null
});
console.log(`[操作日志] ${req.user.username} ${operationType} ${moduleName} - ${operationDesc}`);
} catch (error) {
console.error('记录操作日志失败:', error);
// 不抛出错误,避免影响主业务
}
});
next();
};
/**
* 根据HTTP方法和URL获取操作类型
*/
const getOperationType = (method, url) => {
const methodUpper = method.toUpperCase();
// 特殊URL处理
if (url.includes('/auth/login')) {
return 'LOGIN';
}
if (url.includes('/auth/logout')) {
return 'LOGOUT';
}
if (url.includes('/export')) {
return 'EXPORT';
}
if (url.includes('/import')) {
return 'IMPORT';
}
if (url.includes('/batch-delete')) {
return 'BATCH_DELETE';
}
if (url.includes('/batch-update')) {
return 'BATCH_UPDATE';
}
// 标准HTTP方法映射
switch (methodUpper) {
case 'POST':
return 'CREATE';
case 'PUT':
case 'PATCH':
return 'UPDATE';
case 'DELETE':
return 'DELETE';
case 'GET':
// GET请求记录所有操作
return 'READ';
default:
return null;
}
};
/**
* 根据URL获取模块名称
*/
const getModuleName = (url) => {
const pathSegments = url.split('/').filter(segment => segment);
if (pathSegments.length < 2) {
return '未知模块';
}
const moduleMap = {
'users': '用户管理',
'farms': '农场管理',
'animals': '动物管理',
'devices': '设备管理',
'alerts': '告警管理',
'products': '产品管理',
'orders': '订单管理',
'auth': '认证管理',
'stats': '统计分析',
'map': '地图服务',
'operation-logs': '操作日志',
'roles': '角色管理',
'permissions': '权限管理',
'menus': '菜单管理',
'cattle-batches': '牛只批次管理',
'cattle-pens': '牛舍管理',
'cattle-transfer-records': '牛只转移记录',
'cattle-exit-records': '牛只出栏记录',
'electronic-fences': '电子围栏',
'electronic-fence-points': '围栏点位',
'pens': '圈舍管理'
};
const moduleKey = pathSegments[1];
return moduleMap[moduleKey] || moduleKey || '未知模块';
};
/**
* 根据URL获取表名
*/
const getTableName = (url) => {
const pathSegments = url.split('/').filter(segment => segment);
if (pathSegments.length < 2) {
return 'unknown';
}
const tableMap = {
'users': 'users',
'farms': 'farms',
'animals': 'animals',
'devices': 'devices',
'alerts': 'alerts',
'products': 'products',
'orders': 'orders',
'cattle-batches': 'cattle_batches',
'cattle-pens': 'cattle_pens',
'cattle-transfer-records': 'cattle_transfer_records',
'cattle-exit-records': 'cattle_exit_records',
'electronic-fences': 'electronic_fences',
'electronic-fence-points': 'electronic_fence_points',
'pens': 'pens',
'roles': 'roles',
'permissions': 'permissions',
'menus': 'menu_permissions'
};
const tableKey = pathSegments[1];
return tableMap[tableKey] || tableKey || 'unknown';
};
/**
* 获取记录ID
*/
const getRecordId = (req) => {
// 从URL参数中获取ID
const id = req.params.id || req.params.farmId || req.params.animalId ||
req.params.deviceId || req.params.alertId || req.params.productId ||
req.params.orderId || req.params.batchId || req.params.penId;
return id ? parseInt(id) : null;
};
/**
* 生成操作描述
*/
const generateOperationDesc = (req, responseBody) => {
const method = req.method.toUpperCase();
const url = req.originalUrl;
const moduleName = getModuleName(url);
// 根据操作类型生成描述
switch (method) {
case 'POST':
if (url.includes('/auth/login')) {
return '用户登录';
}
if (url.includes('/auth/logout')) {
return '用户登出';
}
if (url.includes('/export')) {
return `导出${moduleName}数据`;
}
if (url.includes('/import')) {
return `导入${moduleName}数据`;
}
if (url.includes('/batch-delete')) {
return `批量删除${moduleName}数据`;
}
if (url.includes('/batch-update')) {
return `批量更新${moduleName}数据`;
}
return `新增${moduleName}记录`;
case 'PUT':
case 'PATCH':
return `更新${moduleName}记录`;
case 'DELETE':
if (url.includes('/batch-delete')) {
return `批量删除${moduleName}数据`;
}
return `删除${moduleName}记录`;
case 'GET':
if (url.includes('/stats') || url.includes('/analytics')) {
return `查看${moduleName}统计`;
}
if (url.includes('/reports')) {
return `查看${moduleName}报表`;
}
return `查看${moduleName}数据`;
default:
return `${method}操作${moduleName}`;
}
};
/**
* 获取操作前后数据
*/
const getOperationData = async (req, res, operationType) => {
let oldData = null;
let newData = null;
try {
// 对于更新和删除操作,尝试获取操作前的数据
if (operationType === 'UPDATE' || operationType === 'DELETE') {
// 这里可以根据需要实现获取旧数据的逻辑
// 由于需要查询数据库暂时返回null
oldData = null;
}
// 对于新增和更新操作,获取操作后的数据
if (operationType === 'CREATE' || operationType === 'UPDATE') {
// 从响应体中获取新数据
if (res.responseBody && res.responseBody.data) {
newData = res.responseBody.data;
}
}
} catch (error) {
console.error('获取操作数据失败:', error);
}
return { oldData, newData };
};
/**
* 获取客户端IP地址
*/
const getClientIP = (req) => {
return req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
'unknown';
};
module.exports = {
autoOperationLogger
};