feat: 添加设备位置冲突检查功能并优化操作日志

feat(api): 在设备API中添加checkPosition接口用于检查U位冲突
feat(frontend): 在设备表单和空闲设备管理中实现U位冲突检查
refactor(backend): 重构操作日志功能,添加设备描述生成和元数据构建工具
fix(backend): 修复批量导入机柜时的ID验证规则
feat(backend): 为机柜导入添加创建和跳过机柜的详细返回信息
fix(backend): 修复设备删除时未检查关联接线的问题
feat(backend): 添加机柜导入模板生成脚本
perf(frontend): 优化机柜管理页面的导入结果展示
fix(frontend): 修复设备端口删除时的关联接线检查
This commit is contained in:
zhang1106
2026-03-26 14:35:08 +08:00
parent 0b7732e427
commit d9c46245df
14 changed files with 2152 additions and 384 deletions
+137 -51
View File
@@ -4,6 +4,7 @@ const { Op } = require('sequelize');
const DevicePort = require('../models/DevicePort');
const Device = require('../models/Device');
const NetworkCard = require('../models/NetworkCard');
const Cable = require('../models/Cable');
DevicePort.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' });
Device.hasMany(DevicePort, { foreignKey: 'deviceId', as: 'ports' });
@@ -39,6 +40,11 @@ router.get('/', async (req, res) => {
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
],
offset,
@@ -69,6 +75,11 @@ router.get('/device/:deviceId', async (req, res) => {
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
],
order: [['portName', 'ASC']]
@@ -130,59 +141,89 @@ router.post('/', async (req, res) => {
router.post('/batch', async (req, res) => {
try {
const { ports } = req.body;
const { ports, skipExisting = false, updateExisting = false } = req.body;
if (!ports || !Array.isArray(ports) || ports.length === 0) {
return res.status(400).json({ error: '请提供有效的端口数据' });
}
const results = {
total: ports.length,
success: 0,
failed: 0,
skipped: 0,
updated: 0,
errors: []
};
for (let i = 0; i < ports.length; i++) {
const portData = ports[i];
try {
if (!portData.portId || !portData.deviceId || !portData.portName) {
throw new Error('缺少必填字段');
const transaction = await DevicePort.sequelize.transaction();
try {
for (let i = 0; i < ports.length; i++) {
const portData = ports[i];
try {
if (!portData.portId || !portData.deviceId || !portData.portName) {
throw new Error('缺少必填字段');
}
const existingPort = await DevicePort.findOne({
where: { deviceId: portData.deviceId, portName: portData.portName },
transaction
});
if (existingPort) {
if (skipExisting) {
results.skipped++;
continue;
}
if (updateExisting) {
await DevicePort.update({
portType: portData.portType || existingPort.portType,
portSpeed: portData.portSpeed || existingPort.portSpeed,
status: portData.status || existingPort.status,
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
description: portData.description !== undefined ? portData.description : existingPort.description
}, {
where: { portId: existingPort.portId },
transaction
});
results.updated++;
results.success++;
continue;
}
throw new Error('该设备的端口名称已存在');
}
await DevicePort.create({
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description
}, { transaction });
results.success++;
} catch (error) {
results.failed++;
results.errors.push({
index: i + 1,
portId: portData.portId,
error: error.message
});
}
const existingPort = await DevicePort.findOne({
where: { deviceId: portData.deviceId, portName: portData.portName }
});
if (existingPort) {
throw new Error('该设备的端口名称已存在');
}
await DevicePort.create({
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description
});
results.success++;
} catch (error) {
results.failed++;
results.errors.push({
index: i + 1,
portId: portData.portId,
error: error.message
});
}
await transaction.commit();
res.json(results);
} catch (error) {
await transaction.rollback();
throw error;
}
res.json(results);
} catch (error) {
console.error('批量创建端口失败:', error);
res.status(500).json({ error: error.message });
@@ -217,15 +258,38 @@ router.put('/:portId', async (req, res) => {
router.delete('/:portId', async (req, res) => {
try {
const deleted = await DevicePort.destroy({
const port = await DevicePort.findByPk(req.params.portId);
if (!port) {
return res.status(404).json({ error: '端口不存在' });
}
const relatedCables = await Cable.findAll({
where: {
[Op.or]: [
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
{ targetDeviceId: port.deviceId, targetPort: port.portName }
]
}
});
if (relatedCables.length > 0) {
return res.status(400).json({
error: '该端口存在关联的接线记录,请先删除关联的接线',
relatedCables: relatedCables.map(c => ({
cableId: c.cableId,
sourceDeviceId: c.sourceDeviceId,
sourcePort: c.sourcePort,
targetDeviceId: c.targetDeviceId,
targetPort: c.targetPort
}))
});
}
await DevicePort.destroy({
where: { portId: req.params.portId }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: '端口不存在' });
}
res.status(204).json();
} catch (error) {
console.error('删除端口失败:', error);
res.status(500).json({ error: error.message });
@@ -235,15 +299,37 @@ router.delete('/:portId', async (req, res) => {
router.delete('/batch', async (req, res) => {
try {
const { portIds } = req.body;
if (!portIds || !Array.isArray(portIds) || portIds.length === 0) {
return res.status(400).json({ error: '请提供有效的端口ID列表' });
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
});
} catch (error) {
console.error('批量删除端口失败:', error);
res.status(500).json({ error: error.message });
}
});
router.post('/batch-delete', async (req, res) => {
try {
const { portIds } = req.body;
if (!portIds || !Array.isArray(portIds) || portIds.length === 0) {
return res.status(400).json({ error: '请提供有效的端口ID列表' });
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
+176 -41
View File
@@ -16,7 +16,7 @@ const DevicePort = require('../models/DevicePort');
const Cable = require('../models/Cable');
const NetworkCard = require('../models/NetworkCard');
const InventoryRecord = require('../models/InventoryRecord');
const { logDeviceOperation } = require('../utils/operationLogger');
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
const { validateBody, validateQuery } = require('../middleware/validation');
const {
createDeviceSchema,
@@ -710,12 +710,15 @@ router.post('/', validateBody(createDeviceSchema), async (req, res) => {
`功耗: ${device.powerConsumption}W`
].join('');
await logDeviceOperation('create', `创建设备【${device.name}`, {
await logDeviceOperation('create', generateDeviceDescription('创建设备', {
...device.toJSON(),
rackName: rack?.name
}), {
targetId: device.deviceId,
targetName: device.name,
afterState: device.toJSON(),
req,
metadata: { deviceType: device.type, rackName: rack?.name, powerConsumption: device.powerConsumption }
metadata: buildDeviceMetadata({ ...device.toJSON(), rackName: rack?.name })
});
res.status(201).json(device);
@@ -1482,26 +1485,39 @@ router.put('/batch-status', async (req, res) => {
};
const beforeDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } }
where: { deviceId: { [Op.in]: deviceIds } },
include: [{ model: Rack, attributes: ['name'] }]
});
const deviceNames = beforeDevices.map(d => d.name);
const deviceDetails = beforeDevices.map(d => {
const data = d.toJSON();
return {
deviceId: d.deviceId,
name: d.name,
type: d.type,
model: d.model,
serialNumber: d.serialNumber,
ipAddress: d.ipAddress,
rackName: data.Rack?.name || null,
position: d.position,
status: d.status
};
});
// 更新设备状态
const [affectedCount] = await Device.update(
{ status },
{ where: { deviceId: { [Op.in]: deviceIds } } }
);
const deviceNames = deviceDetails.map(d => d.name);
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.rackName ? `,机柜:${d.rackName}` : ''})`
).join('、');
const statusChangeDesc = `批量变更${affectedCount}台设备状态:${deviceNames.join('、')}${statusText[status]}`;
const statusChangeDesc = `批量变更${affectedCount}台设备状态:${deviceSummary}${statusText[status]}`;
await logDeviceOperation('status_change', statusChangeDesc, {
targetId: deviceIds.join(','),
targetName: `${affectedCount}台设备`,
beforeState: beforeDevices.map(d => ({ deviceId: d.deviceId, name: d.name, status: d.status })),
afterState: beforeDevices.map(d => ({ deviceId: d.deviceId, name: d.name, status })),
beforeState: deviceDetails.map(d => ({ deviceId: d.deviceId, name: d.name, status: d.status })),
afterState: deviceDetails.map(d => ({ deviceId: d.deviceId, name: d.name, status })),
req,
metadata: { status, statusText: statusText[status], count: affectedCount, deviceNames }
metadata: { status, statusText: statusText[status], count: affectedCount, devices: deviceDetails }
});
res.json({
@@ -1534,16 +1550,32 @@ router.put('/batch-move', async (req, res) => {
const devicesToMove = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
attributes: ['deviceId', 'name', 'rackId', 'position', 'height']
attributes: ['deviceId', 'name', 'type', 'model', 'serialNumber', 'ipAddress', 'rackId', 'position', 'height', 'powerConsumption']
});
const beforeMoveState = devicesToMove.map(d => ({
const deviceDetails = devicesToMove.map(d => d.toJSON());
const beforeMoveState = deviceDetails.map(d => ({
deviceId: d.deviceId,
name: d.name,
type: d.type,
rackId: d.rackId,
position: d.position
position: d.position,
powerConsumption: d.powerConsumption
}));
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
const sourceRackPowerChanges = new Map();
devicesToMove.forEach(device => {
if (device.rackId) {
const currentChange = sourceRackPowerChanges.get(device.rackId) || 0;
sourceRackPowerChanges.set(device.rackId, currentChange - (device.powerConsumption || 0));
}
});
const deviceHeightMap = new Map(devicesToMove.map(d => [d.deviceId, d.height || 1]));
if (startPosition) {
@@ -1601,6 +1633,8 @@ router.put('/batch-move', async (req, res) => {
}
let movedCount = 0;
const targetRackPowerChange = { rackId: targetRackId, change: 0 };
for (let i = 0; i < deviceIds.length; i++) {
const deviceId = deviceIds[i];
const position = startPosition ? startPosition + i : undefined;
@@ -1616,13 +1650,36 @@ router.put('/batch-move', async (req, res) => {
if (updated) {
movedCount++;
const device = devicesToMove.find(d => d.deviceId === deviceId);
if (device) {
targetRackPowerChange.change += device.powerConsumption || 0;
}
}
}
const deviceNames = devicesToMove.map(d => d.name);
for (const [rackId, powerChange] of sourceRackPowerChanges) {
if (rackId !== targetRackId) {
await Rack.update(
{ currentPower: sequelize.literal(`currentPower + ${powerChange}`) },
{ where: { rackId } }
);
}
}
if (sourceRackPowerChanges.has(targetRackId)) {
targetRackPowerChange.change += sourceRackPowerChanges.get(targetRackId);
}
if (targetRackPowerChange.change !== 0) {
await Rack.update(
{ currentPower: sequelize.literal(`currentPower + ${targetRackPowerChange.change}`) },
{ where: { rackId: targetRackId } }
);
}
const moveDesc = startPosition
? `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceNames.join('、')} → U${startPosition}`
: `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceNames.join('、')}`;
? `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceSummary} → U${startPosition}`
: `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceSummary}`;
await logDeviceOperation('move', moveDesc, {
targetId: deviceIds.join(','),
@@ -1630,7 +1687,7 @@ router.put('/batch-move', async (req, res) => {
beforeState: beforeMoveState,
afterState: { targetRackId, targetRackName: targetRack.name, startPosition },
req,
metadata: { count: movedCount, targetRackId, targetRackName: targetRack.name, startPosition, deviceNames }
metadata: { count: movedCount, targetRackId, targetRackName: targetRack.name, startPosition, devices: deviceDetails }
});
res.json({
@@ -1817,6 +1874,29 @@ router.get('/enhanced-export', async (req, res) => {
}
});
// 检查U位是否可用
router.get('/check-position/:rackId', async (req, res) => {
try {
const { rackId } = req.params;
const { position, height, excludeDeviceId } = req.query;
if (!position) {
return res.status(400).json({ error: '请提供位置参数' });
}
const result = await checkPositionAvailable(
rackId,
parseInt(position),
parseInt(height) || 1,
excludeDeviceId || null
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 获取单个设备
router.get('/:deviceId', async (req, res) => {
try {
@@ -1875,13 +1955,17 @@ router.put('/:deviceId/to-idle', async (req, res) => {
await t.commit();
await logDeviceOperation('to_idle', `设备【${device.name}】转入空闲设备`, {
const deviceData = {
...device.toJSON(),
rackName: device.rack?.name
};
await logDeviceOperation('to_idle', generateDeviceDescription('转入空闲设备', deviceData), {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: false },
afterState: { ...device.toJSON(), isIdle: true },
req,
metadata: { idleReason, type: 'device_to_idle' }
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
});
res.json({
@@ -1944,17 +2028,58 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
const beforeState = oldDevice.toJSON();
const changedFields = {};
const newRackId = req.body.rackId !== undefined ? req.body.rackId : oldDevice.rackId;
const newPosition = req.body.position !== undefined ? req.body.position : oldDevice.position;
const newHeight = req.body.height !== undefined ? req.body.height : oldDevice.height;
if ((req.body.rackId !== undefined || req.body.position !== undefined || req.body.height !== undefined)
&& newRackId && newPosition) {
const positionCheck = await checkPositionAvailable(
newRackId,
newPosition,
newHeight,
req.params.deviceId
);
if (!positionCheck.available) {
return res.status(400).json({ error: positionCheck.reason });
}
}
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 oldRackId = oldDevice.rackId;
const newRackId = req.body.rackId;
const oldPower = oldDevice.powerConsumption || 0;
const newPower = req.body.powerConsumption !== undefined ? req.body.powerConsumption : oldPower;
if (oldRackId === newRackId) {
const rack = await Rack.findByPk(oldRackId);
if (rack) {
const powerDiff = newPower - oldPower;
await rack.update({
currentPower: rack.currentPower + powerDiff
});
}
} else {
if (oldRackId) {
const oldRack = await Rack.findByPk(oldRackId);
if (oldRack) {
await oldRack.update({
currentPower: Math.max(0, oldRack.currentPower - oldPower)
});
}
}
if (newRackId) {
const newRack = await Rack.findByPk(newRackId);
if (newRack) {
await newRack.update({
currentPower: newRack.currentPower + newPower
});
}
}
}
const updatedDevice = await Device.findByPk(req.params.deviceId, {
@@ -1975,6 +2100,13 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
}
}
const deviceData = {
...updatedDevice.toJSON(),
rackName: updatedDevice.Rack?.name,
roomName: updatedDevice.Rack?.Room?.name
};
delete deviceData.Rack;
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
const fieldNames = {
name: '名称', deviceId: '设备编号', type: '类型', model: '型号',
@@ -1986,17 +2118,17 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
return `${displayName}: ${values.from ?? '空'}${values.to ?? '空'}`;
}).join('');
const operationDesc = changeDetails
? `更新设备【${updatedDevice.name}】:${changeDetails}`
: `更新设备【${updatedDevice.name}`;
const operationDesc = generateDeviceDescription('更新设备', deviceData, {
includePosition: false
}) + (changeDetails ? `,变更内容:${changeDetails}` : '');
await logDeviceOperation('update', operationDesc, {
targetId: updatedDevice.deviceId,
targetName: updatedDevice.name,
beforeState,
afterState,
afterState: deviceData,
req,
metadata: { changedFields }
metadata: buildDeviceMetadata(deviceData, { changedFields })
});
res.json(updatedDevice);
@@ -2024,8 +2156,6 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
transaction: t
});
const deviceNames = devices.map(d => d.name).join(', ');
// 1. 删除相关端口 (必须在网卡之前删除,因为端口依赖网卡)
await DevicePort.destroy({
where: { deviceId: { [Op.in]: deviceIds } },
@@ -2081,12 +2211,17 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
await t.commit();
await logDeviceOperation('batch_delete', `批量删除${deletedCount}台设备:${deviceNames}`, {
const deviceDetails = devices.map(d => d.toJSON());
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
).join('、');
await logDeviceOperation('batch_delete', `批量删除${deletedCount}台设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${deletedCount}台设备`,
beforeState: devices.map(d => d.toJSON()),
beforeState: deviceDetails,
req,
metadata: { count: deletedCount, deviceNames }
metadata: { count: deletedCount, devices: deviceDetails }
});
res.json({
@@ -2260,12 +2395,12 @@ router.delete('/:deviceId', async (req, res) => {
console.log(`已删除 ${deletedCables} 条相关接线`);
}
await logDeviceOperation('delete', `删除设备【${deviceName}】(编号${deviceId},类型${device.type},关联删除:${deletedCables}条接线、${deletedPorts}个端口、${deletedNetworkCards}张网卡`, {
await logDeviceOperation('delete', `删除设备【${deviceName}】(编号:${deviceId},类型:${device.type}型号:${device.model || '无'},序列号:${device.serialNumber || '无'}IP:${device.ipAddress || '无'}),关联删除:${deletedCables}条接线、${deletedPorts}个端口、${deletedNetworkCards}张网卡`, {
targetId: deviceId,
targetName: deviceName,
beforeState,
req,
metadata: { deletedCables, deletedPorts, deletedNetworkCards, deviceType: device.type }
metadata: buildDeviceMetadata(device.toJSON(), { deletedCables, deletedPorts, deletedNetworkCards })
});
res.status(200).json({
+62 -24
View File
@@ -4,7 +4,7 @@ const { Op } = require('sequelize');
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const { logDeviceOperation } = require('../utils/operationLogger');
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
async function generateIdleDeviceId() {
const devices = await Device.findAll({
@@ -148,12 +148,19 @@ router.post('/', async (req, res) => {
description: description || ''
});
await logDeviceOperation('create', `新增空闲设备【${device.name || deviceId}`, {
await logDeviceOperation('create', generateDeviceDescription('新增空闲设备', {
deviceId: device.deviceId,
name: device.name || deviceId,
type: device.type,
model: device.model,
serialNumber: device.serialNumber,
ipAddress: device.ipAddress
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
targetName: device.name || deviceId,
afterState: device.toJSON(),
req,
metadata: { sourceType: device.sourceType, type: 'idle_device_create' }
metadata: buildDeviceMetadata(device.toJSON(), { sourceType: device.sourceType, type: 'idle_device_create' })
});
res.status(201).json(device);
@@ -189,13 +196,14 @@ router.post('/from-device/:deviceId', async (req, res) => {
await t.commit();
await logDeviceOperation('to_idle', `设备【${device.name}】转入空闲设备`, {
const deviceData = device.toJSON();
await logDeviceOperation('to_idle', generateDeviceDescription('设备转入空闲设备', deviceData), {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: false },
afterState: { ...device.toJSON(), isIdle: true },
beforeState: { ...deviceData, isIdle: false },
afterState: { ...deviceData, isIdle: true },
req,
metadata: { idleReason, type: 'device_to_idle' }
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
});
res.json({
@@ -244,11 +252,16 @@ router.post('/batch-from-devices', async (req, res) => {
await t.commit();
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备`, {
const deviceDetails = notIdleDevices.map(d => d.toJSON());
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
).join('、');
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${notIdleDevices.length}台设备`,
req,
metadata: { idleReason, type: 'batch_device_to_idle' }
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails }
});
res.json({
@@ -391,11 +404,16 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备`, {
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore' }
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
res.json({
@@ -479,13 +497,19 @@ router.put('/:deviceId/shelve', async (req, res) => {
]
});
await logDeviceOperation('shelve', `空闲设备【${device.name}】上架到机柜【${targetRack.name}】U${position}`, {
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
};
await logDeviceOperation('shelve', generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${position}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...beforeState, isIdle: true },
afterState: updatedDevice.toJSON(),
afterState: deviceData,
req,
metadata: { rackId, position, type: 'idle_device_shelve' }
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' })
});
res.json({
@@ -542,13 +566,13 @@ router.put('/:deviceId', async (req, res) => {
await device.save();
await logDeviceOperation('update', `更新空闲设备【${device.name}`, {
await logDeviceOperation('update', generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
afterState: device.toJSON(),
req,
metadata: { type: 'idle_device_update' }
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' })
});
res.json(device);
@@ -613,13 +637,19 @@ router.put('/:deviceId/restore', async (req, res) => {
]
});
await logDeviceOperation('restore', `空闲设备【${device.name}】恢复到机柜【${targetRack.name}】U${targetPosition}`, {
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
};
await logDeviceOperation('restore', generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${targetPosition}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: true },
afterState: updatedDevice.toJSON(),
afterState: deviceData,
req,
metadata: { targetRackId, targetPosition, type: 'idle_device_restore' }
metadata: buildDeviceMetadata(deviceData, { targetRackId, targetPosition, type: 'idle_device_restore' })
});
res.json({
@@ -761,11 +791,16 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备`, {
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore' }
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
res.json({
@@ -802,12 +837,15 @@ router.delete('/:deviceId', async (req, res) => {
await t.commit();
await logDeviceOperation('delete', `删除空闲设备【${device.name || device.deviceId}`, {
await logDeviceOperation('delete', generateDeviceDescription('删除空闲设备', {
...device.toJSON(),
name: device.name || device.deviceId
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
req,
metadata: { type: 'idle_device_delete' }
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' })
});
res.json({ message: '空闲设备删除成功' });
+23
View File
@@ -161,6 +161,29 @@ router.get('/:nicId/ports', async (req, res) => {
}
});
router.get('/find', async (req, res) => {
try {
const { deviceId, name } = req.query;
if (!deviceId || !name) {
return res.status(400).json({ error: '缺少设备ID或网卡名称' });
}
const networkCard = await NetworkCard.findOne({
where: { deviceId, name }
});
if (!networkCard) {
return res.json({ nicId: null });
}
res.json(networkCard);
} catch (error) {
console.error('查找网卡失败:', error);
res.status(500).json({ error: error.message });
}
});
router.post('/', async (req, res) => {
try {
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body;
+13 -7
View File
@@ -572,9 +572,9 @@ router.post('/import', async (req, res) => {
// 验证数据
processedData.forEach((item) => {
const errors = [];
if (!/^RACK\d+$/.test(item.rackId)) {
errors.push('机柜ID格式应为RACK+数字,如RACK001');
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
errors.push('机柜ID只能包含字母、数字、下划线和横线');
}
if (!item.name || String(item.name).trim() === '') {
errors.push('机柜名称不能为空');
@@ -582,7 +582,8 @@ router.post('/import', async (req, res) => {
if (!item.roomName || String(item.roomName).trim() === '') {
errors.push('所属机房名称不能为空');
} else if (!validRoomNames.has(String(item.roomName).trim())) {
errors.push(`所属机房名称不存在: ${item.roomName}`);
const availableRooms = Array.from(validRoomNames).join('、');
errors.push(`所属机房"${item.roomName}"不存在,可用机房: ${availableRooms}`);
}
if (typeof item.height !== 'number' || item.height <= 0) {
errors.push('高度必须是大于0的数字');
@@ -644,12 +645,17 @@ router.post('/import', async (req, res) => {
// 提交事务
await t.commit();
res.status(200).json({
success: true,
const createdRacks = newData.map(item => ({ rackId: item.rackId, name: item.name }));
const skippedRacks = existingRacks.map(rack => ({ rackId: rack.rackId, name: rack.name }));
res.status(200).json({
success: true,
message: '机柜导入完成',
imported: createdCount,
duplicates: duplicateCount,
total: jsonData.length
total: jsonData.length,
createdRacks,
skippedRacks
});
} finally {
// 删除临时文件