feat: 实现统一日志系统和维护模式功能
This commit is contained in:
@@ -5,6 +5,7 @@ const { createBackup, createIncrementalBackup, getBackupPath } = require('./back
|
||||
const { uploadToRemote } = require('./remoteBackup');
|
||||
const { getEnabledTargets, getGlobalSettings } = require('./remoteBackupConfig');
|
||||
const { createLogEntry, updateLogStatus } = require('./backupLog');
|
||||
const logger = require('./logger').module('AutoBackup');
|
||||
|
||||
const schedulers = new Map();
|
||||
|
||||
@@ -28,7 +29,7 @@ function loadSettings() {
|
||||
return { ...DEFAULT_SETTINGS, ...JSON.parse(content) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载自动备份设置失败:', error);
|
||||
logger.error('加载自动备份设置失败', { error: error.message, stack: error.stack });
|
||||
}
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
@@ -42,7 +43,7 @@ function saveSettings(settings) {
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存自动备份设置失败:', error);
|
||||
logger.error('保存自动备份设置失败', { error: error.message, stack: error.stack });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +83,7 @@ function getFileSize(filePath) {
|
||||
return stats.size;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件大小失败:', error);
|
||||
logger.error('获取文件大小失败', { error: error.message, stack: error.stack });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -95,32 +96,25 @@ function createAutoBackupTask(settings) {
|
||||
throw new Error('无效的 Cron 表达式');
|
||||
}
|
||||
|
||||
console.log('=== 创建自动备份任务 ===');
|
||||
console.log('Cron表达式:', cronExpression);
|
||||
console.log('备份类型:', backupType);
|
||||
console.log('下次执行:', calculateNextRun(cronExpression));
|
||||
logger.info('创建自动备份任务', { cronExpression, backupType, nextRun: calculateNextRun(cronExpression) });
|
||||
|
||||
if (schedulers.has('auto-backup')) {
|
||||
console.log('停止已存在的调度器...');
|
||||
logger.info('停止已存在的调度器...');
|
||||
stopAutoBackup();
|
||||
}
|
||||
|
||||
console.log('创建新调度器...');
|
||||
logger.info('创建新调度器...');
|
||||
const task = cron.schedule(
|
||||
cronExpression,
|
||||
async function () {
|
||||
console.log('');
|
||||
console.log('============================================');
|
||||
console.log('=== 自动备份任务触发 ===');
|
||||
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
|
||||
console.log('============================================');
|
||||
logger.info('自动备份任务触发', { triggerTime: new Date().toLocaleString('zh-CN') });
|
||||
|
||||
let logId = null;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
||||
|
||||
console.log('创建备份日志...');
|
||||
logger.info('创建备份日志...');
|
||||
const log = await createLogEntry({
|
||||
logType: 'auto',
|
||||
description: `${description} - ${timestamp}`,
|
||||
@@ -134,7 +128,7 @@ function createAutoBackupTask(settings) {
|
||||
await updateLogStatus(logId, 'running');
|
||||
}
|
||||
|
||||
console.log('准备执行备份...');
|
||||
logger.info('准备执行备份...');
|
||||
const backupFunction =
|
||||
backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||
|
||||
@@ -148,8 +142,7 @@ function createAutoBackupTask(settings) {
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log('自动备份完成:', result.filename);
|
||||
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
|
||||
logger.info('自动备份完成', { filename: result.filename, type: result.isIncremental ? '增量备份' : '全量备份' });
|
||||
|
||||
const fileSize = getFileSize(result.path);
|
||||
|
||||
@@ -163,28 +156,22 @@ function createAutoBackupTask(settings) {
|
||||
remoteUploads: uploadResults,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('============================================\n');
|
||||
} else {
|
||||
console.log('无数据变化,跳过备份');
|
||||
logger.info('无数据变化,跳过备份');
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
errorMessage: '无数据变化,跳过备份',
|
||||
});
|
||||
}
|
||||
console.log('============================================\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动备份失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
logger.error('自动备份失败', { error: error.message, stack: error.stack });
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'failed', {
|
||||
errorMessage: error.message || '未知错误',
|
||||
});
|
||||
}
|
||||
|
||||
console.error('============================================\n');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -193,7 +180,7 @@ function createAutoBackupTask(settings) {
|
||||
);
|
||||
|
||||
schedulers.set('auto-backup', task);
|
||||
console.log('自动备份任务已成功创建并启动');
|
||||
logger.info('自动备份任务已成功创建并启动');
|
||||
|
||||
return task;
|
||||
}
|
||||
@@ -204,7 +191,7 @@ function startAutoBackup(settings = null) {
|
||||
}
|
||||
|
||||
if (!settings.enabled) {
|
||||
console.log('自动备份已禁用');
|
||||
logger.info('自动备份已禁用');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,7 +199,7 @@ function startAutoBackup(settings = null) {
|
||||
createAutoBackupTask(settings);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('启动自动备份失败:', error.message);
|
||||
logger.error('启动自动备份失败', { error: error.message, stack: error.stack });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -222,7 +209,7 @@ function stopAutoBackup() {
|
||||
const task = schedulers.get('auto-backup');
|
||||
task.stop();
|
||||
schedulers.delete('auto-backup');
|
||||
console.log('自动备份任务已停止');
|
||||
logger.info('自动备份任务已停止');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -291,14 +278,14 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
const globalSettings = getGlobalSettings();
|
||||
|
||||
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
|
||||
console.log('远端备份已禁用');
|
||||
logger.info('远端备份已禁用');
|
||||
return [];
|
||||
}
|
||||
|
||||
const enabledTargets = getEnabledTargets();
|
||||
|
||||
if (enabledTargets.length === 0) {
|
||||
console.log('没有启用的远端备份目标');
|
||||
logger.info('没有启用的远端备份目标');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -306,7 +293,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
|
||||
for (const target of enabledTargets) {
|
||||
try {
|
||||
console.log('开始上传到目标:' + target.name + ' (' + target.protocol + ')');
|
||||
logger.info(`开始上传到目标:${target.name} (${target.protocol})`);
|
||||
|
||||
const remotePath = (target.prefix || 'backups/') + filename;
|
||||
|
||||
@@ -320,9 +307,9 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
...result,
|
||||
});
|
||||
|
||||
console.log('上传到 ' + target.name + ' 成功');
|
||||
logger.info(`上传到 ${target.name} 成功`);
|
||||
} catch (error) {
|
||||
console.error('上传到 ' + target.name + ' 失败:', error.message);
|
||||
logger.error(`上传到 ${target.name} 失败`, { error: error.message, stack: error.stack });
|
||||
uploadResults.push({
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
@@ -337,9 +324,9 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
|
||||
try {
|
||||
fs.unlinkSync(localFilePath);
|
||||
console.log('本地备份文件已删除');
|
||||
logger.info('本地备份文件已删除');
|
||||
} catch (error) {
|
||||
console.error('删除本地备份文件失败:', error.message);
|
||||
logger.error('删除本地备份文件失败', { error: error.message, stack: error.stack });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,10 +334,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
}
|
||||
|
||||
async function executeBackupNow(options = {}) {
|
||||
console.log('');
|
||||
console.log('============================================');
|
||||
console.log('=== 手动触发备份 ===');
|
||||
console.log('============================================');
|
||||
logger.info('手动触发备份');
|
||||
|
||||
let logId = null;
|
||||
|
||||
@@ -358,7 +342,7 @@ async function executeBackupNow(options = {}) {
|
||||
const settings = loadSettings();
|
||||
const backupType = options.backupType || settings.backupType || 'full';
|
||||
|
||||
console.log('创建备份日志...');
|
||||
logger.info('创建备份日志...');
|
||||
const log = await createLogEntry({
|
||||
logType: 'manual',
|
||||
description: options.description || '手动备份',
|
||||
@@ -384,7 +368,7 @@ async function executeBackupNow(options = {}) {
|
||||
maxAgeDays: settings.maxAgeDays,
|
||||
});
|
||||
|
||||
console.log('手动备份完成:', result.filename);
|
||||
logger.info('手动备份完成', { filename: result.filename });
|
||||
|
||||
const fileSize = getFileSize(result.path);
|
||||
|
||||
@@ -399,15 +383,13 @@ async function executeBackupNow(options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('============================================\n');
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
remoteUploads: uploadResults,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('手动备份失败:', error);
|
||||
console.error('============================================\n');
|
||||
logger.error('手动备份失败', { error: error.message, stack: error.stack });
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'failed', {
|
||||
@@ -420,25 +402,22 @@ async function executeBackupNow(options = {}) {
|
||||
}
|
||||
|
||||
function initAutoBackup() {
|
||||
console.log('');
|
||||
console.log('============================================');
|
||||
console.log('=== 初始化自动备份调度器 ===');
|
||||
console.log('============================================');
|
||||
logger.info('初始化自动备份调度器');
|
||||
|
||||
const settings = loadSettings();
|
||||
|
||||
console.log('当前设置:');
|
||||
console.log(' 启用:', settings.enabled ? '是' : '否');
|
||||
console.log(' Cron表达式:', settings.cronExpression);
|
||||
console.log(' 备份类型:', settings.backupType);
|
||||
logger.info('当前设置', {
|
||||
enabled: settings.enabled,
|
||||
cronExpression: settings.cronExpression,
|
||||
backupType: settings.backupType,
|
||||
});
|
||||
|
||||
if (settings.enabled) {
|
||||
startAutoBackup(settings);
|
||||
} else {
|
||||
console.log('自动备份当前为禁用状态');
|
||||
logger.info('自动备份当前为禁用状态');
|
||||
}
|
||||
|
||||
console.log('============================================\n');
|
||||
return getAutoBackupStatus();
|
||||
}
|
||||
|
||||
|
||||
+508
-107
@@ -9,9 +9,225 @@ const crypto = require('crypto');
|
||||
const zlib = require('zlib');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { sequelize, dbDialect } = require('../db');
|
||||
const logger = require('./logger').module('Backup');
|
||||
const { enableMaintenanceMode, disableMaintenanceMode } = require('./maintenanceMode');
|
||||
|
||||
const BACKUP_VERSION = '2.0.0';
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* 还原点管理
|
||||
* 恢复前自动创建当前数据的临时备份,用于中断或失败时回滚
|
||||
*/
|
||||
const SNAPSHOT_DIR = path.join(__dirname, '..', 'backups', '.snapshots');
|
||||
|
||||
/**
|
||||
* 确保还原点目录存在
|
||||
*/
|
||||
function ensureSnapshotDir() {
|
||||
if (!fs.existsSync(SNAPSHOT_DIR)) {
|
||||
fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
||||
}
|
||||
return SNAPSHOT_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成还原点文件名
|
||||
*/
|
||||
function getSnapshotFileName(snapshotId) {
|
||||
return path.join(SNAPSHOT_DIR, `snapshot_${snapshotId}.json.gz`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建还原点(恢复前自动调用)
|
||||
* 备份当前数据库所有表的数据
|
||||
* @returns {Promise<string|null>} 还原点ID,失败返回null
|
||||
*/
|
||||
async function createSnapshot() {
|
||||
try {
|
||||
ensureSnapshotDir();
|
||||
const snapshotId = `pre_restore_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
||||
const snapshotPath = getSnapshotFileName(snapshotId);
|
||||
|
||||
logger.info('创建还原点...', { snapshotId });
|
||||
|
||||
// 收集当前所有表的数据
|
||||
const snapshotData = {
|
||||
version: BACKUP_VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'snapshot',
|
||||
data: {},
|
||||
files: null,
|
||||
};
|
||||
|
||||
for (const config of BACKUP_MODELS_CONFIG) {
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
const records = await Model.findAll({ raw: true });
|
||||
if (records.length > 0) {
|
||||
snapshotData.data[config.name] = records;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`创建还原点时跳过表 ${config.name}`, { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩并保存
|
||||
const jsonStr = JSON.stringify(snapshotData);
|
||||
const compressed = zlib.gzipSync(Buffer.from(jsonStr, 'utf8'));
|
||||
fs.writeFileSync(snapshotPath, compressed);
|
||||
|
||||
logger.info('还原点创建成功', { snapshotId, path: snapshotPath, tables: Object.keys(snapshotData.data).length });
|
||||
return snapshotId;
|
||||
} catch (error) {
|
||||
logger.error('创建还原点失败', { error: error.message, stack: error.stack });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从还原点恢复数据
|
||||
* 当恢复过程被中断或失败时调用,回滚到恢复前的状态
|
||||
* @param {string} snapshotId - 还原点ID
|
||||
* @returns {Promise<boolean>} 是否成功回滚
|
||||
*/
|
||||
async function restoreFromSnapshot(snapshotId) {
|
||||
const snapshotPath = getSnapshotFileName(snapshotId);
|
||||
|
||||
if (!fs.existsSync(snapshotPath)) {
|
||||
logger.error('还原点不存在,无法回滚', { snapshotId });
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('开始从还原点回滚数据...', { snapshotId });
|
||||
|
||||
// 读取还原点数据
|
||||
const compressed = fs.readFileSync(snapshotPath);
|
||||
const decompressed = zlib.gunzipSync(compressed);
|
||||
const snapshotData = JSON.parse(decompressed.toString('utf8'));
|
||||
|
||||
if (!snapshotData.data) {
|
||||
logger.error('还原点数据无效');
|
||||
return false;
|
||||
}
|
||||
|
||||
await disableForeignKeyChecks();
|
||||
|
||||
try {
|
||||
// 清空所有表(按恢复顺序的逆序,避免外键冲突)
|
||||
const clearOrder = [...RESTORE_ORDER].reverse();
|
||||
for (const tableName of clearOrder) {
|
||||
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
|
||||
if (!config) continue;
|
||||
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
await Model.destroy({ where: {}, truncate: true });
|
||||
} catch (error) {
|
||||
logger.warn(`回滚时清空表 ${tableName} 失败`, { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 从还原点恢复数据
|
||||
let restoredCount = 0;
|
||||
for (const tableName of RESTORE_ORDER) {
|
||||
const tableData = snapshotData.data[tableName];
|
||||
if (!tableData || tableData.length === 0) continue;
|
||||
|
||||
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
|
||||
if (!config) continue;
|
||||
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
|
||||
for (let i = 0; i < tableData.length; i += BATCH_SIZE) {
|
||||
const batch = tableData.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
await Model.bulkCreate(batch, {
|
||||
validate: false,
|
||||
individualHooks: false,
|
||||
logging: false,
|
||||
});
|
||||
restoredCount += batch.length;
|
||||
} catch (bulkError) {
|
||||
for (const record of batch) {
|
||||
try {
|
||||
await Model.create(record, { validate: false, silent: true });
|
||||
restoredCount++;
|
||||
} catch (insertError) {
|
||||
logger.warn(`回滚时插入 ${tableName} 记录失败`, { error: insertError.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`回滚表 ${tableName} 失败`, { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('还原点回滚完成', { restoredCount });
|
||||
return true;
|
||||
} finally {
|
||||
await enableForeignKeyChecks();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('从还原点回滚失败', { error: error.message, stack: error.stack });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除还原点
|
||||
* @param {string} snapshotId - 还原点ID
|
||||
*/
|
||||
function deleteSnapshot(snapshotId) {
|
||||
const snapshotPath = getSnapshotFileName(snapshotId);
|
||||
if (fs.existsSync(snapshotPath)) {
|
||||
try {
|
||||
fs.unlinkSync(snapshotPath);
|
||||
logger.info('还原点已删除', { snapshotId });
|
||||
} catch (error) {
|
||||
logger.warn('删除还原点失败', { snapshotId, error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的还原点(保留最近7天)
|
||||
*/
|
||||
function cleanupOldSnapshots() {
|
||||
try {
|
||||
if (!fs.existsSync(SNAPSHOT_DIR)) return;
|
||||
|
||||
const now = Date.now();
|
||||
const maxAgeMs = 7 * 24 * 60 * 60 * 1000; // 7天
|
||||
|
||||
const files = fs.readdirSync(SNAPSHOT_DIR);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.startsWith('snapshot_pre_restore_')) continue;
|
||||
|
||||
const filePath = path.join(SNAPSHOT_DIR, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
const age = now - stat.mtimeMs;
|
||||
|
||||
if (age > maxAgeMs) {
|
||||
fs.unlinkSync(filePath);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
logger.info('清理过期还原点', { deletedCount });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('清理过期还原点失败', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function disableForeignKeyChecks() {
|
||||
if (dbDialect === 'sqlite') {
|
||||
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
@@ -692,8 +908,17 @@ async function validateBackupFile(filePath, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// 用户相关表定义
|
||||
const USER_RELATED_TABLES = ['User', 'UserRole'];
|
||||
|
||||
async function restoreData(backupData, options = {}) {
|
||||
const { overwriteExisting = true, skipTables = [], onProgress = () => {} } = options;
|
||||
const {
|
||||
overwriteExisting = true,
|
||||
skipTables = [],
|
||||
skipUserData = false,
|
||||
shouldStop = () => false,
|
||||
onProgress = () => {}
|
||||
} = options;
|
||||
|
||||
const results = {
|
||||
tablesRestored: 0,
|
||||
@@ -701,6 +926,7 @@ async function restoreData(backupData, options = {}) {
|
||||
errors: [],
|
||||
skipped: [],
|
||||
tableDetails: {},
|
||||
stopped: false,
|
||||
};
|
||||
|
||||
const isIncremental = backupData.backupType === 'incremental';
|
||||
@@ -711,11 +937,29 @@ async function restoreData(backupData, options = {}) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 如果选择跳过用户数据,将用户相关表添加到跳过列表
|
||||
const effectiveSkipTables = [...skipTables];
|
||||
if (skipUserData) {
|
||||
USER_RELATED_TABLES.forEach(table => {
|
||||
if (!effectiveSkipTables.includes(table)) {
|
||||
effectiveSkipTables.push(table);
|
||||
}
|
||||
});
|
||||
console.log('已配置跳过用户相关表:', USER_RELATED_TABLES);
|
||||
}
|
||||
|
||||
await disableForeignKeyChecks();
|
||||
|
||||
try {
|
||||
for (const tableName of RESTORE_ORDER) {
|
||||
if (skipTables.includes(tableName)) {
|
||||
// 检查是否请求停止
|
||||
if (shouldStop()) {
|
||||
results.stopped = true;
|
||||
console.log('恢复操作被用户中断');
|
||||
break;
|
||||
}
|
||||
|
||||
if (effectiveSkipTables.includes(tableName)) {
|
||||
results.skipped.push(tableName);
|
||||
onProgress(tableName, 'skipped');
|
||||
continue;
|
||||
@@ -736,7 +980,8 @@ async function restoreData(backupData, options = {}) {
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
|
||||
if (overwriteExisting) {
|
||||
// 对于用户相关表,即使设置了 overwriteExisting 也不截断
|
||||
if (overwriteExisting && !USER_RELATED_TABLES.includes(tableName)) {
|
||||
await Model.destroy({ where: {}, truncate: true });
|
||||
}
|
||||
|
||||
@@ -764,32 +1009,57 @@ async function restoreData(backupData, options = {}) {
|
||||
});
|
||||
|
||||
let insertedCount = 0;
|
||||
for (const record of processedRecords) {
|
||||
|
||||
for (let i = 0; i < processedRecords.length; i += BATCH_SIZE) {
|
||||
if (shouldStop()) {
|
||||
results.stopped = true;
|
||||
logger.info('恢复操作在插入数据时被用户中断');
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = processedRecords.slice(i, i + BATCH_SIZE);
|
||||
|
||||
try {
|
||||
await Model.create(record, { validate: false, silent: true });
|
||||
insertedCount++;
|
||||
} catch (insertError) {
|
||||
if (insertError.name === 'SequelizeUniqueConstraintError') {
|
||||
await Model.bulkCreate(batch, {
|
||||
validate: false,
|
||||
individualHooks: false,
|
||||
logging: false,
|
||||
});
|
||||
insertedCount += batch.length;
|
||||
} catch (bulkError) {
|
||||
for (const record of batch) {
|
||||
try {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
await Model.create(record, { validate: false, silent: true });
|
||||
insertedCount++;
|
||||
} catch (upsertError) {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: upsertError.message,
|
||||
});
|
||||
} catch (insertError) {
|
||||
if (insertError.name === 'SequelizeUniqueConstraintError') {
|
||||
try {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
insertedCount++;
|
||||
} catch (upsertError) {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: upsertError.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: insertError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: insertError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已停止,跳出外层循环
|
||||
if (results.stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.tablesRestored++;
|
||||
results.recordsRestored += insertedCount;
|
||||
|
||||
@@ -843,36 +1113,51 @@ async function restoreIncrementalData(incrementalData, options = {}) {
|
||||
let updatedCount = 0;
|
||||
|
||||
if (tableIncrement.new && tableIncrement.new.length > 0) {
|
||||
for (const record of tableIncrement.new) {
|
||||
for (let i = 0; i < tableIncrement.new.length; i += BATCH_SIZE) {
|
||||
const batch = tableIncrement.new.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
await Model.create(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} catch (insertError) {
|
||||
if (insertError.name === 'SequelizeUniqueConstraintError') {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} else {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: `新增失败: ${insertError.message}`,
|
||||
});
|
||||
await Model.bulkCreate(batch, {
|
||||
validate: false,
|
||||
individualHooks: false,
|
||||
logging: false,
|
||||
});
|
||||
updatedCount += batch.length;
|
||||
} catch (bulkError) {
|
||||
for (const record of batch) {
|
||||
try {
|
||||
await Model.create(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} catch (insertError) {
|
||||
if (insertError.name === 'SequelizeUniqueConstraintError') {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} else {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: `新增失败: ${insertError.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tableIncrement.updated && tableIncrement.updated.length > 0) {
|
||||
for (const record of tableIncrement.updated) {
|
||||
try {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} catch (updateError) {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: `更新失败: ${updateError.message}`,
|
||||
});
|
||||
for (let i = 0; i < tableIncrement.updated.length; i += BATCH_SIZE) {
|
||||
const batch = tableIncrement.updated.slice(i, i + BATCH_SIZE);
|
||||
for (const record of batch) {
|
||||
try {
|
||||
await Model.upsert(record, { validate: false, silent: true });
|
||||
updatedCount++;
|
||||
} catch (updateError) {
|
||||
results.errors.push({
|
||||
table: tableName,
|
||||
record: record[Object.keys(record)[0]],
|
||||
error: `更新失败: ${updateError.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,77 +1229,189 @@ async function restoreBackup(filePath, options = {}) {
|
||||
overwriteExisting = true,
|
||||
skipTables = [],
|
||||
skipFiles = false,
|
||||
skipUserData = false,
|
||||
shouldStop = () => false,
|
||||
onProgress = () => {},
|
||||
enableRollback = true,
|
||||
} = options;
|
||||
|
||||
console.log('验证备份文件...');
|
||||
const validation = await validateBackupFile(filePath);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`备份文件验证失败: ${validation.error}`);
|
||||
}
|
||||
let snapshotId = null;
|
||||
|
||||
console.log('备份文件信息:');
|
||||
console.log(` 版本: ${validation.version}`);
|
||||
console.log(` 压缩: ${validation.compressed ? '是' : '否'}`);
|
||||
console.log(` 表数: ${validation.metadata.tableCount}`);
|
||||
console.log(` 记录数: ${validation.metadata.totalRecords}`);
|
||||
console.log(` 文件数: ${validation.metadata.fileCount}`);
|
||||
try {
|
||||
logger.info('验证备份文件...');
|
||||
const validation = await validateBackupFile(filePath);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`备份文件验证失败: ${validation.error}`);
|
||||
}
|
||||
|
||||
console.log('\n读取备份数据...');
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const isCompressed = filePath.endsWith('.gz');
|
||||
|
||||
let backupData;
|
||||
if (isCompressed) {
|
||||
console.log('解压备份文件...');
|
||||
const decompressed = zlib.gunzipSync(buffer);
|
||||
backupData = JSON.parse(decompressed.toString('utf8'));
|
||||
} else {
|
||||
backupData = JSON.parse(buffer.toString('utf8'));
|
||||
}
|
||||
|
||||
console.log('\n开始恢复数据...');
|
||||
const dataResults = await restoreData(backupData, {
|
||||
overwriteExisting,
|
||||
skipTables,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
let fileResults = { filesRestored: 0, errors: [] };
|
||||
if (!skipFiles && backupData.files) {
|
||||
console.log('\n恢复上传文件...');
|
||||
const uploadsDir = path.join(__dirname, '..', 'uploads');
|
||||
fileResults = await restoreFiles(backupData.files, uploadsDir);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
console.log('\n恢复完成!');
|
||||
console.log(`备份文件: ${(stat.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`数据记录: ${dataResults.recordsRestored} 条`);
|
||||
console.log(`恢复表数: ${dataResults.tablesRestored} 个`);
|
||||
console.log(`文件恢复: ${fileResults.filesRestored} 个`);
|
||||
|
||||
if (dataResults.errors.length > 0) {
|
||||
console.log('\n恢复错误:');
|
||||
dataResults.errors.forEach(err => {
|
||||
console.log(` - ${err.table}: ${err.error}`);
|
||||
logger.info('备份文件信息', {
|
||||
version: validation.version,
|
||||
compressed: validation.compressed,
|
||||
tableCount: validation.metadata.tableCount,
|
||||
totalRecords: validation.metadata.totalRecords,
|
||||
fileCount: validation.metadata.fileCount,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
restoredAt: new Date().toISOString(),
|
||||
tablesRestored: dataResults.tablesRestored,
|
||||
recordsRestored: dataResults.recordsRestored,
|
||||
filesRestored: fileResults.filesRestored,
|
||||
errors: dataResults.errors,
|
||||
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
|
||||
fileDetails: backupData.files
|
||||
? {
|
||||
avatars: backupData.files.avatars?.length || 0,
|
||||
others: backupData.files.others?.length || 0,
|
||||
// 步骤1: 启用维护模式,防止并发操作导致数据不一致
|
||||
logger.info('启用维护模式...');
|
||||
enableMaintenanceMode('正在恢复备份数据');
|
||||
onProgress('maintenance', 'enabled', 0);
|
||||
|
||||
try {
|
||||
// 步骤2: 创建还原点(如果启用回滚保护)
|
||||
if (enableRollback) {
|
||||
logger.info('创建还原点...');
|
||||
onProgress('snapshot', 'creating', 0);
|
||||
snapshotId = await createSnapshot();
|
||||
|
||||
if (snapshotId) {
|
||||
logger.info('还原点创建成功,开始恢复数据...');
|
||||
onProgress('snapshot', 'created', 5);
|
||||
} else {
|
||||
logger.warn('还原点创建失败,继续恢复但无法回滚');
|
||||
onProgress('snapshot', 'warning', 5);
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 步骤3: 读取备份数据
|
||||
logger.info('读取备份数据...');
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const isCompressed = filePath.endsWith('.gz');
|
||||
|
||||
let backupData;
|
||||
if (isCompressed) {
|
||||
logger.info('解压备份文件...');
|
||||
const decompressed = zlib.gunzipSync(buffer);
|
||||
backupData = JSON.parse(decompressed.toString('utf8'));
|
||||
} else {
|
||||
backupData = JSON.parse(buffer.toString('utf8'));
|
||||
}
|
||||
|
||||
// 步骤4: 恢复数据
|
||||
logger.info('开始恢复数据...');
|
||||
const dataResults = await restoreData(backupData, {
|
||||
overwriteExisting,
|
||||
skipTables,
|
||||
skipUserData,
|
||||
shouldStop,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
// 步骤5: 恢复上传文件(仅在数据恢复未停止时)
|
||||
let fileResults = { filesRestored: 0, errors: [] };
|
||||
if (!skipFiles && backupData.files && !dataResults.stopped) {
|
||||
logger.info('恢复上传文件...');
|
||||
const uploadsDir = path.join(__dirname, '..', 'uploads');
|
||||
fileResults = await restoreFiles(backupData.files, uploadsDir);
|
||||
}
|
||||
|
||||
// 步骤6: 处理恢复结果
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (dataResults.stopped) {
|
||||
// 用户中断恢复 - 自动回滚到还原点
|
||||
logger.info('恢复被用户中断,准备回滚数据...');
|
||||
|
||||
if (snapshotId && enableRollback) {
|
||||
onProgress('rollback', 'starting', 0);
|
||||
const rollbackSuccess = await restoreFromSnapshot(snapshotId);
|
||||
|
||||
if (rollbackSuccess) {
|
||||
logger.info('数据已成功回滚到恢复前的状态');
|
||||
onProgress('rollback', 'completed', 100);
|
||||
} else {
|
||||
logger.error('数据回滚失败!数据可能处于不一致状态');
|
||||
onProgress('rollback', 'failed', 0);
|
||||
}
|
||||
} else {
|
||||
logger.warn('无法回滚:没有可用的还原点');
|
||||
}
|
||||
|
||||
// 清理还原点
|
||||
if (snapshotId) {
|
||||
deleteSnapshot(snapshotId);
|
||||
}
|
||||
|
||||
return {
|
||||
restoredAt: new Date().toISOString(),
|
||||
tablesRestored: dataResults.tablesRestored,
|
||||
recordsRestored: dataResults.recordsRestored,
|
||||
filesRestored: fileResults.filesRestored,
|
||||
errors: dataResults.errors,
|
||||
skipped: dataResults.skipped || [],
|
||||
stopped: true,
|
||||
rolledBack: snapshotId ? true : false,
|
||||
tableDetails: dataResults.tableDetails,
|
||||
fileDetails: backupData.files
|
||||
? {
|
||||
avatars: backupData.files.avatars?.length || 0,
|
||||
others: backupData.files.others?.length || 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 恢复成功
|
||||
logger.info('恢复完成!', {
|
||||
fileSize: `${(stat.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
recordsRestored: dataResults.recordsRestored,
|
||||
tablesRestored: dataResults.tablesRestored,
|
||||
filesRestored: fileResults.filesRestored,
|
||||
});
|
||||
|
||||
// 清理还原点(恢复成功,不再需要)
|
||||
if (snapshotId) {
|
||||
deleteSnapshot(snapshotId);
|
||||
}
|
||||
|
||||
// 清理过期还原点
|
||||
cleanupOldSnapshots();
|
||||
|
||||
if (dataResults.errors.length > 0) {
|
||||
logger.warn('恢复过程中有错误', { errors: dataResults.errors });
|
||||
}
|
||||
|
||||
return {
|
||||
restoredAt: new Date().toISOString(),
|
||||
tablesRestored: dataResults.tablesRestored,
|
||||
recordsRestored: dataResults.recordsRestored,
|
||||
filesRestored: fileResults.filesRestored,
|
||||
errors: dataResults.errors,
|
||||
skipped: dataResults.skipped || [],
|
||||
stopped: false,
|
||||
rolledBack: false,
|
||||
tableDetails: dataResults.tableDetails,
|
||||
fileDetails: backupData.files
|
||||
? {
|
||||
avatars: backupData.files.avatars?.length || 0,
|
||||
others: backupData.files.others?.length || 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
} finally {
|
||||
// 无论成功或中断,都解除维护模式
|
||||
disableMaintenanceMode();
|
||||
onProgress('maintenance', 'disabled', 100);
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复过程发生异常 - 尝试回滚
|
||||
logger.error('恢复过程发生错误', { error: error.message, stack: error.stack });
|
||||
|
||||
if (snapshotId && enableRollback) {
|
||||
logger.info('尝试回滚数据到恢复前的状态...');
|
||||
const rollbackSuccess = await restoreFromSnapshot(snapshotId);
|
||||
|
||||
if (rollbackSuccess) {
|
||||
logger.info('数据已成功回滚');
|
||||
} else {
|
||||
logger.error('数据回滚失败!数据可能处于不一致状态');
|
||||
}
|
||||
|
||||
deleteSnapshot(snapshotId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanOldBackups(options = {}) {
|
||||
@@ -1115,4 +1512,8 @@ module.exports = {
|
||||
restoreBackup,
|
||||
calculateChecksum,
|
||||
cleanOldBackups,
|
||||
createSnapshot,
|
||||
restoreFromSnapshot,
|
||||
deleteSnapshot,
|
||||
cleanupOldSnapshots,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const logger = require('./logger').module('ErrorHandler');
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(code, message, statusCode = 500, details = null) {
|
||||
super(message);
|
||||
@@ -43,9 +45,7 @@ const FRIENDLY_MESSAGES = {
|
||||
};
|
||||
|
||||
function logError(error, req = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logData = {
|
||||
timestamp,
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
statusCode: error.statusCode,
|
||||
@@ -57,8 +57,7 @@ function logError(error, req = null) {
|
||||
method: req?.method,
|
||||
};
|
||||
|
||||
console.error('=== ERROR LOG ===');
|
||||
console.error(JSON.stringify(logData, null, 2));
|
||||
logger.error('应用错误', logData);
|
||||
}
|
||||
|
||||
function buildErrorResponse(error) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 统一日志模块
|
||||
* 基于 winston 的日志系统,支持文件轮转、级别控制、模块标识
|
||||
* 全项目唯一日志入口
|
||||
*/
|
||||
|
||||
const { createLogger, format, transports } = require('winston');
|
||||
const DailyRotateFile = require('winston-daily-rotate-file');
|
||||
const path = require('path');
|
||||
|
||||
// 日志目录
|
||||
const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '../logs');
|
||||
|
||||
// 日志级别
|
||||
const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
|
||||
|
||||
// 单个日志文件最大大小
|
||||
const LOG_MAX_FILE_SIZE = process.env.LOG_MAX_FILE_SIZE || '20m';
|
||||
|
||||
// 日志文件最大保留天数
|
||||
const LOG_MAX_FILES = process.env.LOG_MAX_FILES || '30d';
|
||||
|
||||
/**
|
||||
* 统一日志格式:时间 | 级别 | 模块 | 消息 | 元数据
|
||||
*/
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
|
||||
format.errors({ stack: true }),
|
||||
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'module'] }),
|
||||
format.printf(({ timestamp, level, message, module, metadata, stack }) => {
|
||||
const moduleTag = module ? `[${module}]` : '';
|
||||
const metaStr = metadata && Object.keys(metadata).length
|
||||
? ' ' + JSON.stringify(metadata)
|
||||
: '';
|
||||
const stackStr = stack ? `\n${stack}` : '';
|
||||
return `${timestamp} ${level.toUpperCase().padEnd(5)} ${moduleTag} ${message}${metaStr}${stackStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建 winston logger 实例
|
||||
*/
|
||||
const logger = createLogger({
|
||||
level: LOG_LEVEL,
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'idc-management' },
|
||||
transports: [
|
||||
// 应用日志 - 按天轮转
|
||||
new DailyRotateFile({
|
||||
dirname: path.join(LOG_DIR, 'app'),
|
||||
filename: 'application-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: LOG_MAX_FILES,
|
||||
maxSize: LOG_MAX_FILE_SIZE,
|
||||
level: 'info',
|
||||
}),
|
||||
// 错误日志 - 独立存储
|
||||
new DailyRotateFile({
|
||||
dirname: path.join(LOG_DIR, 'error'),
|
||||
filename: 'error-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: '60d',
|
||||
maxSize: LOG_MAX_FILE_SIZE,
|
||||
level: 'error',
|
||||
}),
|
||||
],
|
||||
// 未捕获异常处理
|
||||
exceptionHandlers: [
|
||||
new DailyRotateFile({
|
||||
dirname: path.join(LOG_DIR, 'error'),
|
||||
filename: 'exceptions-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: LOG_MAX_FILES,
|
||||
}),
|
||||
],
|
||||
// 未处理 Promise 拒绝处理
|
||||
rejectionHandlers: [
|
||||
new DailyRotateFile({
|
||||
dirname: path.join(LOG_DIR, 'error'),
|
||||
filename: 'rejections-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: LOG_MAX_FILES,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 开发环境添加控制台输出(带颜色)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: 'HH:mm:ss' }),
|
||||
format.printf(({ timestamp, level, message, module }) => {
|
||||
const moduleTag = module ? `[${module}]` : '';
|
||||
return `${timestamp} ${level} ${moduleTag} ${message}`;
|
||||
})
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带模块名的子 logger
|
||||
* @param {string} moduleName - 模块名称
|
||||
* @returns {Object} 包含 debug/info/warn/error 方法的对象
|
||||
*/
|
||||
logger.module = (moduleName) => ({
|
||||
debug: (msg, meta) => logger.debug(msg, { module: moduleName, ...meta }),
|
||||
info: (msg, meta) => logger.info(msg, { module: moduleName, ...meta }),
|
||||
warn: (msg, meta) => logger.warn(msg, { module: moduleName, ...meta }),
|
||||
error: (msg, meta) => logger.error(msg, { module: moduleName, ...meta }),
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,79 @@
|
||||
const logger = require('./logger').module('MaintenanceMode');
|
||||
|
||||
const MAINTENANCE_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
const MaintenanceMode = {
|
||||
isActive: false,
|
||||
startTime: null,
|
||||
reason: null,
|
||||
allowedRoutes: ['/api/backup', '/api/auth', '/api/maintenance'],
|
||||
};
|
||||
|
||||
let timeoutTimer = null;
|
||||
|
||||
function clearTimeoutTimer() {
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function isTimedOut() {
|
||||
if (!MaintenanceMode.startTime) return false;
|
||||
return Date.now() - MaintenanceMode.startTime.getTime() > MAINTENANCE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function enableMaintenanceMode(reason = '系统维护中') {
|
||||
clearTimeoutTimer();
|
||||
|
||||
MaintenanceMode.isActive = true;
|
||||
MaintenanceMode.startTime = new Date();
|
||||
MaintenanceMode.reason = reason;
|
||||
|
||||
timeoutTimer = setTimeout(() => {
|
||||
if (MaintenanceMode.isActive) {
|
||||
logger.warn('维护模式超时,自动解除', {
|
||||
reason: MaintenanceMode.reason,
|
||||
startTime: MaintenanceMode.startTime,
|
||||
});
|
||||
disableMaintenanceMode();
|
||||
}
|
||||
}, MAINTENANCE_TIMEOUT_MS);
|
||||
|
||||
logger.info('维护模式已启用', { reason });
|
||||
}
|
||||
|
||||
function disableMaintenanceMode() {
|
||||
clearTimeoutTimer();
|
||||
|
||||
MaintenanceMode.isActive = false;
|
||||
MaintenanceMode.reason = null;
|
||||
MaintenanceMode.startTime = null;
|
||||
|
||||
logger.info('维护模式已解除');
|
||||
}
|
||||
|
||||
function isMaintenanceModeActive() {
|
||||
if (MaintenanceMode.isActive && isTimedOut()) {
|
||||
disableMaintenanceMode();
|
||||
return false;
|
||||
}
|
||||
return MaintenanceMode.isActive;
|
||||
}
|
||||
|
||||
function getMaintenanceStatus() {
|
||||
return {
|
||||
active: MaintenanceMode.isActive,
|
||||
reason: MaintenanceMode.reason,
|
||||
startTime: MaintenanceMode.startTime,
|
||||
timeoutMs: MAINTENANCE_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MaintenanceMode,
|
||||
enableMaintenanceMode,
|
||||
disableMaintenanceMode,
|
||||
isMaintenanceModeActive,
|
||||
getMaintenanceStatus,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const routesConfig = require('../config/routes');
|
||||
const logger = require('./logger').module('RouteLoader');
|
||||
|
||||
function loadRoutes(app) {
|
||||
const routesDir = path.join(__dirname, '../routes');
|
||||
@@ -11,12 +12,12 @@ function loadRoutes(app) {
|
||||
|
||||
if (typeof router === 'function') {
|
||||
app.use(routeConfig.path, router);
|
||||
console.log(`路由已加载: ${routeConfig.path} -> ${routeConfig.file}`);
|
||||
logger.info(`路由已加载: ${routeConfig.path} -> ${routeConfig.file}`);
|
||||
} else {
|
||||
console.warn(`警告: ${routeConfig.file} 没有导出有效的 Express 路由器`);
|
||||
logger.warn(`${routeConfig.file} 没有导出有效的 Express 路由器`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载路由失败: ${routeConfig.file}`, error.message);
|
||||
logger.error(`加载路由失败: ${routeConfig.file}`, { error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user