feat(系统设置): 新增系统设置模块

- 添加SystemSetting模型用于存储系统配置
- 实现系统设置API路由,支持CRUD操作
- 新增系统设置前端页面,包含全局配置、外观设置、数据备份和关于页面
- 设备批量操作增强,支持批量移动、状态变更和导出
- 工单模型允许deviceId为空
This commit is contained in:
zhang1106
2025-12-29 16:10:42 +08:00
parent b7f806f7d3
commit a7499de748
10 changed files with 2089 additions and 136 deletions
+47
View File
@@ -0,0 +1,47 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const SystemSetting = sequelize.define('SystemSetting', {
settingKey: {
type: DataTypes.STRING(100),
primaryKey: true,
allowNull: false,
comment: '设置键名'
},
settingValue: {
type: DataTypes.TEXT,
allowNull: true,
comment: '设置值(JSON格式)'
},
settingType: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'string',
comment: '设置类型: string, number, boolean, json, array'
},
category: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'general',
comment: '设置分类: general, appearance, backup, about'
},
description: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '设置描述'
},
isEditable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: '是否可编辑'
}
}, {
tableName: 'system_settings',
timestamps: true,
indexes: [
{ fields: ['category'] }
]
});
module.exports = SystemSetting;
+1 -1
View File
@@ -17,7 +17,7 @@ const Ticket = sequelize.define('Ticket', {
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
allowNull: true,
comment: '关联设备ID'
},
deviceName: {
+341 -10
View File
@@ -10,6 +10,7 @@ const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const DeviceField = require('../models/DeviceField');
const Ticket = require('../models/Ticket');
// 获取所有设备(支持搜索和筛选)
router.get('/', async (req, res) => {
@@ -653,16 +654,23 @@ router.delete('/batch-delete', async (req, res) => {
try {
const { deviceIds } = req.body;
if (!deviceIds || !Array.isArray(deviceIds)) {
return res.status(400).json({ error: '效的设备ID列表' });
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
// 先获取所有设备的功率信息
const devices = await Device.findAll({
where: { deviceId: deviceIds }
where: { deviceId: { [Op.in]: deviceIds } }
});
await Ticket.update(
{ deviceId: null },
{ where: { deviceId: { [Op.in]: deviceIds } } }
);
const deletedCount = await Device.destroy({
where: { deviceId: { [Op.in]: deviceIds } }
});
// 更新机柜功率
for (const device of devices) {
const rack = await Rack.findByPk(device.rackId);
if (rack) {
@@ -672,12 +680,10 @@ router.delete('/batch-delete', async (req, res) => {
}
}
// 删除设备
const deleted = await Device.destroy({
where: { deviceId: deviceIds }
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个设备`,
deletedCount
});
res.json({ message: `成功删除 ${deleted} 个设备` });
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -714,4 +720,329 @@ router.delete('/:deviceId', async (req, res) => {
}
});
// 批量上线设备
router.put('/batch-online', async (req, res) => {
try {
const { deviceIds } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
// 更新设备状态为运行中
const [affectedCount] = await Device.update(
{ status: 'running' },
{ where: { deviceId: { [Op.in]: deviceIds } } }
);
res.json({
message: `批量上线成功,已更新 ${affectedCount} 个设备`,
affectedCount
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 批量下线设备
router.put('/batch-offline', async (req, res) => {
try {
const { deviceIds } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
// 更新设备状态为离线
const [affectedCount] = await Device.update(
{ status: 'offline' },
{ where: { deviceId: { [Op.in]: deviceIds } } }
);
res.json({
message: `批量下线成功,已更新 ${affectedCount} 个设备`,
affectedCount
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 批量变更设备状态
router.put('/batch-status', async (req, res) => {
try {
const { deviceIds, status } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
const validStatus = ['running', 'maintenance', 'offline', 'fault'];
if (!validStatus.includes(status)) {
return res.status(400).json({
error: `状态值无效,有效值为:${validStatus.join('、')}`
});
}
// 状态映射
const statusText = {
running: '运行中',
maintenance: '维护中',
offline: '离线',
fault: '故障'
};
// 更新设备状态
const [affectedCount] = await Device.update(
{ status },
{ where: { deviceId: { [Op.in]: deviceIds } } }
);
res.json({
message: `批量状态变更成功,已将 ${affectedCount} 个设备状态变更为"${statusText[status]}"`,
affectedCount,
newStatus: status
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 批量移动设备
router.put('/batch-move', async (req, res) => {
try {
const { deviceIds, targetRackId, startPosition } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
if (!targetRackId) {
return res.status(400).json({ error: '请提供目标机柜ID' });
}
if (startPosition === undefined || startPosition === null) {
return res.status(400).json({ error: '请提供起始U位' });
}
// 验证目标机柜是否存在
const targetRack = await Rack.findByPk(targetRackId);
if (!targetRack) {
return res.status(404).json({ error: '目标机柜不存在' });
}
// 获取要移动的设备
const devices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } }
});
if (devices.length === 0) {
return res.status(404).json({ error: '未找到指定的设备' });
}
const movedDevices = [];
let currentPosition = parseInt(startPosition);
// 按原位置排序设备
devices.sort((a, b) => a.position - b.position);
for (const device of devices) {
// 计算新位置
const newPosition = currentPosition;
// 验证位置是否在机柜范围内
if (newPosition < 1 || newPosition > targetRack.height) {
return res.status(400).json({
error: `设备 ${device.name} 的位置 ${newPosition} 超出机柜高度范围(1-${targetRack.height})`
});
}
// 检查目标位置是否被其他设备占用(排除自身)
const existingDevice = await Device.findOne({
where: {
rackId: targetRackId,
position: newPosition,
deviceId: { [Op.ne]: device.deviceId }
}
});
if (existingDevice) {
return res.status(400).json({
error: `机柜 ${targetRack.name} 的位置 ${newPosition} 已被设备 ${existingDevice.name} 占用`
});
}
// 计算功率变化
const oldPower = device.powerConsumption;
// 更新设备位置
await device.update({
rackId: targetRackId,
position: newPosition
});
// 更新原机柜和新机柜的功率
const oldRack = await Rack.findByPk(device.rackId);
if (oldRack) {
await oldRack.update({
currentPower: Math.max(0, oldRack.currentPower - oldPower)
});
}
await targetRack.update({
currentPower: targetRack.currentPower + oldPower
});
movedDevices.push({
deviceId: device.deviceId,
name: device.name,
oldRackId: device.rackId,
newRackId: targetRackId,
oldPosition: device.position,
newPosition: newPosition
});
// 下一个设备的起始位置 = 当前设备位置 + 当前设备高度
currentPosition += device.height;
}
res.json({
message: `批量移动成功,已移动 ${movedDevices.length} 个设备`,
movedDevices
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 增强导出设备数据(支持自定义字段选择和格式选择)
router.get('/enhanced-export', async (req, res) => {
try {
const { deviceIds, format = 'csv', fields, fieldLabels } = req.query;
// 解析字段列表
let selectedFields = [];
try {
selectedFields = fields ? JSON.parse(fields) : [];
} catch (e) {
selectedFields = [];
}
// 解析字段标签
let fieldLabelMap = {};
try {
fieldLabelMap = fieldLabels ? JSON.parse(fieldLabels) : {};
} catch (e) {
fieldLabelMap = {};
}
// 构建查询条件
const where = {};
if (deviceIds) {
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
where.deviceId = { [Op.in]: ids };
}
// 查询设备数据
const devices = await Device.findAll({
where,
include: [
{
model: Rack,
include: [
{ model: Room }
]
}
]
});
if (devices.length === 0) {
return res.status(404).json({ error: '未找到指定的设备' });
}
// 准备导出数据
const exportData = devices.map(device => {
const data = {};
selectedFields.forEach(fieldName => {
// 映射字段名到中文标签
const label = fieldLabelMap[fieldName] || fieldName;
// 根据字段名获取值
if (fieldName === 'rackName') {
data[label] = device.Rack?.name || '';
} else if (fieldName === 'roomName') {
data[label] = device.Rack?.Room?.name || '';
} else if (fieldName === 'status') {
const statusMap = {
running: '运行中',
maintenance: '维护中',
offline: '离线',
fault: '故障'
};
data[label] = statusMap[device.status] || device.status;
} else if (fieldName === 'type') {
const typeMap = {
server: '服务器',
switch: '交换机',
router: '路由器',
storage: '存储设备',
other: '其他设备'
};
data[label] = typeMap[device.type] || device.type;
} else if (fieldName === 'purchaseDate' || fieldName === 'warrantyExpiry') {
data[label] = device[fieldName] ? new Date(device[fieldName]).toLocaleDateString('zh-CN') : '';
} else if (fieldName === 'customFields' && device.customFields) {
// 如果选择导出自定义字段,展开为单独的列
Object.entries(device.customFields).forEach(([key, value]) => {
data[key] = value;
});
} else if (device[fieldName] !== undefined) {
data[label] = device[fieldName];
}
});
return data;
});
if (format === 'json') {
// JSON格式导出
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', 'attachment; filename=devices.json');
res.json({
exportTime: new Date().toISOString(),
totalCount: devices.length,
devices: exportData
});
} else {
// CSV格式导出
const csvWriter = createObjectCsvWriter({
path: path.join(__dirname, '../temp/enhanced_export.csv'),
header: Object.keys(exportData[0] || {}).map(key => ({ id: key, title: key })),
encoding: 'utf8'
});
// 确保temp目录存在
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
fs.mkdirSync(path.join(__dirname, '../temp'));
}
await csvWriter.writeRecords(exportData);
const csvContent = fs.readFileSync(path.join(__dirname, '../temp/enhanced_export.csv'), 'utf8');
const gbkContent = iconv.encode(csvContent, 'gbk');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
res.send(gbkContent);
fs.unlinkSync(path.join(__dirname, '../temp/enhanced_export.csv'));
}
} catch (error) {
console.error('增强导出失败:', error);
res.status(500).json({ error: '增强导出失败' });
}
});
module.exports = router;
+505
View File
@@ -0,0 +1,505 @@
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { Op } = require('sequelize');
const SystemSetting = require('../models/SystemSetting');
// 初始化默认系统设置
const initDefaultSettings = async () => {
const defaultSettings = [
// 全局配置
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
{ settingKey: 'language', settingValue: JSON.stringify('zh-CN'), settingType: 'string', category: 'general', description: '语言设置', isEditable: true },
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '会话超时时间(分钟)', isEditable: true },
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
// 外观设置
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
{ settingKey: 'dark_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '深色模式', isEditable: true },
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), type: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
// 数据备份设置
{ settingKey: 'auto_backup_enabled', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'backup', description: '启用自动备份', isEditable: true },
{ settingKey: 'backup_interval', settingValue: JSON.stringify(24), settingType: 'number', category: 'backup', description: '备份间隔(小时)', isEditable: true },
{ settingKey: 'backup_retention', settingValue: JSON.stringify(7), settingType: 'number', category: 'backup', description: '备份保留天数', isEditable: true },
{ settingKey: 'backup_path', settingValue: JSON.stringify('./backups'), settingType: 'string', category: 'backup', description: '备份存储路径', isEditable: true },
{ settingKey: 'last_backup_time', settingValue: JSON.stringify(null), settingType: 'string', category: 'backup', description: '上次备份时间', isEditable: false },
{ settingKey: 'backup_count', settingValue: JSON.stringify(0), settingType: 'number', category: 'backup', description: '备份文件数量', isEditable: false },
// 关于页面
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
];
for (const setting of defaultSettings) {
try {
const existing = await SystemSetting.findByPk(setting.settingKey);
if (!existing) {
await SystemSetting.create(setting);
}
} catch (error) {
console.error(`初始化设置 ${setting.settingKey} 失败:`, error);
}
}
};
// 初始化默认设置
initDefaultSettings();
// 获取所有设置
router.get('/', async (req, res) => {
try {
const { category } = req.query;
const where = {};
if (category) {
where.category = category;
}
const settings = await SystemSetting.findAll({
where,
order: [['category', 'ASC'], ['settingKey', 'ASC']]
});
// 格式化返回数据
const formattedSettings = {};
settings.forEach(setting => {
formattedSettings[setting.settingKey] = {
value: JSON.parse(setting.settingValue),
type: setting.settingType,
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
};
});
res.json(formattedSettings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 获取单个设置
router.get('/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
res.json({
key: setting.settingKey,
value: JSON.parse(setting.settingValue),
type: setting.settingType,
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 更新设置
router.put('/:key', async (req, res) => {
try {
const { key } = req.params;
const { value } = req.body;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
if (!setting.isEditable) {
return res.status(403).json({ error: '该设置不可编辑' });
}
// 验证值类型
let parsedValue = value;
if (setting.settingType === 'number') {
parsedValue = Number(value);
if (isNaN(parsedValue)) {
return res.status(400).json({ error: '值必须是有效的数字' });
}
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
});
res.json({
message: '设置更新成功',
setting: {
key: setting.settingKey,
value: parsedValue,
updatedAt: setting.updatedAt
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 批量更新设置
router.put('/', async (req, res) => {
try {
const { settings } = req.body;
if (!settings || typeof settings !== 'object') {
return res.status(400).json({ error: '请提供有效的设置对象' });
}
const updatedSettings = [];
const errors = [];
for (const [key, value] of Object.entries(settings)) {
try {
const setting = await SystemSetting.findByPk(key);
if (!setting) {
errors.push({ key, error: '设置不存在' });
continue;
}
if (!setting.isEditable) {
errors.push({ key, error: '该设置不可编辑' });
continue;
}
let parsedValue = value;
if (setting.settingType === 'number') {
parsedValue = Number(value);
if (isNaN(parsedValue)) {
errors.push({ key, error: '值必须是有效的数字' });
continue;
}
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
});
updatedSettings.push({ key, value: parsedValue });
} catch (error) {
errors.push({ key, error: error.message });
}
}
res.json({
message: `成功更新 ${updatedSettings.length} 个设置`,
updatedSettings,
errors: errors.length > 0 ? errors : undefined
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 重置设置为默认值
router.post('/reset/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
const defaultValues = {
site_name: '机柜管理系统',
site_logo: '',
timezone: 'Asia/Shanghai',
date_format: 'YYYY-MM-DD',
language: 'zh-CN',
session_timeout: 30,
max_login_attempts: 5,
maintenance_mode: false,
primary_color: '#667eea',
secondary_color: '#764ba2',
dark_mode: false,
compact_mode: false,
sidebar_collapsed: false,
table_row_height: 'default',
animation_enabled: true,
auto_backup_enabled: false,
backup_interval: 24,
backup_retention: 7,
backup_path: './backups',
company_name: '',
contact_email: '',
contact_phone: '',
company_address: '',
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
privacy_policy: '',
terms_of_service: ''
};
const defaultValue = defaultValues[key];
if (defaultValue === undefined) {
return res.status(400).json({ error: '该设置没有默认值' });
}
await setting.update({
settingValue: JSON.stringify(defaultValue)
});
res.json({
message: '设置已重置为默认值',
key,
value: defaultValue
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 手动执行备份
router.post('/backup', async (req, res) => {
try {
const backupDir = path.join(__dirname, '../backups');
// 确保备份目录存在
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(backupDir, `backup_${timestamp}.json`);
// 获取所有数据库数据
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const Consumable = require('../models/Consumable');
const User = require('../models/User');
const backupData = {
timestamp: new Date().toISOString(),
version: '1.0.0',
data: {
devices: await Device.findAll({ raw: true }),
racks: await Rack.findAll({ raw: true }),
rooms: await Room.findAll({ raw: true }),
consumables: await Consumable.findAll({ raw: true }),
// 不包含敏感用户信息
users: await User.findAll({
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
raw: true
})
}
};
// 写入备份文件
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
// 更新最后备份时间
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
if (lastBackupSetting) {
await lastBackupSetting.update({
settingValue: JSON.stringify(new Date().toISOString())
});
}
// 统计备份文件数量
const backupFiles = fs.readdirSync(backupDir).filter(f => f.startsWith('backup_'));
const countSetting = await SystemSetting.findByPk('backup_count');
if (countSetting) {
await countSetting.update({
settingValue: JSON.stringify(backupFiles.length)
});
}
res.json({
message: '备份成功',
backupFile: `backups/backup_${timestamp}.json`,
fileSize: fs.statSync(backupFile).size,
backupCount: backupFiles.length
});
} catch (error) {
console.error('备份失败:', error);
res.status(500).json({ error: '备份失败' });
}
});
// 获取备份列表
router.get('/backup/list', async (req, res) => {
try {
const backupDir = path.join(__dirname, '../backups');
if (!fs.existsSync(backupDir)) {
return res.json({ backups: [] });
}
const files = fs.readdirSync(backupDir)
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
.map(f => {
const filePath = path.join(backupDir, f);
const stats = fs.statSync(filePath);
return {
filename: f,
path: `backups/${f}`,
size: stats.size,
createdAt: stats.birthtime,
modifiedAt: stats.mtime
};
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({ backups: files });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 恢复备份
router.post('/backup/restore', async (req, res) => {
try {
const { filename } = req.body;
if (!filename) {
return res.status(400).json({ error: '请提供备份文件名' });
}
const backupFile = path.join(__dirname, '../backups', filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
// 恢复数据
const { Device, Rack, Room, Consumable, User } = require('../models');
if (backupData.data.devices) {
for (const device of backupData.data.devices) {
await Device.upsert(device);
}
}
if (backupData.data.racks) {
for (const rack of backupData.data.racks) {
await Rack.upsert(rack);
}
}
if (backupData.data.rooms) {
for (const room of backupData.data.rooms) {
await Room.upsert(room);
}
}
if (backupData.data.consumables) {
for (const consumable of backupData.data.consumables) {
await Consumable.upsert(consumable);
}
}
res.json({
message: '恢复成功',
restoredAt: new Date().toISOString()
});
} catch (error) {
console.error('恢复备份失败:', error);
res.status(500).json({ error: '恢复备份失败' });
}
});
// 删除备份
router.delete('/backup/:filename', async (req, res) => {
try {
const { filename } = req.params;
const backupFile = path.join(__dirname, '../backups', filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
fs.unlinkSync(backupFile);
res.json({ message: '删除成功', filename });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 下载备份文件
router.get('/backup/download/:filename', async (req, res) => {
try {
const { filename } = req.params;
const backupFile = path.join(__dirname, '../backups', filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
res.download(backupFile, filename);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 获取系统信息
router.get('/system/info', async (req, res) => {
try {
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const User = require('../models/User');
const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([
Device.count(),
Rack.count(),
Room.count(),
User.count()
]);
res.json({
system: {
name: '机柜管理系统',
version: '1.0.0',
uptime: process.uptime(),
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
memoryUsage: process.memoryUsage(),
pid: process.pid
},
statistics: {
devices: deviceCount,
racks: rackCount,
rooms: roomCount,
users: userCount
},
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
+7
View File
@@ -39,6 +39,11 @@ sequelize.authenticate()
// 初始化工单模型关联
return require('./models/ticketIndex').initializeModels();
})
.then(() => {
// 同步系统设置模型
const SystemSetting = require('./models/SystemSetting');
return SystemSetting.sync();
})
.catch(err => console.error('数据库操作失败:', err));
// 导入路由
@@ -58,6 +63,7 @@ const operationLogsRoutes = require('./routes/operationLogs');
const ticketRoutes = require('./routes/tickets');
const ticketCategoryRoutes = require('./routes/ticketCategories');
const ticketFieldRoutes = require('./routes/ticketFields');
const systemSettingsRoutes = require('./routes/systemSettings');
// 使用路由
app.use('/api/devices', deviceRoutes);
@@ -76,6 +82,7 @@ app.use('/api/operation-logs', operationLogsRoutes);
app.use('/api/tickets', ticketRoutes);
app.use('/api/ticket-categories', ticketCategoryRoutes);
app.use('/api/ticket-fields', ticketFieldRoutes);
app.use('/api/system-settings', systemSettingsRoutes);
// 静态文件服务
app.use('/uploads', express.static('uploads'));