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