feat: 添加操作日志、危险操作确认和业务关联功能

1. 新增操作日志记录功能,记录关键操作
2. 实现危险操作确认对话框,防止误删
3. 添加业务和库房管理模块
4. 支持设备标记为空闲状态
5. 完善API文档和健康检查
6. 优化前端删除操作的确认流程
7. 添加Swagger API文档支持
8. 实现设备与业务的关联功能
9. 改进设备模型,添加空闲相关字段
10. 优化用户、角色管理操作日志
This commit is contained in:
zhang1106
2026-03-20 17:15:14 +08:00
parent c392df9ce3
commit 73cbe4ac1b
52 changed files with 11149 additions and 1756 deletions
+204
View File
@@ -0,0 +1,204 @@
const fs = require('fs');
const path = require('path');
const LOG_DIR = path.join(__dirname, '../../logs');
const DANGEROUS_OPERATIONS_LOG = path.join(LOG_DIR, 'dangerous-operations.log');
const ensureLogDir = () => {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
};
const formatLogEntry = (entry) => {
const timestamp = new Date().toISOString();
return JSON.stringify({
timestamp,
...entry,
}) + '\n';
};
const logDangerousOperation = async (req, {
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState,
metadata = {},
success = true,
errorMessage = null,
}) => {
ensureLogDir();
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
const userAgent = req?.get?.('User-Agent') || 'unknown';
const userId = req?.user?.userId || req?.session?.userId || 'anonymous';
const username = req?.user?.username || req?.session?.username || 'anonymous';
const logEntry = {
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState: beforeState ? JSON.stringify(beforeState) : null,
metadata,
success,
errorMessage,
clientIp,
userAgent,
userId,
username,
riskLevel: metadata.riskLevel || 'UNKNOWN',
relatedDataCount: metadata.relatedDataCount || 0,
itemCount: metadata.itemCount || 1,
};
try {
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
console.log(`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`);
} catch (error) {
console.error('Failed to write dangerous operation log:', error);
}
};
const getDangerousOperationsLogs = (filters = {}) => {
ensureLogDir();
if (!fs.existsSync(DANGEROUS_OPERATIONS_LOG)) {
return [];
}
try {
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
const lines = content.split('\n').filter(line => line.trim());
let logs = lines.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
}).filter(log => log !== null);
if (filters.operationType) {
logs = logs.filter(log => log.operationType === filters.operationType);
}
if (filters.targetType) {
logs = logs.filter(log => log.targetType === filters.targetType);
}
if (filters.success !== undefined) {
logs = logs.filter(log => log.success === filters.success);
}
if (filters.startDate) {
logs = logs.filter(log => new Date(log.timestamp) >= new Date(filters.startDate));
}
if (filters.endDate) {
logs = logs.filter(log => new Date(log.timestamp) <= new Date(filters.endDate));
}
if (filters.username) {
logs = logs.filter(log => log.username?.toLowerCase().includes(filters.username.toLowerCase()));
}
if (filters.riskLevel) {
logs = logs.filter(log => log.riskLevel === filters.riskLevel);
}
return logs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
} catch (error) {
console.error('Failed to read dangerous operations log:', error);
return [];
}
};
const cleanOldLogs = (daysToKeep = 90) => {
ensureLogDir();
if (!fs.existsSync(DANGEROUS_OPERATIONS_LOG)) {
return { deletedCount: 0 };
}
try {
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
const lines = content.split('\n').filter(line => line.trim());
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const remainingLogs = [];
let deletedCount = 0;
for (const line of lines) {
try {
const logEntry = JSON.parse(line);
if (new Date(logEntry.timestamp) >= cutoffDate) {
remainingLogs.push(line);
} else {
deletedCount++;
}
} catch {
deletedCount++;
}
}
fs.writeFileSync(DANGEROUS_OPERATIONS_LOG, remainingLogs.join('\n') + '\n');
return { deletedCount, remainingCount: remainingLogs.length };
} catch (error) {
console.error('Failed to clean old logs:', error);
throw error;
}
};
const DANGEROUS_OPERATION_TYPES = {
DELETE_SINGLE: 'DELETE_SINGLE',
DELETE_BATCH: 'DELETE_BATCH',
DELETE_ALL: 'DELETE_ALL',
UPDATE_BATCH: 'UPDATE_BATCH',
RESTORE: 'RESTORE',
PURGE: 'PURGE',
BATCH_RESTORE: 'BATCH_RESTORE',
};
const RISK_LEVELS = {
EXTREME: 'EXTREME',
HIGH: 'HIGH',
MEDIUM: 'MEDIUM',
LOW: 'LOW',
};
const calculateRiskLevel = (operationType, itemCount, options = {}) => {
const { hasRelatedData = false, isSystemLevel = false } = options;
if (operationType === DANGEROUS_OPERATION_TYPES.DELETE_ALL || isSystemLevel) {
return RISK_LEVELS.EXTREME;
}
if (operationType === DANGEROUS_OPERATION_TYPES.DELETE_BATCH) {
if (itemCount > 10) {
return RISK_LEVELS.EXTREME;
}
return itemCount > 3 ? RISK_LEVELS.HIGH : RISK_LEVELS.MEDIUM;
}
if (hasRelatedData) {
return RISK_LEVELS.MEDIUM;
}
return RISK_LEVELS.LOW;
};
module.exports = {
logDangerousOperation,
getDangerousOperationsLogs,
cleanOldLogs,
DANGEROUS_OPERATION_TYPES,
RISK_LEVELS,
calculateRiskLevel,
};
+158
View File
@@ -0,0 +1,158 @@
const { sequelize, DB_TYPE, dbDialect } = require('../db');
const checkDatabase = async () => {
const result = {
status: 'ok',
type: dbDialect,
message: '数据库连接正常'
};
try {
await sequelize.authenticate();
result.status = 'ok';
} catch (error) {
result.status = 'error';
result.message = `数据库连接失败: ${error.message}`;
return result;
}
try {
await sequelize.query('SELECT 1');
result.status = 'ok';
} catch (error) {
result.status = 'error';
result.message = `数据库查询失败: ${error.message}`;
}
return result;
};
const checkCriticalConfig = () => {
const checks = [];
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
checks.push({
key: 'JWT_SECRET',
status: 'error',
message: 'JWT_SECRET 未配置'
});
} else if (jwtSecret.length < 32) {
checks.push({
key: 'JWT_SECRET',
status: 'warning',
message: 'JWT_SECRET 长度不足,建议至少 32 字符'
});
} else {
checks.push({
key: 'JWT_SECRET',
status: 'ok',
message: 'JWT_SECRET 已配置'
});
}
const port = process.env.PORT;
checks.push({
key: 'PORT',
status: port ? 'ok' : 'warning',
message: port ? `服务端口: ${port}` : '使用默认端口 8000'
});
const dbType = process.env.DB_TYPE || 'sqlite';
checks.push({
key: 'DB_TYPE',
status: 'ok',
message: `数据库类型: ${dbType}`
});
if (dbType === 'mysql') {
const mysqlHost = process.env.MYSQL_HOST;
const mysqlDb = process.env.MYSQL_DATABASE;
if (!mysqlHost || !mysqlDb) {
checks.push({
key: 'MYSQL_CONFIG',
status: 'warning',
message: 'MySQL 配置不完整'
});
}
}
const overallStatus = checks.every(c => c.status === 'ok')
? 'ok'
: checks.some(c => c.status === 'error')
? 'error'
: 'warning';
return {
status: overallStatus,
checks
};
};
const getSystemInfo = () => {
const memUsage = process.memoryUsage();
const uptime = process.uptime();
const formatUptime = (seconds) => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const parts = [];
if (days > 0) parts.push(`${days}`);
if (hours > 0) parts.push(`${hours}小时`);
if (minutes > 0) parts.push(`${minutes}分钟`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}`);
return parts.join(' ');
};
return {
nodeVersion: process.version,
platform: process.platform,
memory: {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
unit: 'MB'
},
uptime: formatUptime(uptime),
uptimeSeconds: Math.round(uptime)
};
};
const performHealthCheck = async () => {
const [dbCheck] = await Promise.all([checkDatabase()]);
const configCheck = checkCriticalConfig();
const systemInfo = getSystemInfo();
const allChecks = [
{ name: 'database', ...dbCheck },
{ name: 'config', ...configCheck }
];
const overallStatus = allChecks.every(c => c.status === 'ok')
? 'ok'
: allChecks.some(c => c.status === 'error')
? 'error'
: 'warning';
return {
status: overallStatus,
timestamp: new Date().toISOString(),
service: {
name: 'IDC设备管理系统',
version: '1.0.0'
},
checks: allChecks,
system: systemInfo
};
};
module.exports = {
performHealthCheck,
checkDatabase,
checkCriticalConfig,
getSystemInfo
};
+123
View File
@@ -0,0 +1,123 @@
const OperationLog = require('../models/OperationLog');
const generateRecordId = () => {
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
};
const getOperatorInfo = (req) => {
if (!req || !req.user) {
return {
operatorId: 'system',
operatorName: '系统',
operatorRole: null
};
}
return {
operatorId: req.user.userId || req.user.id || 'unknown',
operatorName: req.user.realName || req.user.username || '未知用户',
operatorRole: req.user.roleName || req.user.role || null
};
};
const getClientInfo = (req) => {
if (!req) {
return { ipAddress: null, userAgent: null };
}
const ipAddress = req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection?.remoteAddress ||
req.ip ||
null;
const userAgent = req.headers['user-agent'] || null;
return { ipAddress, userAgent };
};
async function logOperation({
module,
operationType,
operationDescription,
targetId,
targetName,
beforeState,
afterState,
result = 'success',
req,
metadata = {}
}) {
try {
const operatorInfo = getOperatorInfo(req);
const clientInfo = getClientInfo(req);
await OperationLog.create({
recordId: generateRecordId(),
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,
metadata
});
} catch (error) {
console.error('记录操作日志失败:', error);
}
}
async function logDeviceOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
return logOperation({
module: 'device',
operationType,
operationDescription,
targetId,
targetName,
beforeState,
afterState,
result,
req,
metadata
});
}
async function logUserOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
return logOperation({
module: 'user',
operationType,
operationDescription,
targetId,
targetName,
beforeState,
afterState,
result,
req,
metadata
});
}
async function logRoleOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
return logOperation({
module: 'role',
operationType,
operationDescription,
targetId,
targetName,
beforeState,
afterState,
result,
req,
metadata
});
}
module.exports = {
logOperation,
logDeviceOperation,
logUserOperation,
logRoleOperation
};