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:
+137
-51
@@ -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
@@ -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({
|
||||
|
||||
@@ -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: '空闲设备删除成功' });
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
// 删除临时文件
|
||||
|
||||
Reference in New Issue
Block a user