321 lines
8.8 KiB
JavaScript
321 lines
8.8 KiB
JavaScript
/**
|
||
* 自动操作日志中间件
|
||
* @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}报表`;
|
||
}
|
||
// 如果是获取单个记录详情,可能是用于编辑
|
||
if (url.match(/\/\d+$/)) {
|
||
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
|
||
};
|