Files
yunrui_asset/backend/routes/devices.js
T
zhang1106 93a5de2106 feat(设备管理): 添加一键删除所有设备功能
- 前端添加删除所有设备按钮及确认对话框
- 后端实现删除所有设备及相关数据的接口
- 更新.gitignore忽略backend/temp目录
2026-02-09 16:22:00 +08:00

1559 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const router = express.Router();
const { Op } = require('sequelize');
const { sequelize } = require('../db'); // Import sequelize for transactions
const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');
const { createObjectCsvWriter } = require('csv-writer');
const iconv = require('iconv-lite');
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');
const DevicePort = require('../models/DevicePort'); // Import DevicePort
const Cable = require('../models/Cable'); // Import Cable
const NetworkCard = require('../models/NetworkCard'); // Import NetworkCard
const { validateBody, validateQuery } = require('../middleware/validation');
const {
createDeviceSchema,
updateDeviceSchema,
batchDeviceIdsSchema,
batchStatusSchema,
batchMoveSchema,
queryDeviceSchema
} = require('../validation/deviceSchema');
// 获取所有设备(支持搜索和筛选)
router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
try {
const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
// 构建查询条件
const where = {};
// 关键词搜索(所有文本字段)
if (keyword) {
where[Op.or] = [
{ deviceId: { [Op.like]: `%${keyword}%` } },
{ name: { [Op.like]: `%${keyword}%` } },
{ type: { [Op.like]: `%${keyword}%` } },
{ model: { [Op.like]: `%${keyword}%` } },
{ serialNumber: { [Op.like]: `%${keyword}%` } },
{ position: { [Op.like]: `%${keyword}%` } },
{ ipAddress: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
];
}
// 状态筛选
if (status && status !== 'all') {
where.status = status;
}
// 分类筛选
if (type && type !== 'all') {
where.type = type;
}
// 机柜筛选(用于机柜可视化功能)
if (rackId) {
where.rackId = rackId;
}
// 执行查询
const { count, rows } = await Device.findAndCountAll({
where,
include: [
{
model: Rack,
include: [
{ model: Room }
]
}
],
offset,
limit: parseInt(pageSize)
});
res.json({
total: count,
devices: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 生成设备ID的辅助函数
async function generateDeviceId() {
// 获取当前最大的设备ID序号
const devices = await Device.findAll({
where: {
deviceId: {
[require('sequelize').Op.like]: 'DEV%'
}
}
});
let maxNumber = 0;
devices.forEach(device => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) {
maxNumber = num;
}
}
});
// 生成新的设备ID,序号+1,至少3位数字
const newNumber = maxNumber + 1;
return `DEV${String(newNumber).padStart(3, '0')}`;
}
// 创建设备
router.post('/', validateBody(createDeviceSchema), async (req, res) => {
try {
const deviceData = { ...req.body };
// 如果没有提供deviceId或为空,则自动生成
if (!deviceData.deviceId || deviceData.deviceId.trim() === '') {
deviceData.deviceId = await generateDeviceId();
}
const device = await Device.create(deviceData);
// 更新机柜当前功率
const rack = await Rack.findByPk(deviceData.rackId);
if (rack) {
await rack.update({
currentPower: rack.currentPower + deviceData.powerConsumption
});
}
res.status(201).json(device);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 设备导入模板下载
router.get('/import-template', async (req, res) => {
try {
// 查询设备字段配置
const deviceFields = await DeviceField.findAll({
order: [['order', 'ASC']]
});
// 根据字段配置动态生成CSV标题(排除设备ID,由系统自动生成)
// 将rackId字段替换为机房名称+机柜名称,以便唯一定位
// 注意:导入模板包含所有字段(不论visible状态),确保数据完整性
const headers = [];
deviceFields
.filter(field => field.fieldName !== 'deviceId')
.forEach(field => {
// 如果是机柜字段,拆分为机房名称和机柜名称两列
if (field.fieldName === 'rackId') {
headers.push({
id: '所在机房名称',
title: '所在机房名称'
});
headers.push({
id: '所在机柜名称',
title: '所在机柜名称'
});
return;
}
// 直接使用displayName作为列名,不添加任何后缀
headers.push({ id: field.displayName, title: field.displayName });
});
// 准备示例数据(根据字段配置生成,排除设备ID)
// 注意:包含所有字段(不论visible状态)
const exampleData = {};
deviceFields
.filter(field => field.fieldName !== 'deviceId')
.forEach(field => {
// 如果是机柜字段,拆分为机房名称和机柜名称
if (field.fieldName === 'rackId') {
exampleData['所在机房名称'] = '测试机房';
exampleData['所在机柜名称'] = 'A01';
return;
}
switch (field.fieldName) {
case 'name':
exampleData[field.displayName] = '测试服务器001';
break;
case 'type':
exampleData[field.displayName] = 'server';
break;
case 'model':
exampleData[field.displayName] = 'DELL R740';
break;
case 'serialNumber':
exampleData[field.displayName] = 'SN1234567890';
break;
case 'position':
exampleData[field.displayName] = '1';
break;
case 'height':
exampleData[field.displayName] = '2';
break;
case 'powerConsumption':
exampleData[field.displayName] = '500';
break;
case 'status':
exampleData[field.displayName] = 'running';
break;
case 'purchaseDate':
exampleData[field.displayName] = '2023-01-01';
break;
case 'warrantyExpiry':
exampleData[field.displayName] = '2026-12-31';
break;
case 'ipAddress':
exampleData[field.displayName] = '192.168.1.100';
break;
case 'description':
exampleData[field.displayName] = '测试用服务器';
break;
default:
// 自定义字段根据类型生成示例值
if (field.fieldType === 'number') {
exampleData[field.displayName] = '100';
} else if (field.fieldType === 'boolean') {
exampleData[field.displayName] = 'true';
} else if (field.fieldType === 'date') {
exampleData[field.displayName] = '2023-01-01';
} else if (field.fieldType === 'select' && field.options && field.options.length > 0) {
exampleData[field.displayName] = field.options[0].value;
} else {
exampleData[field.displayName] = `示例${field.displayName}`;
}
}
});
const templateData = [exampleData];
// 确保temp目录存在
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const csvWriter = createObjectCsvWriter({
path: path.join(tempDir, 'import_template.csv'),
header: headers
});
// 写入CSV文件
await csvWriter.writeRecords(templateData);
// 读取文件并转换为GBK编码
const csvContent = fs.readFileSync(path.join(tempDir, 'import_template.csv'), 'utf8');
const gbkContent = iconv.encode(csvContent, 'gbk');
// 设置响应头
res.setHeader('Content-Type', 'text/csv; charset=gbk');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('设备导入模板.csv')}`);
// 发送CSV数据
res.send(gbkContent);
// 删除临时文件
fs.unlinkSync(path.join(tempDir, 'import_template.csv'));
} catch (error) {
console.error('生成导入模板失败:', error);
res.status(500).json({ error: '生成导入模板失败' });
}
});
// 导出设备数据为CSV
router.get('/export', async (req, res) => {
try {
const { deviceIds } = req.query;
// 查询条件
const where = {};
if (deviceIds) {
// 支持多个设备ID,使用数组格式
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
where.deviceId = { [Op.in]: ids };
}
// 查询设备字段配置
const deviceFields = await DeviceField.findAll({
order: [['order', 'ASC']]
});
// 查询设备数据
const devices = await Device.findAll({
where,
include: [
{
model: Rack,
include: [
{ model: Room }
]
}
]
});
// 如果没有找到设备
if (devices.length === 0) {
return res.status(404).json({ error: '未找到指定的设备' });
}
// 创建字段名到displayName的映射
const fieldNameToDisplayName = {};
deviceFields.forEach(field => {
fieldNameToDisplayName[field.fieldName] = field.displayName;
});
// 准备CSV数据 - 根据字段配置动态生成,与导入模板保持一致
const csvData = devices.map(device => {
const deviceData = {};
// 根据字段配置生成数据(排除deviceId)
deviceFields
.filter(field => field.visible && field.fieldName !== 'deviceId')
.forEach(field => {
// rackId字段拆分为机房名称和机柜名称
if (field.fieldName === 'rackId') {
deviceData['所在机房名称'] = device.Rack?.Room?.name || '';
deviceData['所在机柜名称'] = device.Rack?.name || '';
return;
}
// 其他字段使用displayName作为列名
const value = device[field.fieldName];
// 日期格式处理
if (field.fieldType === 'date' && value) {
deviceData[field.displayName] = new Date(value).toLocaleDateString();
} else {
deviceData[field.displayName] = value !== undefined && value !== null ? value : '';
}
});
// 添加自定义字段
if (device.customFields) {
Object.entries(device.customFields).forEach(([fieldName, value]) => {
const field = deviceFields.find(f => f.fieldName === fieldName);
if (field && field.visible) {
deviceData[field.displayName] = value;
}
});
}
return deviceData;
});
// 设置CSV标题 - 与导入模板保持一致
const headers = [];
deviceFields
.filter(field => field.visible && field.fieldName !== 'deviceId')
.forEach(field => {
// rackId拆分为两个列
if (field.fieldName === 'rackId') {
headers.push({ id: '所在机房名称', title: '所在机房名称' });
headers.push({ id: '所在机柜名称', title: '所在机柜名称' });
return;
}
headers.push({ id: field.displayName, title: field.displayName });
});
const csvWriter = createObjectCsvWriter({
path: path.join(__dirname, '../temp/devices.csv'),
header: headers,
encoding: 'utf8'
});
// 确保temp目录存在
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
fs.mkdirSync(path.join(__dirname, '../temp'));
}
// 写入CSV文件
await csvWriter.writeRecords(csvData);
// 读取文件并转换为GBK编码
const csvContent = fs.readFileSync(path.join(__dirname, '../temp/devices.csv'), 'utf8');
const gbkContent = iconv.encode(csvContent, 'gbk');
// 设置响应头
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
// 发送CSV数据
res.send(gbkContent);
// 删除临时文件
fs.unlinkSync(path.join(__dirname, '../temp/devices.csv'));
} catch (error) {
console.error('导出设备数据失败:', error);
res.status(500).json({ error: '导出设备数据失败' });
}
});
// 导入设备数据从CSV
router.post('/import', async (req, res) => {
try {
if (!req.files || !req.files.csvFile) {
return res.status(400).json({ error: '请上传CSV文件' });
}
const csvFile = req.files.csvFile;
const stats = { total: 0, success: 0, failed: 0, errors: [] };
// 确保temp目录存在
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
fs.mkdirSync(path.join(__dirname, '../temp'));
}
// 保存上传的文件
const filePath = path.join(__dirname, '../temp', csvFile.name);
await csvFile.mv(filePath);
// 读取并解析CSV文件(GBK编码)
const results = [];
const stream = fs.createReadStream(filePath)
.pipe(iconv.decodeStream('gbk'))
.pipe(csv());
await new Promise((resolve, reject) => {
stream.on('data', (data) => {
stats.total++;
results.push(data);
})
.on('end', resolve)
.on('error', reject);
});
// 获取所有机房和机柜信息用于验证
const rooms = await Room.findAll();
const roomNameToIdMap = new Map(rooms.map(room => [room.name, room.roomId]));
const racks = await Rack.findAll({
include: [{ model: Room }]
});
// 创建机柜查找映射:机房名称+机柜名称 -> rackId
const rackLocationMap = new Map();
racks.forEach(rack => {
const key = `${rack.Room?.name || ''}_${rack.name}`;
rackLocationMap.set(key, rack.rackId);
});
// 获取设备字段配置
const deviceFields = await DeviceField.findAll();
// 创建字段映射:displayName -> fieldConfig
const fieldMapping = {};
deviceFields.forEach(field => {
fieldMapping[field.displayName] = field;
});
// 创建字段名到显示名称的映射(用于识别基础字段)
const fieldNameToDisplayName = {};
deviceFields.forEach(field => {
fieldNameToDisplayName[field.fieldName] = field.displayName;
});
// 处理导入数据
for (let i = 0; i < results.length; i++) {
const row = results[i];
const rowNum = i + 2; // CSV行号(第一行是标题)
// 辅助函数:从带格式说明的字段名中提取原始字段名
// 例如:"设备类型(server/switch/router/storage)" -> "设备类型"
// "位置(U)(必填)" -> "位置(U)"
const extractFieldName = (fieldNameWithFormat) => {
// 匹配规则:提取到 "(必填)"、"(可选)"、"(YYYY-MM-DD)" 或类似格式说明之前的内容
// 但保留字段名本身的括号,如 "位置(U)"
const match = fieldNameWithFormat.match(/^(.+?)(\(必填\)|\(可选\)|\([a-zA-Z0-9\-\/]+\))$/);
return match ? match[1].trim() : fieldNameWithFormat;
};
// 创建一个映射,将原始字段名映射到CSV中的值
const fieldValueMap = {};
Object.entries(row).forEach(([displayName, value]) => {
const originalFieldName = extractFieldName(displayName);
fieldValueMap[originalFieldName] = value;
// 同时保留原始字段名的映射,以便兼容不同格式
fieldValueMap[displayName] = value;
});
try {
// 动态构建必填字段列表(从数据库配置读取,保持与字段管理页面同步)
const trulyRequiredFields = [];
deviceFields.forEach(field => {
// 跳过设备ID(系统生成)
if (field.fieldName === 'deviceId') return;
// 机柜字段特殊处理:拆分为机房名称+机柜名称
if (field.fieldName === 'rackId') {
if (field.required) {
trulyRequiredFields.push('所在机房名称');
trulyRequiredFields.push('所在机柜名称');
}
return;
}
// 其他字段根据数据库配置
if (field.required) {
trulyRequiredFields.push(field.displayName);
}
});
// 验证必填字段
const missingFields = [];
for (const fieldName of trulyRequiredFields) {
const value = fieldValueMap[fieldName];
if (!value || (typeof value === 'string' && value.trim() === '')) {
missingFields.push(fieldName);
}
}
if (missingFields.length > 0) {
throw new Error(`缺少必填字段:${missingFields.join('、')}`);
}
// 获取动态字段显示名称(支持用户在字段管理中修改后的名称)
const getFieldValue = (fieldName) => {
const displayName = fieldNameToDisplayName[fieldName];
return displayName ? fieldValueMap[displayName] : undefined;
};
// 验证设备类型值
const validTypes = ['server', 'switch', 'router', 'storage', 'other'];
const deviceType = getFieldValue('type');
if (!validTypes.includes(deviceType)) {
throw new Error(`设备类型无效,有效值为:${validTypes.join('、')},当前值:${deviceType}`);
}
// 处理设备ID:如果为空则自动生成
let deviceId = getFieldValue('deviceId');
if (!deviceId || deviceId.trim() === '') {
// 自动生成设备ID
const allDevices = await Device.findAll({
where: {
deviceId: {
[require('sequelize').Op.like]: 'DEV%'
}
}
});
let maxNumber = 0;
allDevices.forEach(device => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) {
maxNumber = num;
}
}
});
deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
} else {
// 验证设备ID是否已存在
const existingDeviceById = await Device.findOne({
where: { deviceId: deviceId }
});
if (existingDeviceById) {
throw new Error(`设备ID已存在:${deviceId}`);
}
}
// 验证序列号是否已存在
const serialNumber = getFieldValue('serialNumber');
if (!serialNumber) {
throw new Error('序列号不能为空');
}
const existingDevice = await Device.findOne({
where: { serialNumber: serialNumber }
});
if (existingDevice) {
throw new Error(`序列号已存在:${serialNumber}`);
}
// 获取机房名称和机柜名称
const roomName = fieldValueMap['所在机房名称'];
const rackName = fieldValueMap['所在机柜名称'];
if (!roomName || roomName.trim() === '') {
throw new Error('所在机房名称不能为空');
}
if (!rackName || rackName.trim() === '') {
throw new Error('所在机柜名称不能为空');
}
// 验证机房是否存在
const roomId = roomNameToIdMap.get(roomName.trim());
if (!roomId) {
throw new Error(`机房不存在:${roomName},请先创建机房`);
}
// 根据机房名称+机柜名称查找机柜
const locationKey = `${roomName.trim()}_${rackName.trim()}`;
let rackId = rackLocationMap.get(locationKey);
// 如果机柜不存在,自动创建
if (!rackId) {
// 生成新的机柜ID
const existingRacks = await Rack.findAll({
where: {
rackId: {
[require('sequelize').Op.like]: 'RACK%'
}
}
});
let maxNumber = 0;
existingRacks.forEach(rack => {
const match = rack.rackId.match(/^RACK(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) {
maxNumber = num;
}
}
});
const newRackId = `RACK${String(maxNumber + 1).padStart(3, '0')}`;
const newRack = await Rack.create({
rackId: newRackId,
name: rackName.trim(),
height: 42,
maxPower: 10000,
currentPower: 0,
status: 'active',
roomId: roomId
});
rackId = newRack.rackId;
rackLocationMap.set(locationKey, rackId);
}
// 验证状态值
const validStatus = ['running', 'maintenance', 'offline', 'fault'];
const status = getFieldValue('status');
if (!validStatus.includes(status)) {
throw new Error(`状态值无效,有效值为:${validStatus.join('、')},当前值:${status}`);
}
// 验证数字字段
const position = getFieldValue('position');
const height = getFieldValue('height');
const powerConsumption = getFieldValue('powerConsumption');
if (position !== undefined && isNaN(Number(position))) {
throw new Error(`${fieldNameToDisplayName['position']}必须是数字,当前值:${position}`);
}
if (height !== undefined && isNaN(Number(height))) {
throw new Error(`${fieldNameToDisplayName['height']}必须是数字,当前值:${height}`);
}
if (powerConsumption !== undefined && isNaN(Number(powerConsumption))) {
throw new Error(`${fieldNameToDisplayName['powerConsumption']}必须是数字,当前值:${powerConsumption}`);
}
// 验证日期格式并解析日期
const purchaseDateValue = getFieldValue('purchaseDate');
const warrantyExpiryValue = getFieldValue('warrantyExpiry');
const purchaseDate = purchaseDateValue ? new Date(purchaseDateValue) : null;
const warrantyExpiry = warrantyExpiryValue ? new Date(warrantyExpiryValue) : null;
if (purchaseDateValue && isNaN(purchaseDate.getTime())) {
throw new Error(`${fieldNameToDisplayName['purchaseDate']}格式无效:${purchaseDateValue},请使用YYYY-MM-DD格式`);
}
if (warrantyExpiryValue && isNaN(warrantyExpiry.getTime())) {
throw new Error(`${fieldNameToDisplayName['warrantyExpiry']}格式无效:${warrantyExpiryValue},请使用YYYY-MM-DD格式`);
}
// 验证日期逻辑
if (purchaseDate && warrantyExpiry && warrantyExpiry <= purchaseDate) {
throw new Error(`${fieldNameToDisplayName['warrantyExpiry']}必须晚于${fieldNameToDisplayName['purchaseDate']}${purchaseDateValue} ~ ${warrantyExpiryValue}`);
}
// 构建设备数据
const deviceData = {
deviceId: deviceId,
name: getFieldValue('name'),
type: deviceType,
model: getFieldValue('model'),
serialNumber: serialNumber,
rackId: rackId,
position: position ? parseInt(position) : 0,
height: height ? parseInt(height) : 1,
powerConsumption: powerConsumption ? parseFloat(powerConsumption) : 0,
ipAddress: getFieldValue('ipAddress') || '',
status: status,
purchaseDate: purchaseDate,
warrantyExpiry: warrantyExpiry,
description: getFieldValue('description') || ''
};
// 处理自定义字段(排除基础字段)
const customFields = {};
const baseFieldNames = ['deviceId', 'name', 'type', 'model', 'serialNumber', 'rackId', 'position', 'height', 'powerConsumption', 'ipAddress', 'status', 'purchaseDate', 'warrantyExpiry', 'description'];
Object.entries(row).forEach(([displayName, value]) => {
// 从带格式的字段名中提取原始显示名称
const originalDisplayName = extractFieldName(displayName);
// 查找对应的字段配置
const fieldConfig = fieldMapping[originalDisplayName];
if (fieldConfig && !baseFieldNames.includes(fieldConfig.fieldName)) {
// 这是自定义字段,根据字段类型处理值
let processedValue = value;
if (fieldConfig.fieldType === 'number') {
processedValue = value ? parseFloat(value) : null;
} else if (fieldConfig.fieldType === 'boolean') {
processedValue = value ? (value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === '是') : false;
} else if (fieldConfig.fieldType === 'date') {
processedValue = value ? new Date(value) : null;
}
customFields[fieldConfig.fieldName] = processedValue;
}
});
// 如果有自定义字段,添加到设备数据中
if (Object.keys(customFields).length > 0) {
deviceData.customFields = customFields;
}
// 创建设备
const device = await Device.create(deviceData);
// 更新机柜当前功率
const rack = await Rack.findByPk(rackId);
if (rack) {
await rack.update({
currentPower: rack.currentPower + device.powerConsumption
});
}
stats.success++;
} catch (error) {
stats.failed++;
let errorMessage = error.message;
if (error.name === 'SequelizeUniqueConstraintError') {
const errors = error.errors || [];
for (const err of errors) {
if (err.path === 'deviceId') {
errorMessage = `设备ID已存在`;
break;
} else if (err.path === 'serialNumber') {
errorMessage = `序列号已存在`;
break;
}
}
} else if (error.name === 'SequelizeValidationError') {
errorMessage = '数据验证失败,请检查字段格式';
}
stats.errors.push({ row: rowNum, error: errorMessage, data: row });
}
}
fs.unlinkSync(filePath);
res.json({ statistics: stats });
} catch (error) {
console.error('导入设备数据失败:', error);
const errorMessage = error.message || '导入过程中发生未知错误';
res.status(500).json({
errors: [{ row: 0, error: errorMessage }]
});
}
});
// 批量上线设备
router.put('/batch-online', validateBody(batchDeviceIdsSchema), 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', validateBody(batchDeviceIdsSchema), 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 existingDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
attributes: ['deviceId']
});
// 检查是否有不存在的设备
const existingIds = existingDevices.map(d => d.deviceId);
const missingIds = deviceIds.filter(id => !existingIds.includes(id));
if (missingIds.length > 0) {
return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` });
}
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' });
}
// 验证目标机柜是否存在
const targetRack = await Rack.findByPk(targetRackId);
if (!targetRack) {
return res.status(404).json({ error: '目标机柜不存在' });
}
// 批量更新设备位置
let movedCount = 0;
for (let i = 0; i < deviceIds.length; i++) {
const deviceId = deviceIds[i];
const position = startPosition ? startPosition + i : undefined;
const updateData = { rackId: targetRackId };
if (position) {
updateData.position = position;
}
const [updated] = await Device.update(updateData, {
where: { deviceId }
});
if (updated) {
movedCount++;
}
}
res.json({
message: `批量移动成功,已将 ${movedCount} 个设备移动到机柜 ${targetRackId}`,
movedCount
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 获取单个设备
router.get('/:deviceId', async (req, res) => {
try {
const device = await Device.findByPk(req.params.deviceId, {
include: [
{
model: Rack,
include: [
{ model: Room }
]
}
]
});
if (!device) {
return res.status(404).json({ error: '设备不存在' });
}
res.json(device);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 更新设备
router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
try {
// 获取旧设备信息以更新功率
const oldDevice = await Device.findByPk(req.params.deviceId);
if (!oldDevice) {
return res.status(404).json({ error: '设备不存在' });
}
const [updated] = await Device.update(req.body, {
where: { deviceId: req.params.deviceId }
});
if (updated) {
// 更新机柜当前功率
const rack = await Rack.findByPk(oldDevice.rackId);
if (rack) {
const powerDiff = req.body.powerConsumption - oldDevice.powerConsumption;
await rack.update({
currentPower: rack.currentPower + powerDiff
});
}
const updatedDevice = await Device.findByPk(req.params.deviceId, {
include: [
{
model: Rack,
include: [
{ model: Room }
]
}
]
});
res.json(updatedDevice);
} else {
res.status(404).json({ error: '设备不存在' });
}
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 批量下线设备
router.put('/batch-offline', async (req, res) => {
try {
const { deviceIds } = req.body;
if (!deviceIds || !Array.isArray(deviceIds)) {
return res.status(400).json({ error: '无效的设备ID列表' });
}
// 更新设备状态为离线
const updated = await Device.update(
{ status: 'offline' },
{ where: { deviceId: deviceIds } }
);
res.json({ message: `成功下线 ${updated[0]} 个设备` });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 批量删除设备
router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res) => {
const t = await sequelize.transaction();
try {
const { deviceIds } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
await t.rollback();
return res.status(400).json({ error: '请提供有效的设备ID列表' });
}
const devices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 1. 删除相关接线 (Delete associated Cables)
await Cable.destroy({
where: {
[Op.or]: [
{ sourceDeviceId: { [Op.in]: deviceIds } },
{ targetDeviceId: { [Op.in]: deviceIds } }
]
},
transaction: t
});
// 2. 删除相关网卡 (Delete associated NetworkCards)
await NetworkCard.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 3. 删除相关端口 (Delete associated DevicePorts)
await DevicePort.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 4. 解除工单关联
await Ticket.update(
{ deviceId: null },
{ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }
);
// 5. 删除设备
const deletedCount = await Device.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 更新机柜功率
for (const device of devices) {
if (device.rackId) {
const rack = await Rack.findByPk(device.rackId, { transaction: t });
if (rack) {
await rack.update({
currentPower: Math.max(0, rack.currentPower - device.powerConsumption)
}, { transaction: t });
}
}
}
await t.commit();
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个设备`,
deletedCount
});
} catch (error) {
await t.rollback();
console.error('批量删除设备错误:', error);
res.status(500).json({ error: error.message, stack: error.stack });
}
});
// 删除所有设备(一键清空)
router.delete('/delete-all', async (req, res) => {
const t = await sequelize.transaction();
try {
// 获取所有设备
const allDevices = await Device.findAll({ transaction: t });
if (allDevices.length === 0) {
await t.rollback();
return res.json({ message: '没有设备需要删除', deletedCount: 0 });
}
const deviceIds = allDevices.map(d => d.deviceId);
// 1. 删除相关接线
await Cable.destroy({
where: {
[Op.or]: [
{ sourceDeviceId: { [Op.in]: deviceIds } },
{ targetDeviceId: { [Op.in]: deviceIds } }
]
},
transaction: t
});
// 2. 删除相关网卡
await NetworkCard.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 3. 删除相关端口
await DevicePort.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
});
// 4. 解除工单关联
await Ticket.update(
{ deviceId: null },
{ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }
);
// 5. 更新机柜功率
for (const device of allDevices) {
if (device.rackId) {
const rack = await Rack.findByPk(device.rackId, { transaction: t });
if (rack) {
await rack.update({
currentPower: Math.max(0, rack.currentPower - device.powerConsumption)
}, { transaction: t });
}
}
}
// 6. 删除所有设备
const deletedCount = await Device.destroy({
where: {},
transaction: t
});
await t.commit();
res.json({
message: `成功删除所有设备,共删除 ${deletedCount} 个设备`,
deletedCount
});
} catch (error) {
await t.rollback();
console.error('删除所有设备错误:', error);
res.status(500).json({ error: error.message, stack: error.stack });
}
});
// 删除设备
router.delete('/:deviceId', async (req, res) => {
const t = await sequelize.transaction();
try {
const { deviceId } = req.params;
// 获取设备信息以更新功率
const device = await Device.findByPk(deviceId, { transaction: t });
if (!device) {
await t.rollback();
return res.status(404).json({ error: '设备不存在' });
}
// 1. 删除相关接线 (Delete associated Cables)
// 必须在删除设备之前删除,否则可能触发外键约束错误
const deletedCables = await Cable.destroy({
where: {
[Op.or]: [
{ sourceDeviceId: deviceId },
{ targetDeviceId: deviceId }
]
},
transaction: t
});
// 2. 删除相关网卡 (Delete associated NetworkCards)
const deletedNetworkCards = await NetworkCard.destroy({
where: { deviceId: deviceId },
transaction: t
});
// 3. 删除相关端口 (Delete associated DevicePorts)
// 必须在删除设备之前删除,否则触发外键约束错误
const deletedPorts = await DevicePort.destroy({
where: { deviceId: deviceId },
transaction: t
});
// 4. 解除工单关联 (Unlink Tickets)
await Ticket.update(
{ deviceId: null },
{ where: { deviceId: deviceId }, transaction: t }
);
// 5. 删除设备 (Delete Device)
await Device.destroy({
where: { deviceId: deviceId },
transaction: t
});
// 提交事务
await t.commit();
if (deletedCables > 0) {
console.log(`已删除 ${deletedCables} 条相关接线`);
}
// 更新机柜功率 (Update Rack power)
// 注意:设备已删除,不需要再减去功率?或者需要?
// 原逻辑是:rack.currentPower - device.powerConsumption
// 既然设备已经物理删除了,机柜的当前功率确实应该减少。
if (device.rackId) {
try {
const rack = await Rack.findByPk(device.rackId);
if (rack) {
await rack.update({
currentPower: Math.max(0, rack.currentPower - device.powerConsumption)
});
}
} catch (err) {
console.error('更新机柜功率失败:', err);
}
}
res.status(200).json({
message: '删除成功',
deviceId: deviceId,
deletedCablesCount: deletedCables,
deletedPortsCount: deletedPorts
});
} catch (error) {
await t.rollback();
console.error('删除设备失败:', error);
res.status(500).json({ error: error.message });
}
});
// 批量上线设备
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 existingDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
attributes: ['deviceId']
});
// 检查是否有不存在的设备
const existingIds = existingDevices.map(d => d.deviceId);
const missingIds = deviceIds.filter(id => !existingIds.includes(id));
if (missingIds.length > 0) {
return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` });
}
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 = startPosition;
for (const device of devices) {
const oldRackId = device.rackId;
const oldPosition = device.position;
const deviceHeight = device.height || 1;
const [updated] = await Device.update(
{
rackId: targetRackId,
position: currentPosition
},
{ where: { deviceId: device.deviceId } }
);
if (updated) {
movedDevices.push({
deviceId: device.deviceId,
name: device.name,
oldRackId,
oldPosition,
newRackId: targetRackId,
newPosition: currentPosition
});
}
currentPosition += deviceHeight;
}
res.json({
message: `批量移动成功,已将 ${movedDevices.length} 个设备移动到机柜 ${targetRackId}`,
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;