feat: 实现统一日志系统和维护模式功能

This commit is contained in:
zhang1106
2026-04-28 10:57:17 +08:00
parent 763e425f78
commit bf2cd221b9
43 changed files with 1966 additions and 430 deletions
+17
View File
@@ -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
View File
@@ -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 };
+14 -4
View File
@@ -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(', ')}`
+8 -14
View File
@@ -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();
}
+31
View File
@@ -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 };
+40
View File
@@ -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;
+3 -1
View File
@@ -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,
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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;
+8 -7
View File
@@ -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: '解锁失败',
+7 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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 });
}
});
+6 -5
View File
@@ -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 });
}
});
+5 -4
View File
@@ -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: '风险评估失败' });
}
});
+12 -11
View File
@@ -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
View File
@@ -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 });
}
});
+22 -27
View File
@@ -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 });
}
});
+7 -6
View File
@@ -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 });
}
});
+11 -10
View File
@@ -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 });
}
});
+6 -5
View File
@@ -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: '获取操作日志详情失败',
+4 -3
View File
@@ -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: '导出失败',
+8 -7
View File
@@ -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: '初始化角色失败',
+2 -1
View File
@@ -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 });
}
});
+7 -8
View File
@@ -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({
+2 -1
View File
@@ -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 });
}
});
+3 -2
View File
@@ -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
View File
@@ -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: '操作失败',
+4 -3
View File
@@ -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 });
}
});
+275
View File
@@ -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);
});
+136
View File
@@ -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
View File
@@ -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(
+35 -56
View File
@@ -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
View File
@@ -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,
};
+3 -4
View File
@@ -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) {
+113
View File
@@ -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;
+79
View File
@@ -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,
};
+4 -3
View File
@@ -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;
}
});