From c392df9ce3b457a3dc3f222b54465b44db351633 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 20 Mar 2026 09:34:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0U=E4=BD=8D=E5=86=B2=E7=AA=81=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E9=87=8D=E5=90=AF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/devices.js | 339 +++++++++++++++++- .../src/components/device/ImportModal.jsx | 20 +- update.js | 76 +++- 3 files changed, 410 insertions(+), 25 deletions(-) diff --git a/backend/routes/devices.js b/backend/routes/devices.js index ebf0d35..a7f7e76 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -33,6 +33,107 @@ Room.hasMany(Rack, { foreignKey: 'roomId' }); const PREVIEW_COUNT = 20; +async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) { + if (!position || position <= 0) { + return { available: true, reason: null }; + } + + const deviceHeight = height || 1; + const startU = position; + const endU = position + deviceHeight - 1; + + const queryOptions = { + where: { + rackId: rackId, + position: { [Op.ne]: null } + }, + attributes: ['deviceId', 'position', 'height'] + }; + + if (transaction) { + queryOptions.transaction = transaction; + } + + const existingDevices = await Device.findAll(queryOptions); + + for (const device of existingDevices) { + if (excludeDeviceId && device.deviceId === excludeDeviceId) { + continue; + } + + const existStart = device.position; + const existEnd = device.position + (device.height || 1) - 1; + + if (!(endU < existStart || startU > existEnd)) { + return { + available: false, + reason: `U位冲突:机柜中已有设备 ${device.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''},与当前位置范围 U${startU}-U${endU} 冲突` + }; + } + } + + return { available: true, reason: null }; +} + +async function checkBatchPositions(rackId, devices, excludeDeviceIds = [], transaction = null) { + const conflicts = []; + const sortedDevices = [...devices].sort((a, b) => a.position - b.position); + + for (let i = 0; i < sortedDevices.length; i++) { + const device = sortedDevices[i]; + if (!device.position || device.position <= 0) continue; + + const startU = device.position; + const endU = device.position + (device.height || 1) - 1; + + for (let j = i + 1; j < sortedDevices.length; j++) { + const other = sortedDevices[j]; + if (!other.position || other.position <= 0) continue; + + const otherStart = other.position; + const otherEnd = other.position + (other.height || 1) - 1; + + if (!(endU < otherStart || startU > otherEnd)) { + conflicts.push(`导入数据内部冲突:设备 ${device.deviceId || '新设备'}(U${startU}-U${endU}) 与 设备 ${other.deviceId || '新设备'}(U${otherStart}-U${otherEnd}) U位重叠`); + } + } + } + + const queryOptions = { + where: { + rackId: rackId, + position: { [Op.ne]: null } + }, + attributes: ['deviceId', 'position', 'height'] + }; + + if (transaction) { + queryOptions.transaction = transaction; + } + + const existingDevices = await Device.findAll(queryOptions); + + for (const existing of existingDevices) { + if (excludeDeviceIds.includes(existing.deviceId)) continue; + + const existStart = existing.position; + const existEnd = existing.position + (existing.height || 1) - 1; + + for (const device of devices) { + if (!device.position || device.position <= 0) continue; + + const startU = device.position; + const endU = device.position + (device.height || 1) - 1; + + if (!(endU < existStart || startU > existEnd)) { + conflicts.push(`与已有设备冲突:机柜中已有设备 ${existing.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''},与导入设备 ${device.deviceId || '新设备'}(U${startU}-U${endU}) 冲突`); + } + } + } + + return conflicts; +} + router.post('/import-preview', async (req, res) => { try { if (!req.files || !req.files.csvFile) { @@ -89,6 +190,7 @@ router.post('/import-preview', async (req, res) => { const previewData = []; const allDeviceIds = new Set(); const allSerialNumbers = new Set(); + const rackDevicesMap = new Map(); stats.total = results.length; @@ -224,6 +326,21 @@ router.post('/import-preview', async (req, res) => { if (rowErrors.length === 0) { stats.valid++; parsedRow._hasError = false; + + const rackId = rackLocationMap.get(`${roomName?.trim()}_${rackName?.trim()}`); + if (rackId && position) { + const posNum = parseInt(position); + const heightNum = parseInt(height) || 1; + if (!rackDevicesMap.has(rackId)) { + rackDevicesMap.set(rackId, []); + } + rackDevicesMap.get(rackId).push({ + rowNum, + deviceId: getFieldValue('deviceId') || null, + position: posNum, + height: heightNum + }); + } } else { stats.invalid++; parsedRow._hasError = true; @@ -255,6 +372,68 @@ router.post('/import-preview', async (req, res) => { } } + const positionConflictRows = new Set(); + + for (const [rackId, devices] of rackDevicesMap) { + const existingDevices = await Device.findAll({ + where: { rackId, position: { [Op.ne]: null } }, + attributes: ['deviceId', 'position', 'height'] + }); + + for (const newDevice of devices) { + const startU = newDevice.position; + const endU = newDevice.position + newDevice.height - 1; + let hasConflict = false; + + for (const existing of existingDevices) { + const existStart = existing.position; + const existEnd = existing.position + (existing.height || 1) - 1; + + if (!(endU < existStart || startU > existEnd)) { + const conflictMsg = `U位冲突:机柜中已有设备 ${existing.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''},与第 ${newDevice.rowNum} 行设备(导入)占用 U${startU}${endU !== startU ? '-' + endU : ''} 冲突`; + stats.errors.push({ row: newDevice.rowNum, errors: [conflictMsg] }); + hasConflict = true; + break; + } + } + + if (!hasConflict) { + for (const otherDevice of devices) { + if (otherDevice === newDevice) continue; + + const otherStart = otherDevice.position; + const otherEnd = otherDevice.position + otherDevice.height - 1; + + if (!(endU < otherStart || startU > otherEnd)) { + const conflictMsg = `U位冲突:第 ${newDevice.rowNum} 行设备与第 ${otherDevice.rowNum} 行设备在 U位上重叠 (U${startU}-U${endU} vs U${otherStart}-U${otherEnd})`; + stats.errors.push({ row: newDevice.rowNum, errors: [conflictMsg] }); + hasConflict = true; + break; + } + } + } + + if (hasConflict) { + positionConflictRows.add(newDevice.rowNum); + } + } + } + + if (positionConflictRows.size > 0) { + stats.invalid += positionConflictRows.size; + stats.valid -= positionConflictRows.size; + + for (const rowNum of positionConflictRows) { + const previewItem = previewData.find(item => item._rowNum === rowNum); + if (previewItem) { + previewItem._hasError = true; + const existingErrors = previewItem._errors || []; + const positionErrors = stats.errors.filter(e => e.row === rowNum).flatMap(e => e.errors); + previewItem._errors = [...existingErrors, ...positionErrors]; + } + } + } + const fieldList = deviceFields .filter(field => field.visible && field.fieldName !== 'deviceId') .map(field => ({ @@ -431,14 +610,23 @@ router.post('/', validateBody(createDeviceSchema), async (req, res) => { try { const deviceData = { ...req.body }; - // 如果没有提供deviceId或为空,则自动生成 if (!deviceData.deviceId || deviceData.deviceId.trim() === '') { deviceData.deviceId = await generateDeviceId(); } + if (deviceData.rackId && deviceData.position) { + const positionCheck = await checkPositionAvailable( + deviceData.rackId, + deviceData.position, + deviceData.height + ); + if (!positionCheck.available) { + return res.status(400).json({ error: positionCheck.reason }); + } + } + const device = await Device.create(deviceData); - // 更新机柜当前功率 const rack = await Rack.findByPk(deviceData.rackId); if (rack) { await rack.update({ @@ -977,6 +1165,74 @@ router.post('/import', async (req, res) => { stats.errors.push({ row: rowNum, error: error.message, data: row }); } } + + if (validDevices.length > 0) { + const rackDevicesMap = new Map(); + for (const device of validDevices) { + if (device.rackId && device.position > 0) { + if (!rackDevicesMap.has(device.rackId)) { + rackDevicesMap.set(device.rackId, []); + } + rackDevicesMap.get(device.rackId).push(device); + } + } + + for (const [rackId, devices] of rackDevicesMap) { + const existingDevices = await Device.findAll({ + where: { rackId, position: { [Op.ne]: null } }, + attributes: ['deviceId', 'position', 'height'], + transaction: t + }); + + for (const newDevice of devices) { + const startU = newDevice.position; + const endU = newDevice.position + newDevice.height - 1; + + let hasConflict = false; + + for (const existing of existingDevices) { + const existStart = existing.position; + const existEnd = existing.position + (existing.height || 1) - 1; + + if (!(endU < existStart || startU > existEnd)) { + stats.failed++; + stats.errors.push({ + row: 0, + error: `U位冲突:机柜中已有设备 ${existing.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''},与导入设备 ${newDevice.deviceId}(U${startU}-U${endU}) 冲突`, + data: { deviceId: newDevice.deviceId } + }); + hasConflict = true; + break; + } + } + + if (!hasConflict) { + for (const otherDevice of devices) { + if (otherDevice === newDevice) continue; + + const otherStart = otherDevice.position; + const otherEnd = otherDevice.position + otherDevice.height - 1; + + if (!(endU < otherStart || startU > otherEnd)) { + stats.failed++; + stats.errors.push({ + row: 0, + error: `U位冲突:导入数据内部冲突,设备 ${newDevice.deviceId}(U${startU}-U${endU}) 与设备 ${otherDevice.deviceId}(U${otherStart}-U${otherEnd}) U位重叠`, + data: { deviceId: newDevice.deviceId } + }); + hasConflict = true; + break; + } + } + } + + if (hasConflict) { + const idx = validDevices.indexOf(newDevice); + if (idx > -1) validDevices.splice(idx, 1); + } + } + } + } // 【优化3】批量查询已存在的设备ID和序列号(单次查询) if (validDevices.length > 0) { @@ -1161,41 +1417,100 @@ router.put('/batch-status', async (req, res) => { 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: '目标机柜不存在' }); } - - // 批量更新设备位置 + + const devicesToMove = await Device.findAll({ + where: { deviceId: { [Op.in]: deviceIds } }, + attributes: ['deviceId', 'position', 'height'] + }); + + const deviceHeightMap = new Map(devicesToMove.map(d => [d.deviceId, d.height || 1])); + + if (startPosition) { + const devicesToCheck = []; + for (let i = 0; i < deviceIds.length; i++) { + const deviceId = deviceIds[i]; + const height = deviceHeightMap.get(deviceId) || 1; + devicesToCheck.push({ + deviceId, + position: startPosition + i, + height + }); + } + + const existingDevices = await Device.findAll({ + where: { + rackId: targetRackId, + position: { [Op.ne]: null } + }, + attributes: ['deviceId', 'position', 'height'] + }); + + for (const newDevice of devicesToCheck) { + const startU = newDevice.position; + const endU = newDevice.position + newDevice.height - 1; + + for (const existing of existingDevices) { + if (devicesToCheck.some(d => d.deviceId === existing.deviceId)) { + continue; + } + + const existStart = existing.position; + const existEnd = existing.position + (existing.height || 1) - 1; + + if (!(endU < existStart || startU > existEnd)) { + return res.status(400).json({ + error: `U位冲突:机柜中已有设备 ${existing.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''},与移动设备 ${newDevice.deviceId}(U${startU}-U${endU}) 冲突` + }); + } + } + + for (const other of devicesToCheck) { + if (other === newDevice) continue; + + const otherStart = other.position; + const otherEnd = other.position + other.height - 1; + + if (!(endU < otherStart || startU > otherEnd)) { + return res.status(400).json({ + error: `U位冲突:移动设备 ${newDevice.deviceId}(U${startU}-U${endU}) 与设备 ${other.deviceId}(U${otherStart}-U${otherEnd}) U位重叠` + }); + } + } + } + } + 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 diff --git a/frontend/src/components/device/ImportModal.jsx b/frontend/src/components/device/ImportModal.jsx index 510b68e..89b9d8b 100644 --- a/frontend/src/components/device/ImportModal.jsx +++ b/frontend/src/components/device/ImportModal.jsx @@ -139,13 +139,19 @@ const ImportModal = ({ const optionalFields = deviceFields.filter((f) => f.visible && !f.required); const previewColumns = previewData?.fieldList - ? previewData.fieldList.map((field) => ({ - title: field.displayName + (field.required ? ' *' : ''), - dataIndex: field.fieldName, - key: field.fieldName, - width: 120, - ellipsis: true, - })) + ? [ + ...previewData.fieldList + .filter((field) => field.fieldName !== 'rackId') + .map((field) => ({ + title: field.displayName + (field.required ? ' *' : ''), + dataIndex: field.fieldName, + key: field.fieldName, + width: 120, + ellipsis: true, + })), + { title: '所在机房', dataIndex: 'roomName', key: 'roomName', width: 100 }, + { title: '所在机柜', dataIndex: 'rackName', key: 'rackName', width: 100 }, + ] : [ { title: '行号', dataIndex: '_rowNum', key: '_rowNum', width: 60 }, { title: '设备名称', dataIndex: 'name', key: 'name', width: 120 }, diff --git a/update.js b/update.js index 62f7497..db221af 100644 --- a/update.js +++ b/update.js @@ -192,6 +192,60 @@ function getGitInfo() { } } +function findBackendServiceName() { + try { + const jlistResult = execSync('pm2 jlist', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + const processes = JSON.parse(jlistResult); + + for (const proc of processes) { + const scriptPath = proc.pm2_env?.pm_out_log_path || proc.pm2_env?.pm_err_log_path || ''; + const script = proc.pm2_env?.script || ''; + + if (scriptPath.includes('server.js') || script.includes('server.js')) { + return proc.name; + } + } + + for (const proc of processes) { + if (proc.name === 'server' || proc.name === 'idc-backend' || proc.name === 'backend') { + return proc.name; + } + } + + return null; + } catch { + return null; + } +} + +function findFrontendServiceName() { + try { + const jlistResult = execSync('pm2 jlist', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + const processes = JSON.parse(jlistResult); + + for (const proc of processes) { + const script = proc.pm2_env?.script || ''; + const scriptPath = proc.pm2_env?.pm_out_log_path || ''; + + if (script.includes('vite') || scriptPath.includes('vite') || + script.includes('react') || script.includes('nginx') || + proc.name === 'frontend' || proc.name === 'idc-frontend') { + return proc.name; + } + } + + return null; + } catch { + return null; + } +} + function backupDatabase(options) { if (options.skipBackup || options.dryRun) { if (options.skipBackup) log.info('已跳过数据库备份'); @@ -546,8 +600,18 @@ function restartServices(options) { return startDirectly(); } - log.subStep('重启后端服务...'); - const backendRestart = runCommand('pm2 restart idc-backend 2>nul || pm2 start backend/server.js --name idc-backend'); + const backendServiceName = findBackendServiceName(); + if (!backendServiceName) { + log.warning('未找到后端服务,创建新服务...'); + const startResult = runCommand('pm2 start backend/server.js --name server'); + if (startResult.success) { + log.success('后端服务已创建'); + } + return { success: startResult.success }; + } + + log.subStep(`重启后端服务 (${backendServiceName})...`); + const backendRestart = runCommand(`pm2 restart ${backendServiceName}`); if (backendRestart.success) { log.success('后端服务已重启'); @@ -556,10 +620,10 @@ function restartServices(options) { return { success: false }; } - const frontendCheck = runCommand('pm2 describe idc-frontend', { silent: true }); - if (frontendCheck.success) { - log.subStep('重启前端服务...'); - const frontendRestart = runCommand('pm2 restart idc-frontend'); + const frontendServiceName = findFrontendServiceName(); + if (frontendServiceName) { + log.subStep(`重启前端服务 (${frontendServiceName})...`); + const frontendRestart = runCommand(`pm2 restart ${frontendServiceName}`); if (frontendRestart.success) { log.success('前端服务已重启'); }