refactor: 统一代码风格并迁移至 ESLint 新配置

style(backend): 格式化模型文件代码
style(frontend): 调整组件代码格式
chore: 删除旧 ESLint 配置并添加新配置
refactor(backend): 重构模型定义语法
style: 统一箭头函数和对象属性简写
This commit is contained in:
zhang1106
2026-03-27 19:12:16 +08:00
parent 6a8d4144ff
commit 63f0cb570e
166 changed files with 15483 additions and 11170 deletions
+123 -117
View File
@@ -1,4 +1,3 @@
const cron = require('node-cron');
const path = require('path');
const fs = require('fs');
@@ -61,15 +60,15 @@ function calculateNextRun(cronExpression) {
const parts = cronExpression.split(' ');
const minute = parseInt(parts[0]) || 0;
const hour = parseInt(parts[1]) || 0;
const now = new Date();
const next = new Date(now);
next.setHours(hour, minute, 0, 0);
if (next <= now) {
next.setDate(next.getDate() + 1);
}
return next.toLocaleString('zh-CN');
} catch (error) {
return '计算失败';
@@ -89,7 +88,8 @@ function getFileSize(filePath) {
}
function createAutoBackupTask(settings) {
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } = settings;
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } =
settings;
if (!validateCronExpression(cronExpression)) {
throw new Error('无效的 Cron 表达式');
@@ -106,86 +106,91 @@ function createAutoBackupTask(settings) {
}
console.log('创建新调度器...');
const task = cron.schedule(cronExpression, async function() {
console.log('');
console.log('============================================');
console.log('=== 自动备份任务触发 ===');
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
console.log('============================================');
let logId = null;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'auto',
description: `${description} - ${timestamp}`,
backupType: backupType,
includeFiles: includeFiles,
compressed: compress
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
console.log('准备执行备份...');
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: `${description} - ${timestamp}`,
includeFiles,
compress,
autoClean: true,
maxCount,
maxAgeDays,
});
const task = cron.schedule(
cronExpression,
async function () {
console.log('');
console.log('============================================');
console.log('=== 自动备份任务触发 ===');
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
console.log('============================================');
if (result) {
console.log('自动备份完成:', result.filename);
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults
});
}
console.log('============================================\n');
} else {
console.log('无数据变化,跳过备份');
if (logId) {
await updateLogStatus(logId, 'success', {
errorMessage: '无数据变化,跳过备份'
});
}
console.log('============================================\n');
}
} catch (error) {
console.error('自动备份失败:', error);
console.error('错误堆栈:', error.stack);
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误'
let logId = null;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'auto',
description: `${description} - ${timestamp}`,
backupType: backupType,
includeFiles: includeFiles,
compressed: compress,
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
console.log('准备执行备份...');
const backupFunction =
backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: `${description} - ${timestamp}`,
includeFiles,
compress,
autoClean: true,
maxCount,
maxAgeDays,
});
if (result) {
console.log('自动备份完成:', result.filename);
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults,
});
}
console.log('============================================\n');
} else {
console.log('无数据变化,跳过备份');
if (logId) {
await updateLogStatus(logId, 'success', {
errorMessage: '无数据变化,跳过备份',
});
}
console.log('============================================\n');
}
} catch (error) {
console.error('自动备份失败:', error);
console.error('错误堆栈:', error.stack);
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误',
});
}
console.error('============================================\n');
}
console.error('============================================\n');
},
{
timezone: 'Asia/Shanghai',
}
}, {
timezone: 'Asia/Shanghai'
});
);
schedulers.set('auto-backup', task);
console.log('自动备份任务已成功创建并启动');
@@ -231,14 +236,14 @@ function getAutoBackupStatus() {
if (isActive && settings.enabled) {
const now = new Date();
const [minute, hour] = settings.cronExpression.split(' ').slice(0, 2);
const next = new Date(now);
next.setHours(parseInt(hour), parseInt(minute), 0, 0);
if (next <= now) {
next.setDate(next.getDate() + 1);
}
nextRun = next.toISOString();
}
@@ -284,29 +289,29 @@ function updateAutoBackupSettings(newSettings) {
async function uploadToRemoteTargets(localFilePath, filename) {
const globalSettings = getGlobalSettings();
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
console.log('远端备份已禁用');
return [];
}
const enabledTargets = getEnabledTargets();
if (enabledTargets.length === 0) {
console.log('没有启用的远端备份目标');
return [];
}
const uploadResults = [];
for (const target of enabledTargets) {
try {
console.log('开始上传到目标:' + target.name + ' (' + target.protocol + ')');
const remotePath = (target.prefix || 'backups/') + filename;
const result = await uploadToRemote(target, localFilePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
@@ -314,7 +319,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
success: true,
...result,
});
console.log('上传到 ' + target.name + ' 成功');
} catch (error) {
console.error('上传到 ' + target.name + ' 失败:', error.message);
@@ -327,7 +332,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
});
}
}
const settings = getGlobalSettings();
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
try {
@@ -337,7 +342,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
console.error('删除本地备份文件失败:', error.message);
}
}
return uploadResults;
}
@@ -346,31 +351,33 @@ async function executeBackupNow(options = {}) {
console.log('============================================');
console.log('=== 手动触发备份 ===');
console.log('============================================');
let logId = null;
try {
const settings = loadSettings();
const backupType = options.backupType || settings.backupType || 'full';
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'manual',
description: options.description || '手动备份',
backupType: backupType,
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compressed: options.compress !== undefined ? options.compress : settings.compress
includeFiles:
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compressed: options.compress !== undefined ? options.compress : settings.compress,
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: options.description || '手动备份',
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
includeFiles:
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compress: options.compress !== undefined ? options.compress : settings.compress,
autoClean: true,
maxCount: settings.maxCount,
@@ -378,36 +385,36 @@ async function executeBackupNow(options = {}) {
});
console.log('手动备份完成:', result.filename);
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults
remoteUploads: uploadResults,
});
}
console.log('============================================\n');
return {
success: true,
return {
success: true,
result,
remoteUploads: uploadResults,
};
} catch (error) {
console.error('手动备份失败:', error);
console.error('============================================\n');
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误'
errorMessage: error.message || '未知错误',
});
}
return { success: false, error: error.message };
}
}
@@ -417,14 +424,14 @@ function initAutoBackup() {
console.log('============================================');
console.log('=== 初始化自动备份调度器 ===');
console.log('============================================');
const settings = loadSettings();
console.log('当前设置:');
console.log(' 启用:', settings.enabled ? '是' : '否');
console.log(' Cron表达式:', settings.cronExpression);
console.log(' 备份类型:', settings.backupType);
if (settings.enabled) {
startAutoBackup(settings);
} else {
@@ -447,4 +454,3 @@ module.exports = {
executeBackupNow,
initAutoBackup,
};
+105 -84
View File
@@ -30,31 +30,31 @@ async function enableForeignKeyChecks() {
// 数据表名称中英文映射
const TABLE_NAME_MAPPING = {
'User': '用户',
'Role': '角色',
'UserRole': '用户角色关联',
'Permission': '权限',
'Room': '机房',
'Rack': '机柜',
'Device': '设备',
'DeviceField': '设备自定义字段',
'DevicePort': '设备端口',
'NetworkCard': '网卡',
'Cable': '线缆',
'PendingDevice': '待入库设备',
'FaultCategory': '故障分类',
'Ticket': '工单',
'TicketField': '工单自定义字段',
'TicketOperationRecord': '工单操作记录',
'ConsumableCategory': '耗材分类',
'Consumable': '耗材',
'ConsumableRecord': '耗材记录',
'ConsumableLog': '耗材操作日志',
'ConsumableLogArchive': '耗材操作日志归档',
'InventoryPlan': '盘点计划',
'InventoryTask': '盘点任务',
'InventoryRecord': '盘点记录',
'SystemSetting': '系统设置',
User: '用户',
Role: '角色',
UserRole: '用户角色关联',
Permission: '权限',
Room: '机房',
Rack: '机柜',
Device: '设备',
DeviceField: '设备自定义字段',
DevicePort: '设备端口',
NetworkCard: '网卡',
Cable: '线缆',
PendingDevice: '待入库设备',
FaultCategory: '故障分类',
Ticket: '工单',
TicketField: '工单自定义字段',
TicketOperationRecord: '工单操作记录',
ConsumableCategory: '耗材分类',
Consumable: '耗材',
ConsumableRecord: '耗材记录',
ConsumableLog: '耗材操作日志',
ConsumableLogArchive: '耗材操作日志归档',
InventoryPlan: '盘点计划',
InventoryTask: '盘点任务',
InventoryRecord: '盘点记录',
SystemSetting: '系统设置',
};
// 增量备份配置
@@ -161,8 +161,13 @@ function getBackupPath() {
*/
function getLastBackupTime() {
const backupDir = getBackupPath();
const files = fs.readdirSync(backupDir)
.filter(f => (f.startsWith('backup_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupDir)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('incremental_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(f => {
const filePath = path.join(backupDir, f);
return {
@@ -193,7 +198,7 @@ async function collectIncrementalData(lastBackupTime) {
try {
const Model = require(config.modelPath);
// 查询自上次备份以来新增或更新的记录
const newRecords = await Model.findAll({
where: {
@@ -246,7 +251,7 @@ async function collectAllData(tableNames = null) {
const data = {};
let totalRecords = 0;
const configsToProcess = tableNames
const configsToProcess = tableNames
? BACKUP_MODELS_CONFIG.filter(c => tableNames.includes(c.name))
: BACKUP_MODELS_CONFIG;
@@ -397,7 +402,9 @@ async function createBackup(options = {}) {
console.log('\n检查旧备份文件...');
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
if (cleanResult.deletedCount > 0) {
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
console.log(
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
);
} else {
console.log('无需清理旧备份');
}
@@ -440,10 +447,11 @@ async function createIncrementalBackup(options = {}) {
}
console.log(`开始增量备份(上次备份时间:${lastBackupTime.toISOString()})...`);
// 收集增量数据
const { data: incrementalData, totalChangedRecords } = await collectIncrementalData(lastBackupTime);
const { data: incrementalData, totalChangedRecords } =
await collectIncrementalData(lastBackupTime);
if (totalChangedRecords === 0) {
console.log('自上次备份以来没有数据变化,跳过备份');
return null;
@@ -520,7 +528,9 @@ async function createIncrementalBackup(options = {}) {
console.log('\n检查旧备份文件...');
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
if (cleanResult.deletedCount > 0) {
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
console.log(
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
);
} else {
console.log('无需清理旧备份');
}
@@ -541,7 +551,7 @@ async function createIncrementalBackup(options = {}) {
async function validateBackupFile(filePath, options = {}) {
const { isCompressed: forceCompressed } = options;
if (!fs.existsSync(filePath)) {
return { valid: false, error: '备份文件不存在' };
}
@@ -550,7 +560,7 @@ async function validateBackupFile(filePath, options = {}) {
try {
const buffer = fs.readFileSync(filePath);
const isCompressed = forceCompressed !== undefined ? forceCompressed : filePath.endsWith('.gz');
if (isCompressed) {
const decompressed = zlib.gunzipSync(buffer);
backupData = JSON.parse(decompressed.toString('utf8'));
@@ -638,28 +648,32 @@ async function validateBackupFile(filePath, options = {}) {
avatars: backupData.files?.avatars?.length || 0,
others: backupData.files?.others?.length || 0,
total: (backupData.files?.avatars?.length || 0) + (backupData.files?.others?.length || 0),
avatarList: backupData.files?.avatars?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
otherList: backupData.files?.others?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
avatarList:
backupData.files?.avatars?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
otherList:
backupData.files?.others?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
};
const metadata = isIncremental ? {
tableCount: Object.keys(backupData.fullData || {}).length,
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
totalRecords,
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
fileCount: fileDetails.total,
lastBackupTime: backupData.lastBackupTime,
} : {
tableCount: Object.keys(backupData.data).length,
totalRecords,
fileCount: fileDetails.total,
};
const metadata = isIncremental
? {
tableCount: Object.keys(backupData.fullData || {}).length,
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
totalRecords,
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
fileCount: fileDetails.total,
lastBackupTime: backupData.lastBackupTime,
}
: {
tableCount: Object.keys(backupData.data).length,
totalRecords,
fileCount: fileDetails.total,
};
return {
valid: true,
@@ -679,11 +693,7 @@ async function validateBackupFile(filePath, options = {}) {
}
async function restoreData(backupData, options = {}) {
const {
overwriteExisting = true,
skipTables = [],
onProgress = () => {},
} = options;
const { overwriteExisting = true, skipTables = [], onProgress = () => {} } = options;
const results = {
tablesRestored: 0,
@@ -725,25 +735,31 @@ async function restoreData(backupData, options = {}) {
try {
const Model = require(config.modelPath);
if (overwriteExisting) {
await Model.destroy({ where: {}, truncate: true });
}
const processedRecords = tableData.map(record => {
const processed = { ...record };
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
if (
tableName === 'Device' &&
processed.customFields !== undefined &&
processed.customFields !== null
) {
if (typeof processed.customFields === 'string') {
try {
processed.customFields = JSON.parse(processed.customFields);
} catch (e) {
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
console.warn(
`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`
);
processed.customFields = {};
}
}
}
return processed;
});
@@ -776,13 +792,13 @@ async function restoreData(backupData, options = {}) {
results.tablesRestored++;
results.recordsRestored += insertedCount;
results.tableDetails[tableName] = {
recordCount: insertedCount,
displayName: TABLE_NAME_MAPPING[tableName] || tableName,
success: insertedCount > 0,
};
onProgress(tableName, 'restored', insertedCount);
} catch (error) {
results.errors.push({ table: tableName, error: error.message });
@@ -816,7 +832,7 @@ async function restoreIncrementalData(incrementalData, options = {}) {
for (const tableName of Object.keys(incrementalData)) {
const tableIncrement = incrementalData[tableName];
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
if (!config) {
results.errors.push({ table: tableName, error: '未找到模型配置' });
continue;
@@ -947,7 +963,7 @@ async function restoreBackup(filePath, options = {}) {
console.log('\n读取备份数据...');
const buffer = fs.readFileSync(filePath);
const isCompressed = filePath.endsWith('.gz');
let backupData;
if (isCompressed) {
console.log('解压备份文件...');
@@ -992,31 +1008,34 @@ async function restoreBackup(filePath, options = {}) {
filesRestored: fileResults.filesRestored,
errors: dataResults.errors,
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
fileDetails: backupData.files ? {
avatars: backupData.files.avatars?.length || 0,
others: backupData.files.others?.length || 0,
} : null,
fileDetails: backupData.files
? {
avatars: backupData.files.avatars?.length || 0,
others: backupData.files.others?.length || 0,
}
: null,
};
}
function cleanOldBackups(options = {}) {
const {
maxCount = 30,
maxAgeDays = 90,
dryRun = false,
} = options;
const { maxCount = 30, maxAgeDays = 90, dryRun = false } = options;
const backupPath = getBackupPath();
if (!fs.existsSync(backupPath)) {
return { deleted: [], kept: [], totalSize: 0, freedSize: 0 };
}
const now = Date.now();
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const files = fs.readdirSync(backupPath)
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupPath)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(f => {
const filePath = path.join(backupPath, f);
const stats = fs.statSync(filePath);
@@ -1070,7 +1089,9 @@ function cleanOldBackups(options = {}) {
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
if (bytes === 0) {
return '0 B';
}
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
+42 -29
View File
@@ -1,10 +1,9 @@
const BackupLog = require('../models/BackupLog');
const fs = require('fs');
async function createLogEntry(options) {
const { logType, description, backupType, includeFiles, compressed } = options;
try {
const log = await BackupLog.create({
logType: logType || 'manual',
@@ -13,7 +12,7 @@ async function createLogEntry(options) {
backupType: backupType || 'full',
includeFiles: includeFiles || false,
compressed: compressed || false,
startTime: new Date()
startTime: new Date(),
});
return log;
} catch (error) {
@@ -25,30 +24,40 @@ async function createLogEntry(options) {
async function updateLogStatus(logId, status, options = {}) {
try {
const updateData = { status };
if (status === 'running') {
updateData.startTime = new Date();
}
if (status === 'success' || status === 'failed') {
updateData.endTime = new Date();
const log = await BackupLog.findByPk(logId);
if (log && log.startTime) {
updateData.duration = new Date() - new Date(log.startTime);
}
}
if (options.filename) updateData.filename = options.filename;
if (options.filePath) updateData.filePath = options.filePath;
if (options.fileSize) updateData.fileSize = options.fileSize;
if (options.errorMessage) updateData.errorMessage = options.errorMessage;
if (options.remoteUploads) updateData.remoteUploads = options.remoteUploads;
if (options.filename) {
updateData.filename = options.filename;
}
if (options.filePath) {
updateData.filePath = options.filePath;
}
if (options.fileSize) {
updateData.fileSize = options.fileSize;
}
if (options.errorMessage) {
updateData.errorMessage = options.errorMessage;
}
if (options.remoteUploads) {
updateData.remoteUploads = options.remoteUploads;
}
await BackupLog.update(updateData, {
where: { id: logId }
where: { id: logId },
});
return true;
} catch (error) {
console.error('更新备份日志失败:', error);
@@ -60,25 +69,29 @@ async function getBackupLogs(options = {}) {
try {
const { page = 1, pageSize = 20, logType, status } = options;
const where = {};
if (logType) where.logType = logType;
if (status) where.status = status;
if (logType) {
where.logType = logType;
}
if (status) {
where.status = status;
}
const offset = (page - 1) * pageSize;
const { count, rows } = await BackupLog.findAndCountAll({
where,
order: [['createdAt', 'DESC']],
limit: pageSize,
offset
offset,
});
return {
logs: rows,
total: count,
page,
pageSize,
totalPages: Math.ceil(count / pageSize)
totalPages: Math.ceil(count / pageSize),
};
} catch (error) {
console.error('获取备份日志失败:', error);
@@ -99,15 +112,15 @@ async function deleteOldLogs(days = 30) {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const deletedCount = await BackupLog.destroy({
where: {
createdAt: {
[require('sequelize').Op.lt]: cutoffDate
}
}
[require('sequelize').Op.lt]: cutoffDate,
},
},
});
console.log(`删除了 ${deletedCount} 条旧备份日志`);
return deletedCount;
} catch (error) {
@@ -121,5 +134,5 @@ module.exports = {
updateLogStatus,
getBackupLogs,
getBackupLogById,
deleteOldLogs
deleteOldLogs,
};
+36 -25
View File
@@ -10,25 +10,30 @@ const ensureLogDir = () => {
}
};
const formatLogEntry = (entry) => {
const formatLogEntry = entry => {
const timestamp = new Date().toISOString();
return JSON.stringify({
timestamp,
...entry,
}) + '\n';
return (
JSON.stringify({
timestamp,
...entry,
}) + '\n'
);
};
const logDangerousOperation = async (req, {
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState,
metadata = {},
success = true,
errorMessage = null,
}) => {
const logDangerousOperation = async (
req,
{
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState,
metadata = {},
success = true,
errorMessage = null,
}
) => {
ensureLogDir();
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
@@ -57,7 +62,9 @@ const logDangerousOperation = async (req, {
try {
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
console.log(`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`);
console.log(
`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`
);
} catch (error) {
console.error('Failed to write dangerous operation log:', error);
}
@@ -74,13 +81,15 @@ const getDangerousOperationsLogs = (filters = {}) => {
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);
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);
@@ -103,7 +112,9 @@ const getDangerousOperationsLogs = (filters = {}) => {
}
if (filters.username) {
logs = logs.filter(log => log.username?.toLowerCase().includes(filters.username.toLowerCase()));
logs = logs.filter(log =>
log.username?.toLowerCase().includes(filters.username.toLowerCase())
);
}
if (filters.riskLevel) {
+30 -22
View File
@@ -4,7 +4,7 @@ const checkDatabase = async () => {
const result = {
status: 'ok',
type: dbDialect,
message: '数据库连接正常'
message: '数据库连接正常',
};
try {
@@ -35,19 +35,19 @@ const checkCriticalConfig = () => {
checks.push({
key: 'JWT_SECRET',
status: 'error',
message: 'JWT_SECRET 未配置'
message: 'JWT_SECRET 未配置',
});
} else if (jwtSecret.length < 32) {
checks.push({
key: 'JWT_SECRET',
status: 'warning',
message: 'JWT_SECRET 长度不足,建议至少 32 字符'
message: 'JWT_SECRET 长度不足,建议至少 32 字符',
});
} else {
checks.push({
key: 'JWT_SECRET',
status: 'ok',
message: 'JWT_SECRET 已配置'
message: 'JWT_SECRET 已配置',
});
}
@@ -55,14 +55,14 @@ const checkCriticalConfig = () => {
checks.push({
key: 'PORT',
status: port ? 'ok' : 'warning',
message: port ? `服务端口: ${port}` : '使用默认端口 8000'
message: port ? `服务端口: ${port}` : '使用默认端口 8000',
});
const dbType = process.env.DB_TYPE || 'sqlite';
checks.push({
key: 'DB_TYPE',
status: 'ok',
message: `数据库类型: ${dbType}`
message: `数据库类型: ${dbType}`,
});
if (dbType === 'mysql') {
@@ -72,7 +72,7 @@ const checkCriticalConfig = () => {
checks.push({
key: 'MYSQL_CONFIG',
status: 'warning',
message: 'MySQL 配置不完整'
message: 'MySQL 配置不完整',
});
}
}
@@ -85,7 +85,7 @@ const checkCriticalConfig = () => {
return {
status: overallStatus,
checks
checks,
};
};
@@ -93,17 +93,25 @@ const getSystemInfo = () => {
const memUsage = process.memoryUsage();
const uptime = process.uptime();
const formatUptime = (seconds) => {
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}`);
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(' ');
};
@@ -112,13 +120,13 @@ const getSystemInfo = () => {
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'
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)
uptimeSeconds: Math.round(uptime),
};
};
@@ -129,7 +137,7 @@ const performHealthCheck = async () => {
const allChecks = [
{ name: 'database', ...dbCheck },
{ name: 'config', ...configCheck }
{ name: 'config', ...configCheck },
];
const overallStatus = allChecks.every(c => c.status === 'ok')
@@ -143,10 +151,10 @@ const performHealthCheck = async () => {
timestamp: new Date().toISOString(),
service: {
name: 'IDC设备管理系统',
version: '1.0.0'
version: '1.0.0',
},
checks: allChecks,
system: systemInfo
system: systemInfo,
};
};
@@ -154,5 +162,5 @@ module.exports = {
performHealthCheck,
checkDatabase,
checkCriticalConfig,
getSystemInfo
getSystemInfo,
};
+30 -17
View File
@@ -4,26 +4,27 @@ const generateRecordId = () => {
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
};
const getOperatorInfo = (req) => {
const getOperatorInfo = req => {
if (!req || !req.user) {
return {
operatorId: 'system',
operatorName: '系统',
operatorRole: null
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
operatorRole: req.user.roleName || req.user.role || null,
};
};
const getClientInfo = (req) => {
const getClientInfo = req => {
if (!req) {
return { ipAddress: null, userAgent: null };
}
const ipAddress = req.headers['x-forwarded-for'] ||
const ipAddress =
req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection?.remoteAddress ||
req.ip ||
@@ -39,7 +40,7 @@ const DEVICE_TYPE_MAP = {
storage: '存储设备',
firewall: '防火墙',
loadbalancer: '负载均衡器',
other: '其他设备'
other: '其他设备',
};
const generateDeviceDescription = (operation, device, options = {}) => {
@@ -48,7 +49,7 @@ const generateDeviceDescription = (operation, device, options = {}) => {
includePosition = true,
includeSerial = true,
includeIp = true,
includeModel = true
includeModel = true,
} = options;
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
@@ -94,7 +95,7 @@ const buildDeviceMetadata = (device, extra = {}) => {
position: device.position !== undefined ? device.position : null,
roomId: device.roomId || null,
roomName: device.roomName || null,
...extra
...extra,
};
};
@@ -108,7 +109,7 @@ async function logOperation({
afterState,
result = 'success',
req,
metadata = {}
metadata = {},
}) {
try {
const operatorInfo = getOperatorInfo(req);
@@ -129,14 +130,18 @@ async function logOperation({
result,
ipAddress: clientInfo.ipAddress,
userAgent: clientInfo.userAgent,
metadata
metadata,
});
} catch (error) {
console.error('记录操作日志失败:', error);
}
}
async function logDeviceOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
async function logDeviceOperation(
operationType,
operationDescription,
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
) {
return logOperation({
module: 'device',
operationType,
@@ -147,11 +152,15 @@ async function logDeviceOperation(operationType, operationDescription, { targetI
afterState,
result,
req,
metadata
metadata,
});
}
async function logUserOperation(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,
@@ -162,11 +171,15 @@ async function logUserOperation(operationType, operationDescription, { targetId,
afterState,
result,
req,
metadata
metadata,
});
}
async function logRoleOperation(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,
@@ -177,7 +190,7 @@ async function logRoleOperation(operationType, operationDescription, { targetId,
afterState,
result,
req,
metadata
metadata,
});
}
@@ -187,5 +200,5 @@ module.exports = {
logUserOperation,
logRoleOperation,
generateDeviceDescription,
buildDeviceMetadata
buildDeviceMetadata,
};
+32 -30
View File
@@ -28,9 +28,9 @@ const PROTOCOL_LABELS = {
*/
async function uploadViaFTP(config, localFilePath, remotePath) {
const { Client } = require('basic-ftp');
const client = new Client();
try {
await client.access({
host: config.host,
@@ -42,16 +42,16 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
rejectUnauthorized: config.rejectUnauthorized !== false,
},
});
await client.cd(config.rootPath || '/');
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'ftp');
}
await client.uploadFrom(localFilePath, path.basename(remotePath));
return {
success: true,
message: `FTP 上传成功:${remotePath}`,
@@ -69,7 +69,7 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
async function uploadViaSFTP(config, localFilePath, remotePath) {
const Client = require('ssh2-sftp-client');
const client = new Client();
try {
await client.connect({
host: config.host,
@@ -80,14 +80,14 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
passphrase: config.passphrase,
readyTimeout: config.timeout || 10000,
});
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'sftp');
}
await client.put(fs.createReadStream(localFilePath), remotePath);
return {
success: true,
message: `SFTP 上传成功:${remotePath}`,
@@ -104,7 +104,7 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
*/
async function uploadViaWebDAV(config, localFilePath, remotePath) {
const { createClient } = require('webdav');
const client = createClient(config.url, {
username: config.username,
password: config.password,
@@ -113,18 +113,18 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
'User-Agent': 'IDC-Backup-Client/1.0',
},
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '/') {
await ensureRemoteDir(client, dirPath, 'webdav');
}
const fileContent = fs.readFileSync(localFilePath);
await client.putFileContents(remotePath, fileContent, {
overwrite: true,
});
return {
success: true,
message: `WebDAV 上传成功:${remotePath}`,
@@ -139,22 +139,22 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
*/
async function uploadViaSMB(config, localFilePath, remotePath) {
const smb = require('smb2');
const client = new smb({
share: `\\\\${config.host}\\${config.share}`,
domain: config.domain || '',
username: config.username,
password: config.password,
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'smb');
}
await client.writeFile(remotePath, fs.readFileSync(localFilePath));
return {
success: true,
message: `SMB 上传成功:${remotePath}`,
@@ -216,11 +216,11 @@ async function ensureRemoteDir(client, dirPath, protocol) {
*/
async function uploadToRemote(config, localFilePath, remotePath) {
console.log(`开始上传到远端 [${config.protocol}]: ${remotePath}`);
const startTime = Date.now();
let result;
switch (config.protocol) {
case PROTOCOL_TYPES.FTP:
result = await uploadViaFTP(config, localFilePath, remotePath);
@@ -237,12 +237,14 @@ async function uploadToRemote(config, localFilePath, remotePath) {
default:
throw new Error(`不支持的协议类型:${config.protocol}`);
}
const duration = Date.now() - startTime;
const fileSize = fs.statSync(localFilePath).size;
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
console.log(
`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`
);
return {
...result,
protocol: config.protocol,
@@ -258,22 +260,22 @@ async function uploadToRemote(config, localFilePath, remotePath) {
*/
async function testRemoteConnection(config) {
console.log(`测试远端连接 [${config.protocol}]...`);
try {
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
fs.writeFileSync(testFile, testContent);
const testRemotePath = `test/backup-connection-test-${Date.now()}.txt`;
const result = await uploadToRemote(config, testFile, testRemotePath);
try {
fs.unlinkSync(testFile);
} catch (e) {
console.warn('删除测试文件失败:', e.message);
}
return {
success: true,
message: '连接测试成功',
+27 -23
View File
@@ -28,7 +28,9 @@ const DEFAULT_CONFIG = {
* 加密敏感信息
*/
function encrypt(text) {
if (!text) return '';
if (!text) {
return '';
}
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const iv = crypto.randomBytes(16);
@@ -42,7 +44,9 @@ function encrypt(text) {
* 解密敏感信息
*/
function decrypt(text) {
if (!text) return '';
if (!text) {
return '';
}
try {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
@@ -113,11 +117,11 @@ function getAllTargets() {
function getTarget(id) {
const config = loadConfig();
const target = config.targets.find(t => t.id === id);
if (!target) {
return null;
}
return {
...target,
password: target.password ? decrypt(target.password) : undefined,
@@ -133,7 +137,7 @@ function getTarget(id) {
*/
function addTarget(targetData) {
const config = loadConfig();
const newTarget = {
id: `target_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: targetData.name,
@@ -142,7 +146,7 @@ function addTarget(targetData) {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
switch (targetData.protocol) {
case PROTOCOL_TYPES.FTP:
case PROTOCOL_TYPES.SFTP:
@@ -155,7 +159,7 @@ function addTarget(targetData) {
secure: targetData.secure,
});
break;
case PROTOCOL_TYPES.WEBDAV:
Object.assign(newTarget, {
url: targetData.url,
@@ -165,7 +169,7 @@ function addTarget(targetData) {
rootPath: targetData.rootPath || '/',
});
break;
case PROTOCOL_TYPES.SMB:
Object.assign(newTarget, {
host: targetData.host,
@@ -176,17 +180,17 @@ function addTarget(targetData) {
rootPath: targetData.rootPath || '/',
});
break;
default:
throw new Error(`不支持的协议类型:${targetData.protocol}`);
}
config.targets.push(newTarget);
if (saveConfig(config)) {
return newTarget;
}
throw new Error('保存配置失败');
}
@@ -196,14 +200,14 @@ function addTarget(targetData) {
function updateTarget(id, updates) {
const config = loadConfig();
const targetIndex = config.targets.findIndex(t => t.id === id);
if (targetIndex === -1) {
throw new Error('目标不存在');
}
const existingTarget = config.targets[targetIndex];
const updatedTarget = { ...existingTarget, ...updates, updatedAt: new Date().toISOString() };
if (updates.password) {
updatedTarget.password = encrypt(updates.password);
}
@@ -219,13 +223,13 @@ function updateTarget(id, updates) {
if (updates.passphrase) {
updatedTarget.passphrase = encrypt(updates.passphrase);
}
config.targets[targetIndex] = updatedTarget;
if (saveConfig(config)) {
return updatedTarget;
}
throw new Error('保存配置失败');
}
@@ -235,16 +239,16 @@ function updateTarget(id, updates) {
function deleteTarget(id) {
const config = loadConfig();
const initialLength = config.targets.length;
config.targets = config.targets.filter(t => t.id !== id);
if (config.targets.length < initialLength) {
if (saveConfig(config)) {
return true;
}
throw new Error('保存配置失败');
}
return false;
}
@@ -262,11 +266,11 @@ function getGlobalSettings() {
function updateGlobalSettings(settings) {
const config = loadConfig();
config.globalSettings = { ...config.globalSettings, ...settings };
if (saveConfig(config)) {
return config.globalSettings;
}
throw new Error('保存配置失败');
}