433 lines
10 KiB
JavaScript
433 lines
10 KiB
JavaScript
/**
|
|
* 操作日志控制器
|
|
* @file operationLogController.js
|
|
* @description 处理操作日志相关的API请求
|
|
*/
|
|
const { OperationLog, User } = require('../models');
|
|
const { validationResult } = require('express-validator');
|
|
const { Op } = require('sequelize');
|
|
|
|
/**
|
|
* 获取操作日志列表
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getOperationLogs = async (req, res) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
pageSize = 20,
|
|
userId,
|
|
username,
|
|
operationType,
|
|
moduleName,
|
|
tableName,
|
|
startDate,
|
|
endDate,
|
|
sortBy = 'created_at',
|
|
sortOrder = 'DESC'
|
|
} = req.query;
|
|
|
|
// 构建查询条件
|
|
const whereClause = {};
|
|
|
|
if (userId) {
|
|
whereClause.user_id = userId;
|
|
}
|
|
|
|
if (username) {
|
|
whereClause.username = {
|
|
[Op.like]: `%${username}%`
|
|
};
|
|
}
|
|
|
|
if (operationType) {
|
|
whereClause.operation_type = operationType;
|
|
}
|
|
|
|
if (moduleName) {
|
|
whereClause.module_name = {
|
|
[Op.like]: `%${moduleName}%`
|
|
};
|
|
}
|
|
|
|
if (tableName) {
|
|
whereClause.table_name = tableName;
|
|
}
|
|
|
|
if (startDate && endDate) {
|
|
whereClause.created_at = {
|
|
[Op.between]: [startDate, endDate]
|
|
};
|
|
}
|
|
|
|
// 构建排序条件
|
|
const orderClause = [[sortBy, sortOrder.toUpperCase()]];
|
|
|
|
// 执行分页查询
|
|
const result = await OperationLog.paginate(
|
|
{
|
|
where: whereClause,
|
|
order: orderClause,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'username', 'email', 'roles']
|
|
}
|
|
]
|
|
},
|
|
parseInt(page),
|
|
parseInt(pageSize)
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
pagination: result.pagination,
|
|
message: '获取操作日志列表成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('获取操作日志列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取操作日志列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取操作日志详情
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getOperationLogById = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const operationLog = await OperationLog.findByPk(id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'username', 'email', 'roles']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!operationLog) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: '操作日志不存在'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: operationLog,
|
|
message: '获取操作日志详情成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('获取操作日志详情失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取操作日志详情失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取操作统计
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getOperationStats = async (req, res) => {
|
|
try {
|
|
const { type, userId, moduleName, startDate, endDate } = req.query;
|
|
|
|
let stats = {};
|
|
|
|
if (type === 'user' && userId) {
|
|
stats = await OperationLog.getUserOperationStats(userId, startDate, endDate);
|
|
} else if (type === 'module' && moduleName) {
|
|
stats = await OperationLog.getModuleOperationStats(moduleName, startDate, endDate);
|
|
} else {
|
|
// 获取总体统计
|
|
const whereClause = {};
|
|
if (startDate && endDate) {
|
|
whereClause.created_at = {
|
|
[Op.between]: [startDate, endDate]
|
|
};
|
|
}
|
|
|
|
const result = await OperationLog.findAll({
|
|
where: whereClause,
|
|
attributes: [
|
|
'operation_type',
|
|
[OperationLog.sequelize.fn('COUNT', OperationLog.sequelize.col('id')), 'count']
|
|
],
|
|
group: ['operation_type'],
|
|
raw: true
|
|
});
|
|
|
|
stats = result.reduce((acc, stat) => {
|
|
acc[stat.operation_type] = parseInt(stat.count);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: stats,
|
|
message: '获取操作统计成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('获取操作统计失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取操作统计失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取操作日志图表数据
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getOperationChartData = async (req, res) => {
|
|
try {
|
|
const { type = 'daily', startDate, endDate } = req.query;
|
|
|
|
let dateFormat, groupBy;
|
|
switch (type) {
|
|
case 'hourly':
|
|
dateFormat = '%Y-%m-%d %H:00:00';
|
|
groupBy = 'DATE_FORMAT(created_at, "%Y-%m-%d %H:00:00")';
|
|
break;
|
|
case 'daily':
|
|
dateFormat = '%Y-%m-%d';
|
|
groupBy = 'DATE(created_at)';
|
|
break;
|
|
case 'monthly':
|
|
dateFormat = '%Y-%m';
|
|
groupBy = 'DATE_FORMAT(created_at, "%Y-%m")';
|
|
break;
|
|
default:
|
|
dateFormat = '%Y-%m-%d';
|
|
groupBy = 'DATE(created_at)';
|
|
}
|
|
|
|
const whereClause = {};
|
|
if (startDate && endDate) {
|
|
whereClause.created_at = {
|
|
[Op.between]: [startDate, endDate]
|
|
};
|
|
}
|
|
|
|
const result = await OperationLog.findAll({
|
|
where: whereClause,
|
|
attributes: [
|
|
[OperationLog.sequelize.fn('DATE_FORMAT', OperationLog.sequelize.col('created_at'), dateFormat), 'date'],
|
|
'operation_type',
|
|
[OperationLog.sequelize.fn('COUNT', OperationLog.sequelize.col('id')), 'count']
|
|
],
|
|
group: ['date', 'operation_type'],
|
|
order: [['date', 'ASC']],
|
|
raw: true
|
|
});
|
|
|
|
// 处理图表数据
|
|
const chartData = {
|
|
dates: [],
|
|
series: {
|
|
CREATE: [],
|
|
UPDATE: [],
|
|
DELETE: []
|
|
}
|
|
};
|
|
|
|
const dateSet = new Set();
|
|
result.forEach(item => {
|
|
dateSet.add(item.date);
|
|
});
|
|
|
|
chartData.dates = Array.from(dateSet).sort();
|
|
|
|
chartData.dates.forEach(date => {
|
|
const createItem = result.find(item => item.date === date && item.operation_type === 'CREATE');
|
|
const updateItem = result.find(item => item.date === date && item.operation_type === 'UPDATE');
|
|
const deleteItem = result.find(item => item.date === date && item.operation_type === 'DELETE');
|
|
|
|
chartData.series.CREATE.push(createItem ? parseInt(createItem.count) : 0);
|
|
chartData.series.UPDATE.push(updateItem ? parseInt(updateItem.count) : 0);
|
|
chartData.series.DELETE.push(deleteItem ? parseInt(deleteItem.count) : 0);
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: chartData,
|
|
message: '获取操作日志图表数据成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('获取操作日志图表数据失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取操作日志图表数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 清理过期日志
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const cleanExpiredLogs = async (req, res) => {
|
|
try {
|
|
const { daysToKeep = 90 } = req.body;
|
|
|
|
const deletedCount = await OperationLog.cleanExpiredLogs(daysToKeep);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { deletedCount },
|
|
message: `成功清理了 ${deletedCount} 条过期日志`
|
|
});
|
|
} catch (error) {
|
|
console.error('清理过期日志失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '清理过期日志失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 导出操作日志
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const exportOperationLogs = async (req, res) => {
|
|
try {
|
|
const {
|
|
userId,
|
|
username,
|
|
operationType,
|
|
moduleName,
|
|
tableName,
|
|
startDate,
|
|
endDate
|
|
} = req.query;
|
|
|
|
// 构建查询条件
|
|
const whereClause = {};
|
|
|
|
if (userId) {
|
|
whereClause.user_id = userId;
|
|
}
|
|
|
|
if (username) {
|
|
whereClause.username = {
|
|
[Op.like]: `%${username}%`
|
|
};
|
|
}
|
|
|
|
if (operationType) {
|
|
whereClause.operation_type = operationType;
|
|
}
|
|
|
|
if (moduleName) {
|
|
whereClause.module_name = {
|
|
[Op.like]: `%${moduleName}%`
|
|
};
|
|
}
|
|
|
|
if (tableName) {
|
|
whereClause.table_name = tableName;
|
|
}
|
|
|
|
if (startDate && endDate) {
|
|
whereClause.created_at = {
|
|
[Op.between]: [startDate, endDate]
|
|
};
|
|
}
|
|
|
|
const logs = await OperationLog.findAll({
|
|
where: whereClause,
|
|
order: [['created_at', 'DESC']],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'username', 'email', 'roles']
|
|
}
|
|
]
|
|
});
|
|
|
|
// 转换为CSV格式
|
|
const csvHeaders = [
|
|
'ID',
|
|
'操作用户',
|
|
'用户角色',
|
|
'操作类型',
|
|
'模块名称',
|
|
'数据表名',
|
|
'记录ID',
|
|
'操作描述',
|
|
'IP地址',
|
|
'请求URL',
|
|
'请求方法',
|
|
'响应状态',
|
|
'执行时间(ms)',
|
|
'创建时间'
|
|
];
|
|
|
|
const csvRows = logs.map(log => [
|
|
log.id,
|
|
log.username,
|
|
log.user_role,
|
|
log.operation_type,
|
|
log.module_name,
|
|
log.table_name,
|
|
log.record_id || '',
|
|
log.operation_desc,
|
|
log.ip_address || '',
|
|
log.request_url || '',
|
|
log.request_method || '',
|
|
log.response_status || '',
|
|
log.execution_time || '',
|
|
log.created_at
|
|
]);
|
|
|
|
const csvContent = [csvHeaders, ...csvRows]
|
|
.map(row => row.map(field => `"${field}"`).join(','))
|
|
.join('\n');
|
|
|
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=operation_logs.csv');
|
|
res.send('\ufeff' + csvContent); // 添加BOM以支持中文
|
|
} catch (error) {
|
|
console.error('导出操作日志失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '导出操作日志失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getOperationLogs,
|
|
getOperationLogById,
|
|
getOperationStats,
|
|
getOperationChartData,
|
|
cleanExpiredLogs,
|
|
exportOperationLogs
|
|
};
|