From 258b7af7c8506b08deb36429ee447772d801d214 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 6 Feb 2026 13:33:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9C=BA=E6=9F=9C=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=9C=BA=E6=9F=9CID=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改机柜创建逻辑,当rackId为空时自动生成RACKxxx格式的ID - 更新验证规则,rackId改为可选字段并允许空值 - 调整前端表单提示,明确机柜ID可留空自动生成 - 优化导入功能,支持自动生成机柜ID并改进验证逻辑 - 提供机柜导入模板下载功能,模板中明确ID可留空 --- backend/routes/racks.js | 248 +++++++++++++++++--------- backend/validation/rackSchema.js | 6 +- frontend/src/pages/RackManagement.jsx | 7 +- 3 files changed, 166 insertions(+), 95 deletions(-) diff --git a/backend/routes/racks.js b/backend/routes/racks.js index 353a272..018bb01 100644 --- a/backend/routes/racks.js +++ b/backend/routes/racks.js @@ -40,6 +40,64 @@ router.get('/', async (req, res) => { } }); +// 导出机柜导入模板 - 必须放在 /:rackId 路由之前,避免被当作 rackId 参数 +router.get('/import-template', async (req, res) => { + try { + // 准备模板数据 - 机柜ID留空表示自动生成 + const templateData = [ + { + '机柜ID(留空自动生成)': '', + '机柜名称': '测试机柜1', + '所属机房名称': '测试机房1', + '高度(U)': 42, + '最大功率(W)': 5000, + '状态': 'active' + }, + { + '机柜ID(留空自动生成)': 'RACK001', + '机柜名称': '测试机柜2', + '所属机房名称': '测试机房1', + '高度(U)': 42, + '最大功率(W)': 3000, + '状态': 'maintenance' + } + ]; + + // 使用xlsx创建工作簿 + const wb = XLSX.utils.book_new(); + + // 将数据转换为工作表 + const ws = XLSX.utils.json_to_sheet(templateData); + + // 设置列宽 + ws['!cols'] = [ + { wch: 15 }, + { wch: 20 }, + { wch: 15 }, + { wch: 10 }, + { wch: 15 }, + { wch: 15 } + ]; + + // 添加工作表到工作簿 + XLSX.utils.book_append_sheet(wb, ws, '机柜模板'); + + // 生成Excel文件的Buffer + const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); + + // 设置响应头 + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`); + + // 发送文件 + res.send(excelBuffer); + + } catch (error) { + console.error('生成导入模板失败:', error); + res.status(500).json({ error: '生成导入模板失败' }); + } +}); + // 获取单个机柜 router.get('/:rackId', async (req, res) => { try { @@ -58,10 +116,44 @@ router.get('/:rackId', async (req, res) => { } }); +// 生成机柜ID的辅助函数 +async function generateRackId() { + // 获取当前最大的机柜ID序号 + const racks = await Rack.findAll({ + where: { + rackId: { + [require('sequelize').Op.like]: 'RACK%' + } + } + }); + + let maxNumber = 0; + racks.forEach(rack => { + const match = rack.rackId.match(/^RACK(\d+)$/); + if (match) { + const num = parseInt(match[1], 10); + if (num > maxNumber) { + maxNumber = num; + } + } + }); + + // 生成新的机柜ID,序号+1,至少3位数字 + const newNumber = maxNumber + 1; + return `RACK${String(newNumber).padStart(3, '0')}`; +} + // 创建机柜 router.post('/', validateBody(createRackSchema), async (req, res) => { try { - const rack = await Rack.create(req.body); + const rackData = { ...req.body }; + + // 如果没有提供rackId或为空,则自动生成 + if (!rackData.rackId || rackData.rackId.trim() === '') { + rackData.rackId = await generateRackId(); + } + + const rack = await Rack.create(rackData); res.status(201).json(rack); } catch (error) { res.status(400).json({ error: error.message }); @@ -112,74 +204,6 @@ router.delete('/:rackId', async (req, res) => { } }); -// 导出机柜导入模板 -router.get('/import-template', async (req, res) => { - try { - // 准备模板数据 - const templateData = [ - { - '机柜ID': 'RACK001', - '机柜名称': '测试机柜1', - '所属机房名称': '测试机房1', - '高度(U)': 42, - '最大功率(W)': 5000, - '状态': 'active' - }, - { - '机柜ID': 'RACK002', - '机柜名称': '测试机柜2', - '所属机房名称': '测试机房1', - '高度(U)': 42, - '最大功率(W)': 3000, - '状态': 'maintenance' - } - ]; - - // 设置CSV标题(包含格式说明) - const headers = [ - { id: '机柜ID', title: '机柜ID' }, - { id: '机柜名称', title: '机柜名称' }, - { id: '所属机房名称', title: '所属机房名称' }, - { id: '高度(U)', title: '高度(U)' }, - { id: '最大功率(W)', title: '最大功率(W)' }, - { id: '状态', title: '状态(active/maintenance/inactive)' } - ]; - - // 使用xlsx创建工作簿 - const wb = XLSX.utils.book_new(); - - // 将数据转换为工作表 - const ws = XLSX.utils.json_to_sheet(templateData); - - // 设置列宽 - ws['!cols'] = [ - { wch: 15 }, - { wch: 20 }, - { wch: 15 }, - { wch: 10 }, - { wch: 15 }, - { wch: 15 } - ]; - - // 添加工作表到工作簿 - XLSX.utils.book_append_sheet(wb, ws, '机柜模板'); - - // 生成Excel文件的Buffer - const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); - - // 设置响应头 - res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`); - - // 发送文件 - res.send(excelBuffer); - - } catch (error) { - console.error('生成导入模板失败:', error); - res.status(500).json({ error: '生成导入模板失败' }); - } -}); - // 导入机柜数据 router.post('/import', async (req, res) => { try { @@ -254,12 +278,29 @@ router.post('/import', async (req, res) => { const roomNameToIdMap = new Map(allRooms.map(room => [room.name, room.roomId])); const validRoomNames = new Set(roomNameToIdMap.keys()); - jsonData.forEach((item, index) => { - const rowNumber = index + 2; // 实际行号(加1是因为从0开始,加1是因为跳过了标题行) + // 为没有rackId的记录自动生成 + let autoGeneratedIdIndex = 0; + const processedData = jsonData.map((item, index) => { + const rowNumber = index + 2; + + // 如果rackId为空,先标记为null,后续统一生成 + if (!item.rackId || item.rackId.trim() === '') { + return { ...item, rackId: null, rowNumber, _autoGenerate: true }; + } + + return { ...item, rowNumber, _autoGenerate: false }; + }); + + // 验证数据 + processedData.forEach((item) => { const errors = []; - if (!item.rackId || item.rackId.trim() === '') { - errors.push('机柜ID不能为空'); + // rackId为null表示需要自动生成,跳过验证 + if (item.rackId !== null) { + // 验证机柜ID格式 + if (!/^RACK\d+$/.test(item.rackId.trim())) { + errors.push('机柜ID格式应为RACK+数字,如RACK001'); + } } if (!item.name || item.name.trim() === '') { @@ -286,7 +327,7 @@ router.post('/import', async (req, res) => { if (errors.length > 0) { validationResults.push({ - row: rowNumber, + row: item.rowNumber, data: item, errors: errors }); @@ -305,39 +346,72 @@ router.post('/import', async (req, res) => { // 批量创建机柜 let createdCount = 0; let duplicateCount = 0; + let autoGeneratedCount = 0; try { + // 获取当前最大的机柜ID序号(用于自动生成) + const allRacks = await Rack.findAll({ + where: { + rackId: { + [require('sequelize').Op.like]: 'RACK%' + } + } + }); + + let maxNumber = 0; + allRacks.forEach(rack => { + const match = rack.rackId.match(/^RACK(\d+)$/); + if (match) { + const num = parseInt(match[1], 10); + if (num > maxNumber) { + maxNumber = num; + } + } + }); + + // 为需要自动生成的记录分配rackId + const dataWithGeneratedIds = processedData.map(item => { + if (item._autoGenerate) { + maxNumber++; + autoGeneratedCount++; + return { + ...item, + rackId: `RACK${String(maxNumber).padStart(3, '0')}` + }; + } + return item; + }); + // 检查重复的机柜ID const existingRacks = await Rack.findAll({ where: { - rackId: jsonData.map(item => item.rackId) + rackId: dataWithGeneratedIds.map(item => item.rackId) } }); const existingIds = new Set(existingRacks.map(rack => rack.rackId)); - const newData = jsonData.filter(item => !existingIds.has(item.rackId)); - const duplicateData = jsonData.filter(item => existingIds.has(item.rackId)); + const newData = dataWithGeneratedIds.filter(item => !existingIds.has(item.rackId)); + const duplicateData = dataWithGeneratedIds.filter(item => existingIds.has(item.rackId)); duplicateCount = duplicateData.length; // 将roomName转换为roomId - const dataWithRoomId = jsonData.map(item => { + const dataWithRoomId = newData.map(item => { const trimmedRoomName = item.roomName.trim(); return { - ...item, + rackId: item.rackId, + name: item.name, + height: item.height, + maxPower: item.maxPower, + status: item.status, roomId: roomNameToIdMap.get(trimmedRoomName), - roomName: undefined // 移除不需要的字段 + currentPower: 0 }; }); // 只创建新的机柜 - if (newData.length > 0) { - const dataToCreate = dataWithRoomId.filter(item => - jsonData.map(d => d.rackId).includes(item.rackId) && - !existingIds.has(item.rackId) - ); - - const result = await Rack.bulkCreate(dataToCreate, { + if (dataWithRoomId.length > 0) { + const result = await Rack.bulkCreate(dataWithRoomId, { ignoreDuplicates: true // 忽略重复的机柜ID }); createdCount = result.length; diff --git a/backend/validation/rackSchema.js b/backend/validation/rackSchema.js index cb98b85..1487c2f 100644 --- a/backend/validation/rackSchema.js +++ b/backend/validation/rackSchema.js @@ -6,14 +6,12 @@ const RACK_STATUS = ['active', 'inactive', 'maintenance']; // 创建机柜验证Schema const createRackSchema = Joi.object({ rackId: Joi.string() - .required() .max(50) .pattern(/^[a-zA-Z0-9_-]+$/) + .allow('', null) .messages({ - 'string.empty': '机柜ID不能为空', 'string.max': '机柜ID不能超过50个字符', - 'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线', - 'any.required': '机柜ID是必填字段' + 'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线' }), name: Joi.string() diff --git a/frontend/src/pages/RackManagement.jsx b/frontend/src/pages/RackManagement.jsx index 2ebba04..a0c40f8 100644 --- a/frontend/src/pages/RackManagement.jsx +++ b/frontend/src/pages/RackManagement.jsx @@ -794,7 +794,7 @@ function RackManagement() { destroyOnHidden styles={{ body: { padding: '24px' }, - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 50px 16px 24px' } }} style={{ borderRadius: '16px' }} > @@ -803,10 +803,9 @@ function RackManagement() {