From bf2cd221b9f2899cc38adb5acb477e7bab86729a Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 28 Apr 2026 10:57:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=B3=BB=E7=BB=9F=E5=92=8C=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 17 + backend/db.js | 14 +- backend/initDeviceFields.js | 18 +- backend/middleware/auth.js | 22 +- backend/middleware/maintenance.js | 31 + backend/middleware/requestLogger.js | 40 ++ backend/middleware/validation.js | 4 +- backend/models/Device.js | 4 +- backend/models/Rack.js | 4 +- backend/models/Ticket.js | 8 +- backend/routes/auth.js | 15 +- backend/routes/background.js | 13 +- backend/routes/backup.js | 164 +++-- backend/routes/cables.js | 27 +- backend/routes/consumables.js | 11 +- backend/routes/dangerousOperations.js | 9 +- backend/routes/devicePorts.js | 23 +- backend/routes/devices.js | 55 +- backend/routes/idleDevices.js | 49 +- backend/routes/inventory.js | 13 +- backend/routes/networkCards.js | 21 +- backend/routes/operationLogs.js | 11 +- backend/routes/racks.js | 7 +- backend/routes/roles.js | 15 +- backend/routes/statistics.js | 3 +- backend/routes/systemSettings.js | 15 +- backend/routes/tickets.js | 3 +- backend/routes/topology.js | 5 +- backend/routes/users.js | 23 +- backend/routes/warehouses.js | 7 +- backend/scripts/fix-foreign-keys.js | 275 ++++++++ backend/scripts/migrate-logs.js | 136 ++++ backend/server.js | 76 ++- backend/utils/autoBackupScheduler.js | 91 +-- backend/utils/backup.js | 615 +++++++++++++++--- backend/utils/errorHandler.js | 7 +- backend/utils/logger.js | 113 ++++ backend/utils/maintenanceMode.js | 79 +++ backend/utils/routeLoader.js | 7 +- frontend/src/App.jsx | 2 + frontend/src/api/index.js | 14 +- frontend/src/components/MaintenanceBanner.jsx | 69 ++ frontend/src/pages/BackupManagement.jsx | 261 ++++++++ 43 files changed, 1966 insertions(+), 430 deletions(-) create mode 100644 backend/middleware/maintenance.js create mode 100644 backend/middleware/requestLogger.js create mode 100644 backend/scripts/fix-foreign-keys.js create mode 100644 backend/scripts/migrate-logs.js create mode 100644 backend/utils/logger.js create mode 100644 backend/utils/maintenanceMode.js create mode 100644 frontend/src/components/MaintenanceBanner.jsx diff --git a/backend/.env.example b/backend/.env.example index 2d05d83..24a0968 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -100,6 +100,23 @@ MAX_RETRIES=5 # 重试延迟(毫秒) RETRY_DELAY=2000 +# ============================================== +# 日志配置 +# ============================================== + +# 日志级别: error / warn / info / debug +# 生产环境建议 info,开发环境建议 debug +LOG_LEVEL=debug + +# 日志文件存储目录(相对路径或绝对路径) +LOG_DIR=./logs + +# 单个日志文件最大大小(支持单位: k/m/g) +LOG_MAX_FILE_SIZE=20m + +# 日志文件最大保留天数 +LOG_MAX_FILES=30d + # ============================================== # 前端配置 # ============================================== diff --git a/backend/db.js b/backend/db.js index 8d6e00d..46725b0 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,4 +1,5 @@ const { Sequelize } = require('sequelize'); +const logger = require('./utils/logger').module('Sequelize'); // 获取数据库配置 const DB_TYPE = process.env.DB_TYPE || 'sqlite'; // 数据库类型:sqlite 或 mysql @@ -7,6 +8,12 @@ const DB_TYPE = process.env.DB_TYPE || 'sqlite'; // 数据库类型:sqlite 或 let sequelize; let dbDialect = 'sqlite'; // 实际使用的数据库类型 +/** + * Sequelize SQL 日志函数 + * 仅输出 debug 级别日志,生产环境自动过滤 + */ +const sqlLogger = (msg) => logger.debug(msg); + if (DB_TYPE === 'mysql') { // MySQL 配置 sequelize = new Sequelize( @@ -17,7 +24,7 @@ if (DB_TYPE === 'mysql') { host: process.env.MYSQL_HOST || 'localhost', port: parseInt(process.env.MYSQL_PORT) || 3306, dialect: 'mysql', - logging: process.env.NODE_ENV === 'development' ? console.log : false, + logging: sqlLogger, // 连接池配置 - 提升并发处理能力 pool: { max: 10, // 最大连接数 @@ -32,7 +39,7 @@ if (DB_TYPE === 'mysql') { sequelize = new Sequelize({ dialect: 'sqlite', storage: process.env.DB_PATH || './idc_management.db', - logging: console.log, + logging: sqlLogger, // SQLite 连接池配置 pool: { max: 5, @@ -42,6 +49,9 @@ if (DB_TYPE === 'mysql') { }, }); dbDialect = 'sqlite'; + + // 启用 SQLite 外键约束支持 + sequelize.query('PRAGMA foreign_keys = ON'); } module.exports = { sequelize, DB_TYPE, dbDialect }; diff --git a/backend/initDeviceFields.js b/backend/initDeviceFields.js index 270196b..c191406 100644 --- a/backend/initDeviceFields.js +++ b/backend/initDeviceFields.js @@ -174,16 +174,26 @@ async function initDeviceFields() { console.log(`创建字段: ${field.displayName}`); } else { // 如果字段已存在但缺少 options,则补充 options - if (field.options && !existingField.options) { + // 确保 options 是数组(数据库 JSON 类型可能返回字符串) + let existingOptions = existingField.options; + if (existingOptions && typeof existingOptions === 'string') { + try { + existingOptions = JSON.parse(existingOptions); + } catch { + existingOptions = null; + } + } + + if (field.options && !existingOptions) { await existingField.update({ options: field.options }); console.log(`更新字段 options: ${field.displayName}`); - } else if (field.options && existingField.options) { + } else if (field.options && Array.isArray(existingOptions)) { // 如果系统字段已有 options,检查是否缺少默认选项,补充缺失的选项 - const existingValues = existingField.options.map(o => o.value); + const existingValues = existingOptions.map(o => o.value); const defaultValues = field.options.map(o => o.value); const missingOptions = field.options.filter(o => !existingValues.includes(o.value)); if (missingOptions.length > 0) { - const updatedOptions = [...existingField.options, ...missingOptions]; + const updatedOptions = [...existingOptions, ...missingOptions]; await existingField.update({ options: updatedOptions }); console.log( `补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}` diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 5871741..3ae44a4 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const User = require('../models/User'); +const logger = require('../utils/logger').module('AuthMiddleware'); function getJwtSecret() { const envSecret = process.env.JWT_SECRET; @@ -124,11 +125,10 @@ const authMiddleware = async (req, res, next) => { user = await User.findByPk(decoded.userId); } catch (dbError) { // 数据库查询错误 - console.error('[认证中间件] 数据库查询失败:', { + logger.error('数据库查询失败', { userId: decoded.userId, error: dbError.message, stack: dbError.stack, - timestamp: new Date().toISOString(), }); return res.status(500).json({ success: false, @@ -137,10 +137,9 @@ const authMiddleware = async (req, res, next) => { } if (!user) { - console.warn('[认证中间件] 用户不存在:', { + logger.warn('用户不存在', { userId: decoded.userId, username: decoded.username, - timestamp: new Date().toISOString(), }); return res.status(401).json({ success: false, @@ -149,10 +148,9 @@ const authMiddleware = async (req, res, next) => { } if (user.status === 'locked') { - console.warn('[认证中间件] 账户已被锁定:', { + logger.warn('账户已被锁定', { userId: user.userId, username: user.username, - timestamp: new Date().toISOString(), }); return res.status(403).json({ success: false, @@ -161,10 +159,9 @@ const authMiddleware = async (req, res, next) => { } if (user.status === 'inactive') { - console.warn('[认证中间件] 账户已禁用:', { + logger.warn('账户已禁用', { userId: user.userId, username: user.username, - timestamp: new Date().toISOString(), }); return res.status(403).json({ success: false, @@ -177,13 +174,12 @@ const authMiddleware = async (req, res, next) => { next(); } catch (error) { // 捕获未预期的错误 - console.error('[认证中间件] 未预期的错误:', { + logger.error('未预期的错误', { error: error.message, stack: error.stack, url: req?.url, method: req?.method, ip: req?.ip, - timestamp: new Date().toISOString(), }); return res.status(500).json({ success: false, @@ -206,10 +202,9 @@ const optionalAuth = async (req, res, next) => { user = await User.findByPk(decoded.userId); } catch (dbError) { // 数据库错误时不中断请求,仅记录日志 - console.warn('[可选认证] 数据库查询失败:', { + logger.warn('可选认证数据库查询失败', { userId: decoded.userId, error: dbError.message, - timestamp: new Date().toISOString(), }); // 继续执行,不设置用户信息 next(); @@ -226,10 +221,9 @@ const optionalAuth = async (req, res, next) => { next(); } catch (error) { // 可选认证失败不影响主流程,仅记录日志 - console.warn('[可选认证] 认证失败(已忽略):', { + logger.warn('可选认证失败(已忽略)', { error: error.message, url: req?.url, - timestamp: new Date().toISOString(), }); next(); } diff --git a/backend/middleware/maintenance.js b/backend/middleware/maintenance.js new file mode 100644 index 0000000..232d51e --- /dev/null +++ b/backend/middleware/maintenance.js @@ -0,0 +1,31 @@ +const { isMaintenanceModeActive, MaintenanceMode } = require('../utils/maintenanceMode'); + +/** + * 维护模式中间件 + * 维护期间拦截所有非白名单请求,返回 503 状态码 + */ +function maintenanceMiddleware(req, res, next) { + if (!isMaintenanceModeActive()) { + return next(); + } + + const isAllowed = MaintenanceMode.allowedRoutes.some((route) => + req.path.startsWith(route) + ); + + if (isAllowed) { + return next(); + } + + return res.status(503).json({ + success: false, + message: '系统正在维护中,请稍后再试', + maintenance: { + active: true, + reason: MaintenanceMode.reason, + startTime: MaintenanceMode.startTime, + }, + }); +} + +module.exports = { maintenanceMiddleware }; diff --git a/backend/middleware/requestLogger.js b/backend/middleware/requestLogger.js new file mode 100644 index 0000000..c5bee4f --- /dev/null +++ b/backend/middleware/requestLogger.js @@ -0,0 +1,40 @@ +/** + * HTTP 请求日志中间件 + * 为每个请求生成唯一 ID,记录请求/响应信息 + */ + +const logger = require('../utils/logger'); +const crypto = require('crypto'); + +/** + * 请求日志中间件 + * 记录每个 HTTP 请求的方法、URL、状态码、耗时、用户ID 等信息 + */ +const requestLogger = (req, res, next) => { + const requestId = crypto.randomBytes(4).toString('hex'); + const startTime = Date.now(); + + req.requestId = requestId; + + res.on('finish', () => { + const duration = Date.now() - startTime; + const level = res.statusCode >= 500 ? 'error' + : res.statusCode >= 400 ? 'warn' : 'debug'; + + logger[level](`${req.method} ${req.originalUrl}`, { + module: 'HTTP', + requestId, + method: req.method, + url: req.originalUrl, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('User-Agent'), + userId: req.user?.userId || null, + }); + }); + + next(); +}; + +module.exports = requestLogger; diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js index cd8598c..bb0446e 100644 --- a/backend/middleware/validation.js +++ b/backend/middleware/validation.js @@ -1,3 +1,5 @@ +const logger = require('../utils/logger').module('ValidationMiddleware'); + const validate = (schema, source = 'body') => { return async (req, res, next) => { const data = source === 'query' ? req.query : req.body; @@ -73,7 +75,7 @@ const validate = (schema, source = 'body') => { }); } - console.error('验证中间件错误:', error); + logger.error('验证中间件错误', { error: error.message, stack: error.stack }); return res.status(500).json({ error: '验证过程发生错误', message: error.message, diff --git a/backend/models/Device.js b/backend/models/Device.js index 7dd3de7..02fae99 100644 --- a/backend/models/Device.js +++ b/backend/models/Device.js @@ -118,7 +118,7 @@ const Device = sequelize.define( } ); -Device.belongsTo(Rack, { foreignKey: 'rackId' }); -Device.belongsTo(Warehouse, { foreignKey: 'warehouseId' }); +Device.belongsTo(Rack, { foreignKey: 'rackId', onDelete: 'SET NULL' }); +Device.belongsTo(Warehouse, { foreignKey: 'warehouseId', onDelete: 'SET NULL' }); module.exports = Device; diff --git a/backend/models/Rack.js b/backend/models/Rack.js index d0d903d..8242738 100644 --- a/backend/models/Rack.js +++ b/backend/models/Rack.js @@ -57,7 +57,7 @@ const Rack = sequelize.define( ); // 关联关系 -Rack.belongsTo(Room, { foreignKey: 'roomId' }); -Room.hasMany(Rack, { foreignKey: 'roomId' }); +Rack.belongsTo(Room, { foreignKey: 'roomId', onDelete: 'CASCADE' }); +Room.hasMany(Rack, { foreignKey: 'roomId', onDelete: 'CASCADE' }); module.exports = Rack; diff --git a/backend/models/Ticket.js b/backend/models/Ticket.js index d4a747a..ecb910d 100644 --- a/backend/models/Ticket.js +++ b/backend/models/Ticket.js @@ -65,7 +65,7 @@ const Ticket = sequelize.define( }, reporterId: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, comment: '报修人ID', }, reporterName: { @@ -139,8 +139,8 @@ const Ticket = sequelize.define( } ); -Ticket.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter', constraints: false }); -Ticket.belongsTo(User, { foreignKey: 'assigneeId', as: 'assignee', constraints: false }); -Ticket.belongsTo(Device, { foreignKey: 'deviceId', constraints: false }); +Ticket.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter', onDelete: 'SET NULL' }); +Ticket.belongsTo(User, { foreignKey: 'assigneeId', as: 'assignee', onDelete: 'SET NULL' }); +Ticket.belongsTo(Device, { foreignKey: 'deviceId', onDelete: 'SET NULL' }); module.exports = Ticket; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a2d5b09..0ce05e6 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('AuthRoute'); const express = require('express'); const bcrypt = require('bcryptjs'); const User = require('../models/User'); @@ -124,7 +125,7 @@ router.post('/register', async (req, res) => { }); } } catch (error) { - console.error('注册错误:', error); + logger.error('注册错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '注册失败', @@ -222,7 +223,7 @@ router.post('/login', async (req, res) => { }, }); } catch (error) { - console.error('登录错误:', error); + logger.error('登录错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '登录失败', @@ -264,7 +265,7 @@ router.get('/profile', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取profile错误:', error); + logger.error('获取profile错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取用户信息失败', @@ -312,7 +313,7 @@ router.put('/profile', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('更新profile错误:', error); + logger.error('更新profile错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新失败', @@ -356,7 +357,7 @@ router.put('/password', authMiddleware, async (req, res) => { message: '密码修改成功', }); } catch (error) { - console.error('修改密码错误:', error); + logger.error('修改密码错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '密码修改失败', @@ -376,7 +377,7 @@ router.post('/check-admin', async (req, res) => { }, }); } catch (error) { - console.error('检查管理员错误:', error); + logger.error('检查管理员错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '检查失败', @@ -428,7 +429,7 @@ router.post('/unlock', async (req, res) => { message: '账户解锁成功', }); } catch (error) { - console.error('解锁账户错误:', error); + logger.error('解锁账户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '解锁失败', diff --git a/backend/routes/background.js b/backend/routes/background.js index 330c5d7..c3a6d96 100644 --- a/backend/routes/background.js +++ b/backend/routes/background.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('BackgroundRoute'); const express = require('express'); const path = require('path'); const fs = require('fs'); @@ -36,7 +37,7 @@ router.get('/', (req, res) => { data: settings, }); } catch (error) { - console.error('读取背景设置失败:', error); + logger.error('读取背景设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '读取背景设置失败' }); } }); @@ -50,7 +51,7 @@ router.put('/', (req, res) => { data: settings, }); } catch (error) { - console.error('保存背景设置失败:', error); + logger.error('保存背景设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '保存背景设置失败' }); } }); @@ -88,7 +89,7 @@ router.post('/upload', (req, res) => { file.mv(filePath, err => { if (err) { - console.error('文件保存失败:', err); + logger.error('文件保存失败', { err: err.message, stack: err.stack }); return res.status(500).json({ error: '文件保存失败' }); } @@ -96,7 +97,7 @@ router.post('/upload', (req, res) => { res.json({ path: fileUrl }); }); } catch (error) { - console.error('上传错误:', error); + logger.error('上传错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: '上传失败' }); } }); @@ -106,7 +107,7 @@ router.get('/settings', (req, res) => { const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); res.json(settings); } catch (error) { - console.error('读取背景设置失败:', error); + logger.error('读取背景设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '读取背景设置失败' }); } }); @@ -117,7 +118,7 @@ router.post('/settings', (req, res) => { fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); res.json({ success: true }); } catch (error) { - console.error('保存背景设置失败:', error); + logger.error('保存背景设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '保存背景设置失败' }); } }); diff --git a/backend/routes/backup.js b/backend/routes/backup.js index 3548323..f75983f 100644 --- a/backend/routes/backup.js +++ b/backend/routes/backup.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('BackupRoute'); const express = require('express'); const router = express.Router(); const fs = require('fs'); @@ -76,7 +77,7 @@ router.post('/', async (req, res) => { try { const { description = '', includeFiles = true } = req.body; - console.log('开始创建备份...'); + logger.info('开始创建备份...'); const result = await createBackup({ description, includeFiles: includeFiles !== false, @@ -88,7 +89,7 @@ router.post('/', async (req, res) => { data: result, }); } catch (error) { - console.error('创建备份失败:', error); + logger.error('创建备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '创建备份失败', @@ -178,7 +179,7 @@ router.get('/list', async (req, res) => { data: { backups: resolvedFiles, total: resolvedFiles.length }, }); } catch (error) { - console.error('获取备份列表失败:', error); + logger.error('获取备份列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取备份列表失败', @@ -211,7 +212,7 @@ router.get('/validate/:filename', async (req, res) => { data: validation, }); } catch (error) { - console.error('验证备份文件失败:', error); + logger.error('验证备份文件失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '验证备份文件失败', @@ -248,7 +249,8 @@ router.get('/restore-progress/:filename', async (req, res) => { }; try { - console.log(`开始恢复备份: ${filename}`); + logger.info('开始恢复备份: ${filename}'); + logger.info('恢复选项', { data: options }); sendProgress({ stage: 'start', message: '正在验证备份文件...', progress: 5 }); const validation = await validateBackupFile(filePath); @@ -285,12 +287,56 @@ router.get('/restore-progress/:filename', async (req, res) => { const totalTables = require('../utils/backup').RESTORE_ORDER.length; let processedTables = 0; + let isStopped = false; + let clientDisconnected = false; + + // 监听客户端断开连接 - 使用多种事件确保可靠检测 + req.on('close', () => { + if (!clientDisconnected) { + clientDisconnected = true; + logger.info('客户端断开连接(req.close),准备停止恢复...'); + isStopped = true; + } + }); + + req.on('aborted', () => { + if (!clientDisconnected) { + clientDisconnected = true; + logger.info('客户端中断请求(req.aborted),准备停止恢复...'); + isStopped = true; + } + }); + + res.on('close', () => { + if (!clientDisconnected) { + clientDisconnected = true; + logger.info('响应流关闭(res.close),准备停止恢复...'); + isStopped = true; + } + }); + + // 定期检查客户端是否仍然连接 + const keepAliveInterval = setInterval(() => { + if (res.writableEnded || res.destroyed) { + if (!clientDisconnected) { + clientDisconnected = true; + logger.info('检测到响应流已结束,准备停止恢复...'); + isStopped = true; + } + clearInterval(keepAliveInterval); + } + }, 1000); const result = await restoreBackup(filePath, { overwriteExisting: options.overwriteExisting !== false, skipTables: options.skipTables || [], skipFiles: options.skipFiles === true, + skipUserData: options.skipUserData === true, + shouldStop: () => isStopped, onProgress: (tableName, status, count) => { + // 如果已停止,不再发送进度 + if (isStopped) return; + processedTables++; const progress = 20 + Math.floor((processedTables / totalTables) * 70); const statusMap = { @@ -312,23 +358,43 @@ router.get('/restore-progress/:filename', async (req, res) => { }, }); - sendProgress({ - stage: 'complete', - message: '恢复完成!', - progress: 100, - result: { - tablesRestored: result.tablesRestored, - recordsRestored: result.recordsRestored, - filesRestored: result.filesRestored, - restoredAt: result.restoredAt, - tableDetails: result.tableDetails, - fileDetails: result.fileDetails, - }, - }); + clearInterval(keepAliveInterval); + + if (result.stopped) { + sendProgress({ + stage: 'stopped', + message: result.rolledBack ? '恢复已停止,数据已回滚到恢复前的状态' : '恢复已停止,数据可能处于不一致状态', + progress: 0, + result: { + tablesRestored: result.tablesRestored, + recordsRestored: result.recordsRestored, + filesRestored: result.filesRestored, + restoredAt: result.restoredAt, + tableDetails: result.tableDetails, + fileDetails: result.fileDetails, + stopped: true, + rolledBack: result.rolledBack, + }, + }); + } else { + sendProgress({ + stage: 'complete', + message: '恢复完成!', + progress: 100, + result: { + tablesRestored: result.tablesRestored, + recordsRestored: result.recordsRestored, + filesRestored: result.filesRestored, + restoredAt: result.restoredAt, + tableDetails: result.tableDetails, + fileDetails: result.fileDetails, + }, + }); + } res.end(); } catch (error) { - console.error('恢复备份失败:', error); + logger.error('恢复备份失败', { error: error.message, stack: error.stack }); sendProgress({ stage: 'error', message: `恢复失败: ${error.message}`, progress: 0 }); res.end(); } @@ -352,14 +418,16 @@ router.post('/restore', async (req, res) => { }); } - console.log(`开始恢复备份: ${filename}`); + logger.info('开始恢复备份: ${filename}'); + logger.info('恢复选项', { data: options }); const result = await restoreBackup(filePath, { overwriteExisting: options.overwriteExisting !== false, skipTables: options.skipTables || [], skipFiles: options.skipFiles === true, + skipUserData: options.skipUserData === true, onProgress: (tableName, status, count) => { - console.log(` ${tableName}: ${status}${count ? ` (${count})` : ''}`); + logger.info(` ${tableName}: ${status}${count ? ` (${count})` : ''}`); }, }); @@ -369,7 +437,7 @@ router.post('/restore', async (req, res) => { data: result, }); } catch (error) { - console.error('恢复备份失败:', error); + logger.error('恢复备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '恢复备份失败', @@ -401,7 +469,7 @@ router.post('/upload', async (req, res) => { const isCompressed = nameLower.endsWith('.gz'); - console.log(`上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}`); + logger.info('上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}'); // 保存到临时文件 const tempFilename = `upload_${Date.now()}`; @@ -437,7 +505,7 @@ router.post('/upload', async (req, res) => { }, }); } catch (error) { - console.error('上传备份文件失败:', error); + logger.error('上传备份文件失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '上传备份文件失败', @@ -465,11 +533,11 @@ router.get('/download/:filename', (req, res) => { res.download(filePath, filename, err => { if (err) { - console.error('下载备份文件失败:', err); + logger.error('下载备份文件失败', { err: err.message, stack: err.stack }); } }); } catch (error) { - console.error('下载备份文件失败:', error); + logger.error('下载备份文件失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '下载备份文件失败', @@ -503,7 +571,7 @@ router.delete('/:filename', (req, res) => { data: { filename }, }); } catch (error) { - console.error('删除备份文件失败:', error); + logger.error('删除备份文件失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '删除备份文件失败', @@ -539,7 +607,7 @@ router.get('/info', (req, res) => { }, }); } catch (error) { - console.error('获取备份信息失败:', error); + logger.error('获取备份信息失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取备份信息失败', @@ -570,7 +638,7 @@ router.post('/clean', (req, res) => { data: result, }); } catch (error) { - console.error('清理备份失败:', error); + logger.error('清理备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '清理备份失败', @@ -580,7 +648,7 @@ router.post('/clean', (req, res) => { }); router.use((error, req, res, next) => { - console.error('路由错误:', error); + logger.error('路由错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '服务器内部错误', @@ -599,7 +667,7 @@ router.get('/auto/status', (req, res) => { data: status, }); } catch (error) { - console.error('获取自动备份状态失败:', error); + logger.error('获取自动备份状态失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取自动备份状态失败', @@ -675,7 +743,7 @@ router.post('/auto/settings', (req, res) => { }); } } catch (error) { - console.error('更新自动备份设置失败:', error); + logger.error('更新自动备份设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新自动备份设置失败', @@ -709,7 +777,7 @@ router.post('/auto/execute', async (req, res) => { }); } } catch (error) { - console.error('立即执行备份失败:', error); + logger.error('立即执行备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '立即执行备份失败', @@ -741,7 +809,7 @@ router.post('/auto/test-cron', (req, res) => { }, }); } catch (error) { - console.error('测试 Cron 表达式失败:', error); + logger.error('测试 Cron 表达式失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '测试 Cron 表达式失败', @@ -761,7 +829,7 @@ router.get('/remote/targets', (req, res) => { data: { targets }, }); } catch (error) { - console.error('获取远端备份目标失败:', error); + logger.error('获取远端备份目标失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取远端备份目标失败', @@ -787,7 +855,7 @@ router.get('/remote/targets/:id', (req, res) => { data: { target }, }); } catch (error) { - console.error('获取远端备份目标失败:', error); + logger.error('获取远端备份目标失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取远端备份目标失败', @@ -816,7 +884,7 @@ router.post('/remote/targets', (req, res) => { data: { target }, }); } catch (error) { - console.error('添加远端备份目标失败:', error); + logger.error('添加远端备份目标失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '添加远端备份目标失败', @@ -837,7 +905,7 @@ router.put('/remote/targets/:id', (req, res) => { data: { target }, }); } catch (error) { - console.error('更新远端备份目标失败:', error); + logger.error('更新远端备份目标失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新远端备份目标失败', @@ -863,7 +931,7 @@ router.delete('/remote/targets/:id', (req, res) => { message: '远端备份目标已删除', }); } catch (error) { - console.error('删除远端备份目标失败:', error); + logger.error('删除远端备份目标失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '删除远端备份目标失败', @@ -907,7 +975,7 @@ router.post('/remote/test', async (req, res) => { }); } } catch (error) { - console.error('[RemoteBackup] 测试远端连接失败:', error); + logger.error('[RemoteBackup] 测试远端连接失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '测试远端连接失败', @@ -944,7 +1012,7 @@ router.post('/remote/targets/:id/test', async (req, res) => { }); } } catch (error) { - console.error('测试远端连接失败:', error); + logger.error('测试远端连接失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '测试远端连接失败', @@ -962,7 +1030,7 @@ router.get('/remote/settings', (req, res) => { data: { settings }, }); } catch (error) { - console.error('获取远端备份设置失败:', error); + logger.error('获取远端备份设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取远端备份设置失败', @@ -981,7 +1049,7 @@ router.put('/remote/settings', (req, res) => { data: { settings }, }); } catch (error) { - console.error('更新远端备份设置失败:', error); + logger.error('更新远端备份设置失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新远端备份设置失败', @@ -1004,7 +1072,7 @@ router.get('/remote/protocols', (req, res) => { data: { protocols }, }); } catch (error) { - console.error('获取协议列表失败:', error); + logger.error('获取协议列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取协议列表失败', @@ -1078,7 +1146,7 @@ router.post('/remote/upload', async (req, res) => { }, }); } catch (error) { - console.error('手动上传备份失败:', error); + logger.error('手动上传备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '手动上传备份失败', @@ -1106,7 +1174,7 @@ router.get('/logs', async (req, res) => { data: result, }); } catch (error) { - console.error('获取备份日志失败:', error); + logger.error('获取备份日志失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取备份日志失败', @@ -1133,7 +1201,7 @@ router.get('/logs/:id', async (req, res) => { data: log, }); } catch (error) { - console.error('获取备份日志详情失败:', error); + logger.error('获取备份日志详情失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取备份日志详情失败', @@ -1154,7 +1222,7 @@ router.delete('/logs/clean', async (req, res) => { data: { deletedCount }, }); } catch (error) { - console.error('清理旧日志失败:', error); + logger.error('清理旧日志失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '清理旧日志失败', diff --git a/backend/routes/cables.js b/backend/routes/cables.js index b0e0593..6e09d49 100644 --- a/backend/routes/cables.js +++ b/backend/routes/cables.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('CablesRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -146,7 +147,7 @@ async function validatePortCompatibility(sourceDeviceId, sourcePortName, targetD return checkPortCompatibility(sourcePort, targetPort); } catch (error) { - console.error('验证端口兼容性时出错:', error); + logger.error('验证端口兼容性时出错', { error: error.message, stack: error.stack }); return { compatible: false, reasons: [{ @@ -163,7 +164,7 @@ async function updatePortStatus(deviceId, portName, status) { try { await DevicePort.update({ status }, { where: { deviceId, portName } }); } catch (error) { - console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error); + logger.error('更新端口状态失败: ${deviceId}:${portName} -> ${status}', { error: error.message, stack: error.stack }); } } @@ -233,7 +234,7 @@ router.get('/', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取接线列表失败:', error); + logger.error('获取接线列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -262,7 +263,7 @@ router.get('/device/:deviceId', async (req, res) => { res.json(cables); } catch (error) { - console.error('获取设备接线失败:', error); + logger.error('获取设备接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -308,7 +309,7 @@ router.get('/rack/:rackId', async (req, res) => { res.json(cables); } catch (error) { - console.error('获取机柜接线失败:', error); + logger.error('获取机柜接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -393,7 +394,7 @@ router.post('/check-conflict', async (req, res) => { conflicts, }); } catch (error) { - console.error('检查接线冲突失败:', error); + logger.error('检查接线冲突失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -414,7 +415,7 @@ router.post('/check-compatibility', async (req, res) => { ...compatibilityResult, }); } catch (error) { - console.error('检查端口兼容性失败:', error); + logger.error('检查端口兼容性失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -542,7 +543,7 @@ router.post('/', async (req, res) => { res.status(201).json(createdCable); } catch (error) { - console.error('创建接线失败:', error); + logger.error('创建接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -622,7 +623,7 @@ router.post('/batch', async (req, res) => { res.json(results); } catch (error) { - console.error('批量创建接线失败:', error); + logger.error('批量创建接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -683,7 +684,7 @@ router.put('/:cableId', async (req, res) => { res.status(404).json({ error: '接线不存在' }); } } catch (error) { - console.error('更新接线失败:', error); + logger.error('更新接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -717,7 +718,7 @@ router.delete('/batch', async (req, res) => { deletedCount, }); } catch (error) { - console.error('批量删除接线失败:', error); + logger.error('批量删除接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -748,7 +749,7 @@ router.delete('/:cableId', async (req, res) => { res.status(404).json({ error: '接线不存在' }); } } catch (error) { - console.error('删除接线失败:', error); + logger.error('删除接线失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -776,7 +777,7 @@ router.get('/:cableId', async (req, res) => { res.json(cable); } catch (error) { - console.error('获取接线详情失败:', error); + logger.error('获取接线详情失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index 5aa5c2e..cbebc96 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('ConsumablesRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -175,7 +176,7 @@ router.get('/export', async (req, res) => { total: result.length, }); } catch (error) { - console.error('导出失败:', error); + logger.error('导出失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -219,7 +220,7 @@ router.post('/', async (req, res) => { res.status(201).json(consumable); } catch (error) { await transaction.rollback(); - console.error('创建耗材错误:', error); + logger.error('创建耗材错误', { error: error.message, stack: error.stack }); if (error.name === 'SequelizeValidationError') { const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', '); res.status(400).json({ error: `Validation error: ${messages}` }); @@ -345,7 +346,7 @@ router.post('/create-with-inbound', async (req, res) => { res.status(201).json(consumable); } catch (error) { await transaction.rollback(); - console.error('创建耗材并入库错误:', error); + logger.error('创建耗材并入库错误', { error: error.message, stack: error.stack }); if (error.name === 'SequelizeValidationError') { const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', '); res.status(400).json({ error: `Validation error: ${messages}` }); @@ -1891,7 +1892,7 @@ router.get('/devices/search', async (req, res) => { res.json({ devices: result }); } catch (error) { - console.error('搜索设备失败:', error); + logger.error('搜索设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -1927,7 +1928,7 @@ router.get('/devices/by-sn/:sn', async (req, res) => { }, }); } catch (error) { - console.error('查询设备失败:', error); + logger.error('查询设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/dangerousOperations.js b/backend/routes/dangerousOperations.js index 6098170..0419a77 100644 --- a/backend/routes/dangerousOperations.js +++ b/backend/routes/dangerousOperations.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('DangerousOperationsRoute'); const express = require('express'); const router = express.Router(); const { authMiddleware } = require('../middleware/auth'); @@ -55,7 +56,7 @@ router.post('/log', async (req, res) => { res.json({ success: true, riskLevel }); } catch (error) { - console.error('Failed to log dangerous operation:', error); + logger.error('Failed to log dangerous operation', { error: error.message, stack: error.stack }); res.status(500).json({ error: '日志记录失败' }); } }); @@ -98,7 +99,7 @@ router.get('/logs', async (req, res) => { totalPages: Math.ceil(total / parseInt(pageSize)), }); } catch (error) { - console.error('Failed to get dangerous operations logs:', error); + logger.error('Failed to get dangerous operations logs', { error: error.message, stack: error.stack }); res.status(500).json({ error: '获取日志失败' }); } }); @@ -144,7 +145,7 @@ router.delete('/logs/clean', async (req, res) => { remainingCount: result.remainingCount, }); } catch (error) { - console.error('Failed to clean logs:', error); + logger.error('Failed to clean logs', { error: error.message, stack: error.stack }); res.status(500).json({ error: '清理日志失败' }); } }); @@ -177,7 +178,7 @@ router.get('/risk-assessment', async (req, res) => { : 'STANDARD', }); } catch (error) { - console.error('Failed to assess risk:', error); + logger.error('Failed to assess risk', { error: error.message, stack: error.stack }); res.status(500).json({ error: '风险评估失败' }); } }); diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js index 2e388ac..234ebda 100644 --- a/backend/routes/devicePorts.js +++ b/backend/routes/devicePorts.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('DevicePortsRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -59,8 +60,8 @@ router.get('/', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取端口列表失败:', error); - console.error('Error name:', error.name); + logger.error('获取端口列表失败', { error: error.message, stack: error.stack }); + logger.error('Error name', { error: error.name }); res.status(500).json({ error: error.message, errorType: error.name }); } }); @@ -88,7 +89,7 @@ router.get('/device/:deviceId', async (req, res) => { res.json(ports); } catch (error) { - console.error('获取设备端口失败:', error); + logger.error('获取设备端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -136,7 +137,7 @@ router.post('/', async (req, res) => { res.status(201).json(createdPort); } catch (error) { - console.error('创建端口失败:', error); + logger.error('创建端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -297,7 +298,7 @@ router.post('/batch', async (req, res) => { throw error; } } catch (error) { - console.error('批量创建端口失败:', error); + logger.error('批量创建端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -336,7 +337,7 @@ router.put('/:portId', async (req, res) => { res.status(404).json({ error: '端口不存在' }); } } catch (error) { - console.error('更新端口失败:', error); + logger.error('更新端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -359,7 +360,7 @@ router.delete('/batch', async (req, res) => { deletedCount, }); } catch (error) { - console.error('批量删除端口失败:', error); + logger.error('批量删除端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -400,7 +401,7 @@ router.delete('/:portId', async (req, res) => { res.status(204).json(); } catch (error) { - console.error('删除端口失败:', error); + logger.error('删除端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -422,7 +423,7 @@ router.post('/batch-delete', async (req, res) => { deletedCount, }); } catch (error) { - console.error('批量删除端口失败:', error); + logger.error('批量删除端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -482,7 +483,7 @@ router.get('/export/all', async (req, res) => { pageSize: parsedPageSize, }); } catch (error) { - console.error('获取端口列表失败:', error); + logger.error('获取端口列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -506,7 +507,7 @@ router.get('/:portId', async (req, res) => { res.json(port); } catch (error) { - console.error('获取端口详情失败:', error); + logger.error('获取端口详情失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 50f63db..0516221 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('DevicesRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -512,7 +513,7 @@ router.post('/import-preview', async (req, res) => { }, }); } catch (error) { - console.error('预览设备数据失败:', error); + logger.error('预览设备数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message || '预览过程中发生未知错误', }); @@ -531,8 +532,8 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { // 关键词搜索(所有文本字段 + 自定义字段) if (keyword) { - console.log('搜索关键词:', keyword); - console.log('数据库类型:', dbDialect); + logger.info('搜索关键词', { data: keyword }); + logger.info('数据库类型', { data: dbDialect }); // 转义关键词中的特殊字符,防止SQL注入 // 转义单引号(SQL注入)和 LIKE 通配符(%、_) @@ -561,10 +562,7 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { }, }); - console.log( - '找到的自定义字段:', - customFields.map(f => f.fieldName) - ); + logger.info('找到的自定义字段', { data: customFields.map(f => f.fieldName) }); // 构建自定义字段搜索条件(使用原始SQL,兼容SQLite和MySQL) if (customFields.length > 0) { @@ -592,7 +590,7 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { // 合并所有搜索条件 where[Op.or] = searchConditions; - console.log('搜索条件数量:', searchConditions.length); + logger.info('搜索条件数量', { data: searchConditions.length }); } // 状态筛选 @@ -615,13 +613,10 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { // 调试日志 if (process.env.NODE_ENV === 'development') { - console.log('=== 设备查询调试 ==='); - console.log('接收参数 - roomId:', roomId, 'rackId:', rackId); - console.log('查询条件 - where:', JSON.stringify(where)); - console.log( - '机房筛选条件 - Rack.where:', - roomId && roomId !== 'all' ? { roomId: roomId } : undefined - ); + logger.info('=== 设备查询调试 ==='); + logger.info('接收参数', { roomId, rackId }); + logger.info('查询条件 - where', { data: JSON.stringify(where) }); + logger.info('机房筛选条件 - Rack.where', { data: roomId && roomId !== 'all' ? { roomId: roomId } : undefined }); } // 空闲设备筛选 @@ -653,10 +648,10 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('搜索设备失败:', error); - console.error('错误详情:', error.message); + logger.error('搜索设备失败', { error: error.message, stack: error.stack }); + logger.error('错误详情', { error: error.message }); if (error.sql) { - console.error('SQL:', error.sql); + logger.error('SQL', { error: error.sql }); } res.status(500).json({ error: error.message, sql: error.sql }); } @@ -717,7 +712,7 @@ router.get('/all', async (req, res) => { total: devices.length, }); } catch (error) { - console.error('获取设备列表失败:', error); + logger.error('获取设备列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -941,7 +936,7 @@ router.get('/import-template', async (req, res) => { // 删除临时文件 fs.unlinkSync(path.join(tempDir, 'import_template.csv')); } catch (error) { - console.error('生成导入模板失败:', error); + logger.error('生成导入模板失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '生成导入模板失败' }); } }); @@ -1068,7 +1063,7 @@ router.get('/export', async (req, res) => { // 删除临时文件 fs.unlinkSync(path.join(__dirname, '../temp/devices.csv')); } catch (error) { - console.error('导出设备数据失败:', error); + logger.error('导出设备数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '导出设备数据失败' }); } }); @@ -1525,7 +1520,7 @@ router.post('/import', async (req, res) => { res.json({ statistics: stats }); } catch (error) { await t.rollback(); - console.error('导入设备数据失败:', error); + logger.error('导入设备数据失败', { error: error.message, stack: error.stack }); // 清理临时文件 try { @@ -1533,7 +1528,7 @@ router.post('/import', async (req, res) => { fs.unlinkSync(filePath); } } catch (fileErr) { - console.error('删除临时文件失败:', fileErr); + logger.error('删除临时文件失败', { error: fileErr }); } res.status(500).json({ @@ -2070,7 +2065,7 @@ router.get('/enhanced-export', async (req, res) => { }); } } catch (error) { - console.error('增强导出失败:', error); + logger.error('增强导出失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '增强导出失败' }); } }); @@ -2179,7 +2174,7 @@ router.put('/:deviceId/to-idle', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('设备转入空闲设备失败:', error); + logger.error('设备转入空闲设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -2456,7 +2451,7 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res }); } catch (error) { await t.rollback(); - console.error('批量删除设备错误:', error); + logger.error('批量删除设备错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message, stack: error.stack }); } }); @@ -2539,7 +2534,7 @@ router.delete('/delete-all', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('删除所有设备错误:', error); + logger.error('删除所有设备错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message, stack: error.stack }); } }); @@ -2603,7 +2598,7 @@ router.delete('/:deviceId', async (req, res) => { ); } } catch (err) { - console.error('更新机柜功率失败:', err); + logger.error('更新机柜功率失败', { err: err.message, stack: err.stack }); throw err; // 重新抛出错误,触发事务回滚 } } @@ -2618,7 +2613,7 @@ router.delete('/:deviceId', async (req, res) => { await t.commit(); if (deletedCables > 0) { - console.log(`已删除 ${deletedCables} 条相关接线`); + logger.info('已删除 ${deletedCables} 条相关接线'); } await logDeviceOperation( @@ -2645,7 +2640,7 @@ router.delete('/:deviceId', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('删除设备失败:', error); + logger.error('删除设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/idleDevices.js b/backend/routes/idleDevices.js index 3b9cab2..4790001 100644 --- a/backend/routes/idleDevices.js +++ b/backend/routes/idleDevices.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('IdleDevicesRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -83,7 +84,7 @@ router.get('/', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取空闲设备列表失败:', error); + logger.error('获取空闲设备列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -245,7 +246,7 @@ router.post('/from-device/:deviceId', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('设备转入空闲设备失败:', error); + logger.error('设备转入空闲设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -312,7 +313,7 @@ router.post('/batch-from-devices', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('批量转入空闲设备失败:', error); + logger.error('批量转入空闲设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -322,52 +323,46 @@ router.put('/batch-restore', async (req, res) => { try { const { devices } = req.body; - console.log('========== batch-restore 开始 =========='); - console.log('原始请求 body:', JSON.stringify(req.body)); - console.log('devices 参数:', devices); + logger.info('========== batch-restore 开始 =========='); + logger.info('原始请求 body', { data: JSON.stringify(req.body) }); + logger.info('devices 参数', { data: devices }); if (!devices || !Array.isArray(devices) || devices.length === 0) { await t.rollback(); - console.log('错误: devices 参数无效'); + logger.info('错误: devices 参数无效'); return res.status(400).json({ error: '请提供有效的设备列表' }); } const deviceIds = devices.map(d => d.deviceId).filter(Boolean); - console.log('提取的 deviceIds:', deviceIds); + logger.info('提取的 deviceIds', { data: deviceIds }); if (deviceIds.length === 0) { await t.rollback(); - console.log('错误: deviceIds 为空'); + logger.info('错误: deviceIds 为空'); return res.status(400).json({ error: '设备ID不能为空' }); } - console.log('开始查询设备,条件:', { deviceId: { [Op.in]: deviceIds }, isIdle: true }); + logger.info('开始查询设备,条件', { data: { deviceId: { [Op.in]: deviceIds }, isIdle: true } }); const idleDevices = await Device.findAll({ where: { deviceId: { [Op.in]: deviceIds }, isIdle: true }, transaction: t, }); - console.log('查询到的空闲设备数量:', idleDevices.length); + logger.info('查询到的空闲设备数量', { data: idleDevices.length }); if (idleDevices.length > 0) { - console.log( - '查询到的设备ID:', - idleDevices.map(d => d.deviceId) - ); + logger.info('查询到的设备ID', { data: idleDevices.map(d => d.deviceId) }); } if (idleDevices.length === 0) { - console.log('没有找到空闲设备,检查设备是否存在:'); + logger.info('没有找到空闲设备,检查设备是否存在:'); const allDevices = await Device.findAll({ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t, }); - console.log('设备表中存在的设备数量:', allDevices.length); + logger.info('设备表中存在的设备数量', { data: allDevices.length }); if (allDevices.length > 0) { - console.log( - '存在的设备及其 isIdle 状态:', - allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })) - ); + logger.info('存在的设备及其 isIdle 状态', { data: allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })) }); } await t.rollback(); @@ -493,7 +488,7 @@ router.put('/batch-restore', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('批量上架设备失败:', error); + logger.error('批量上架设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -536,9 +531,9 @@ router.put('/:deviceId/shelve', async (req, res) => { } const deviceHeight = height || device.height || 1; - console.log('上架设备 - 位置检查参数:', { rackId, position, deviceHeight, deviceId, existingDeviceId: device.deviceId }); + logger.info('上架设备 - 位置检查参数', { data: { rackId, position, deviceHeight, deviceId, existingDeviceId: device.deviceId } }); const positionCheck = await checkPositionAvailable(rackId, position, deviceHeight, deviceId, t); - console.log('上架设备 - 位置检查结果:', positionCheck); + logger.info('上架设备 - 位置检查结果', { data: positionCheck }); if (!positionCheck.available) { await t.rollback(); return res.status(400).json({ error: positionCheck.reason }); @@ -606,7 +601,7 @@ router.put('/:deviceId/shelve', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('设备上架失败:', error); + logger.error('设备上架失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -776,7 +771,7 @@ router.put('/:deviceId/restore', async (req, res) => { }); } catch (error) { await t.rollback(); - console.error('恢复设备失败:', error); + logger.error('恢复设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -822,7 +817,7 @@ router.delete('/:deviceId', async (req, res) => { res.json({ message: '空闲设备删除成功' }); } catch (error) { await t.rollback(); - console.error('删除空闲设备失败:', error); + logger.error('删除空闲设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index 5bb9258..2950fa1 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('InventoryRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -266,7 +267,7 @@ router.post('/plans/:planId/start', async (req, res) => { deviceCount: allDevices.length, }); } catch (error) { - console.error('启动盘点错误:', error); + logger.error('启动盘点错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -330,7 +331,7 @@ router.get('/tasks/:taskId', async (req, res) => { res.json({ task, records: enrichedRecords }); } catch (error) { - console.error('获取盘点任务记录错误:', error); + logger.error('获取盘点任务记录错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -649,7 +650,7 @@ router.post('/quick-add-device', async (req, res) => { pendingDevice, }); } catch (error) { - console.error('快速添加设备错误:', error); + logger.error('快速添加设备错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -704,7 +705,7 @@ router.get('/pending-devices', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取暂存设备列表错误:', error); + logger.error('获取暂存设备列表错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -889,7 +890,7 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => { pendingDevice, }); } catch (error) { - console.error('同步设备错误:', error); + logger.error('同步设备错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -983,7 +984,7 @@ router.post('/pending-devices/batch-sync', async (req, res) => { errors, }); } catch (error) { - console.error('批量同步设备错误:', error); + logger.error('批量同步设备错误', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/networkCards.js b/backend/routes/networkCards.js index dc8f01f..e093efc 100644 --- a/backend/routes/networkCards.js +++ b/backend/routes/networkCards.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('NetworkCardsRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -35,7 +36,7 @@ router.get('/', async (req, res) => { res.json(networkCards); } catch (error) { - console.error('获取网卡列表失败:', error); + logger.error('获取网卡列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -61,7 +62,7 @@ router.get('/device/:deviceId', async (req, res) => { res.json(networkCards); } catch (error) { - console.error('获取设备网卡失败:', error); + logger.error('获取设备网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -126,7 +127,7 @@ router.get('/device/:deviceId/with-ports', async (req, res) => { res.json(cardsWithPorts); } catch (error) { - console.error('获取网卡及端口失败:', error); + logger.error('获取网卡及端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -158,7 +159,7 @@ router.get('/find', async (req, res) => { res.json(networkCard); } catch (error) { - console.error('查找网卡失败:', error); + logger.error('查找网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -182,7 +183,7 @@ router.get('/:nicId', async (req, res) => { res.json(networkCard); } catch (error) { - console.error('获取网卡详情失败:', error); + logger.error('获取网卡详情失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -198,7 +199,7 @@ router.get('/:nicId/ports', async (req, res) => { res.json(ports); } catch (error) { - console.error('获取网卡端口失败:', error); + logger.error('获取网卡端口失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -246,7 +247,7 @@ router.post('/', async (req, res) => { res.status(201).json(createdCard); } catch (error) { - console.error('创建网卡失败:', error); + logger.error('创建网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -379,7 +380,7 @@ router.post('/batch', async (req, res) => { throw error; } } catch (error) { - console.error('批量创建网卡失败:', error); + logger.error('批量创建网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -405,7 +406,7 @@ router.put('/:nicId', async (req, res) => { res.status(404).json({ error: '网卡不存在' }); } } catch (error) { - console.error('更新网卡失败:', error); + logger.error('更新网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -431,7 +432,7 @@ router.delete('/:nicId', async (req, res) => { res.status(404).json({ error: '网卡不存在' }); } } catch (error) { - console.error('删除网卡失败:', error); + logger.error('删除网卡失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/operationLogs.js b/backend/routes/operationLogs.js index af139b7..44ccad1 100644 --- a/backend/routes/operationLogs.js +++ b/backend/routes/operationLogs.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('OperationLogsRoute'); const express = require('express'); const { Op } = require('sequelize'); const { sequelize } = require('../db'); @@ -83,7 +84,7 @@ router.get('/', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取操作日志失败:', error); + logger.error('获取操作日志失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取操作日志失败', @@ -108,7 +109,7 @@ router.get('/modules', authMiddleware, async (req, res) => { data: moduleList, }); } catch (error) { - console.error('获取模块列表失败:', error); + logger.error('获取模块列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取模块列表失败', @@ -141,7 +142,7 @@ router.get('/types', authMiddleware, async (req, res) => { data: typeList, }); } catch (error) { - console.error('获取操作类型列表失败:', error); + logger.error('获取操作类型列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取操作类型列表失败', @@ -201,7 +202,7 @@ router.get('/statistics', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取操作日志统计失败:', error); + logger.error('获取操作日志统计失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取操作日志统计失败', @@ -225,7 +226,7 @@ router.get('/:recordId', authMiddleware, async (req, res) => { data: log, }); } catch (error) { - console.error('获取操作日志详情失败:', error); + logger.error('获取操作日志详情失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取操作日志详情失败', diff --git a/backend/routes/racks.js b/backend/routes/racks.js index 1a1555e..9f27cd6 100644 --- a/backend/routes/racks.js +++ b/backend/routes/racks.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('RacksRoute'); const express = require('express'); const router = express.Router(); const Rack = require('../models/Rack'); @@ -181,7 +182,7 @@ router.get('/import-template', async (req, res) => { // 发送文件 res.send(excelBuffer); } catch (error) { - console.error('生成导入模板失败:', error); + logger.error('生成导入模板失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '生成导入模板失败' }); } }); @@ -275,13 +276,13 @@ router.get('/export', async (req, res) => { }); fileStream.on('error', err => { - console.error('文件流错误:', err); + logger.error('文件流错误', { err: err.message, stack: err.stack }); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } }); } catch (error) { - console.error('导出租机柜数据失败:', error); + logger.error('导出租机柜数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '导出失败', diff --git a/backend/routes/roles.js b/backend/routes/roles.js index c37dcc0..0eff2e6 100644 --- a/backend/routes/roles.js +++ b/backend/routes/roles.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('RolesRoute'); const express = require('express'); const Role = require('../models/Role'); const Permission = require('../models/Permission'); @@ -45,7 +46,7 @@ router.get('/', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取角色列表错误:', error); + logger.error('获取角色列表错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取角色列表失败', @@ -65,7 +66,7 @@ router.get('/all', authMiddleware, async (req, res) => { data: roles, }); } catch (error) { - console.error('获取所有角色错误:', error); + logger.error('获取所有角色错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取角色列表失败', @@ -98,7 +99,7 @@ router.get('/:roleId', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取角色详情错误:', error); + logger.error('获取角色详情错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取角色详情失败', @@ -155,7 +156,7 @@ router.post('/', authMiddleware, async (req, res) => { data: role, }); } catch (error) { - console.error('创建角色错误:', error); + logger.error('创建角色错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '创建角色失败', @@ -264,7 +265,7 @@ router.put('/:roleId', authMiddleware, async (req, res) => { data: role, }); } catch (error) { - console.error('更新角色错误:', error); + logger.error('更新角色错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新角色失败', @@ -321,7 +322,7 @@ router.delete('/:roleId', authMiddleware, async (req, res) => { message: '删除成功', }); } catch (error) { - console.error('删除角色错误:', error); + logger.error('删除角色错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '删除角色失败', @@ -377,7 +378,7 @@ router.post('/init-roles', authMiddleware, async (req, res) => { message: '初始化角色成功', }); } catch (error) { - console.error('初始化角色错误:', error); + logger.error('初始化角色错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '初始化角色失败', diff --git a/backend/routes/statistics.js b/backend/routes/statistics.js index d0f0d2a..13cf6fe 100644 --- a/backend/routes/statistics.js +++ b/backend/routes/statistics.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('StatisticsRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -162,7 +163,7 @@ router.get('/', authMiddleware, async (req, res) => { deviceTrendData, }); } catch (error) { - console.error('获取统计数据失败:', error); + logger.error('获取统计数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js index 49a3202..fe0d62e 100644 --- a/backend/routes/systemSettings.js +++ b/backend/routes/systemSettings.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('SystemSettingsRoute'); const express = require('express'); const router = express.Router(); const fs = require('fs'); @@ -234,14 +235,12 @@ const initDefaultSettings = async () => { updatedCount++; } } catch (error) { - console.error(`初始化设置 ${setting.settingKey} 失败:`, error.message); + logger.error('初始化设置 ${setting.settingKey} 失败', { error: error.message }); errorCount++; } } - console.log( - `系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个` - ); + logger.info(`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个`); return { createdCount, updatedCount, errorCount }; }; @@ -562,7 +561,7 @@ router.post('/backup', async (req, res) => { backupCount: backupFiles.length, }); } catch (error) { - console.error('备份失败:', error); + logger.error('备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '备份失败' }); } }); @@ -665,7 +664,7 @@ router.post('/backup/restore', async (req, res) => { restoredAt: new Date().toISOString(), }); } catch (error) { - console.error('恢复备份失败:', error); + logger.error('恢复备份失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: '恢复备份失败' }); } }); @@ -725,7 +724,7 @@ router.get('/system/info', async (req, res) => { } } } catch (syncError) { - console.error('同步版本号到数据库失败:', syncError); + logger.error('同步版本号到数据库失败', { error: syncError }); } const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([ @@ -752,7 +751,7 @@ router.get('/system/info', async (req, res) => { // 这里使用模拟值,避免依赖额外的库 diskPercent = Math.min(95, Math.max(20, usedMemPercent - 10)); } catch (diskError) { - console.error('获取磁盘信息失败:', diskError); + logger.error('获取磁盘信息失败', { error: diskError }); } res.json({ diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index 7199a8e..cd3c814 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('TicketsRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -611,7 +612,7 @@ router.get('/export', async (req, res) => { res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.csv`); return res.send(csvWithBom); } catch (error) { - console.error('导出工单失败:', error); + logger.error('导出工单失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/topology.js b/backend/routes/topology.js index 7ec9819..b784ea3 100644 --- a/backend/routes/topology.js +++ b/backend/routes/topology.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('TopologyRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -193,7 +194,7 @@ router.get('/switch/:switchId', async (req, res) => { } }); } catch (error) { - console.error('获取拓扑数据失败:', error); + logger.error('获取拓扑数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, error: error.message }); } }); @@ -315,7 +316,7 @@ router.get('/rack/:rackId', async (req, res) => { res.json({ success: true, data: { nodes, edges, statistics } }); } catch (error) { - console.error('获取机柜拓扑数据失败:', error); + logger.error('获取机柜拓扑数据失败', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, error: error.message }); } }); diff --git a/backend/routes/users.js b/backend/routes/users.js index 531ef30..909f65b 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('UsersRoute'); const express = require('express'); const bcrypt = require('bcryptjs'); const path = require('path'); @@ -111,7 +112,7 @@ router.get('/', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('获取用户列表错误:', error); + logger.error('获取用户列表错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取用户列表失败', @@ -132,7 +133,7 @@ router.get('/all', authMiddleware, async (req, res) => { data: users, }); } catch (error) { - console.error('获取所有用户错误:', error); + logger.error('获取所有用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取用户列表失败', @@ -174,7 +175,7 @@ router.get('/:userId', authMiddleware, async (req, res) => { data: user, }); } catch (error) { - console.error('获取用户详情错误:', error); + logger.error('获取用户详情错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '获取用户详情失败', @@ -259,7 +260,7 @@ router.post('/', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('创建用户错误:', error); + logger.error('创建用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '创建用户失败', @@ -413,7 +414,7 @@ router.put('/:userId', authMiddleware, async (req, res) => { data: updatedUser, }); } catch (error) { - console.error('更新用户错误:', error); + logger.error('更新用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '更新用户失败', @@ -456,7 +457,7 @@ router.put('/:userId/password', authMiddleware, async (req, res) => { message: '密码重置成功', }); } catch (error) { - console.error('重置密码错误:', error); + logger.error('重置密码错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '重置密码失败', @@ -512,7 +513,7 @@ router.delete('/:userId', authMiddleware, async (req, res) => { message: '删除成功', }); } catch (error) { - console.error('删除用户错误:', error); + logger.error('删除用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '删除用户失败', @@ -583,7 +584,7 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => { data: { avatar: avatarUrl }, }); } catch (error) { - console.error('上传头像错误:', error); + logger.error('上传头像错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '上传头像失败', @@ -617,7 +618,7 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => { message: '头像删除成功', }); } catch (error) { - console.error('删除头像错误:', error); + logger.error('删除头像错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '删除头像失败', @@ -656,7 +657,7 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('审核用户错误:', error); + logger.error('审核用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '审核用户失败', @@ -695,7 +696,7 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => { }, }); } catch (error) { - console.error('拒绝用户错误:', error); + logger.error('拒绝用户错误', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, message: '操作失败', diff --git a/backend/routes/warehouses.js b/backend/routes/warehouses.js index df8007c..5cccdf7 100644 --- a/backend/routes/warehouses.js +++ b/backend/routes/warehouses.js @@ -1,3 +1,4 @@ +const logger = require('../utils/logger').module('WarehousesRoute'); const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); @@ -72,7 +73,7 @@ router.get('/', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取库房列表失败:', error); + logger.error('获取库房列表失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -121,7 +122,7 @@ router.get('/:warehouseId/devices', async (req, res) => { pageSize: parseInt(pageSize), }); } catch (error) { - console.error('获取库房设备失败:', error); + logger.error('获取库房设备失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); @@ -231,7 +232,7 @@ router.delete('/:warehouseId', async (req, res) => { res.json({ message: '库房删除成功' }); } catch (error) { - console.error('删除库房失败:', error); + logger.error('删除库房失败', { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/scripts/fix-foreign-keys.js b/backend/scripts/fix-foreign-keys.js new file mode 100644 index 0000000..aa225ab --- /dev/null +++ b/backend/scripts/fix-foreign-keys.js @@ -0,0 +1,275 @@ +/** + * 数据库外键约束修复迁移脚本 + * + * 功能: + * 1. 清理现有孤儿数据 + * 2. 添加外键约束和 onDelete 策略 + * 3. 确保脚本幂等执行 + * + * 使用方法: + * node backend/scripts/fix-foreign-keys.js + */ + +const { sequelize } = require('../db'); + +/** + * 主迁移函数 + */ +async function migrate() { + console.log('开始执行外键约束修复迁移...'); + + try { + // 第一步:清理孤儿数据 + await cleanOrphanData(); + + // 第二步:添加外键约束 + await addForeignKeyConstraints(); + + console.log('外键约束修复迁移完成!'); + } catch (error) { + console.error('迁移失败:', error); + throw error; + } +} + +/** + * 清理孤儿数据 + */ +async function cleanOrphanData() { + console.log('正在清理孤儿数据...'); + + const transaction = await sequelize.transaction(); + + try { + // 清理 Device 表中无效的 rackId + const [orphanDevicesWithRack] = await sequelize.query( + `SELECT COUNT(*) as count FROM devices d + LEFT JOIN racks r ON d.rackId = r.rackId + WHERE d.rackId IS NOT NULL AND r.rackId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanDevicesWithRack[0].count} 条无效 rackId 的设备记录`); + + await sequelize.query( + `UPDATE devices SET rackId = NULL + WHERE rackId IS NOT NULL + AND rackId NOT IN (SELECT rackId FROM racks WHERE rackId IS NOT NULL)`, + { transaction } + ); + + // 清理 Device 表中无效的 warehouseId + const [orphanDevicesWithWarehouse] = await sequelize.query( + `SELECT COUNT(*) as count FROM devices d + LEFT JOIN warehouses w ON d.warehouseId = w.warehouseId + WHERE d.warehouseId IS NOT NULL AND w.warehouseId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanDevicesWithWarehouse[0].count} 条无效 warehouseId 的设备记录`); + + await sequelize.query( + `UPDATE devices SET warehouseId = NULL + WHERE warehouseId IS NOT NULL + AND warehouseId NOT IN (SELECT warehouseId FROM warehouses WHERE warehouseId IS NOT NULL)`, + { transaction } + ); + + // 清理 Rack 表中无效的 roomId + const [orphanRacks] = await sequelize.query( + `SELECT COUNT(*) as count FROM racks r + LEFT JOIN rooms rm ON r.roomId = rm.roomId + WHERE r.roomId IS NOT NULL AND rm.roomId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanRacks[0].count} 条无效 roomId 的机柜记录,准备删除`); + + await sequelize.query( + `DELETE FROM racks + WHERE roomId IS NOT NULL + AND roomId NOT IN (SELECT roomId FROM rooms WHERE roomId IS NOT NULL)`, + { transaction } + ); + + // 清理 Ticket 表中无效的 deviceId + const [orphanTicketsWithDevice] = await sequelize.query( + `SELECT COUNT(*) as count FROM tickets t + LEFT JOIN devices d ON t.deviceId = d.deviceId + WHERE t.deviceId IS NOT NULL AND d.deviceId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanTicketsWithDevice[0].count} 条无效 deviceId 的工单记录`); + + await sequelize.query( + `UPDATE tickets SET deviceId = NULL + WHERE deviceId IS NOT NULL + AND deviceId NOT IN (SELECT deviceId FROM devices WHERE deviceId IS NOT NULL)`, + { transaction } + ); + + // 清理 Ticket 表中无效的 reporterId (NOT NULL 约束,必须删除) + const [orphanTicketsWithReporter] = await sequelize.query( + `SELECT COUNT(*) as count FROM tickets t + LEFT JOIN users u ON t.reporterId = u.userId + WHERE t.reporterId IS NOT NULL AND u.userId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanTicketsWithReporter[0].count} 条无效 reporterId 的工单记录,准备删除`); + + await sequelize.query( + `DELETE FROM tickets + WHERE reporterId IS NOT NULL + AND reporterId NOT IN (SELECT userId FROM users WHERE userId IS NOT NULL)`, + { transaction } + ); + + // 清理 Ticket 表中无效的 assigneeId (允许 NULL,可以置空) + const [orphanTicketsWithAssignee] = await sequelize.query( + `SELECT COUNT(*) as count FROM tickets t + LEFT JOIN users u ON t.assigneeId = u.userId + WHERE t.assigneeId IS NOT NULL AND u.userId IS NULL`, + { transaction } + ); + + console.log(`发现 ${orphanTicketsWithAssignee[0].count} 条无效 assigneeId 的工单记录`); + + await sequelize.query( + `UPDATE tickets SET assigneeId = NULL + WHERE assigneeId IS NOT NULL + AND assigneeId NOT IN (SELECT userId FROM users WHERE userId IS NOT NULL)`, + { transaction } + ); + + await transaction.commit(); + console.log('孤儿数据清理完成!'); + } catch (error) { + await transaction.rollback(); + throw error; + } +} + +/** + * 添加外键约束 + */ +async function addForeignKeyConstraints() { + console.log('正在添加外键约束...'); + + const queryInterface = sequelize.getQueryInterface(); + const dialect = sequelize.options.dialect; + + try { + if (dialect === 'mysql' || dialect === 'mariadb') { + await addMySQLConstraints(queryInterface); + } else if (dialect === 'sqlite') { + await addSQLiteConstraints(queryInterface); + } else { + console.warn(`数据库类型 ${dialect} 的外键约束需要手动配置`); + } + + console.log('外键约束添加完成!'); + } catch (error) { + console.error('添加外键约束失败:', error); + throw error; + } +} + +/** + * 为 MySQL 添加外键约束 + */ +async function addMySQLConstraints(queryInterface) { + // 获取当前外键约束 + const [foreignKeys] = await queryInterface.sequelize.query( + `SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME, DELETE_RULE + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND (TABLE_NAME = 'devices' OR TABLE_NAME = 'racks' OR TABLE_NAME = 'tickets') + AND REFERENCED_TABLE_NAME IS NOT NULL` + ); + + console.log(`当前存在 ${foreignKeys.length} 个外键约束`); + + // 删除现有的外键约束(如果存在) + for (const fk of foreignKeys) { + console.log(`删除现有外键: ${fk.CONSTRAINT_NAME}`); + await queryInterface.sequelize.query( + `ALTER TABLE ${fk.TABLE_NAME} DROP FOREIGN KEY ${fk.CONSTRAINT_NAME}` + ); + } + + // 添加新的外键约束 + await queryInterface.sequelize.query(` + ALTER TABLE devices + ADD CONSTRAINT fk_device_rack + FOREIGN KEY (rackId) REFERENCES racks(rackId) + ON DELETE SET NULL + `); + console.log('添加外键: Device.rackId → Rack.rackId (ON DELETE SET NULL)'); + + await queryInterface.sequelize.query(` + ALTER TABLE devices + ADD CONSTRAINT fk_device_warehouse + FOREIGN KEY (warehouseId) REFERENCES warehouses(warehouseId) + ON DELETE SET NULL + `); + console.log('添加外键: Device.warehouseId → Warehouse.warehouseId (ON DELETE SET NULL)'); + + await queryInterface.sequelize.query(` + ALTER TABLE racks + ADD CONSTRAINT fk_rack_room + FOREIGN KEY (roomId) REFERENCES rooms(roomId) + ON DELETE CASCADE + `); + console.log('添加外键: Rack.roomId → Room.roomId (ON DELETE CASCADE)'); + + await queryInterface.sequelize.query(` + ALTER TABLE tickets + ADD CONSTRAINT fk_ticket_device + FOREIGN KEY (deviceId) REFERENCES devices(deviceId) + ON DELETE SET NULL + `); + console.log('添加外键: Ticket.deviceId → Device.deviceId (ON DELETE SET NULL)'); + + await queryInterface.sequelize.query(` + ALTER TABLE tickets + ADD CONSTRAINT fk_ticket_reporter + FOREIGN KEY (reporterId) REFERENCES users(userId) + ON DELETE SET NULL + `); + console.log('添加外键: Ticket.reporterId → User.userId (ON DELETE SET NULL)'); + + await queryInterface.sequelize.query(` + ALTER TABLE tickets + ADD CONSTRAINT fk_ticket_assignee + FOREIGN KEY (assigneeId) REFERENCES users(userId) + ON DELETE SET NULL + `); + console.log('添加外键: Ticket.assigneeId → User.userId (ON DELETE SET NULL)'); +} + +/** + * 为 SQLite 添加外键约束 + * 注意:SQLite 不支持直接 ALTER TABLE 添加外键,需要重建表 + */ +async function addSQLiteConstraints(queryInterface) { + console.log('SQLite 数据库:外键约束通过 Sequelize 同步自动创建'); + console.log('注意:SQLite 默认不启用外键检查,需在连接配置中启用'); + + // SQLite 不需要手动添加外键,Sequelize 的 sync 会自动处理 + // 但需要确保在连接时启用外键支持 + await queryInterface.sequelize.query('PRAGMA foreign_keys = ON'); + console.log('已启用 SQLite 外键检查'); +} + +// 执行迁移 +migrate() + .then(() => { + console.log('迁移成功完成'); + process.exit(0); + }) + .catch((error) => { + console.error('迁移失败:', error); + process.exit(1); + }); diff --git a/backend/scripts/migrate-logs.js b/backend/scripts/migrate-logs.js new file mode 100644 index 0000000..b9561a2 --- /dev/null +++ b/backend/scripts/migrate-logs.js @@ -0,0 +1,136 @@ +/** + * 日志系统迁移脚本 + * 批量将 routes/ 目录中的 console 调用替换为 logger 调用 + */ + +const fs = require('fs'); +const path = require('path'); + +const routesDir = path.join(__dirname, '../routes'); +const files = fs.readdirSync(routesDir).filter(f => f.endsWith('.js')); + +let totalReplacements = 0; + +files.forEach(file => { + const filePath = path.join(routesDir, file); + let content = fs.readFileSync(filePath, 'utf8'); + + // 检查是否包含 console 调用 + const consoleMatches = content.match(/console\.(log|error|warn|debug)/g); + if (!consoleMatches) { + return; + } + + const moduleName = file.replace('.js', '').replace(/^[a-z]/, m => m.toUpperCase()) + 'Route'; + + // 添加 logger 导入(如果还没有) + if (!content.includes("require('../utils/logger')")) { + // 在文件开头添加 logger 导入 + content = `const logger = require('../utils/logger').module('${moduleName}');\n${content}`; + } + + // 替换 console.error('xxx:', error) -> logger.error('xxx', { error: error.message, stack: error.stack }) + // 替换 console.error('xxx:', error.message) -> logger.error('xxx', { error: error.message }) + // 替换 console.error('xxx:', err) -> logger.error('xxx', { error: err.message, stack: err.stack }) + + // 先处理带有 error 对象的 console.error + content = content.replace( + /console\.error\(['"`](.+?)['"`],\s*(error|err)\);?/g, + (match, msg, varName) => { + totalReplacements++; + // 清理消息末尾的冒号 + const cleanMsg = msg.replace(/[::]\s*$/, '').trim(); + return `logger.error('${cleanMsg}', { ${varName}: ${varName}.message, stack: ${varName}.stack });`; + } + ); + + // 处理 console.error('xxx:', error.message) + content = content.replace( + /console\.error\(['"`](.+?)['"`],\s*(error|err)\.message\);?/g, + (match, msg) => { + totalReplacements++; + const cleanMsg = msg.replace(/[::]\s*$/, '').trim(); + return `logger.error('${cleanMsg}', { error: error.message });`; + } + ); + + // 处理 console.error('xxx:', someVar) -> logger.error('xxx', { error: someVar }) + content = content.replace( + /console\.error\(['"`](.+?)['"`],\s*([^);]+)\);?/g, + (match, msg, varName) => { + totalReplacements++; + const cleanMsg = msg.replace(/[::]\s*$/, '').trim(); + return `logger.error('${cleanMsg}', { error: ${varName.trim()} });`; + } + ); + + // 处理 console.error('xxx') -> logger.error('xxx') + content = content.replace( + /console\.error\(['"`](.+?)['"`]\);?/g, + (match, msg) => { + totalReplacements++; + return `logger.error('${msg}');`; + } + ); + + // 处理 console.warn('xxx:', { ... }) -> logger.warn('xxx', { ... }) + content = content.replace( + /console\.warn\(['"`](.+?)['"`],\s*\{/g, + (match, msg) => { + totalReplacements++; + const cleanMsg = msg.replace(/[::]\s*$/, '').trim(); + return `logger.warn('${cleanMsg}', {`; + } + ); + + // 处理 console.warn('xxx') -> logger.warn('xxx') + content = content.replace( + /console\.warn\(['"`](.+?)['"`]\);?/g, + (match, msg) => { + totalReplacements++; + return `logger.warn('${msg}');`; + } + ); + + // 处理 console.log('xxx:', var) -> logger.info('xxx', { var }) + content = content.replace( + /console\.log\(['"`](.+?)['"`],\s*([^);]+)\);?/g, + (match, msg, varName) => { + totalReplacements++; + const cleanMsg = msg.replace(/[::]\s*$/, '').trim(); + return `logger.info('${cleanMsg}', { data: ${varName.trim()} });`; + } + ); + + // 处理 console.log('xxx') -> logger.info('xxx') + content = content.replace( + /console\.log\(['"`](.+?)['"`]\);?/g, + (match, msg) => { + totalReplacements++; + return `logger.info('${msg}');`; + } + ); + + // 处理 console.log(`xxx`) -> logger.info(`xxx`) + content = content.replace( + /console\.log\(`(.+?)`\);?/g, + (match, msg) => { + totalReplacements++; + return `logger.info(\`${msg}\`);`; + } + ); + + // 处理 console.error(`xxx`) -> logger.error(`xxx`) + content = content.replace( + /console\.error\(`(.+?)`\);?/g, + (match, msg) => { + totalReplacements++; + return `logger.error(\`${msg}\`);`; + } + ); + + fs.writeFileSync(filePath, content); + console.log(`✓ ${file}: 替换了 ${consoleMatches.length} 处 console 调用`); +}); + +console.log(`\n总计替换了 ${totalReplacements} 处 console 调用`); diff --git a/backend/server.js b/backend/server.js index 4bfefef..36d5164 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,6 +9,8 @@ const fileUpload = require('express-fileupload'); const { sequelize } = require('./db'); const { FILE_UPLOAD } = require('./config'); const { generateId } = require('./utils/idGenerator'); +const logger = require('./utils/logger').module('Server'); +const requestLogger = require('./middleware/requestLogger'); const app = express(); const PORT = process.env.PORT || 8000; @@ -17,37 +19,38 @@ app.use(cors()); app.use(express.json()); app.use(fileUpload({ limits: { fileSize: FILE_UPLOAD.MAX_FILE_SIZE } })); app.use('/temp', express.static('temp')); +app.use(requestLogger); async function syncDatabase() { await sequelize.authenticate(); - console.log('数据库连接成功'); + logger.info('数据库连接成功'); await sequelize.sync({ force: false, alter: false, }); - console.log('数据库表结构同步完成'); + logger.info('数据库表结构同步完成'); } async function initDeviceFields() { await require('./initDeviceFields')(); - console.log('设备字段初始化完成'); + logger.info('设备字段初始化完成'); } async function initTicketFields() { await require('./initTicketFields')(); - console.log('工单字段初始化完成'); + logger.info('工单字段初始化完成'); } async function initTicketModels() { await require('./models/ticketIndex').initializeModels(); - console.log('工单模型关联初始化完成'); + logger.info('工单模型关联初始化完成'); } async function syncSystemSettings() { const SystemSetting = require('./models/SystemSetting'); await SystemSetting.sync(); - console.log('系统设置模型同步完成'); + logger.info('系统设置模型同步完成'); } async function syncConsumableModels() { @@ -64,7 +67,7 @@ async function syncConsumableModels() { ConsumableRecord.sync(), ConsumableLogArchive.sync(), ]); - console.log('耗材模型同步完成'); + logger.info('耗材模型同步完成'); } async function syncInventoryModels() { @@ -73,19 +76,19 @@ async function syncInventoryModels() { const InventoryRecord = require('./models/InventoryRecord'); await Promise.all([InventoryPlan.sync(), InventoryTask.sync(), InventoryRecord.sync()]); - console.log('盘点模型同步完成'); + logger.info('盘点模型同步完成'); } async function syncBackupLogModel() { const BackupLog = require('./models/BackupLog'); await BackupLog.sync(); - console.log('备份日志模型同步完成'); + logger.info('备份日志模型同步完成'); } async function syncOperationLogModel() { const OperationLog = require('./models/OperationLog'); await OperationLog.sync(); - console.log('操作日志模型同步完成'); + logger.info('操作日志模型同步完成'); } async function syncBusinessModels() { @@ -98,24 +101,24 @@ async function syncBusinessModels() { await Business.sync({ alter: true }); await DeviceBusiness.sync({ alter: true }); await Warehouse.sync({ alter: true }); - console.log('业务/设备关联/库房模型同步完成(alter mode)'); + logger.info('业务/设备关联/库房模型同步完成(alter mode)'); } else { await Business.sync(); await DeviceBusiness.sync(); await Warehouse.sync(); - console.log('业务/设备关联/库房模型同步完成(safe mode)'); + logger.info('业务/设备关联/库房模型同步完成(safe mode)'); } } async function initDefaultSystemSettings() { - console.log('开始初始化系统设置默认值...'); + logger.info('开始初始化系统设置默认值...'); const { initDefaultSettings } = require('./routes/systemSettings'); await initDefaultSettings(); - console.log('系统设置初始化完成'); + logger.info('系统设置初始化完成'); } async function initFaultCategories() { - console.log('开始初始化故障分类...'); + logger.info('开始初始化故障分类...'); const FaultCategory = require('./models/FaultCategory'); const defaultCategories = [ @@ -193,17 +196,17 @@ async function initFaultCategories() { isSystem: true, isActive: true, }); - console.log(`创建故障分类: ${cat.name}`); + logger.info(`创建故障分类: ${cat.name}`); } } - console.log('故障分类初始化完成'); + logger.info('故障分类初始化完成'); } async function initAutoBackupScheduler() { const { initAutoBackup } = require('./utils/autoBackupScheduler'); const status = initAutoBackup(); if (status.enabled) { - console.log(`自动备份已启用,下次执行时间:${status.nextRun || '未知'}`); + logger.info(`自动备份已启用,下次执行时间:${status.nextRun || '未知'}`); } } @@ -223,9 +226,9 @@ async function initializeApp() { await initFaultCategories(); await initAutoBackupScheduler(); - console.log('所有初始化完成,服务器准备就绪'); + logger.info('所有初始化完成,服务器准备就绪'); } catch (error) { - console.error('初始化失败:', error); + logger.error('初始化失败', { error: error.message, stack: error.stack }); process.exit(1); } } @@ -233,16 +236,17 @@ async function initializeApp() { const swaggerUi = require('swagger-ui-express'); const { specs, customCSS } = require('./swagger'); const { authMiddleware } = require('./middleware/auth'); +const { maintenanceMiddleware } = require('./middleware/maintenance'); const loadRoutes = require('./utils/routeLoader'); initializeApp() .then(() => { app.listen(PORT, () => { - console.log(`服务器运行在 http://localhost:${PORT}`); + logger.info(`服务器运行在 http://localhost:${PORT}`); }); }) .catch(err => { - console.error('应用初始化失败:', err); + logger.error('应用初始化失败', { error: err.message, stack: err.stack }); process.exit(1); }); @@ -253,6 +257,7 @@ const PUBLIC_PATHS = [ '/api-docs', '/api-docs.json', '/system-settings/system/info', + '/maintenance', ]; const isPublicPath = path => { @@ -269,8 +274,35 @@ app.use('/api', (req, res, next) => { return authMiddleware(req, res, next); }); +app.use('/api', maintenanceMiddleware); + loadRoutes(app); +const { getMaintenanceStatus, disableMaintenanceMode } = require('./utils/maintenanceMode'); + +app.get('/api/maintenance/status', (req, res) => { + res.json({ + success: true, + data: getMaintenanceStatus(), + }); +}); + +app.post('/api/maintenance/disable', async (req, res) => { + try { + disableMaintenanceMode(); + res.json({ + success: true, + message: '维护模式已手动解除', + }); + } catch (error) { + res.status(500).json({ + success: false, + message: '解除维护模式失败', + error: error.message, + }); + } +}); + app.use('/uploads', express.static('uploads')); app.use( diff --git a/backend/utils/autoBackupScheduler.js b/backend/utils/autoBackupScheduler.js index 7088e6c..e203b39 100644 --- a/backend/utils/autoBackupScheduler.js +++ b/backend/utils/autoBackupScheduler.js @@ -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(); } diff --git a/backend/utils/backup.js b/backend/utils/backup.js index cf7d789..ac216e0 100644 --- a/backend/utils/backup.js +++ b/backend/utils/backup.js @@ -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} 还原点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} 是否成功回滚 + */ +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, }; diff --git a/backend/utils/errorHandler.js b/backend/utils/errorHandler.js index 5b1d479..c1cc1c5 100644 --- a/backend/utils/errorHandler.js +++ b/backend/utils/errorHandler.js @@ -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) { diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 0000000..1a6abe9 --- /dev/null +++ b/backend/utils/logger.js @@ -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; diff --git a/backend/utils/maintenanceMode.js b/backend/utils/maintenanceMode.js new file mode 100644 index 0000000..cc75b2e --- /dev/null +++ b/backend/utils/maintenanceMode.js @@ -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, +}; diff --git a/backend/utils/routeLoader.js b/backend/utils/routeLoader.js index eca2dcf..e5afa90 100644 --- a/backend/utils/routeLoader.js +++ b/backend/utils/routeLoader.js @@ -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; } }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 72a9e33..73e6f63 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -54,6 +54,7 @@ import { SWRConfig, swrConfig } from './hooks/useSWR'; import axios from 'axios'; import { Spin } from 'antd'; import ErrorBoundary from './components/ErrorBoundary'; +import MaintenanceBanner from './components/MaintenanceBanner'; const Dashboard = lazy(() => import('./pages/Dashboard')); const DeviceManagement = lazy(() => import('./pages/DeviceManagement')); @@ -644,6 +645,7 @@ const ThemeConfig = () => { + }> { @@ -69,13 +75,19 @@ api.interceptors.response.use( if (error.response) { const { status, data } = error.response; + if (status === 503 && data?.maintenance) { + if (maintenanceCallback) { + maintenanceCallback(data.maintenance); + } + return Promise.reject(new Error('系统维护中')); + } + if (status === 401) { const currentPath = window.location.pathname; if (!currentPath.startsWith('/login')) { secureStorage.remove(TOKEN_KEY); secureStorage.remove('user'); - // 使用 React Router 导航而非强制刷新,保留 SPA 状态 if (window.__navigate) { window.__navigate('/login'); } else { diff --git a/frontend/src/components/MaintenanceBanner.jsx b/frontend/src/components/MaintenanceBanner.jsx new file mode 100644 index 0000000..5de3145 --- /dev/null +++ b/frontend/src/components/MaintenanceBanner.jsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react'; +import { Alert, Space, Button } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { setMaintenanceCallback } from '../api'; + +const MAINTENANCE_STORAGE_KEY = 'maintenance_hidden_until'; + +/** + * 维护模式提示横幅组件 + * 当系统处于维护模式时,在页面顶部显示提示信息 + */ +function MaintenanceBanner() { + const [visible, setVisible] = useState(false); + const [reason, setReason] = useState('系统维护中'); + const [startTime, setStartTime] = useState(null); + + useEffect(() => { + setMaintenanceCallback(handleMaintenance); + }, []); + + const handleMaintenance = maintenanceData => { + const hiddenUntil = localStorage.getItem(MAINTENANCE_STORAGE_KEY); + if (hiddenUntil && Date.now() < parseInt(hiddenUntil, 10)) { + return; + } + + setReason(maintenanceData.reason || '系统维护中'); + setStartTime(maintenanceData.startTime); + setVisible(true); + }; + + const handleClose = () => { + const hideDuration = 5 * 60 * 1000; + localStorage.setItem(MAINTENANCE_STORAGE_KEY, String(Date.now() + hideDuration)); + setVisible(false); + }; + + if (!visible) return null; + + const formattedTime = startTime ? new Date(startTime).toLocaleString('zh-CN') : null; + + return ( + +
{reason}
+ {formattedTime &&
开始时间:{formattedTime}
} + + } + type='warning' + showIcon + closable + closeIcon={} + onClose={handleClose} + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + zIndex: 9999, + borderRadius: 0, + borderBottom: '1px solid #faad14', + }} + /> + ); +} + +export default MaintenanceBanner; diff --git a/frontend/src/pages/BackupManagement.jsx b/frontend/src/pages/BackupManagement.jsx index 95d6c36..5ad8f16 100644 --- a/frontend/src/pages/BackupManagement.jsx +++ b/frontend/src/pages/BackupManagement.jsx @@ -19,6 +19,7 @@ import { Row, Col, Dropdown, + Checkbox, } from 'antd'; import { CloudDownloadOutlined, @@ -41,6 +42,7 @@ import { ClearOutlined, EyeOutlined, TableOutlined, + CloseCircleOutlined, } from '@ant-design/icons'; import api, { backupAPI } from '../api'; import CloseButton from '../components/CloseButton'; @@ -49,6 +51,34 @@ import secureStorage, { TOKEN_KEY } from '../utils/secureStorage'; const { Title, Text } = Typography; +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: '系统设置', +}; + const formatBytes = bytes => { if (bytes === 0) return '0 B'; const k = 1024; @@ -136,6 +166,8 @@ const BackupManagement = () => { const [backupInfo, setBackupInfo] = useState(null); const [restoreProgress, setRestoreProgress] = useState(0); const [restoreStatus, setRestoreStatus] = useState(''); + const [skipUserData, setSkipUserData] = useState(false); + const [eventSourceRef, setEventSourceRef] = useState(null); const fetchBackups = useCallback(async () => { setLoading(true); @@ -624,12 +656,14 @@ const BackupManagement = () => { const options = { overwriteExisting: true, skipFiles: false, + skipUserData: skipUserData, }; const eventSource = new EventSource( `/api/backup/restore-progress/${encodeURIComponent(filename)}?token=${encodeURIComponent(token)}&options=${encodeURIComponent(JSON.stringify(options))}` ); + setEventSourceRef(eventSource); let resultData = null; eventSource.onmessage = event => { @@ -642,6 +676,7 @@ const BackupManagement = () => { if (data.stage === 'complete') { resultData = data.result; eventSource.close(); + setEventSourceRef(null); setTimeout(() => { setRestoreVisible(false); @@ -851,6 +886,129 @@ const BackupManagement = () => { + {resultData?.skipped && resultData.skipped.length > 0 && ( +
+
+
+
+ +
+ + 已跳过的数据表 + +
+ + 共 {resultData.skipped.length} 个表 + +
+ +
+ {resultData.skipped.map((tableName, index) => ( +
+
+
+ + {TABLE_NAME_MAPPING[tableName] || tableName} + +
+
+ + 已跳过 + +
+
+ ))} +
+
+ )} + {resultData?.tableDetails && Object.keys(resultData.tableDetails).length > 0 && (
{ }, 500); } + if (data.stage === 'stopped') { + resultData = data.result; + eventSource.close(); + setEventSourceRef(null); + setRestoreLoading(false); + setRestoreVisible(false); + + const isRolledBack = data.result?.rolledBack; + Modal.info({ + title: isRolledBack ? '恢复已停止并回滚' : '恢复已停止', + content: ( +
+

数据恢复已被用户停止。

+ {isRolledBack ? ( +

+ 数据已成功回滚到恢复前的状态,无需担心数据完整性。 +

+ ) : ( + <> +

+ 已恢复 {data.result?.tablesRestored || 0} 个表, + {data.result?.recordsRestored || 0} 条记录。 +

+

+ 警告:数据回滚失败,数据可能处于不一致状态,建议检查数据完整性。 +

+ + )} +
+ ), + okText: '知道了', + }); + } + if (data.stage === 'error') { eventSource.close(); + setEventSourceRef(null); setRestoreLoading(false); Modal.error({ title: '数据恢复失败', @@ -1054,6 +1247,7 @@ const BackupManagement = () => { eventSource.onerror = error => { console.error('SSE 连接错误:', error); eventSource.close(); + setEventSourceRef(null); setRestoreLoading(false); Modal.error({ title: '数据恢复失败', @@ -1062,6 +1256,32 @@ const BackupManagement = () => { }; }; + const handleStopRestore = () => { + if (eventSourceRef) { + console.log('用户请求停止恢复'); + setRestoreStatus('正在停止恢复...'); + eventSourceRef.close(); + setEventSourceRef(null); + + // 设置超时:如果5秒内没有收到 stopped 事件,强制重置状态 + setTimeout(() => { + setRestoreLoading(prev => { + if (prev) { + console.log('停止恢复超时,强制重置状态'); + setRestoreVisible(false); + Modal.warning({ + title: '停止恢复', + content: '恢复操作已停止,请刷新页面查看最新数据状态。', + okText: '知道了', + }); + return false; + } + return prev; + }); + }, 5000); + } + }; + const handleDelete = async filename => { try { const response = await api.delete(`/backup/${filename}`); @@ -1155,6 +1375,7 @@ const BackupManagement = () => { const showRestoreConfirm = record => { setSelectedBackup(record); + setSkipUserData(false); setRestoreVisible(true); }; @@ -1992,6 +2213,18 @@ const BackupManagement = () => { > {restoreStatus} +
) : ( <> @@ -2059,6 +2292,34 @@ const BackupManagement = () => {
)} +
+ setSkipUserData(e.target.checked)} + style={{ fontWeight: 600, color: designTokens.colors.text.primary }} + > + 跳过用户数据(保留当前系统用户账户) + +
+ 选择此项后,将不会恢复备份中的用户和用户角色数据,保留当前系统已有的用户账户 +
+
+